teqo-backup/docs/plans/ops46-infra-teste-e2e-local-allocator-impl.md
2026-08-23 14:02:19 -03:00

6.7 KiB

OPS46 — Infra de teste e2e: e2e local não coleta (next/cache) + allocator de município com wrap

Status: implementado Atualizado em: 2026-08-23 Issue: #733 Impl plan do plano de intenção: docs/plans/ops46-infra-teste-e2e-local-allocator.md

Fase 1 — e2e local não coleta (Cannot find module 'next/cache')

Causa raiz (medida)

  • next@15.4.11 não declara exports no package.json → ESM nativo do Node nunca resolve o subpath next/cache (o arquivo existe como cache.js, mas ESM não tenta extensões — ERR_MODULE_NOT_FOUND com "Did you mean to import next/cache.js?").
  • server-only (0.0.1) resolve index.js (que lança) sem a condição de export react-server (--conditions=react-server).
  • pnpm test:e2e carrega ambas as correções via NODE_OPTIONS="--no-deprecation --conditions=react-server --import=tsx/esm" — por isso o CI passa: o CI nunca roda playwright bare, sempre via script. Qualquer invocação bare (extensão VS Code, pnpm exec playwright test --list, runs ad-hoc) quebra na coleta.
  • CI vs máquina: sem delta — o CI usa o mesmo comando com os mesmos flags; a hipótese do plano ("investigar o que difere no ambiente CI") se resolveu em "nada difere — o CI só roda pelo script".

Abordagem implementada (aprovada no GATE)

Fix no nível do runner, zero mudança em código de produção:

  1. tests/helpers/e2eEsmResolve.mjs — hook de resolução ESM com 2 rewrites:
    • next/cache → next/cache.js (entry oficial, TS mapeia para cache.d.ts);
    • injeta react-server nas conditions de resolução (equivalente exato do flag --conditions=react-server, aplicado em runtime via nextResolve).
  2. tests/helpers/e2eEsmLoader.mjs — entry --import que registra a hook (o padrão do tsx: --import só funciona se o módulo chamar module.register).
  3. playwright.config.ts — quando NODE_OPTIONS não tem --conditions=react-server (invocação bare): register() a hook no processo main (a coleta do --list acontece aqui, pós-load do config) e propaga --import=<loader> via NODE_OPTIONS para os workers (herdam o env no spawn). Com os flags presentes (script), nada muda.

Opções rejeitadas

  • next/cache → next/cache.js na fonte (~40 arquivos): toca código de produção para um problema de infra de teste e não resolve server-only (o comando bare continuaria quebrando); não idiomático.
  • Patch do next via pnpm patchedDependencies adicionando exports: exports parcial quebra qualquer subpath não listado; frágil por versão.
  • --import/flags no script apenas + documentar: não satisfaz o aceite (pnpm exec playwright test --list coleta na máquina do humano).

Verificação Fase 1

  • pnpm exec playwright test --list (bare, sem flags): 198 testes em 41 arquivos (antes: Total: 0 tests in 0 files).
  • Run real bare --project=admin: 5 passed (workers com a hook).
  • Run real bare --project=campaign campaignHomeActions: 18 passed.
  • pnpm test:e2e --list (script, flags): inalterado, 198.

Fase 2 — allocator de município com wrap destrutivo

Medição (plano: "medir primeiro")

  • Claims por ciclo completo num DB persistente: ~505 (432 getMunicipality() no int + 73 claimMunicipality() no e2e) > 435 do catálogo → o wrap acontece em todo ciclo de suíte completa. Bug agudo (não teórico): após o wrap, dois runs concorrentes podem receber o mesmo índice e o purge-on-claim de cada um apaga as linhas vivas do outro.

Abordagem implementada

Registry de claims em módulo compartilhado vitest-free tests/helpers/campaignMunicipalityAllocator.ts (mesma convenção do campaignResidue.ts — importável pelo processo Playwright do e2e):

  • Tabela campaign_fixture_municipality_claims (index PK, run_id text, claimed_at timestamptz default now()) criada lazy com a sequence (CREATE IF NOT EXISTS, corrida 23505 tolerada).
  • claimMunicipalityIndex(payload, catalogSize, runID): nextval → índice → INSERT ON CONFLICT ("index") DO NOTHING RETURNING; conflito = run vivo segura o slot → pula; claim stale (TTL 2h, run crashado) → rouba e retenta; tudo ocupado por claims vivos → erro claro ("exhausted") em vez de colisão silenciosa.
  • releaseMunicipalityClaims(payload, runID): DELETE WHERE run_id — chamado no cleanup() de ambos os fixtures (int CampaignFixtures.cleanup, e2e CampaignE2EOwnership.cleanup), só quando o fixture claimou (Set claimedMunicipalityIndexes — cleanup sem claims não toca a tabela).
  • O fixture int perdeu o allocator inline (nextAllocatedMunicipalityIndex
    • a sequence própria); o e2e idem (CREATE SEQUENCE + nextval inline).

Guards e testes

  • Guard estático novo no testMunicipalityAllocatorConventions.unit.spec.ts: proíbe nextval(/campaign_fixture_municipality_alloc em tests/int, tests/e2e e tests/helpers fora do módulo do allocator — todo claim passa pelo registry.
  • Spec int comportamental tests/int/campaignMunicipalityAllocator.int.spec.ts (catalogSize sintético, robusto a concorrência da suíte paralela):
    1. exclusividade viva através de múltiplos wraps (âncora segura 1 slot por 120 claims de runs efêmeros > 3 wraps do espaço de 40);
    2. exaustão (erro claro) + recuperação após release;
    3. steal de claim stale (envelhecida 3h);
    4. release libera slots para reuso.

Verificação Fase 2

  • Spec do allocator: 4/4 verdes, estável em re-runs.
  • Suíte int completa: 92 arquivos / 819 testes, todos verdes (o fixture refatorado claima ~432 municípios sob 8 workers paralelos).
  • Tabela de claims zerada ao final da suíte (release funcionando).
  • e2e campaign (campaignHomeActions, wizard com claims reais): 18/18, claims zeradas após o run.

Riscos e notas

  • A hook depende de module.register (estável desde Node 20.6); só ativa em invocações bare — o path do script (pnpm test:e2e) fica intocado e é o fallback se algo mudar.
  • TTL 2h > duração de qualquer run vivo; claim crashado bloqueia slots até o TTL (erro de exaustão menciona o prazo).
  • Um run vivo não pode claimar mais de catalogSize slots (a própria exclusividade exaure) — correto por design; fixtures reais liberam por teste.
  • Durante a entrega, o --list bare regenerou o importMap.js sem envs S3_* (classe OPS69/OPS70 #87, já documentada) — restaurado com generate:importmap + envs dummy.

Aceite

  • Fase 1: pnpm exec playwright test --list coleta na máquina do humano
  • Fase 2: sem colisão de claim após N wraps (stress test com wraps reais)
  • Guards: testMunicipalityAllocatorConventions verde após a mudança