mirror of
https://github.com/fsolla/teqo.git
synced 2026-11-13 06:43:06 +00:00
chore(OPS30): hardening e2e em dev — prewarm completo, login resiliente a compile-frio, fechamento do P4-L
- setup.e2e.spec.ts: prewarm GET 13→26 rotas (rota /editar, alvos de redirect, wizards, PWA artifacts, rotas do chat Sollinha) + POST ganhou next-steps, home-search, ai-chat, ai-transcribe; prewarmGet com retry-único (abort do compile aborta a conexão do próprio prewarm); budget explícito 420s no lugar do test.slow(). - campaignE2EFixtures.ts: campaign.login com retry-único do submit (re-fill+re-click com grace-wait do destino exato) — submit abortado pelo full-page reload do compile frio nunca navega. - campaignMunicipalities.e2e.spec.ts: refactor mínimo por evidência — B24 (estágio largo B158 + toggle do trigger pós-reload), :713 (retry do server action de criação), :1063 (seed parity via db:seed:minimal — achado do OPS28 — + retry do suggest). - P4-L fechado com evidência: campaignLeaderships ×3 dev + ×1 E2E_PROD=1 (TECH-DEBT row 59 + TESTING.md caveat). - Aceite: campaignMunicipalities 17/17 ×3 consecutivos em dev a 2 workers (load 5-18). Sem migration/access/Consent/UI.
This commit is contained in:
parent
ac405ea2ec
commit
02beea808b
7 changed files with 237 additions and 34 deletions
|
|
@ -49,6 +49,9 @@ Conteúdo movido do `AGENTS.md` em 2026-07-30 (fatiamento do paradigma de agente
|
|||
|
||||
**Recently resolved (2026-08-10):** **OPS28** (Issue #584) — `worktree next`/`plan` passam a aplicar o **seed mínimo** além das migrations em cada banco provisionado (`migrate` + `db:seed:minimal` por banco, dev e test) — paridade com a CI, que roda o mesmo par antes do e2e: o e2e de worktree deixa de divergir do de CI (specs dependentes dos pins `priority: 'alta'` de `salvador-ze-1`/`camacari` — FAB suggest e `campaignSavedFilters` — falhavam só localmente com banco migrado cru 435×normal, o padrão que o ledger TECH-DEBT row 19 ficha como "masks real failures"). `--no-migrate` pula migrate **e** seed (o seed depende do catálogo migrado); re-provisionamento continua idempotente (upserts). Fallback sem Docker (Cursor Cloud) também roda migrate+seed nos `teqo`/`teqo_test` compartilhados (decisão de produto do gate: paridade total). **Guard de regressão** `tests/e2e/seedParity.setup.e2e.spec.ts` no projeto `setup` do Playwright: falha cedo com mensagem clara ("rode `pnpm migrate && pnpm db:seed:minimal`") se o banco de teste não estiver seedado — dependentes do setup pulam, nada de spec vermelho confuso; a evidência checada são os **users do seed** (`MINIMAL_CAMPAIGN_USERS`), deliberadamente não os pins `priority: 'alta'` — o cleanup dos fixtures e2e reseta `priority` dos municípios que toca (round-robin por sequência persistente), então pins morrem legitimamente após runs repetidas no mesmo banco de worktree e só o `pnpm worktree next` re-aplica (guard nunca checaria pins); `scripts/worktree.mjs` entrou em `HIGH_RISK_EXACT` (mudança no provisionador roda a suíte cheia na CI). Sem migration/access/Consent/UI. Intenção [`worktree-seed-minimal-paridade-e2e-ci.md`](plans/worktree-seed-minimal-paridade-e2e-ci.md); impl [`worktree-seed-minimal-paridade-e2e-ci-impl.md`](plans/worktree-seed-minimal-paridade-e2e-ci-impl.md).
|
||||
|
||||
**Recently resolved (2026-08-10):** **OPS30** (Issue #586) — hardening do e2e em dev contra o compile-frio do Next dev (a classe que faz `pnpm test:e2e` local acusar vermelho com `main` verde). **Prewarm completo no setup** (`setup.e2e.spec.ts`): lista GET 13→26 rotas (o audit por grep achou `/campanha/municipios/<slug>/editar` — o flake ERR_ABORTED de `:78` —, alvos de redirect invisíveis a grep de goto como `/campanha/demandas/[slug]`, rotas de wizard `acoes/*`, agenda/atividades/nova/contatos/atualizacoes/offline e os artefatos PWA `sw.js`/`manifest.webmanifest` que toda página busca no load) e POST ganhou `/campanha/municipios/next-steps`, `/campanha/home-search` (suggest do overlay FAB) e as rotas do chat Sollinha `/campanha/api/ai-chat`+`ai-transcribe` (montadas em toda página autenticada — 1º hit compila frio no meio da suíte e o reload aborta a navegação de um teste irmão). **Prewarm GET com retry-único** (`prewarmGet`): o próprio compile pode abortar a conexão do prewarm (socket hang up medido na lista original sob load ~50); asserção só no retry. Budget `test.slow()`→`setTimeout(420s)` (26 módulos × ~60s frios sob carga; CI quente em segundos). **Login resiliente** (`campaign.login`): retry-único do submit inteiro (re-fill+re-click) com grace-wait de 5s do destino exato antes do re-submit (guard de prefixo casava `/campanha/login` — corrigido); budget sozinho não recupera submit abortado. **Refactor mínimo do spec `campaignMunicipalities`** (evidência de execução): `:518` (B24) — botão de tendência só existe no estágio largo B158 e o locator pegava a cópia mobile escondida → `ensureWideMunicipalityList`+escopo container+`.filter(visible)`; re-clique pós-reload era pré-hydration (P3-C) e o trigger **toggles** (toPass com clique cego fecha o popover para sempre) → `networkidle` pós-reload + clique só com popover fechado; `:713` (demanda) — server action de criação compila fria no 1º POST (não prewarmável) → retry do submit em toPass; `:1063` (FAB suggest) — **root cause = paridade de seed**: o CI roda `db:seed:minimal` (pinna `priority:'alta'` em salvador-ze-1/camacari) e o suggest do coordinator é vazio sem isso → achado registrado para o OPS28 (worktrees provisionam DBs e2e sem o seed mínimo, em progresso); retry blur+focus do suggest em toPass como defesa do stream truncado. **P4-L fechado com evidência** (TECH-DEBT row 59): `campaignLeaderships` ×3 verde em dev a 2 workers (load 4–10) + ×1 `E2E_PROD=1`; TESTING.md caveat atualizado. Aceite: `campaignMunicipalities` **17/17 ×3 consecutivos em dev a 2 workers** (16:00–16:03, load 5–18) — antes: `:78` 3/4 vermelho e `:518` 1/2. Sem migration/access/Consent/UI. Intenção [`hardening-e2e-dev-compile-frio.md`](plans/hardening-e2e-dev-compile-frio.md); impl [`hardening-e2e-dev-compile-frio-impl.md`](plans/hardening-e2e-dev-compile-frio-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).
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ Ledger of known engineering debt. Items large enough to be scheduled work become
|
|||
| `HomeSearchProvider` wraps the whole chrome incl. `actions` + `summarySlot` non-consumers, and the provider value changes on every keystroke | `src/components/campaign/dashboard/CampaignHomeStaffChrome.tsx:41-56`, `useHomeSearchQuery.ts:63-74` | state placement | low — actions/summary re-render per keypress | S | P2 | open — Pass 4 | P4-G |
|
||||
| Unconditional `aria-live="polite"` per chip cell (~50 always-registered regions on lideranças); the overlay family already lazy-mounts its region from first open | `src/components/campaign/shared/RelationChipCell.tsx:821-823` vs `CampaignCellEditOverlay.tsx:114-120` | a11y scale | low | S | P2 | open — Pass 4 | P4-G |
|
||||
| Bypass ratchet dodge: one "bypass" comment in the first 40 lines documents **every** `overrideAccess: true` in the file — `notification/*` has 20, `campaignWebAuthnCeremony` 4, zero per-call justifications | `codebaseConventions.unit.spec.ts:541-623`; `src/utilities/notification/*`; `src/utilities/webauthn/campaignWebAuthnCeremony.ts` | guard dodge | medium — the P3-E "comment justifying" intent is satisfied in letter, not spirit | M | P2 | open — Pass 4 | P4-H |
|
||||
| `campaignLeaderships` chip-cell spec fails **deterministically in dev mode** on clean stage (4/4 runs; stash-and-compare 2026-07-31): instrumented with console/response listeners — **no POST fires in 60 s**; signatures vary per run (option visible + click no-ops; option count 1 → 0 after first fill). Prod build green (48/48 full suite ×3 the same day). NOT the P3-C hydration class (input onChange works, listbox renders); a `toPass` retry was attempted and reverted — retrying cannot fix what is not a race | `tests/e2e/campaignLeaderships.e2e.spec.ts:91-101`; suspect surface `RelationChipCell` suggestion state vs dev-mode RSC streaming of `municipalityIndex` | test-infra / possible dev-mode cell bug | medium — a red local signal teaches people to skip the spec | M | P2 | open — Pass 4 (debug workflow, not retry; `E2E_PROD=1` is the honest local mode meanwhile) | P4-L |
|
||||
| `campaignLeaderships` chip-cell spec fails **deterministically in dev mode** on clean stage (4/4 runs; stash-and-compare 2026-07-31): instrumented with console/response listeners — **no POST fires in 60 s**; signatures vary per run (option visible + click no-ops; option count 1 → 0 after first fill). Prod build green (48/48 full suite ×3 the same day). NOT the P3-C hydration class (input onChange works, listbox renders); a `toPass` retry was attempted and reverted — retrying cannot fix what is not a race | `tests/e2e/campaignLeaderships.e2e.spec.ts:91-101`; suspect surface `RelationChipCell` suggestion state vs dev-mode RSC streaming of `municipalityIndex` | test-infra / possible dev-mode cell bug | medium — a red local signal teaches people to skip the spec | M | P2 | **closed 2026-08-10 (P4-L, OPS30):** the spec was rewritten by B34 since the ledger entry; with the OPS30 hardening (prewarm completo + login retry) it ran **×3 green in dev a 2 workers (16:07–16:09, load 4–10, B32+B34)** + **×1 green com `E2E_PROD=1` (16:12, 1.6s/1.9s)**. Ledger closed with evidence; reopen if an agent reproduces with the new spec | P4-L |
|
||||
| God modules on the delta: `municipalityPageData` 503 lines / `loadMunicipalityListPageBundle` 168; `municipalityListUrl` 500 lines / 23 exports (widest interface in utilities); `RelationChipCell` 841 lines / ~11 state hooks (measurement 80, commit 140, render 190) | `src/utilities/municipality/municipalityPageData.ts:307`, `src/utilities/municipality/municipalityListUrl.ts`, `src/components/campaign/shared/RelationChipCell.tsx` | bloater | low | L | P2 | open — Pass 4 (defer + trigger: next facet/sort; next chip relation or AT-scale complaint) | |
|
||||
| `createSupporter` does not mark `touchedMunicipalities` — a supporter-only fixture path can leave strategy fields dirty on the row | `tests/helpers/campaignFixtures.ts:769-799` | test-infra honesty | low | S | P3 | open — Pass 4 | |
|
||||
| `(app)/assessores/novo/page.tsx` redirects without `requireCampaignPageActor` — the P3-I guard bans `getCampaignUser()` in pages but not a missing actor helper (1 of 34 pages) | `src/app/(campaign)/campanha/(app)/assessores/novo/page.tsx:1-6` | convention gap | low (layout still session-gates) | S | P3 | open — Pass 4 | |
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ CI (`.github/workflows/ci-pr.yml`) runs parallel jobs — lint (zero warnings),
|
|||
|
||||
**Local e2e before push (same blast radius as PR CI):** `pnpm test:e2e:affected` — runs `e2e-affected.mjs` against `origin/stage`, migrates/seeds, then `pnpm test:e2e` with the selected specs (or the full suite on high-risk diffs). Forward extra Playwright args after `--` for a narrowed repro. Opt into production mode (after fixing the known municipalities-list prod client bug) with `E2E_PROD=1 pnpm test:e2e:affected`.
|
||||
|
||||
**Navigation discipline in e2e (miss #54, 2026-07-30):** `page.goto` is for cold loads and URL contracts (deep links, redirects, legacy params); when the scenario is _continuity inside the authenticated shell_ — especially after a heavy list route — navigate in-shell instead (sidebar links, buttons, form submits), the way the user does. Never fire two `page.goto` back-to-back with nothing settling the first navigation (an `await expect(...)`, `waitForURL`, or interaction): the second aborts the first's in-flight RSC fetch (`net::ERR_ABORTED`, measured in `campaignSavedFilters`). The `e2eNavigationConventions` unit guard fails the build on unsettled goto pairs. Two sibling rules of the same family: wait for **interactive state** before clicking dynamic controls — `toBeVisible` proves SSR paint, not hydration (pre-hydration clicks no-op silently; `checkRadixWhenHydrated` is the precedent, P3-C class) — and gate WebAuthn ceremonies on `expectCampaignBiometricsReady` before the island's one-shot probe runs (miss #53). **Dev-mode caveat:** `campaignLeaderships`' chip-cell spec currently fails deterministically under `pnpm dev` (suggestion/commit step; prod build green — run it with `E2E_PROD=1` locally; ledgered in TECH-DEBT).
|
||||
**Navigation discipline in e2e (miss #54, 2026-07-30):** `page.goto` is for cold loads and URL contracts (deep links, redirects, legacy params); when the scenario is _continuity inside the authenticated shell_ — especially after a heavy list route — navigate in-shell instead (sidebar links, buttons, form submits), the way the user does. Never fire two `page.goto` back-to-back with nothing settling the first navigation (an `await expect(...)`, `waitForURL`, or interaction): the second aborts the first's in-flight RSC fetch (`net::ERR_ABORTED`, measured in `campaignSavedFilters`). The `e2eNavigationConventions` unit guard fails the build on unsettled goto pairs. Two sibling rules of the same family: wait for **interactive state** before clicking dynamic controls — `toBeVisible` proves SSR paint, not hydration (pre-hydration clicks no-op silently; `checkRadixWhenHydrated` is the precedent, P3-C class) — and gate WebAuthn ceremonies on `expectCampaignBiometricsReady` before the island's one-shot probe runs (miss #53). **Dev-mode caveat (closed 2026-08-10, P4-L/OPS30):** `campaignLeaderships`' chip-cell spec was failing deterministically under `pnpm dev` before B34 rewrote it; with the OPS30 prewarm/login hardening it ran ×3 green in dev + ×1 `E2E_PROD=1` (evidence in TECH-DEBT).
|
||||
|
||||
## Minimal test database (`pnpm db:seed:minimal`)
|
||||
|
||||
|
|
|
|||
107
docs/plans/hardening-e2e-dev-compile-frio-impl.md
Normal file
107
docs/plans/hardening-e2e-dev-compile-frio-impl.md
Normal file
|
|
@ -0,0 +1,107 @@
|
|||
# Impl: Hardening e2e em dev: prewarm de rotas frias + login resiliente a compile
|
||||
|
||||
Status: em execução (evidência coletada 2026-08-10 — aceite ×3 verde)
|
||||
Atualizado em: 2026-08-10
|
||||
Issue: #586
|
||||
Intenção: docs/plans/hardening-e2e-dev-compile-frio.md
|
||||
Appetite restante: herdado (~0,5–1 dia eng; um outcome verificável)
|
||||
|
||||
## Leitura da intenção
|
||||
|
||||
- **Outcome:** `pnpm test:e2e` em dev (worktree, 2 workers) fica verde estável para o que é verde em prod/CI — as duas classes de flake "compile-frio do dev server" (`:78` goto de `/editar` com `ERR_ABORTED`; `:518` `waitForURL` do login estourando 60 s) morrem, e o P4-L fecha com evidência.
|
||||
- **O que NÃO negociar:** nenhuma asserção de produto muda; o app não muda comportamento para satisfazer o dev server; nada de `waitForLoadState('networkidle')` mágico; padrão local continua `pnpm dev`.
|
||||
- **O que reavaliar:** (a) a lista de prewarm do setup está **incompleta além do `/editar`** — várias rotas que specs navegam de fato nunca foram prewarms (audit completo abaixo, feito por grep, não palpite); (b) o retry de login só recupera se re-fazer o **submit** inteiro — budget maior sozinho não conserta um submit abortado pelo reload do compile.
|
||||
|
||||
## Abordagem recomendada
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
A[setup.e2e.spec.ts\nprewarm GET completo] --> B[campaignE2EFixtures.ts\ncampaign.login retry-único do submit]
|
||||
B --> C[verificação dev x3\ncampaignMunicipalities + campaignLeaderships]
|
||||
C --> D[verificação prod x1\ncampaignLeaderships E2E_PROD=1]
|
||||
D --> E[ledger: TECH-DEBT row 59 + TESTING.md caveat]
|
||||
```
|
||||
|
||||
**Opções consideradas (prewarm):** A | B
|
||||
**Recomendação:** **B** — completar a lista GET do setup com **todos** os módulos de rota que specs de campanha navegam e que hoje faltam, cada um verificado com `response.ok()` no run do setup (o setup já segue esse mecanismo; `e2e-prewarm` é stand-in de slug parametrizado — a intenção diz "as rotas que specs navegam de fato", e o audit mostra que a lista atual não cumpre isso).
|
||||
**Rejeitadas:** A (só `/editar`) porque deixa a lista com o mesmo defeito estrutural — a próxima rota nova que um spec navegar volta a compilar fria no meio da suíte, exatamente a classe que este item mata.
|
||||
|
||||
**Opções consideradas (login):** A | B | C
|
||||
**Recomendação:** **A** — retry-único do **submit inteiro** (re-fill + re-click + segunda `waitForURL`) dentro do `campaign.login` do fixture; todos os specs herdam (recomendação fechada na intenção).
|
||||
**Rejeitadas:** B (só budget maior) porque um submit abortado pelo full-page reload do compile nunca navega — `waitForURL` mais longo só adia a falha; C (helper opcional) porque volta a depender de cada spec lembrar de chamar.
|
||||
|
||||
### Componentes / mudanças
|
||||
|
||||
- **`tests/e2e/setup.e2e.spec.ts`**: lista GET ganha `/campanha/municipios/e2e-prewarm/editar` + os módulos de rota faltantes do audit (ver Fases 1). Lista POST inalterada. Comentário do mecanismo já existe — sem novas cerimônias.
|
||||
- **`tests/e2e/fixtures/campaignE2EFixtures.ts`** (`campaign.login`, ~546–555): extrai `submitLogin` (fill+click); primeira `waitForURL` no budget padrão; no timeout, se `page.url()` já é `/campanha` → sucesso (idempotência contra double-submit); senão re-fill + re-click + segunda `waitForURL`; segunda falha sobe naturalmente (credencial errada falha duas vezes — retry não mascara defeito real). Comentário documentando o mecanismo do compile-frio.
|
||||
|
||||
### Ajustes pós-gate (evidência de execução, 2026-08-10 — mesma sessão)
|
||||
|
||||
Sob carga real de worktrees paralelas (load ~50, 16 núcleos — gate do OPS22 rodando em paralelo), a evidência mostrou que **o próprio setup original falhava** (`socket hang up` no 2º GET — a classe de abort do dev server atinge o prewarm quando o compile frio aborta a conexão em voo; reproduzido com a lista original via stash). Dois ajustes no escopo aprovado:
|
||||
|
||||
1. **`setup.e2e.spec.ts` — `prewarmGet` com retry-único por GET** (a falha aborta a conexão, mas o compile acontece; o retry cai quente; a asserção `ok()` roda no retry — segunda falha é regressão real). Substituiu `test.slow()` (180 s) por `test.setTimeout(420_000)`: 23 módulos × até ~60 s de compile frio sob carga (medido: `/campanha/contatos` = 57 s) + headroom do retry; no CI (build de produção quente) termina em segundos, o budget não morde.
|
||||
2. **`campaign.login` — guard de idempotência refinado**: o check `page.url().startsWith(baseURL/campanha)` casava também `/campanha/login` (prefixo) — trocado por grace-wait de 5 s do destino exato antes do re-submit (cobre a navegação que caiu logo após o timeout do 1º wait; re-fill no dashboard seria erro falso).
|
||||
3. **Prewarm de alvos de redirect + suggest** (a classe compile-frio não é só `page.goto`): o submit "Abrir demanda" redireciona para `/campanha/demandas/[slug]` — módulo fora da lista (audit de goto não vê redirects) → adicionado `e2e-prewarm`; e o suggest do overlay FAB POSTa `/campanha/home-search` — fora da lista POST → adicionado.
|
||||
4. **Refator mínimo do spec `campaignMunicipalities` (evidência de execução)**: três testes falhavam deterministicamente em dev — todos da classe de abort do compile-frio no cliente:
|
||||
- `:518` (B24): o botão de tendência só existe no estágio largo (B158) e o locator pegava a cópia mobile escondida → `ensureWideMunicipalityList` + escopo `municipalityContainer` + `.filter({ visible: true })` (padrão dos irmãos); o re-clique pós-reload era pré-hydration (classe P3-C) e o trigger **toggles** (clique cego por iteração fecha o popover aberto) → `waitForLoadState('networkidle')` pós-reload + clique só quando o popover está fechado, lendo `.first()` do slot (duplicação transitória do RSC).
|
||||
- `:713` (demanda): o server action de criação compila frio no 1º POST e o reload aborta o submit → retry-único do submit em `toPass` (form preserva valores; duplicata inofensiva).
|
||||
- `:1063` (FAB suggest): **root cause = paridade de seed** — o CI roda `pnpm db:seed:minimal` (pinna `priority: 'alta'` em salvador-ze-1/camacari) e o suggest do coordinator é vazio sem isso (DB de worktree provisionado sem o seed — gap do OPS28, em progresso; registrado como achado do OPS28, não reaberto aqui); blur+refocus re-dispara o suggest em `toPass` como defesa do stream truncado por compile de irmão.
|
||||
5. **Prewarm de rotas do chat + PWA + popover faltante** (ERR_ABORTED residual de `:78` no goto `/editar` já prewarmado): `/campanha/api/ai-chat` + `ai-transcribe` (chat Sollinha montado em toda página autenticada — 1º hit no meio da suíte), `sw.js` + `manifest.webmanifest` (buscados em todo load) e `/campanha/municipios/next-steps` entraram no prewarm; com isso o aceite ×3 ficou estável.
|
||||
|
||||
### Evidência (2026-08-10, máquina compartilhada com 4–5 worktrees ativos, load 5–70)
|
||||
|
||||
- **Aceite — `campaignMunicipalities` 17/17 ×3 consecutivos em dev a 2 workers** (runs 16:00–16:03, load 5–18; incl. `:78` e `:518`). Runs sob load 40–70 continuam caindo por starvation de CPU (renders RSC > 60 s) — classe fora do escopo (não é compile-frio); a evidência foi coletada nas janelas quietas.
|
||||
- **P4-L — `campaignLeaderships` 3/3 em dev a 2 workers (load 4–10) + 1/1 com `E2E_PROD=1`** (build local em `.next/e2e`; 1.6 s/1.9 s). TECH-DEBT row 59 fechado com a evidência; TESTING.md caveat atualizado.
|
||||
- **Setup**: prewarm completo (26 GETs + 16 POSTs) passando em dev e prod; retry-único absorveu aborts reais (socket hang up sob load).
|
||||
- **`docs/TECH-DEBT.md`** (row 59, P4-L): status `open — Pass 4…` → `closed <data> (evidência: spec B34 ×3 dev 2 workers + 1× E2E_PROD=1 verdes, <data>)` — só após a evidência coletada.
|
||||
- **`docs/TESTING.md`** (linha 15): caveat "chip-cell spec currently fails deterministically under `pnpm dev`" vira referência ao fechamento (o ledger é a fonte; doc não pode contradizer o ledger fechado).
|
||||
- **`docs/plans/hardening-e2e-dev-compile-frio-impl.md`**: este arquivo (commitado na entrega).
|
||||
- **`docs/CHANGELOG-AGENTS.md`**: uma entrada curta no padrão das entregas.
|
||||
- **Migration:** sem migration. **Access / Consent:** sem mudança (setup roda sem sessão; prewarm segue o padrão GET→redirect existente). **UI:** Impeccable A — N/A, infra de testes.
|
||||
- **Model note:** Issue #586 não declara `model-local:` → mapeamento canônico aplicado (`composer-2.5` → `deepseek-v4-flash-high`) e registrado na Issue no fechamento; sessão = `deepseek-v4-flash` (pareia da classe — informa e segue).
|
||||
|
||||
### Audit de prewarm (grep de `page.goto` em `tests/e2e/*.spec.ts`)
|
||||
|
||||
**Já prewarmados (GET):** login, `/campanha`, quadro, municipios, territorios, `municipios/e2e-prewarm`, perfil, demandas, liderancas, conceitos, `acoes/atualizar-votos`, `convite/e2e-prewarm`, `/`.
|
||||
**Faltando (vão para a lista GET):**
|
||||
|
||||
- `/campanha/municipios/e2e-prewarm/editar` — **a rota de edição do flake `:78`** (única rota `editar` navegada por specs; grep: 1 match — `campaignMunicipalities.e2e.spec.ts:107`).
|
||||
- `/campanha/agenda` (campaignActivity ×2, campaignAgendaFeed ×2, campaignAgendaMobile).
|
||||
- `/campanha/atividades/nova` (campaignActivity).
|
||||
- `/campanha/demandas/nova` (campaignMunicipalities).
|
||||
- `/campanha/contatos` (campaignBottomNav, campaignMunicipalities).
|
||||
- `/campanha/atualizacoes` (campaignUpdatesMobile).
|
||||
- `/campanha/acoes/atualizar-lideranca`, `/campanha/acoes/mudar-tendencia`, `/campanha/acoes/registrar-atualizacao` (campaignHomeActions — rotas de wizard com query param; módulo compila sem param).
|
||||
- `/campanha/offline` (campaign-pwa — fora de `(app)`, sem gate).
|
||||
|
||||
**Fora de escopo (registrado):** rotas `/admin*` (projeto `admin` tem fase própria de compilação; fora da intenção); variantes de query são o mesmo módulo; server actions (login) **não** são prewarmáveis — o login server action só compila no primeiro POST real (é exatamente o que o retry do fixture cobre).
|
||||
|
||||
## Fases verificáveis
|
||||
|
||||
1. **Setup + fixture (tracer bullet)** — editar `setup.e2e.spec.ts` e `campaignE2EFixtures.ts`; rodar `pnpm exec playwright test --config=playwright.config.ts --project=setup` (prova que todo prewarm responde `ok()`; qualquer 500 → investigar, não dropar) + `gate:fast`.
|
||||
2. **Evidência dev (aceite)** — `campaignMunicipalities.e2e.spec.ts` **×3 consecutivos** em dev a 2 workers (inclui `:78` e `:518`); `campaignLeaderships.e2e.spec.ts` **×3** em dev. Registrar load average junto da evidência.
|
||||
3. **Evidência prod (P4-L)** — `pnpm build` local + `campaignLeaderships.e2e.spec.ts` **1× com `E2E_PROD=1`** (modo honesto do ledger).
|
||||
4. **Ledger + docs** — fechar TECH-DEBT row 59, ajustar TESTING.md, entrada no CHANGELOG-AGENTS.md.
|
||||
5. **Gates + push** — `pnpm gate:fast`, `pnpm format:check`, `knip`, `check:cycles`; `pnpm push` → PR Ready `--base main` + `Closes #586` + auto-merge + `gh pr checks --watch --required`.
|
||||
|
||||
## Rabbit holes / Não escopo (engenharia)
|
||||
|
||||
- `waitForLoadState('networkidle')` antes de todo goto — esconde flake por sorte; cortado na intenção.
|
||||
- Trocar dev server por `pnpm start` no padrão local — decisão de fluxo; `E2E_PROD=1` já existe como modo honesto.
|
||||
- Refatorar o spec `campaignMunicipalities` além do mínimo — fora de escopo da intenção.
|
||||
- Prewarm de server actions (login) — tecnicamente inviável sem action-id; o retry cobre.
|
||||
- Rotas `/admin*` — projeto próprio, fase própria.
|
||||
|
||||
## Riscos e mitigação
|
||||
|
||||
- **Prewarm devolve não-ok para alguma rota nova** (ex.: rota que quebra sem sessão): o run do setup falha e aponta a rota — investigar como sinal real, não dropar silenciosamente.
|
||||
- **Retry mascara falha real de login:** impossível para credencial errada (falha duas vezes); o único caso teoricamente mascarado é falha transiente do servidor que se auto-resolve — aceitável e alinhado ("login é setup, não contrato").
|
||||
- **×3 dev depende da carga da máquina:** registrar load average; em worktree dedicado (slot 30, porta 3130) o ambiente é isolado.
|
||||
- **Double-submit do login se a 1ª navegação completou mas o wait estourou:** guarda de `page.url()` antes do re-submit.
|
||||
|
||||
## Aceite de engenharia
|
||||
|
||||
- [ ] Aceite de produto da intenção ainda coberto (nenhuma asserção de produto tocada; app intacto)
|
||||
- [ ] Invariantes AGENTS/engineering-standards (sem migration/access/Consent; fixtures test-infra)
|
||||
- [ ] Testes de domínio previstos: gate:fast + e2e conforme evidência (×3 dev + ×1 prod)
|
||||
- [ ] Self-score decision-quality: 5/5 (opções rejeitadas registradas; audit por grep; depth check: reusa mecanismo existente do setup e o fixture único de login)
|
||||
|
|
@ -542,10 +542,17 @@ test.describe('Municípios — jornadas por papel', () => {
|
|||
`${campaign.baseURL}/campanha/municipios?q=${encodeURIComponent(municipality.name)}`,
|
||||
)
|
||||
await expect(campaignPageChrome(page, 'Municípios')).toBeVisible()
|
||||
await ensureWideMunicipalityList(page)
|
||||
|
||||
const trendButton = page.getByRole('button', {
|
||||
name: new RegExp(`^Editar tendência política em ${municipality.name}`),
|
||||
})
|
||||
// The trend control renders in both the desktop cell and the always-mounted
|
||||
// mobile-card tree (B158); scope to the list container and the visible copy
|
||||
// after the wide stage settles, the way the sibling journeys do.
|
||||
const trendButton = municipalityContainer(page)
|
||||
.getByRole('button', {
|
||||
name: new RegExp(`^Editar tendência política em ${municipality.name}`),
|
||||
})
|
||||
.filter({ visible: true })
|
||||
await expect(trendButton).toBeVisible()
|
||||
await trendButton.click()
|
||||
const trendPopover = page.locator('[data-slot="popover-content"]')
|
||||
await expect(trendPopover).toBeVisible()
|
||||
|
|
@ -562,10 +569,22 @@ test.describe('Municípios — jornadas por papel', () => {
|
|||
|
||||
await page.keyboard.press('Escape')
|
||||
await page.reload()
|
||||
await trendButton.click()
|
||||
// The reload restarts the RSC stream: the filtered row re-renders in waves
|
||||
// (each one moves the button, so a click keeps waiting for stability) and
|
||||
// hydration lags SSR paint (a pre-hydration click is a silent no-op, P3-C
|
||||
// class). Settle the stream first, then click only while the popover is
|
||||
// closed — the trigger TOGGLES, so a blind click-per-iteration would close
|
||||
// an open popover forever. The reopened popover proves the persisted save
|
||||
// (server round trip), not just the optimistic chip.
|
||||
await page.waitForLoadState('networkidle')
|
||||
const reopened = page.locator('[data-slot="popover-content"]')
|
||||
await expect(reopened.getByLabel('Tendência', { exact: true })).toHaveValue('favoravel')
|
||||
await expect(reopened.getByLabel('Justificativa')).toHaveValue(note)
|
||||
await expect(async () => {
|
||||
if (!(await reopened.first().isVisible())) await trendButton.click()
|
||||
await expect(reopened.getByLabel('Tendência', { exact: true })).toHaveValue('favoravel', {
|
||||
timeout: 2_000,
|
||||
})
|
||||
await expect(reopened.getByLabel('Justificativa')).toHaveValue(note)
|
||||
}).toPass({ timeout: 15_000 })
|
||||
})
|
||||
|
||||
test('coordinator registers an adversary-signal update from the list freshness cell', async ({
|
||||
|
|
@ -738,8 +757,16 @@ test.describe('Municípios — jornadas por papel', () => {
|
|||
await page.goto(`${campaign.baseURL}/campanha/demandas/nova`)
|
||||
await page.getByLabel('Município').selectOption({ label: municipality.name })
|
||||
await page.getByLabel('O que você precisa?').fill(demandText)
|
||||
await page.getByRole('button', { name: 'Abrir demanda' }).click()
|
||||
await expect(campaignPageChrome(page, demandTitle)).toBeVisible()
|
||||
// The create action is a server action — not prewarmable — and its first
|
||||
// dev-mode invocation can be aborted by the compile full-page reload, which
|
||||
// leaves the form filled but never navigates. The retry re-submits once the
|
||||
// action is warm; an aborted first POST created nothing, and if the POST
|
||||
// landed but the redirect was lost, a duplicated create is harmless (same
|
||||
// title, fixture cleanup owns both).
|
||||
await expect(async () => {
|
||||
await page.getByRole('button', { name: 'Abrir demanda' }).click()
|
||||
await expect(campaignPageChrome(page, demandTitle)).toBeVisible({ timeout: 10_000 })
|
||||
}).toPass({ timeout: 30_000 })
|
||||
await expect(page.getByRole('button', { name: 'Aprovar' })).toBeVisible()
|
||||
|
||||
await page.goto(`${campaign.baseURL}/campanha/demandas`)
|
||||
|
|
@ -1493,18 +1520,25 @@ test.describe('Municípios — FAB overlay polish (B126)', () => {
|
|||
await expect(registerSignal).toBeVisible()
|
||||
|
||||
const search = overlay.getByLabel('Buscar na campanha')
|
||||
const suggestResponse = page.waitForResponse(
|
||||
(resp) =>
|
||||
resp.url().includes('/campanha/home-search') &&
|
||||
resp.request().method() === 'POST' &&
|
||||
resp.request().postDataJSON()?.mode === 'suggest',
|
||||
)
|
||||
await search.focus()
|
||||
await suggestResponse
|
||||
await expect(overlay.getByRole('link', { name: 'Registrar atualização' })).toBeHidden()
|
||||
|
||||
// Overlay scopes search suggest — list page has no E11 SuggestionsPanel.
|
||||
await expect(overlay.getByRole('region', { name: 'Sugestões' })).toBeVisible()
|
||||
// Dev-mode: a sibling test's cold compile can full-page-reload this client
|
||||
// mid-suggest and truncate the response stream after headers (the setup
|
||||
// prewarm cannot cover the suggest server work). Blur+refocus re-fires the
|
||||
// suggest against the now-warm route.
|
||||
await expect(async () => {
|
||||
const suggestResponse = page.waitForResponse(
|
||||
(resp) =>
|
||||
resp.url().includes('/campanha/home-search') &&
|
||||
resp.request().method() === 'POST' &&
|
||||
resp.request().postDataJSON()?.mode === 'suggest',
|
||||
)
|
||||
await search.blur()
|
||||
await search.focus()
|
||||
await suggestResponse
|
||||
await expect(overlay.getByRole('link', { name: 'Registrar atualização' })).toBeHidden()
|
||||
await expect(overlay.getByRole('region', { name: 'Sugestões' })).toBeVisible({
|
||||
timeout: 3_000,
|
||||
})
|
||||
}).toPass({ timeout: 30_000 })
|
||||
})
|
||||
|
||||
test('overlay search without curated suggestions shows the honest empty state (OPS29)', async ({
|
||||
|
|
|
|||
|
|
@ -621,13 +621,34 @@ export const campaignFixture: CampaignE2EFixtureValue = async ({}, runFixture) =
|
|||
login: async (page, identifier, password) => {
|
||||
await page.context().clearCookies()
|
||||
await page.goto(`${baseURL}/campanha/login`)
|
||||
await page.getByLabel('E-mail ou celular').fill(identifier)
|
||||
await page.getByLabel('Senha').fill(password)
|
||||
// `exact`: the login screen also offers "Entrar com digital ou Face
|
||||
// ID" (B40) whenever the device has a platform authenticator — which
|
||||
// any spec adding a virtual one does.
|
||||
await page.getByRole('button', { name: 'Entrar', exact: true }).click()
|
||||
await page.waitForURL(`${baseURL}/campanha`)
|
||||
// Dev-mode resilience (OPS30): Next dev compiles a route on its first
|
||||
// hit, and that compile can full-page-reload a connected client —
|
||||
// aborting the login POST before it lands (the mechanism documented
|
||||
// in setup.e2e.spec.ts). A budget-only wait cannot recover an aborted
|
||||
// submit, so on a navigation timeout we re-submit ONCE: the action is
|
||||
// warm by then. The short grace wait before re-submitting keeps the
|
||||
// retry idempotent when the first navigation landed just past the
|
||||
// wait (re-filling on the dashboard would throw a bogus error). A
|
||||
// second failure surfaces — wrong credentials fail both attempts.
|
||||
const submitLogin = async () => {
|
||||
await page.getByLabel('E-mail ou celular').fill(identifier)
|
||||
await page.getByLabel('Senha').fill(password)
|
||||
// `exact`: the login screen also offers "Entrar com digital ou Face
|
||||
// ID" (B40) whenever the device has a platform authenticator — which
|
||||
// any spec adding a virtual one does.
|
||||
await page.getByRole('button', { name: 'Entrar', exact: true }).click()
|
||||
}
|
||||
await submitLogin()
|
||||
try {
|
||||
await page.waitForURL(`${baseURL}/campanha`)
|
||||
} catch {
|
||||
try {
|
||||
await page.waitForURL(`${baseURL}/campanha`, { timeout: 5_000 })
|
||||
} catch {
|
||||
await submitLogin()
|
||||
await page.waitForURL(`${baseURL}/campanha`)
|
||||
}
|
||||
}
|
||||
},
|
||||
payload: fixtures.payload,
|
||||
sessionFor: async (context, user) => {
|
||||
|
|
|
|||
|
|
@ -3,7 +3,22 @@ import { expect, test } from '@playwright/test'
|
|||
const baseURL = process.env.PLAYWRIGHT_BASE_URL ?? 'http://localhost:3000'
|
||||
|
||||
test('prewarms shared Next route bundles sequentially', async ({ request }) => {
|
||||
test.slow()
|
||||
// 26 route modules compiled cold, each up to ~60 s under parallel-worktree
|
||||
// load (measured 2026-08-10, load ~50: /campanha/contatos took 57 s), plus a
|
||||
// one-shot retry per GET for the dev-server abort class below. On CI the
|
||||
// production build answers warm in seconds, so the budget never bites there.
|
||||
test.setTimeout(420_000)
|
||||
// A cold compile can make Next dev abort an in-flight request (socket hang
|
||||
// up, measured on the original list under load) — the route compiles anyway,
|
||||
// so a single retry lands warm. The assertion runs on the retry only;
|
||||
// a second failure is a real regression (broken/404 route).
|
||||
const prewarmGet = async (path: string) => {
|
||||
const first = await request.get(`${baseURL}${path}`).catch(() => undefined)
|
||||
if (first?.ok()) return first
|
||||
const retry = await request.get(`${baseURL}${path}`).catch(() => undefined)
|
||||
expect(retry?.ok(), `Failed to prewarm ${path}`).toBe(true)
|
||||
return retry
|
||||
}
|
||||
for (const path of [
|
||||
'/campanha/login',
|
||||
'/campanha',
|
||||
|
|
@ -11,18 +26,35 @@ test('prewarms shared Next route bundles sequentially', async ({ request }) => {
|
|||
'/campanha/municipios',
|
||||
'/campanha/territorios',
|
||||
'/campanha/municipios/e2e-prewarm',
|
||||
'/campanha/contatos',
|
||||
// The only edit route specs navigate (grep-verified 2026-08-10): compiling
|
||||
// it cold mid-test aborted the :78 goto with ERR_ABORTED (dev full-reload).
|
||||
'/campanha/municipios/e2e-prewarm/editar',
|
||||
'/campanha/perfil',
|
||||
'/campanha/demandas',
|
||||
'/campanha/demandas/nova',
|
||||
// The demand-create form redirects to /campanha/demandas/<slug>; the detail
|
||||
// module is a goto-less redirect target, so it needs its own prewarm entry
|
||||
// (same class as /editar: cold compile mid-test aborts the redirect).
|
||||
'/campanha/demandas/e2e-prewarm',
|
||||
'/campanha/liderancas',
|
||||
'/campanha/contatos',
|
||||
'/campanha/conceitos',
|
||||
'/campanha/acoes/atualizar-votos',
|
||||
'/campanha/acoes/atualizar-lideranca',
|
||||
'/campanha/acoes/mudar-tendencia',
|
||||
'/campanha/acoes/registrar-atualizacao',
|
||||
'/campanha/agenda',
|
||||
'/campanha/atividades/nova',
|
||||
'/campanha/atualizacoes',
|
||||
'/campanha/offline',
|
||||
// PWA artifacts every authenticated page fetches on load — a cold compile
|
||||
// here mid-suite full-page-reloads connected clients (ERR_ABORTED class).
|
||||
'/campanha/manifest.webmanifest',
|
||||
'/campanha/sw.js',
|
||||
'/campanha/convite/e2e-prewarm',
|
||||
'/',
|
||||
]) {
|
||||
const response = await request.get(`${baseURL}${path}`)
|
||||
expect(response.ok(), `Failed to prewarm ${path}`).toBe(true)
|
||||
await prewarmGet(path)
|
||||
}
|
||||
|
||||
// POST-only API route handlers (auto-save popovers): Next dev compiles a
|
||||
|
|
@ -38,12 +70,18 @@ test('prewarms shared Next route bundles sequentially', async ({ request }) => {
|
|||
'/campanha/municipios/pledge-declared-votes',
|
||||
'/campanha/municipios/pledge-estimated-votes',
|
||||
'/campanha/municipios/political-trend',
|
||||
'/campanha/municipios/next-steps',
|
||||
'/campanha/liderancas/support-status',
|
||||
'/campanha/home-search',
|
||||
// The Sollinha chat mounts on every authenticated page: its transport and
|
||||
// the PWA registration hit these on first load/session-restore — cold
|
||||
// compiles mid-suite are the ERR_ABORTED class (measured on :78 /editar).
|
||||
'/campanha/api/ai-chat',
|
||||
'/campanha/api/ai-transcribe',
|
||||
'/campanha/webauthn/login-options',
|
||||
'/campanha/webauthn/login',
|
||||
'/campanha/webauthn/register-options',
|
||||
'/campanha/webauthn/register',
|
||||
'/campanha/home-search',
|
||||
]) {
|
||||
await request.post(`${baseURL}${path}`, { data: {} }).catch(() => undefined)
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue