test(e2e): escopa locators do card de responsáveis ao form visível

Falha pré-existente reproduzida em origin/main: a página de demanda monta o
form de responsáveis duas vezes no DOM (visível + cópia oculta) e
campaignDemandVisibility batia nos dois com locator estrito — classe
"locator não-escopado" do plano OPS83/#824. O CI curado do PR roda a spec
(diff high-risk) e ficaria vermelho; os locators agora miram o elemento
visível.

Registro no impl plan do C231 e no changelog.
This commit is contained in:
Francisco Solla 2026-09-26 05:21:19 -03:00
parent 446a873c84
commit 45f88fa6ac
3 changed files with 28 additions and 10 deletions

View file

@ -1 +1 @@
**C231 — acervo de fotos do Flickr: ingestão completa para o storage privado (2026-09-26):** as 6.577 fotos da conta própria `depjorgesolla` passam a ter casa no Teqo — collection privada `archivePhoto` (upload, grupo Comunicação) com `flickrId` unique+index como identidade e metadados do Flickr estruturados (`title`/`description`/`tags`/`takenAt`/`postedAt`/`albums`/`geo` + `exif` json, `sourceUrl`/`owner`/`license` de proveniência; `alt` derivado), leitura pelos papéis de comunicação e mutação só do admin do Payload, com entrada no mapa S3 do plugin (sem ela o original cairia em disco efêmero em produção). A migration `add_archive_photo` é aditiva e o snapshot que a acompanha **repara a base da cadeia**: o `.json` anterior (`20260924_123940…`) estava dessincronizado e a migration gerada re-criava objetos já existentes (`internet_speech_media`, `recording_rels/topics/scopes`, colunas de `speech`/`content_piece`/`recording`); a migration foi aparada para só o DDL do acervo e o snapshot completo (106 tabelas) volta a ser o base das próximas `migrate:create`. A ingestão é idempotente por `flickrId` (`src/utilities/flickr/archivePhotoIngest.ts`: re-checa, grava linha+upload numa transação e devolve `created`/`existing`/`failed` com motivo) e o CLI é `pnpm flickr:import`: **plano/dry-run por padrão**, `--apply` (exige `FLICKR_IMPORT_CONFIRM=1` fora de dev local, `TEQO_ENV` casando o nome exato do banco e as 4 `S3_*`), `--verify` (inventário read-only: contagem, bytes, cobertura por álbum, metadados e ficheiros faltantes), `--limit`/`--page`/`--out`, recibos JSON datados em `data/flickr/reports/`. O client (`scripts/lib/flickrApi.mjs`) usa `fetch` nativo com `User-Agent`, pacing ~1 req/s, retry 429/5xx, `stat=fail` nomeado, EXIF tolerante e fallback para o maior tamanho quando não há `url_o`; o plano/recibo puro (`scripts/lib/flickrPlan.mjs`) conta vídeos pulados e itens sem id, resolve o mapa foto→álbuns antes de qualquer escrita e classifica novas/já existiam/fallback/inválidas; `downloadUrlToFile` ganha `headers` (UA do Flickr); e o guard de alvo dos imports saiu de `reel-ingest.mjs` para `scripts/lib/cli.mjs` (`assertEnvironmentDatabaseTarget` + `TEQO_ENV_DATABASE_BY_ENV`, com o C195 delegando sem mudar contrato). Envs `FLICKR_API_KEY`/`FLICKR_USER_ID` documentadas no `.env.example` (só env, nunca repo) e runbook do homeserver com staging-primeiro. Testes: unit novos de mapeamento/geo (`archivePhoto.unit`), client Flickr, plano/parser (`flickrPlan.unit`) e guardas em subprocesso (`flickrImportCli.unit`), int `archivePhotoIngest` (criação+objeto, convergência sem duplicar, falha honesta sem linha, access por papel e mutação admin); `pnpm gate:fast` verde (unit 431 files) e int full 105 files. Canary/backfill ficam para quando a chave da API for provisionada (Q1). Impl plan: `docs/plans/acervo-fotos-flickr-ingestao-impl.md`.
**C231 — acervo de fotos do Flickr: ingestão completa para o storage privado (2026-09-26):** as 6.577 fotos da conta própria `depjorgesolla` passam a ter casa no Teqo — collection privada `archivePhoto` (upload, grupo Comunicação) com `flickrId` unique+index como identidade e metadados do Flickr estruturados (`title`/`description`/`tags`/`takenAt`/`postedAt`/`albums`/`geo` + `exif` json, `sourceUrl`/`owner`/`license` de proveniência; `alt` derivado), leitura pelos papéis de comunicação e mutação só do admin do Payload, com entrada no mapa S3 do plugin (sem ela o original cairia em disco efêmero em produção). A migration `add_archive_photo` é aditiva e o snapshot que a acompanha **repara a base da cadeia**: o `.json` anterior (`20260924_123940…`) estava dessincronizado e a migration gerada re-criava objetos já existentes (`internet_speech_media`, `recording_rels/topics/scopes`, colunas de `speech`/`content_piece`/`recording`); a migration foi aparada para só o DDL do acervo e o snapshot completo (106 tabelas) volta a ser o base das próximas `migrate:create`. A ingestão é idempotente por `flickrId` (`src/utilities/flickr/archivePhotoIngest.ts`: re-checa, grava linha+upload numa transação e devolve `created`/`existing`/`failed` com motivo) e o CLI é `pnpm flickr:import`: **plano/dry-run por padrão**, `--apply` (exige `FLICKR_IMPORT_CONFIRM=1` fora de dev local, `TEQO_ENV` casando o nome exato do banco e as 4 `S3_*`), `--verify` (inventário read-only: contagem, bytes, cobertura por álbum, metadados e ficheiros faltantes), `--limit`/`--page`/`--out`, recibos JSON datados em `data/flickr/reports/`. O client (`scripts/lib/flickrApi.mjs`) usa `fetch` nativo com `User-Agent`, pacing ~1 req/s, retry 429/5xx, `stat=fail` nomeado, EXIF tolerante e fallback para o maior tamanho quando não há `url_o`; o plano/recibo puro (`scripts/lib/flickrPlan.mjs`) conta vídeos pulados e itens sem id, resolve o mapa foto→álbuns antes de qualquer escrita e classifica novas/já existiam/fallback/inválidas; `downloadUrlToFile` ganha `headers` (UA do Flickr); e o guard de alvo dos imports saiu de `reel-ingest.mjs` para `scripts/lib/cli.mjs` (`assertEnvironmentDatabaseTarget` + `TEQO_ENV_DATABASE_BY_ENV`, com o C195 delegando sem mudar contrato). Envs `FLICKR_API_KEY`/`FLICKR_USER_ID` documentadas no `.env.example` (só env, nunca repo) e runbook do homeserver com staging-primeiro. Testes: unit novos de mapeamento/geo (`archivePhoto.unit`), client Flickr, plano/parser (`flickrPlan.unit`) e guardas em subprocesso (`flickrImportCli.unit`), int `archivePhotoIngest` (criação+objeto, convergência sem duplicar, falha honesta sem linha, access por papel e mutação admin); `pnpm gate:fast` verde (unit 431 files), int full 105 files, e2e curado do PR 95/95 no espelho local (modo CI: build `NEXT_PUBLIC_SITE_URL=https://feed.e2e.teqo.test`) — no caminho, uma falha **pré-existente** reproduzida em `origin/main` foi destravada: `campaignDemandVisibility` batia com locator estrito nos dois mounts do form de responsáveis (visível + cópia oculta), classe "locator não-escopado" do plano OPS83/#824; os locators agora miram o elemento visível. Canary/backfill ficam para quando a chave da API for provisionada (Q1). Impl plan: `docs/plans/acervo-fotos-flickr-ingestao-impl.md`.

View file

@ -169,6 +169,7 @@ N/A — operação/ingestão; o recibo é arquivo JSON operacional (conferência
- **Factory dos predicados de leitura da Central (S1).** `canReadArchivePhoto` segue o padrão intencional de predicado próprio por superfície (o comentário "never an alias" em `contentPieces.ts`; cada vertical pode ampliar seu gate sem conceder escrita alheia). Reabrir só se uma ampliação de read precisar valer para todas as verticais de uma vez.
- **Tipagem do bag do recibo (S3)** e **`--out` absoluto (S4).** Doc-only/paridade com o irmão `import-web-speeches.mjs`; tipar se o formatter for tocado de novo; o recibo é arquivo operacional do operador.
- **Duplicata oculta no DOM do card de responsáveis (OPS83/#824).** O e2e curado destravou uma falha pré-existente (reproduzida em `origin/main`): a página de demanda monta o form de responsáveis duas vezes (visível + cópia oculta) e `campaignDemandVisibility` batia nos dois com locator estrito. Esta entrega escopa os locators ao elemento visível (classe "locator não-escopado" do plano OPS83/#824, que segue dono da causa-raiz); sem Issue nova.
- **Drift pré-existente `activity.tags`.** Coluna sobra no banco (a config não a declara; o snapshot de base também não) e é invisível ao diff — fora do escopo. O reparo do snapshot desta entrega (106 tabelas como novo base) elimina a re-geração de DDL alheia nas próximas `migrate:create`.
## Aceite de engenharia

View file

@ -51,7 +51,10 @@ test.describe('C143 — demand visibility (explicit responsibles)', () => {
// The list rendered for him: the empty state proves the page settled
// (and that the demand is absent — the link assertion below is the
// fail-closed complement).
await expect(page.getByText('Nenhuma demanda por aqui')).toBeVisible()
// The list mounts the desktop table and the mobile cards together (one
// hidden) — assert the rendered empty state, not every DOM copy
// (OPS83/#824, same class).
await expect(page.getByText('Nenhuma demanda por aqui').filter({ visible: true })).toBeVisible()
await expect(page.getByRole('link', { name: demand.title })).toHaveCount(0)
// The URL does not open either — the demand simply does not exist for him
@ -69,20 +72,32 @@ test.describe('C143 — demand visibility (explicit responsibles)', () => {
const { peer, responsible, demand } = await setupDemand(campaign, fixtures)
const demandURL = `${campaign.baseURL}/campanha/demandas/${demand.slug}`
// Pre-existing duplication (OPS83/#824 — the "locator não-escopado" class):
// the detail page mounts the responsibles form twice in the DOM — the
// visible one plus a hidden copy with the same markup. Scope every card
// interaction to the visible form; the search dialog stays page-level (it
// portals outside).
const responsiblesCard = page
.locator('form')
.filter({ hasText: 'Responsáveis pela demanda' })
.filter({ visible: true })
// The responsible manages the list: add the peer through the search dialog.
await campaign.sessionFor(context, responsible)
await page.goto(demandURL)
await expect(page.getByText('Só quem é responsável vê esta demanda.')).toBeVisible()
await expect(responsiblesCard.getByText('Só quem é responsável vê esta demanda.')).toBeVisible()
// The trigger announces the current selection (e.g. "…: 1 responsável").
await page.getByRole('button', { name: /Responsáveis pela demanda/ }).click()
await responsiblesCard.getByRole('button', { name: /Responsáveis pela demanda/ }).click()
await page.getByPlaceholder('Buscar assessor…').fill(peer.name)
await page.getByRole('option', { name: peer.name }).click()
// The multi-add dialog stays open (modal) — close it before saving.
await page.keyboard.press('Escape')
await page.getByRole('button', { name: 'Salvar responsáveis' }).click()
await expect(page.getByText('Responsáveis atualizados.')).toBeVisible()
await expect(page.getByRole('button', { name: `Remover ${peer.name}` })).toBeVisible()
await responsiblesCard.getByRole('button', { name: 'Salvar responsáveis' }).click()
await expect(responsiblesCard.getByText('Responsáveis atualizados.')).toBeVisible()
await expect(
responsiblesCard.getByRole('button', { name: `Remover ${peer.name}` }),
).toBeVisible()
// The peer now opens the demand.
await campaign.sessionFor(context, peer)
@ -92,15 +107,17 @@ test.describe('C143 — demand visibility (explicit responsibles)', () => {
// Back as the responsible, remove the peer and revoke the access again.
await campaign.sessionFor(context, responsible)
await page.goto(demandURL)
await page.getByRole('button', { name: `Remover ${peer.name}` }).click()
await responsiblesCard.getByRole('button', { name: `Remover ${peer.name}` }).click()
// The responsibles save is an optimistic server-action write — wait for the
// POST to land BEFORE swapping to the peer, or the peer's navigation can
// race the revoke (OPS83 run #16: the accessible demand still served).
await Promise.all([
expectPostResponse(page, demandURL),
page.getByRole('button', { name: 'Salvar responsáveis' }).click(),
responsiblesCard.getByRole('button', { name: 'Salvar responsáveis' }).click(),
])
await expect(page.getByRole('button', { name: `Remover ${peer.name}` })).toHaveCount(0)
await expect(
responsiblesCard.getByRole('button', { name: `Remover ${peer.name}` }),
).toHaveCount(0)
// The peer already rendered THIS URL (granted, line 88), so the Next RSC
// cache can serve the previous payload on a plain goto even after the