diff --git a/docs/CHANGELOG-AGENTS.md b/docs/CHANGELOG-AGENTS.md index 88ce4abd..617603a8 100644 --- a/docs/CHANGELOG-AGENTS.md +++ b/docs/CHANGELOG-AGENTS.md @@ -4,7 +4,7 @@ Conteúdo movido do `AGENTS.md` em 2026-07-30 (fatiamento do paradigma de agente --- -**Recently resolved (2026-08-10):** **OPS34** (Issue #599) — o job `e2e` do CI (o mais longo da cascata, 435 s medidos no run 31412425553: ~54 s fixos + build 172 s + testes 196 s) roda agora **em 2 shards paralelos** (`--shard=N/2`, matrix `fail-fast: false`, build por shard — opção A da intenção validada pela medição: por shard ≈ 5–5,5 min; artefato de build compartilhado rejeitado porque os shards só começam após o build → wall maior). **Cadeia de projetos do Playwright (`setup → campaign → frontend → admin`) + prewarm do `setup` viraram dev-only:** `playwright.config.ts` ganhou `isProdMode = CI || E2E_PROD=1` (mesma condição do `webServer.command`, agora reusada) — em prod o projeto `setup` sai do array (arquivo não casado = não coletado) e as `dependencies` zeram (famílias em paralelo contra o build de produção, onde não há compile a preaquecer); dev intocado (provado: `--list` mostra `[setup]` só sem CI; smoke e2e dev rodou setup→campaignAuth). `ci-pr.yml`: o job `scope` emite `e2e_matrix`/`e2e_shards` via novo `e2eShardConfig(mode)` em `scripts/lib/test-affected-core.mjs` (fonte única, unit-pinada: full → `[1,2]`/2, selected/none → `[1]`/1 — pagar 2 builds por 3 specs é desperdício); `ci.yml` hardcodeia `[1,2]`/2 com comentário apontando o single-source. `PLAYWRIGHT_WORKERS: 2` explícito no env dos jobs e2e (aceite: explícito, default local intocado). Armadilha registrada nos workflows: flags do playwright vão **sem** o separador `--` — pnpm repassa o `--` literal ao playwright e a flag vira file pattern ("No tests found"). Specs intactas (o `setup` não foi removido — virou dev-only, já documentado como prewarm dev-only); sem migration/access/Consent/UI. **Medição pós-merge (main, run 31418206512):** job e2e 435 s → **326 s (shard 1) / 369 s (shard 2)** — shard 2 = fixo 68 s + build 176 s + testes 124 s (a fase de testes caiu de 196 s para 124 s com a cadeia removida; o shard 2 pagou retry da flake conhecida de `campaignMunicipalities`); gate completo do pipeline (checks) ~7,5 → ~6,4 min. Build passou a dominar o shard — gatilho da intenção (opção B, artefato compartilhado) reavaliável quando os testes encolherem mais. Intenção [`ci-e2e-paralelizar-job.md`](plans/ci-e2e-paralelizar-job.md); impl [`ci-e2e-paralelizar-job-impl.md`](plans/ci-e2e-paralelizar-job-impl.md). +**Recently resolved (2026-08-10):** **OPS34** (Issue #599) — o job `e2e` do CI (o mais longo da cascata, 435 s medidos no run 31412425553: ~67 s fixos + build 172 s + testes 196 s) roda agora **em 2 shards paralelos** (`--shard=N/2`, matrix `fail-fast: false`, build por shard — opção A da intenção validada pela medição: por shard ≈ 5–5,5 min; artefato de build compartilhado rejeitado porque os shards só começam após o build → wall maior). **Cadeia de projetos do Playwright (`setup → campaign → frontend → admin`) + prewarm do `setup` viraram dev-only:** `playwright.config.ts` ganhou `isProdMode = CI || E2E_PROD=1` (mesma condição do `webServer.command`, agora reusada) — em prod o projeto `setup` sai do array (arquivo não casado = não coletado) e as `dependencies` zeram (famílias em paralelo contra o build de produção, onde não há compile a preaquecer); dev intocado (provado: `--list` mostra `[setup]` só sem CI; smoke e2e dev rodou setup→campaignAuth). `ci-pr.yml`: o job `scope` emite `e2e_matrix`/`e2e_shards` via novo `e2eShardConfig(mode)` em `scripts/lib/test-affected-core.mjs` (fonte única, unit-pinada: full → `[1,2]`/2, selected/none → `[1]`/1 — pagar 2 builds por 3 specs é desperdício); `ci.yml` hardcodeia `[1,2]`/2 com comentário apontando o single-source. `PLAYWRIGHT_WORKERS: 2` explícito no env dos jobs e2e (aceite: explícito, default local intocado). Armadilha registrada nos workflows: flags do playwright vão **sem** o separador `--` — pnpm repassa o `--` literal ao playwright e a flag vira file pattern ("No tests found"). Specs intactas (o `setup` não foi removido — virou dev-only, já documentado como prewarm dev-only); sem migration/access/Consent/UI. **Medição pós-merge (main, run 31418206512):** job e2e 435 s → **326 s (shard 1) / 369 s (shard 2)** — shard 2 = fixo 68 s + build 176 s + testes 124 s (a fase de testes caiu de 196 s seriais para 124 s — metade da suite por runner + cadeia removida; o shard 2 ainda pagou retry da flake conhecida de `campaignMunicipalities`; shard 1: testes ~82 s); gate completo do pipeline (checks) ~7,5 → ~6,4 min. Build passou a dominar o shard — gatilho da intenção (opção B, artefato compartilhado) reavaliável quando os testes encolherem mais. Intenção [`ci-e2e-paralelizar-job.md`](plans/ci-e2e-paralelizar-job.md); impl [`ci-e2e-paralelizar-job-impl.md`](plans/ci-e2e-paralelizar-job-impl.md). **Recently resolved (2026-08-10):** **OPS22** (Issue #562) — CI de `main` vermelho determinístico desde o merge do B188 (05:54): os 3 e2e da agenda mobile (`campaignAgendaMobile` :96/:242/:295) falhavam com todas as retries → deploy bloqueado. **Root cause (reproduzida no prod build local + error-contexts com o drawer aberto):** o login e2e roda em **viewport desktop** → o settle de B167 abre o chat (`open=true`) → o **efeito persist do B188** (status `ready` já nasce) grava `{open:true}` no `sessionStorage` → o spec muda para 390px na **mesma aba** → o **restore do B188** reabre o **drawer mobile sozinho** → o modal Radix **aria-hide + inert** a página e cobre o viewport (combobox fora da a11y tree no :242, swipe cai no drawer no :96, clique do seletor bloqueado no :295; teclado/:173 passam por não usarem hit-testing). **Defeito real de produto** (aba desktop → mobile na mesma aba faz o chat "saltar"), não só timing de teste. **Fix na origem:** `SollinhaChatSession` ganhou **`openBy: 'user' | 'settle'`** (opcional — sessão legada sem o campo = `'settle'`, fail-closed; sem bump de versão) e o provider agora distingue **intenção do usuário** (`setOpen`/`toggle` do context value marcam `userToggledOpenRef`; FAB/header/drawer já passam por eles) de **verdade de layout** (settle B167 usa o setter cru): o **restore espera o `measured`** (viewport real) e honra `open:true` em mobile **só quando `openBy==='user'`**; o persist grava `openBy` conforme a intenção. Todos os contratos B188/resize mantidos (reload restaura conversa; drawer aberto por usuário volta aberto; visita mobile nova fechada; crossing ao vivo B167 intocado). Testes: unit (round-trip/invalidez/legado de `openBy`) + **e2e pin OPS22** no spec B188 (login desktop → poll do persist `open:true` → viewport 390 → `/campanha/agenda` → sem dialog + botão do Sollinha visível); spec da agenda intacto (4/4 no prod build). Sem migration/access/Consent/schema. Intenção [`ci-e2e-agenda-mobile-regressao-b188.md`](plans/ci-e2e-agenda-mobile-regressao-b188.md); impl [`ci-e2e-agenda-mobile-regressao-b188-impl.md`](plans/ci-e2e-agenda-mobile-regressao-b188-impl.md). diff --git a/docs/plans/ci-e2e-paralelizar-job-impl.md b/docs/plans/ci-e2e-paralelizar-job-impl.md index 3ba56daf..586885c4 100644 --- a/docs/plans/ci-e2e-paralelizar-job-impl.md +++ b/docs/plans/ci-e2e-paralelizar-job-impl.md @@ -64,7 +64,9 @@ build + artefato `.next/e2e` compartilhado · C) 3 shards já de início **Recomendação:** **A com 2 shards** — porque a medição real decide: por shard = 54 s (fixo) + 172 s (build) + ~98 s (196/2) ≈ **324 s ≈ 5,4 min**; e com a cadeia removida os testes encolhem mais (~120–150 s full → ~75 s por shard), -chegando a ~5 min. O caminho crítico do pipeline (max entre e2e/build/int) +chegando a ~5 min. **Medido pós-merge (run 31418206512): 326 s (shard 1) / +369 s (shard 2, com retry da flake conhecida) — dentro do aceite "≤ ~6 min" +e do gatilho de revisão de 6,5 min.** O caminho crítico do pipeline (max entre e2e/build/int) passa de 435 s para ~300–330 s. **Rejeitadas:** **B** porque os shards só podem começar depois do build terminar: wall = build 172 + upload + download + teste/2 ≫ A (a transferência @@ -93,8 +95,10 @@ por shard passar de ~6,5 min ou a distribuição de testes ficar desbalanceada. helper acima). - **`.github/workflows/ci.yml`**: job `e2e` vira matrix `shard: [1, 2]` (`fail-fast: false`; comentário apontando o single-source em - test-affected-core); step de teste vira `pnpm test:e2e -- --shard=${{ matrix.shard }}/2`; - env de job `PLAYWRIGHT_WORKERS: 2` explícito (aceite #4). + test-affected-core); step de teste vira `pnpm test:e2e --shard=${{ matrix.shard }}/2` + — **sem** o separador `--`: pnpm repassa o `--` literal ao playwright e a + flag viraria file pattern ("No tests found"); env de job + `PLAYWRIGHT_WORKERS: 2` explícito (aceite #4). - **`.github/workflows/ci-pr.yml`**: job `scope` emite `e2e_matrix` + `e2e_shards`; job `e2e` usa `strategy.matrix.shard: ${{ fromJson(...) }}` (full → `[1,2]`, selected → `[1]` — selected continua num runner só: pagar @@ -159,8 +163,9 @@ produto (campaign/frontend/admin) roda em main e PR full, intacta. ## Aceite de engenharia -- [x] Aceite de produto da intenção ainda coberto (mapa: wall ≤ ~6 min ✓, - suite completa em main/PR-full ✓, dev intocado ✓, workers explícitos ✓, +- [x] Aceite de produto da intenção ainda coberto (mapa: wall ≤ ~6 min ✓ — + medido 326/369 s pós-merge, ver linha de medição acima; suite completa + em main/PR-full ✓, dev intocado ✓, workers explícitos ✓, flakes: mesma carga por run) - [x] Invariantes AGENTS/engineering-standards (sem migration, sem access, sem UI; gate:fast + gate de CI no fechamento) diff --git a/playwright.config.ts b/playwright.config.ts index d99591d5..92227ab6 100644 --- a/playwright.config.ts +++ b/playwright.config.ts @@ -36,6 +36,9 @@ if (!Number.isInteger(workers) || workers < 2) { * the ~30-file campaign family. Prod mode therefore drops the setup project and * the dependencies so all families run in parallel; CI shards the suite across * runners instead (ci.yml / ci-pr.yml pass `--shard`). Dev mode is unchanged. + * Note the chain is an ordering guarantee for dev cold compiles, not a data + * dependency: specs are self-contained in both modes (fixtures + advisory + * locks), so dropping it in prod cannot reorder shared fixtures. */ const isProdMode = Boolean(process.env.CI) || process.env.E2E_PROD === '1' @@ -131,8 +134,10 @@ export default defineConfig({ reuseExistingServer: false, url: baseURL, // Local-only tolerance for parallel-worktree load (machine load ~60); CI - // boots the production build and keeps the usual 60s budget. - timeout: isProdMode ? undefined : 240_000, + // boots the production build and keeps the usual 60s budget. Keyed on CI + // alone (not `isProdMode`) on purpose: a local `E2E_PROD=1` run keeps the + // 240s budget — prod-mode here is a local mirror, not a real CI runner. + timeout: process.env.CI ? undefined : 240_000, /* Force the isolated test database into the server process. */ env: { DATABASE_URL: process.env.DATABASE_URL as string, diff --git a/scripts/ci-scope.mjs b/scripts/ci-scope.mjs index e3f126f6..967ad975 100644 --- a/scripts/ci-scope.mjs +++ b/scripts/ci-scope.mjs @@ -34,6 +34,9 @@ try { build: { mode: 'build', reason: full.reason }, test: { ...full }, e2e: { mode: 'full', specs: [], reason: full.reason, unmapped: [] }, + // The scope job always reads e2e_shards.matrix/total — keep the fallback + // total (ci-pr.yml's fromJson must never receive a null/empty matrix). + e2e_shards: e2eShardConfig('full'), }), ) process.exit(0) diff --git a/tests/unit/testAffected.unit.spec.ts b/tests/unit/testAffected.unit.spec.ts index 7996b3c6..04d41f1a 100644 --- a/tests/unit/testAffected.unit.spec.ts +++ b/tests/unit/testAffected.unit.spec.ts @@ -1,5 +1,9 @@ // @vitest-environment node +import { readFileSync } from 'node:fs' +import { join } from 'node:path' +import { fileURLToPath } from 'node:url' + import { describe, expect, it } from 'vitest' import { @@ -126,4 +130,11 @@ describe('e2eShardConfig (OPS34)', () => { expect(e2eShardConfig('selected')).toEqual({ matrix: [1], total: 1 }) expect(e2eShardConfig('none')).toEqual({ matrix: [1], total: 1 }) }) + + it('pins the ci.yml matrix literal against the single source', () => { + const repoRoot = join(fileURLToPath(new URL('.', import.meta.url)), '../..') + const workflow = readFileSync(join(repoRoot, '.github/workflows/ci.yml'), 'utf8') + const { matrix } = e2eShardConfig('full') + expect(workflow).toContain(`shard: [${matrix.join(', ')}]`) + }) })