teqo-backup/docs/plans/hardening-e2e-dev-compile-frio-impl.md
Francisco Solla 48088d8162 refactor(OPS30): fixes do /simplify — pessoas no prewarm, login com sub-budgets, retry da demanda via existência, B24 no helper do arquivo
Review 3× read-only (0 P1):
- setup: /campanha/pessoas faltava (campaignPeople); log do 1º hit
  quando o retry salva; comentário do budget ancorado no medido.
- login: sub-budgets 15+5+30 (antes 30+5+30 estourava o test timeout
  e credencial errada morria de timeout genérico); retry bifurca pela
  URL real (dashboard/fora do login/re-submit) — re-submit cego
  contra página que saiu do login falhava com fill na dashboard.
- :713: retry cego criaria duplicata que o UNIQUE do slug refuta —
  o retry consulta por título e navega ao detail quando o 1º POST
  criou mas o redirect se perdeu.
- B24: locator reusa visibleMunicipalityButton (escapeRegExp +
  fronteira de nome, 23/435 com prefixo) e ensureWideMunicipalityList
  re-rodado pós-reload (padrão do irmão dobradinhas).
- :1063: waitForResponse com timeout 5s para o toPass decidir.

Re-prova: campaignMunicipalities 22/22 (arquivo atual, B193+OPS29 do
main) ×3 consecutivos em dev a 2 workers (load 5-17).
2026-08-12 13:34:42 -03:00

14 KiB
Raw Permalink Blame History

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

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) + os ajustes pós-gate (POSTs novos, PWA/chat, /campanha/pessoas). prewarmGet com retry-único e budget explícito (ver ajustes pós-gate).
  • 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): 27 módulos, total medido ~3–6 min de compile frio sob carga (medido: /campanha/contatos = 57 s a load ~50) + 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 22/22 (arquivo atual, incl. B193 ×4 + OPS29) ×3 consecutivos em dev a 2 workers (runs 16:28–16:39, load 5–17; incl. :78 e :518; as 3 runs verdes anteriores com 17 testes foram absorvidas pelo rebase que trouxe B193/OPS29 do main — re-prova com o arquivo atual). 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 (27 GETs + 16 POSTs) passando em dev e prod; retry-único absorveu aborts reais (socket hang up sob load).

Ajustes do /simplify (3 revisores read-only, 2026-08-10 — 0 P1, P2/P3 aplicados)

  • /campanha/pessoas (campaignPeople) entrou no prewarm GET — o audit por grep de goto tinha passado batido (audit refeito por grep completo).
  • prewarmGet loga o status do 1º hit quando o retry salva (rastro do masking residual).
  • Login: sub-budgets explícitos somando <60 s (15+5+30) — antes o 2º wait nunca completava dentro do test timeout e credencial errada morria de "Test timeout" genérico; retry agora bifurca pela URL real (dashboard → return; fora do login → throw com a URL; login → re-submit) — o re-submit cego contra página que saiu do login falhava com fill na dashboard.
  • B24: locator reusa visibleMunicipalityButton (escapeRegExp + fronteira — nomes com prefixo compartilhado, 23/435) e ensureWideMunicipalityList re-rodado após o reload (padrão do irmão dobradinhas).
  • :713: o retry cego do submit criaria duplicata que o UNIQUE do campaignDemand.slug refuta — o retry agora consulta por título e navega direto ao detail quando o 1º POST já criou (redirect perdido).
  • :1063: waitForResponse com timeout curto (5 s) — sem ele o budget do toPass queimava no wait e o erro apontava a classe errada.
  • 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)