fix(S6-FOLLOWUP): test:e2e:affected filtra de verdade — --no-deps desliga o closure de deps dos projects #76

Merged
fsolla merged 1 commit from S6-FOLLOWUP-e2e-cli-playwright-ignora-filtros-grep-posiciona into main 2026-08-18 22:44:33 +00:00
10 changed files with 276 additions and 11 deletions

View file

@ -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<slot>` / `teqo_wt<slot>_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 -- <path>` ou `--no-deps --project=<família>`).
- 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 <symbol>`) 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.

View file

@ -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 <passthrough verbatim> --no-deps -- <paths>` 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=<família> --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 <flags> -- <paths>` 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=<família>`; `-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 target="_blank" rel="noopener noreferrer">`); 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).

View file

@ -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 <passthrough verbatim> --no-deps -- <paths>` 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=<família> --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`.

View file

@ -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<br/>scripts/lib/playwright-e2e-args.mjs]
B --> C[test:e2e --no-deps<br/>flags antes do --<br/>-- paths posicionais]
C --> D[playwright 1.58.2 dev mode<br/>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`) + `-- <scopeSpecPaths>` 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=<família> --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=<família> --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.

View file

@ -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

View file

@ -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
}

View file

@ -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',

View file

@ -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' } : {}),

View file

@ -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',
]) {

View file

@ -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'])
})
})