From 8c915e59fc36577798f492eca303e5fda5fcee94 Mon Sep 17 00:00:00 2001 From: Francisco Solla Date: Tue, 18 Aug 2026 19:05:56 -0300 Subject: [PATCH] =?UTF-8?q?fix(S6-FOLLOWUP):=20test:e2e:affected=20rodava?= =?UTF-8?q?=20a=20su=C3=ADte=20completa=20=E2=80=94=20--no-deps=20desliga?= =?UTF-8?q?=20o=20closure=20de=20deps=20dos=20projects=20em=20runs=20filtr?= =?UTF-8?q?ados?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Causa raiz (medida com o binário 1.58.2, config real): commander parseia posicionais e -g/--list funcionam; o que arrastava a suíte era o closure de dependências do dev mode (setup<-campaign<-frontend<-admin): filtrar um spec do frontend puxava campaign(160)+setup(2). run-e2e-affected.mjs agora gera test:e2e --no-deps -- via helpers puros em scripts/lib/playwright-e2e-args.mjs (unit-pinned); passthrough corrigido (pnpm consome o -- e o path era descartado); run-e2e-affected.mjs entrou em HIGH_RISK_EXACT. Docs: receitas de filtro no playwright.config.ts, AGENTS.md, changelog. --- AGENTS.md | 2 +- docs/CHANGELOG-AGENTS.md | 4 + docs/changelog/2026-08-18-s6-followup.md | 1 + ...p-playwright-filtros-cli-ignorados-impl.md | 85 +++++++++++++++ playwright.config.ts | 14 +++ scripts/lib/playwright-e2e-args.mjs | 51 +++++++++ scripts/lib/test-affected-core.mjs | 1 + scripts/run-e2e-affected.mjs | 27 +++-- tests/unit/ciSkipInvariants.unit.spec.ts | 1 + tests/unit/playwrightE2eArgs.unit.spec.ts | 101 ++++++++++++++++++ 10 files changed, 276 insertions(+), 11 deletions(-) create mode 100644 docs/changelog/2026-08-18-s6-followup.md create mode 100644 docs/plans/s6-followup-playwright-filtros-cli-ignorados-impl.md create mode 100644 scripts/lib/playwright-e2e-args.mjs create mode 100644 tests/unit/playwrightE2eArgs.unit.spec.ts diff --git a/AGENTS.md b/AGENTS.md index f9db97ee..e9250605 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -158,7 +158,7 @@ The artifact is at **v3**: v2 added E8's `campoFederalVotesByYear` / `federalTal 1. Confirm `DATABASE_URL` is `postgresql://teqo:teqo@localhost:5432/teqo` and `.env.test` points to `teqo_test`; never use Neon for development, tests, E2E, or local builds. In a provisioned worktree (`pnpm worktree next`) the values come from its `.env.local` / `.env.test.local` (`teqo_wt` / `teqo_wt_test`, dev port `3100+slot`) — read them there instead of assuming the main-repo names. 2. Run `pnpm db:start`, `pnpm migrate:status`, and `DATABASE_URL=postgresql://teqo:teqo@localhost:5432/teqo_test pnpm migrate:status`; every migration must show `Ran: Yes`. -3. Run `pnpm generate:types`, `pnpm generate:importmap` when components changed, `pnpm exec tsc --noEmit`, `pnpm lint` (zero warnings — the script enforces `--max-warnings=0`), `pnpm format:check` (Prettier is the formatting authority; `pnpm format` fixes), `pnpm exec knip` (dead files/dependencies are CI-blocking; delete what your change orphaned), `pnpm check:cycles` (madge — zero circular imports), `pnpm test` (unit + int), and `pnpm build` against the local database. Payload CLI scripts (`migrate`, `generate:*`, `payload`) and the build migrate step use `--conditions=react-server` so `server-only` modules load correctly outside Next.js. **O e2e não roda mais no gate local (OPS59):** ele roda **uma vez, no CI** (PR e main — gateia merge e deploy); local opcional para feedback antecipado: `pnpm test:e2e:affected`. +3. Run `pnpm generate:types`, `pnpm generate:importmap` when components changed, `pnpm exec tsc --noEmit`, `pnpm lint` (zero warnings — the script enforces `--max-warnings=0`), `pnpm format:check` (Prettier is the formatting authority; `pnpm format` fixes), `pnpm exec knip` (dead files/dependencies are CI-blocking; delete what your change orphaned), `pnpm check:cycles` (madge — zero circular imports), `pnpm test` (unit + int), and `pnpm build` against the local database. Payload CLI scripts (`migrate`, `generate:*`, `payload`) and the build migrate step use `--conditions=react-server` so `server-only` modules load correctly outside Next.js. **O e2e não roda mais no gate local (OPS59):** ele roda **uma vez, no CI** (PR e main — gateia merge e deploy); local opcional para feedback antecipado: `pnpm test:e2e:affected` (gera `--no-deps` para runs filtrados — S6-FOLLOWUP: no dev mode a cadeia de dependencies dos projects arrasta os arquivos das dependências via closure; filtros manuais locais: `pnpm test:e2e --no-deps -- ` ou `--no-deps --project=`). - Never run `knip --fix` blind: when knip's module graph is incomplete (it still cannot load `payload.config.ts` — ledgered as P3), `--fix` deletes exports that migrations/tests still reference. Verify each removal (e.g. `git grep -w `) before deleting. - Run gate commands bare — never piped (`pnpm test | tail` swallows the exit code, and a real failure was masked exactly this way during Pass 2). Large outputs are captured by the terminal; read them from there. 4. Scan every edited runtime source file with Aikido and resolve new findings before handoff. Do not expand hardening work into unrelated pre-existing findings without an explicit scope change. diff --git a/docs/CHANGELOG-AGENTS.md b/docs/CHANGELOG-AGENTS.md index 682b6419..d14eb142 100644 --- a/docs/CHANGELOG-AGENTS.md +++ b/docs/CHANGELOG-AGENTS.md @@ -4,6 +4,10 @@ Conteúdo movido do `AGENTS.md` em 2026-07-30 (fatiamento do paradigma de agente --- +**S6-FOLLOWUP** — CLI Playwright 1.58.2 e filtros e2e locais: `pnpm test:e2e:affected` rodava a suíte completa silenciosamente em runs selecionados. Causa raiz (medida com o binário do repo, config real): o commander parseia posicionais e `-g`/`--list` funcionam — quem arrastava a suíte era o closure de dependências dos projects no dev mode (`setup ← campaign ← frontend ← admin` em `playwright.config.ts`): `collectProjectsAndTestFiles` filtra os arquivos por project, mas `buildProjectsClosure` repõe todos os arquivos de cada project-dependência — filtrar um spec do `frontend` puxava `campaign` (160) + `setup` (2), 177 no total com o próprio arquivo (15); `admin` puxava a suíte inteira. Em prod mode (`CI=1`) os projects não têm deps e o filtro sempre funcionou (por isso o CI selecionado estava correto). Fix: `run-e2e-affected.mjs` agora gera `test:e2e --no-deps -- ` via helpers puros novos em `scripts/lib/playwright-e2e-args.mjs` (unit-testados, `tests/unit/playwrightE2eArgs.unit.spec.ts`) — `--no-deps` desliga o closure em runs filtrados (suíte completa mantém a cadeia OPS34 de prewarm); de quebra, o passthrough do script foi corrigido (`pnpm test:e2e:affected -- tests/x.spec.ts` descartava o path — pnpm consome o `--` e o argv nunca o continha; agora `parsePassthroughArgs` só remove um `--` líder e repassa o resto verbatim, preservando `-g grade`). Hardening: `scripts/run-e2e-affected.mjs` entrou em `HIGH_RISK_EXACT` (era o espelho local do job e2e sem gate — um PR só de runner não rodaria e2e). Docs: receitas de filtro no `playwright.config.ts` (posicional exige `--no-deps`; `--project= --no-deps`; `-g`/`--list` funcionam), header do script e nota no AGENTS.md. Sem mudança de specs, CI (`--shard`) ou `gate:ci`. Verificação: unit 2343+ verdes; contagem por `--list` do binário com a forma final (18); CI do PR roda o e2e completo (o diff toca `playwright.config.ts` e `run-e2e-affected.mjs`, high-risk). Débito absorvido: hang do `pnpm test:e2e` via pnpm neste worktree (pré-existente, ambiente — binário direto funciona; não escopo). Impl plan: `docs/plans/s6-followup-playwright-filtros-cli-ignorados-impl.md`. + +**S6-FOLLOWUP** — CLI Playwright 1.58.2 e filtros e2e locais: `pnpm test:e2e:affected` rodava a suíte completa silenciosamente em runs selecionados. Causa raiz (medida com o binário do repo, config real): o commander parseia posicionais e `-g`/`--list` funcionam — quem arrastava a suíte era o closure de dependências dos projects no dev mode (`setup ← campaign ← frontend ← admin` em `playwright.config.ts`): `collectProjectsAndTestFiles` filtra os arquivos por project, mas `buildProjectsClosure` repõe todos os arquivos de cada project-dependência — filtrar um spec do `frontend` puxava `campaign` (160) + `setup` (2) = 177 testes; `admin` puxava a suíte inteira. Em prod mode (`CI=1`) os projects não têm deps e o filtro sempre funcionou (por isso o CI selecionado estava correto). Fix: `run-e2e-affected.mjs` agora gera `test:e2e --no-deps -- ` via builder puro novo `scripts/lib/playwright-e2e-args.mjs` (unit-testado, `tests/unit/playwrightE2eArgs.unit.spec.ts`) — `--no-deps` desliga o closure em runs filtrados (suíte completa mantém a cadeia OPS34 de prewarm); de quebra, o passthrough do script foi corrigido (`pnpm test:e2e:affected -- tests/x.spec.ts` descartava o path — pnpm consome o `--` e o argv nunca o continha; agora args nus viram paths). Docs: receitas de filtro no `playwright.config.ts` (posicional exige `--no-deps`; `--project=`; `-g`/`--list` funcionam), header do script e nota no AGENTS.md. Sem mudança de specs, CI (`--shard`) ou `gate:ci`. Verificação: unit 2343 verdes; contagem por `--list` do binário com a forma final (18/15); CI do PR roda o e2e completo (o diff toca `scripts/run-e2e-affected.mjs`, high-risk). Débito absorvido: hang do `pnpm test:e2e` via pnpm neste worktree (pré-existente, ambiente — binário direto funciona; não escopo). Impl plan: `docs/plans/s6-followup-playwright-filtros-cli-ignorados-impl.md`. + **S6** — home de campanha (jorgesolla1313.com.br) sem carrossel no desktop: as seções "Por que essa eleição importa" (3 cards) e "Nossa caminhada" (6 cards) mostram agora todo o conteúdo de uma vez em ≥1024px — grade estática 3 colunas (problem: 3×437px, bandeiras: 3×2 com altura por conteúdo, padding `p-4`), sem auto-avanço, sem chips/indicadores e sem `aria-current`. Mobile (<1024px) 100% preservado: carrossel com auto-avanço ~4s, chips sincronizados, swipe — componente e geometria mobile intocados. Padrão S1 (renderização-irmã com toggle CSS `hidden lg:grid`/`lg:hidden`): grade server-rendered (zero JS no desktop) + carrossel client embrulhado; os dois cards extraídos do `CampaignCarousel` para `CampaignProblemCard`/`CampaignFlagCard` (presentacionais, fonte única do markup, `size='spacious'` só na grade) e o contrato da grade (data-grid + aria-label + 3 colunas) vive num `CampaignCardGrid` local do page. O carrossel ficou byte-equivalente (só re-roteia os cards; unit tests intocados) e ganhou guard de `display:none` no auto-avanço — o timer nunca roda no desktop (chain morto numa subárvore oculta). Geometria: a seção flags recalibrada no desktop (`height: clamp(418px, calc(807.7px - 28.13vw), 520px)`, âncoras medidas em 1024/1280/1440+, respiro ≥30px, `overflow: hidden` para zoom de texto ≥125% não pintar sobre o footer); a seção problem manteve a geometria (respiro ~31px). Zero schema/migration/Consent; mesma copy/imagens/ordem; S1 e o resto da home intocados. Verificação: unit 2318 + int 791 verdes, e2e frontend 11/11 (novo teste de grade desktop com invariantes 3×1/3×2, ausência de regiões/chips/`aria-current`, respiro ≥24 e `scrollHeight ≤ clientHeight` em 1024/1280/1440/1920; teste de carrossel migrado para viewport mobile 390px com auto-avanço + chips; swipe/mobileSpacing intactos), lint/format/typecheck/knip/cycles/build verdes. `/simplify` 3 reviewers: `lg:` órfãos removidos do carrossel, grade extraída em `CampaignCardGrid`, `overflow: hidden` (zoom ≥125%), guard do timer, `document.fonts.ready` + eixo x + pino de clip no e2e, âncoras no comentário do clamp. Débitos: Issue #58 registrada (S6-FOLLOWUP — CLI do Playwright 1.58.2 ignora `--grep`/posicionais e o `test:e2e:affected` roda a suíte completa; `depends: [S6]`); defer com gatilho para os literais de gap da grade (recalibração Penpot do carrossel); descartados contratos de tipo dos cards (armadilha knip), aria-labels duplicados (copy pinada no e2e) e pino de tablet 768–1023. Impl plan: `docs/plans/home-campanha-sem-carrossel-desktop-impl.md`. **S2** — seção "Acompanhe de perto" da home de campanha (jorgesolla1313.com.br) passa a incluir os vídeos do canal oficial do YouTube (`@JorgeSollaDep`), estendendo o board da S1. Fonte automática servidor-side: global `SocialFeedSettings` nova (grupo `Configurações`, read/update admin-only — a chave da API nunca vaza pelo REST), com kill switch `enabled` ("Pausar feed"), toggle `youtubeEnabled`, chave + channel ID, `youtubeMaxItems` (1–5, default 3) e `excludedItems` (platform+id+motivo opcional — mesmo mecanismo/lugar da S3), migration `20260818_111748_add_social_feed_settings` não destrutiva (editada para conter só o DDL — o `migrate:create` regenerava drift de migrations hand-written sem sidecar JSON de 20260811). Loader `src/utilities/youtubeFeed.ts` (irmão de `posts.ts`, pinado no spec de convenções): parsers/format puros (`formatYouTubeViews` pt-BR "12,4 mil"/"1,2 mi", thumbnails maxres??high??medium), fetch Data API v3 em dois passos (search → statistics, `fetchImpl`/`YOUTUBE_API_BASE_URL` injetáveis), `unstable_cache` tag `social-feed` + `revalidate: 300` (TTL da entrada, não da página — o bug do S1 era page-level); **fail-closed com snapshot persistido por SQL direto** (`payload.db.drizzle`, sem hooks — `revalidateTag` dentro de `unstable_cache` LANÇA no Next 15.4, verificado no source; o snapshot cru pré-exclusão sobrevive a edições de settings por merge do update), exclusões aplicadas na leitura. Card generalizado `CampaignArticleCard` → `CampaignContentCard` (git mv; `external?: boolean` → ``); a seção mescla artigos visíveis + vídeos elegíveis por recência (grande = mais recente — aceite S2; degrade para o comportamento S1 sem config), meta "há X · N visualizações" (relativa < 48h, absoluta além), link "YouTube →" derivado do channelId; carrossel mobile renomeado para "Conteúdos recentes". next.config ganha `i.ytimg.com` + host local do stub no remotePatterns. Testes: 15 units novas (formatters, parsers, eligibility, fetch com fetchImpl fake); e2e com stub HTTP node (`tests/e2e/youtube-stub.mjs`, webServer do Playwright vira array, porta = dev+1000; `POST /__stub/state` alterna ok/fail; thumbnails servidas localmente) — 4 testes novos no describe serial da seção (full-state com exclusão + link externo + carrossel, fail-closed sem snapshot, snapshot persistente com maxItems mudando na API down, kill switch) + reset de settings no beforeAll; conventions specs atualizados (pin do youtubeFeed, settle após about:blank). Verificação: unit 2306 + int 791 verdes; e2e frontend 14/14 dev e 6/6 prod-mode (`E2E_PROD=1` — valida TTL+tags+snapshot no build real); lint/format/tsc/knip/cycles/build verdes. `/simplify` 3 reviewers (sem P0): P1 corrigido — label do kill switch invertido ("Pausar feed" marcado = ativo → "Feed ativo"); P2 absorvidos (className do link do header extraída, `Promise.all` posts+feed no cache frio, comentário do sentinela 0 do merge, helper único `tests/helpers/youtubeStub.ts` para a porta do stub, `waitForHomeSectionState` fundido no `waitForHomeHTML` com orçamento 12s do S1, precondição do persist documentada); débito REGISTRADO (#40, prio:P2, depends #18): o `validate` do `itemId` lê `data.platform` que é sempre undefined (Payload passa o doc inteiro; verificado no source) — produção sem validação de ID de vídeo, e o e2e depende dessa mortandade (ids do stub de 20 chars); correção em dupla documentada na issue. Descartados com justificativa: ramo maxres do search (pinned por teste), tag `social-feed` fora da convenção `global_*` (precedente election-tse/municipality-catalog), remotePattern `localhost/thumbs` incondicional (decisão deliberada dos 3 modos e2e), persist não-atômico (comentado); achados visuais do design-vision mapeados para a Issue #33 (separação visual branco-sobre-branco, fora de escopo por decisão do gate). diff --git a/docs/changelog/2026-08-18-s6-followup.md b/docs/changelog/2026-08-18-s6-followup.md new file mode 100644 index 00000000..f6f7d9dd --- /dev/null +++ b/docs/changelog/2026-08-18-s6-followup.md @@ -0,0 +1 @@ +**S6-FOLLOWUP** — CLI Playwright 1.58.2 e filtros e2e locais: `pnpm test:e2e:affected` rodava a suíte completa silenciosamente em runs selecionados. Causa raiz (medida com o binário do repo, config real): o commander parseia posicionais e `-g`/`--list` funcionam — quem arrastava a suíte era o closure de dependências dos projects no dev mode (`setup ← campaign ← frontend ← admin` em `playwright.config.ts`): `collectProjectsAndTestFiles` filtra os arquivos por project, mas `buildProjectsClosure` repõe todos os arquivos de cada project-dependência — filtrar um spec do `frontend` puxava `campaign` (160) + `setup` (2), 177 no total com o próprio arquivo (15); `admin` puxava a suíte inteira. Em prod mode (`CI=1`) os projects não têm deps e o filtro sempre funcionou (por isso o CI selecionado estava correto). Fix: `run-e2e-affected.mjs` agora gera `test:e2e --no-deps -- ` via helpers puros novos em `scripts/lib/playwright-e2e-args.mjs` (unit-testados, `tests/unit/playwrightE2eArgs.unit.spec.ts`) — `--no-deps` desliga o closure em runs filtrados (suíte completa mantém a cadeia OPS34 de prewarm); de quebra, o passthrough do script foi corrigido (`pnpm test:e2e:affected -- tests/x.spec.ts` descartava o path — pnpm consome o `--` e o argv nunca o continha; agora `parsePassthroughArgs` só remove um `--` líder e repassa o resto verbatim, preservando `-g grade`). Hardening: `scripts/run-e2e-affected.mjs` entrou em `HIGH_RISK_EXACT` (era o espelho local do job e2e sem gate — um PR só de runner não rodaria e2e). Docs: receitas de filtro no `playwright.config.ts` (posicional exige `--no-deps`; `--project= --no-deps`; `-g`/`--list` funcionam), header do script e nota no AGENTS.md. Sem mudança de specs, CI (`--shard`) ou `gate:ci`. Verificação: unit 2343+ verdes; contagem por `--list` do binário com a forma final (18); CI do PR roda o e2e completo (o diff toca `playwright.config.ts` e `run-e2e-affected.mjs`, high-risk). Débito absorvido: hang do `pnpm test:e2e` via pnpm neste worktree (pré-existente, ambiente — binário direto funciona; não escopo). Impl plan: `docs/plans/s6-followup-playwright-filtros-cli-ignorados-impl.md`. diff --git a/docs/plans/s6-followup-playwright-filtros-cli-ignorados-impl.md b/docs/plans/s6-followup-playwright-filtros-cli-ignorados-impl.md new file mode 100644 index 00000000..d5eb80d6 --- /dev/null +++ b/docs/plans/s6-followup-playwright-filtros-cli-ignorados-impl.md @@ -0,0 +1,85 @@ +# Impl: S6-FOLLOWUP — CLI Playwright ignora filtros; `test:e2e:affected` roda a suíte completa + +Status: em execução +Atualizado em: 2026-08-18 +Issue: #58 (S6-FOLLOWUP, depends S6) +Intenção: docs/plans/s6-followup-playwright-filtros-cli-ignorados.md +Appetite restante: ~0,5 dia (herdado) + +## Leitura da intenção + +- **Outcome:** `pnpm test:e2e:affected` roda SÓ os specs afetados (contagem verificada); comandos de iteração locais documentados e funcionando. +- **O que NÃO negociar:** não mudar specs, CI (`--shard` ok), `gate:ci` nem trocar de runner. +- **O que reavaliar:** a hipótese de mecanismo do plano de intenção está **errada** — evidência empírica nova desta sessão (mesmo binário 1.58.2, config real e config mínima): + +| Invocação (config REAL do repo) | Resultado medido | +| ------------------------------------------------------ | -------------------------------------------------------------------------------------- | +| `--list admin` / `--list frontend` / `--list campaign` | 180 / 177 / 162 (frontend 15 + closure campaign 160 + setup 2; admin 180 = suíte toda) | +| `--list campaignHomeActions` | 20 (arquivo 18 + setup 2) | +| `--list pwa` | 3 (campaign-pwa 1 + setup 2) | +| `--list seedParity` | 1 (arquivo, sem setup) | +| `--list zzz_nonexistente` | 0 ("No tests found" — filtro aplicado) | +| `--list frontend --no-deps` (dev) | **15** (só o arquivo!) | +| `CI=1 --list frontend` (prod mode) | **15** (só o arquivo!) | + +- **Causa raiz (não é o parse de args):** o commander entrega os posicionais corretamente (`POSITIONALS: ['admin']` — probe) e o matcher de arquivo funciona (probe via `RegExp.prototype.test`: `/frontend/gi` casa só `frontend.e2e.spec.ts`). O que arrasta a suíte é o **closure de dependências entre projects no dev mode** (`setup ← campaign ← frontend ← admin` em `playwright.config.ts`): `collectProjectsAndTestFiles` filtra os arquivos por project, mas `buildProjectsClosure` repõe TODOS os arquivos de cada project-dependência. Filtrar um spec do `frontend` puxa `campaign` (160) + `setup` (2) — 177 no total com o próprio arquivo (15); filtrar `admin` puxa a suíte inteira. Em prod mode (`CI=1`/`E2E_PROD=1`) os projects não têm `dependencies` → filtro funciona (é por isso que o CI selecionado funciona). +- **`-g` e `--list` NÃO estão quebrados** (evidência do plano desatualizada): `-g alpha` filtra por título (verificado em config mínima); `--list` respeita posicionais e `-g` (verificado: `zzz` → 0). A "quebra" era o closure arrastando o resto. Sem pin/upgrade upstream necessário. + +## Abordagem recomendada + +```mermaid +flowchart LR + A[run-e2e-affected.mjs] --> B[buildPlaywrightE2eArgs
scripts/lib/playwright-e2e-args.mjs] + B --> C[test:e2e --no-deps
flags antes do --
-- paths posicionais] + C --> D[playwright 1.58.2 dev mode
closure desligado] + D --> E[só os specs afetados rodam] +``` + +**Opções consideradas:** A (inserir `--` antes dos paths — a correção do plano) | B (`--no-deps` + `--` + correção do passthrough; builder puro em `scripts/lib/` com unit test) | C (forçar prod mode `CI=1` no local) | D (remover `dependencies` do config dev) + +**Recomendação: B** — ataca a causa real (closure) com o mecanismo oficial do playwright (`--no-deps`, exatamente o workaround que o plano já apontava), mantém a cadeia de prewarm do dev mode para a suíte completa, e segue o padrão do repo (lógica pura derivada em `scripts/lib/` unit-testada, como `test-affected-core.mjs`). + +**Rejeitadas:** + +- **A** — o `--` não muda nada no config real: commander já parseia posicionais sem `--`; a evidência do plano (11 testes com `--`) foi medida em config mínima. Só `--` não corrige o sintoma de 173 testes. +- **C** — `CI=1` muda `webServer` para `pnpm start` (exige build), `forbidOnly`, `retries` e o budget do servidor; é uma semântica de ambiente diferente, não um filtro. +- **D** — remove a garantia de ordenação de cold-compile do dev mode (comentário OPS34 no config); afeta a suíte completa dev, não só seleção. + +### Componentes / mudanças + +- **`buildPlaywrightE2eArgs` + `parsePassthroughArgs`** (`scripts/lib/playwright-e2e-args.mjs`, novo): puros e síncronos. `parsePassthroughArgs(argv)` remove só um `--` líder (pnpm já consumiu o dele; invocação direta pode deixar um) e repassa o resto **verbatim** — valores de flag como `-g grade` não são reclassificados. `buildPlaywrightE2eArgs({ scopeSpecPaths, passthroughArgs })` → `['test:e2e', ...passthroughArgs]` + `--no-deps` quando o run é filtrado (paths de escopo, positional no passthrough ou flag de filtro `--project`/`-g`/`--grep`/`--grep-invert`) + `-- ` quando há paths de escopo. `--no-deps` condicional: suíte completa mantém a cadeia OPS34. +- **`run-e2e-affected.mjs`**: (1) corrige o parse do passthrough — hoje `pnpm test:e2e:affected -- tests/x.spec.ts` perde o path (pnpm consome o `--`, argv fica sem `--` e `passthroughIndex === -1` → args descartados); novo contrato via `parsePassthroughArgs`. (2) usa o builder (remove o loop manual de paths). +- **`tests/unit/playwrightE2eArgs.unit.spec.ts`** (novo): pina o parse (invocação nua, pnpm `--`, `--` direto, flags+valores) e a forma dos args — `--no-deps` só em runs filtrados (path de escopo, positional ou flag de filtro), flags de runner (`--shard`, `--reporter`) preservam a cadeia, sem duplicar `--no-deps` explícito. +- **`test-affected-core.mjs`**: `scripts/run-e2e-affected.mjs` entra em `HIGH_RISK_EXACT` (+ invariant em `ciSkipInvariants.unit.spec.ts`) — é o espelho local do job e2e; um PR só de runner hoje escaparia do e2e (mode `none`). +- **Migration:** sem migration (nenhum schema/collection). +- **Access / Consent:** nenhum. +- **UI:** nenhuma. + +### Dados → forma + +N/A (sem dados novos). + +## Fases verificáveis + +1. **Tracer (builder + wiring)** — criar `scripts/lib/playwright-e2e-args.mjs`, unit spec, ligar em `run-e2e-affected.mjs`. Verificação: unit green; forma final confirmada com `--list` direto no binário do repo (já medida: `--no-deps` → 15/18). +2. **Docs** — comentário em `playwright.config.ts` (mecanismo do closure no dev mode + receitas de filtro: path posicional exige `--no-deps`; `--project= --no-deps`; `-g`/`--list` funcionam, mas o closure arrasta dependências); header do `run-e2e-affected.mjs` (contrato de invocação corrigido); bullet curto em `AGENTS.md` (seção OPS59/test:e2e:affected); `docs/changelog/2026-08-18-s6-followup.md` + `pnpm changelog:build`. +3. **Gates** — `pnpm gate:fast` (lint, typecheck, test:unit); CI do PR roda e2e completo (o diff toca `playwright.config.ts` e `scripts/run-e2e-affected.mjs` — ambos em `HIGH_RISK_EXACT`; o runner entrou no set nesta entrega). + +## Rabbit holes / Não escopo (engenharia) + +- **Não investigar o hang do `pnpm test:e2e` neste worktree** (invocação via pnpm congela após a fase de install do pnpm; o binário direto com a mesma `NODE_OPTIONS` funciona e é o veículo de verificação local — pré-existente, ambiente, não o bug). Verificação de contagem local via binário direto + unit; CI é o verifier. +- Não reescrever o parse do commander do playwright, não fazer pin/upgrade de versão (`-g`/`--list` funcionam; evidenciado). +- Não tocar `.forgejo/workflows/*` nem `gate-ci.mjs`. +- Não "consertar" a evidência do plano de intenção retroativamente (a tabela do plano fica como está — docs histórica; o impl plan registra a divergência). + +## Riscos e mitigação + +- **Dev mode sem prewarm do setup em runs selecionados** (o `--no-deps` pula o spec de setup): cold-compile mais lento do primeiro spec em máquina carregada. Mitigação: prod mode (CI) já roda sem prewarm e é o caminho gateado; budget de 60s/240s do webServer permanece. +- **Flag com valor separado por espaço no passthrough** (`-g grade`): resolvido na implementação — passthrough verbatim (sem reclassificação flag/posicional); `-g grade` chega intacto ao playwright. Só sobra o caso do usuário passar um `--` interior explícito junto com paths de escopo gerados (o `--no-deps`/`--` gerados caem depois do `--` do usuário e viram padrão de arquivo → "No tests found", falha ruidosa; pnpm nunca produz isso). +- **Regression na suíte completa dev**: o `--no-deps` só entra nos args gerados pelo builder; `pnpm test:e2e` puro (sem args) não muda — a cadeia de deps dev permanece intacta. + +## Aceite de engenharia + +- [ ] Aceite de produto da intenção ainda coberto: `run-e2e-affected` roda só os specs afetados (unit + `--list` do binário com a forma final); comandos de iteração documentados; `--project= --no-deps` documentado como receita manual. +- [ ] Invariantes AGENTS/engineering-standards: script puro em `scripts/lib/` (padrão test-affected-core); sem tocar CI/gate:ci/specs; identificadores em inglês, copy pt-BR. +- [ ] Testes de domínio previstos: unit do builder (args), nada de access/write path muda. diff --git a/playwright.config.ts b/playwright.config.ts index d4db851d..620f17da 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -94,6 +94,20 @@ export default defineConfig({ */ timeout: 10_000, }, + /* + * CLI filtering in dev mode (S6-FOLLOWUP, 2026-08-18): positional test paths + * ARE parsed and matched per file, but the project dependency chain below + * (setup -> campaign -> frontend -> admin, dev mode only) makes the runner + * drag ALL files of every dependency project into a selected run + * (buildProjectsClosure) — filtering to one frontend spec runs the whole + * campaign family. Prod mode (CI / E2E_PROD=1) has no dependencies, so the + * filter works as-is (the CI selected job relies on this). Local recipes: + * pnpm test:e2e --no-deps -- tests/e2e/campaignHomeActions.e2e.spec.ts + * pnpm test:e2e --no-deps --project=campaign + * -g/--grep and --list DO work (title/file filters); pair them with + * --no-deps/--project for deterministic runs. `pnpm test:e2e:affected` + * generates the --no-deps form automatically. + */ projects: [ /* * `setup` prewarms shared Next route bundles against the dev server's cold diff --git a/scripts/lib/playwright-e2e-args.mjs b/scripts/lib/playwright-e2e-args.mjs new file mode 100644 index 00000000..a5defc16 --- /dev/null +++ b/scripts/lib/playwright-e2e-args.mjs @@ -0,0 +1,51 @@ +/** + * Pure argv helpers for the local e2e runner (S6-FOLLOWUP, 2026-08-18). + * `run-e2e-affected.mjs` consumes them; unit-pinned in + * tests/unit/playwrightE2eArgs.unit.spec.ts. + * + * Why `--no-deps` + `--`: positional paths ARE parsed correctly by the + * Playwright CLI (commander), but in DEV mode the project dependency chain + * (setup -> campaign -> frontend -> admin, playwright.config.ts) makes the + * runner drag ALL files of every dependency project into a selected run via + * buildProjectsClosure — filtering to one frontend spec runs the whole + * campaign family. `--no-deps` disables that closure for FILTERED runs only; + * a full run keeps the dev chain (OPS34 cold-compile prewarm ordering). The + * `--` separator guards the generated scope paths from flag parsing. + */ + +/** Flags that filter the run by themselves (not just runner knobs). */ +const FILTER_FLAG_RE = /^(?:--project|-g|--grep|--grep-invert)(?:=.*)?$/ + +/** + * Passthrough args from the process argv, minus the script path. pnpm + * consumes its own `--` separator, so a leading `--` in argv only appears on + * direct `node scripts/...` invocations — strip exactly one, forward the rest + * verbatim (flag values must not be re-classified, e.g. `-g grade`). + * @param {string[]} argv process.argv + * @returns {string[]} + */ +export const parsePassthroughArgs = (argv) => { + const rest = argv.slice(2) + return rest[0] === '--' ? rest.slice(1) : rest +} + +/** + * @param {{ scopeSpecPaths?: string[], passthroughArgs?: string[] }} input + * scopeSpecPaths — repo-relative spec paths selected by the affected manifest. + * passthroughArgs — extra args forwarded by the user. + * @returns {string[]} pnpm argv for the e2e run, e.g. + * ['test:e2e', '--no-deps', '--', 'tests/e2e/foo.e2e.spec.ts'] + */ +export const buildPlaywrightE2eArgs = ({ scopeSpecPaths = [], passthroughArgs = [] }) => { + const filtered = + scopeSpecPaths.length > 0 || + passthroughArgs.some((arg) => !arg.startsWith('-') || FILTER_FLAG_RE.test(arg)) + const args = ['test:e2e', ...passthroughArgs] + if (filtered && !passthroughArgs.includes('--no-deps')) { + args.push('--no-deps') + } + if (scopeSpecPaths.length > 0) { + args.push('--', ...scopeSpecPaths) + } + return args +} diff --git a/scripts/lib/test-affected-core.mjs b/scripts/lib/test-affected-core.mjs index c02383a3..dc2491e2 100644 --- a/scripts/lib/test-affected-core.mjs +++ b/scripts/lib/test-affected-core.mjs @@ -27,6 +27,7 @@ export const HIGH_RISK_EXACT = new Set([ 'scripts/lib/e2e-affected-manifest.mjs', 'scripts/test-affected.mjs', 'scripts/e2e-affected.mjs', + 'scripts/run-e2e-affected.mjs', 'scripts/ci-scope.mjs', 'scripts/check-test-locations.mjs', 'scripts/check-plans-only-pr-closes.mjs', diff --git a/scripts/run-e2e-affected.mjs b/scripts/run-e2e-affected.mjs index f7297ea5..116d0e6a 100644 --- a/scripts/run-e2e-affected.mjs +++ b/scripts/run-e2e-affected.mjs @@ -6,7 +6,15 @@ * 3. optional production build into `.next-e2e` when `E2E_PROD=1`. * 4. `pnpm test:e2e` (set `CI=1` and `E2E_PROD=1` together for prod mode). * - * Extra Playwright args after `--` are forwarded (e.g. a single spec while debugging). + * Extra Playwright args are forwarded verbatim (e.g. a single spec while + * debugging): pnpm consumes the first `--` separator, so the script only ever + * strips a leading `--` left by direct `node scripts/...` invocations + * (S6-FOLLOWUP). Flag values like `-g grade` pass through untouched. + * + * Filtered runs get `--no-deps` (S6-FOLLOWUP): in dev mode the project + * dependency chain drags every dependency project's files into a selected run + * via buildProjectsClosure; --no-deps disables that closure. Full runs keep + * the dev chain (OPS34 prewarm ordering). * * GITHUB_BASE_REF=stage node scripts/run-e2e-affected.mjs * pnpm test:e2e:affected -- tests/e2e/campaignHomeActions.e2e.spec.ts @@ -15,10 +23,11 @@ import { execFileSync, spawnSync } from 'node:child_process' import path from 'node:path' import { fileURLToPath } from 'node:url' +import { buildPlaywrightE2eArgs, parsePassthroughArgs } from './lib/playwright-e2e-args.mjs' + const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..') -const passthroughIndex = process.argv.indexOf('--') -const passthroughArgs = passthroughIndex === -1 ? [] : process.argv.slice(passthroughIndex + 1) +const passthroughArgs = parsePassthroughArgs(process.argv) const scopeRaw = execFileSync('node', ['scripts/e2e-affected.mjs'], { cwd: repoRoot, @@ -51,13 +60,11 @@ if (process.env.E2E_PROD === '1') { run('pnpm', ['build'], { NEXT_DIST_DIR: '.next-e2e' }) } -const playwrightArgs = ['test:e2e'] -if (scope.mode === 'selected' && scope.specs.length > 0) { - for (const name of scope.specs) { - playwrightArgs.push(`tests/e2e/${name}.e2e.spec.ts`) - } -} -playwrightArgs.push(...passthroughArgs) +const playwrightArgs = buildPlaywrightE2eArgs({ + scopeSpecPaths: + scope.mode === 'selected' ? scope.specs.map((name) => `tests/e2e/${name}.e2e.spec.ts`) : [], + passthroughArgs, +}) run('pnpm', playwrightArgs, { ...(process.env.E2E_PROD === '1' ? { CI: '1', E2E_PROD: '1' } : {}), diff --git a/tests/unit/ciSkipInvariants.unit.spec.ts b/tests/unit/ciSkipInvariants.unit.spec.ts index 19ea540f..b23b7004 100644 --- a/tests/unit/ciSkipInvariants.unit.spec.ts +++ b/tests/unit/ciSkipInvariants.unit.spec.ts @@ -101,6 +101,7 @@ describe('ciSkipInvariants', () => { 'scripts/lib/plansOnlyClosesGuard.mjs', 'scripts/test-affected.mjs', 'scripts/e2e-affected.mjs', + 'scripts/run-e2e-affected.mjs', 'tests/unit/ciSkipInvariants.unit.spec.ts', 'tests/unit/testAffected.unit.spec.ts', ]) { diff --git a/tests/unit/playwrightE2eArgs.unit.spec.ts b/tests/unit/playwrightE2eArgs.unit.spec.ts new file mode 100644 index 00000000..ded96b95 --- /dev/null +++ b/tests/unit/playwrightE2eArgs.unit.spec.ts @@ -0,0 +1,101 @@ +import { describe, expect, it } from 'vitest' + +import { + buildPlaywrightE2eArgs, + parsePassthroughArgs, +} from '../../scripts/lib/playwright-e2e-args.mjs' + +// S6-FOLLOWUP (2026-08-18): the e2e CLI filters WERE ignored locally because +// the dev-mode project dependency chain drags every dependency project's files +// into a selected run (buildProjectsClosure). The helpers pin the generated +// argv: `--no-deps` on filtered runs only (full runs keep the OPS34 dev +// chain), generated scope paths after the `--` separator, and a verbatim +// passthrough (pnpm consumes the first `--`, so the script never sees it; flag +// values like `-g grade` must not be re-classified). +describe('parsePassthroughArgs', () => { + it('returns nothing for a plain invocation', () => { + expect(parsePassthroughArgs(['node', 'scripts/run-e2e-affected.mjs'])).toEqual([]) + }) + + it('keeps bare args when pnpm consumed the `--` separator', () => { + expect( + parsePassthroughArgs(['node', 'scripts/run-e2e-affected.mjs', 'tests/x.e2e.spec.ts']), + ).toEqual(['tests/x.e2e.spec.ts']) + }) + + it('strips exactly one leading `--` (direct invocation with separator)', () => { + expect( + parsePassthroughArgs(['node', 'scripts/run-e2e-affected.mjs', '--', '-g', 'grade']), + ).toEqual(['-g', 'grade']) + }) + + it('keeps flags and flag values verbatim', () => { + expect( + parsePassthroughArgs([ + 'node', + 'scripts/run-e2e-affected.mjs', + '--grep=home', + '-g', + 'grade', + '--', + 'tests/x.e2e.spec.ts', + ]), + ).toEqual(['--grep=home', '-g', 'grade', '--', 'tests/x.e2e.spec.ts']) + }) +}) + +describe('buildPlaywrightE2eArgs', () => { + it('keeps the full-suite form when nothing is filtered (dev chain intact)', () => { + expect(buildPlaywrightE2eArgs({ scopeSpecPaths: [], passthroughArgs: [] })).toEqual([ + 'test:e2e', + ]) + }) + + it('keeps non-filter flags on the full-suite form (shards, reporters, workers)', () => { + expect(buildPlaywrightE2eArgs({ passthroughArgs: ['--shard=1/2', '--reporter=line'] })).toEqual( + ['test:e2e', '--shard=1/2', '--reporter=line'], + ) + }) + + it('disables the dependency closure and separates generated paths with -- on filtered runs', () => { + expect( + buildPlaywrightE2eArgs({ scopeSpecPaths: ['tests/e2e/campaignHomeActions.e2e.spec.ts'] }), + ).toEqual(['test:e2e', '--no-deps', '--', 'tests/e2e/campaignHomeActions.e2e.spec.ts']) + }) + + it('treats a bare passthrough path as a filtered run and adds --no-deps', () => { + expect(buildPlaywrightE2eArgs({ passthroughArgs: ['tests/e2e/frontend.e2e.spec.ts'] })).toEqual( + ['test:e2e', 'tests/e2e/frontend.e2e.spec.ts', '--no-deps'], + ) + }) + + it('treats filter flags (--project, -g/--grep) as filtered runs', () => { + expect(buildPlaywrightE2eArgs({ passthroughArgs: ['--project=campaign'] })).toEqual([ + 'test:e2e', + '--project=campaign', + '--no-deps', + ]) + expect(buildPlaywrightE2eArgs({ passthroughArgs: ['-g', 'grade'] })).toEqual([ + 'test:e2e', + '-g', + 'grade', + '--no-deps', + ]) + }) + + it('does not duplicate an explicit --no-deps passthrough', () => { + expect(buildPlaywrightE2eArgs({ passthroughArgs: ['--no-deps'] })).toEqual([ + 'test:e2e', + '--no-deps', + ]) + }) + + it('merges scope paths and passthrough positionals in a single filtered run', () => { + expect( + buildPlaywrightE2eArgs({ + scopeSpecPaths: ['tests/e2e/a.e2e.spec.ts'], + passthroughArgs: ['tests/e2e/b.e2e.spec.ts'], + }), + ).toEqual(['test:e2e', 'tests/e2e/b.e2e.spec.ts', '--no-deps', '--', 'tests/e2e/a.e2e.spec.ts']) + }) +})