# Testing audit — 2026-08-25 Primeira execução da skill `/testing-audit` (OPS96, Issue #898 · impl plan [`ops96-skill-testing-audit-impl.md`](../plans/ops96-skill-testing-audit-impl.md)). Auditoria + implementação das melhorias seguras na mesma noite; PR único ready, sem auto-merge armado. ## Sumário executivo - **16 specs movidos de `tests/int` para `tests/unit`** com conservação exata de totais (prova de movimento puro: soma das camadas constante em 3333 → 3344 testes, delta explicado pelos testes novos do próprio tooling). Nenhum teste foi removido. - **1 assertion reforçada** (`api.int.spec.ts`) e **1 referência morta corrigida** (`measure-e2e-family.mjs` apontava família e2e removida). - **Tooling novo**: `scripts/testing-audit-metrics.mjs` (+ core puro testado) e cobertura first-party sob flag (`@vitest/coverage-v8`). - **Flake documentado com evidência forte e registrado como [#906](https://github.com/fsolla/teqo/issues/906)**: família Google Calendar int falhou em 4 de 7 runs full tonight (sempre verde isolada). - **Nenhuma edição** em: ci-scope/e2e-manifest/workflows/husky (intocáveis estruturais), specs e2e, arquivos LGPD/RBAC/liderança (intocáveis absolutos), nem remoções autônomas — as propostas ficam na seção própria abaixo. ## Metodologia e tetos respeitados Fases da SKILL.md executadas na ordem: baseline (00:41) → inventário paralelo por 3 leitores read-only (~25min, unit/int/e2e) → rubrica com validação de cada achado contra o fonte (~20min) → loop de melhorias sequencial → fecho. Tetos: budget ~6h ✓ (uso real ≈ 3h30 até este relatório); ≤6 melhorias ✓ (5 aplicadas); ≤45min inventário ✓; ≤30min/passo ✓; debug ≤10min ✓ (sonda do flake revertida após diagnóstico). Provas: cada passo provou a suíte relevante verde antes/depois com contagens literais. A partir do run D, os runs int usaram `VITEST_MAX_WORKERS=4` — mesmo knob documentado no `vitest.config.mts` para contenção — porque outras sessões saturavam a máquina (load 16/16 núcleos). ## Retrato da suíte ### Baseline (00:41, SHA inicial da noite) | camada | arquivos | testes | parede | | ------ | -------: | -----: | -------------------------------------: | | unit | 259 | 2488 | 19,1s | | int | 96 | 845 | 53,6s | | e2e | 44 | 208 | não medido — execução noturna proibida | ### Final (SHA deste PR) | camada | arquivos | testes | parede | | ------ | ----------: | ------------: | ---------: | | unit | 276 (+17)\* | 2603 (+115)\* | 63,1s† | | int | 80 (−16) | 741 (−104) | ~55s‡ | | e2e | 44 (=) | 208 (=) | não medido | \* unit ganhou os 16 specs movidos + o spec novo do tooling (`testingAuditMetricsCore`, 10 testes). Delta de testes +115 = testes movidos (+104) + spec novo (8→10 após pins de fallback do simplify) + reforço (mesma contagem). † parede unit subiu com a carga externa da máquina e com specs pesados migrados (`eslintConventions` roda ESLint programático: 2,8s). ‡ int enxugou: os specs puristas saíram; o que resta é fronteira Payload/DB de verdade (pirâmide OPS90 mais honesta). Top-8 unit mais lentas no SHA final (tabela completa via `node scripts/testing-audit-metrics.mjs unit --top 15`): | arquivo | testes | duração | teste mais lento | | --------------------------------------------------- | -----: | ------: | --------------------------------------- | | tests/unit/activityOverlay.unit.spec.tsx | 14 | 7,6s | troca dia no calendário… (2,6s) | | tests/unit/campaignCellEditOverlay.unit.spec.ts | 19 | 3,1s | opens 'tendência' in a Drawer… (0,9s) | | tests/unit/petitionLead.unit.spec.tsx | 4 | 3,0s | fires exactly one Lead… (1,1s) | | tests/unit/municipalityRelationEditor.unit.spec.tsx | 9 | 2,9s | serializes same-cell mutations… (1,0s) | | tests/unit/eslintConventions.unit.spec.ts | 12 | 2,8s | enforces PascalCase… filenames (2,4s) | | tests/unit/campaignQuickActionsDrawer.unit.spec.tsx | 15 | 2,7s | renders when closed… (0,7s) | | tests/unit/campaignColumnPicker.unit.spec.ts | 8 | 2,4s | batches a whole editing session… (1,2s) | | tests/unit/codebaseConventions.unit.spec.ts | 25 | 1,9s | keeps conflict markers out… (1,4s) | Cobertura diagnóstica disponível sob flag (nunca regra): `pnpm vitest run --config ./vitest.unit.config.mts --coverage `. ## Disposições | id | camada | candidato | disposição | prioridade | prova/decisão | | --- | -------- | -------------------------------------------------------------------------------------------- | ------------------------- | ---------- | ------------------------------------------------------------------------------------------------ | | M1 | int→unit | relationship, schemaPrimitives, assertTestDatabase, petition-page-layout (specs 100% puros) | mover | alta | imports verificados zero Payload/DB; renames R100 | | M2 | int→unit | formData, supporterListFilters, siteMetadataFallback, eslintConventions (puros c/ mock/stub) | mover | alta | módulos-alvo sem DB (`supporterListFilters.ts`: só `import type Where`); verdes em unit | | M3 | int→unit | 8 specs de artefatos geográficos (readFileSync+createHash sobre `src/lib`) | mover | média | grupo homogêneo, zero Payload; pirâmide: snapshot de dados = small test | | R1 | int | api.int.spec "fetches users" → `toBeDefined()` | reforçar | baixa | shape `docs`/`totalDocs` assertado; aposentar total vira proposta A10 | | D1 | scripts | measure-e2e-family docblock citava `campaignPermissionProfile.e2e.spec.ts` (removida) | remover referência | baixa | arquivo confirmado inexistente; manifesto usa `campaignPermissionProfileHttp` (existe) — intacto | | K1 | unit | quick-action registry repete catálogos por role já pinados nos specs de domínio | proposta | baixa | seams distintos (registry vs catálogo) — não é duplicata comprovada | | K2 | int | googleCalendarSync sleep 3,5s + família flaky sob carga | proposta → **Issue #906** | alta | evidência de recidiva 4/7 runs nesta noite | Intocáveis varridos e intactos: consentimento/LGPD, access control/RBAC, lockdown de liderança/estimatedVotes — nenhum toque, nenhuma duplicata envolvendo-os virou remoção. ## Evidências verde antes/depois | passo | antes | depois | commit | | -------- | ------------------------------------------------------------------------------ | ----------------------------------------------------------------------------------------------- | -------- | | M1 | unit 264/2525 ✓ (01:43) · int 91–95 files ✓ exceto flake conhecido (01:47, w4) | unit 264/2525 ✓ (01:44) · int conservação −28/+28 ✓ | 4b54cbfb | | M2 | = anterior | unit 268/2553 ✓ (01:51) · **int 88/789 FULL GREEN** (01:52, w4) | c0bdb9fc | | M3 | = anterior | unit 276/2601 ✓ (01:54) · int 79–80/80 files, única falha = família flake (#906), conservação ✓ | dc1e342b | | R1+D1 | api.int isolado 1/1 ✓ (02:00) | idem | 36dde76c | | simplify | spec core 8/8 ✓ | spec core 10/10 ✓ (02:07) · lint/format ✓ | bd977cd2 | \* hash abreviado; ver `git log`. Conservação global provada em cada passo: unit+int = 3342 testes nos passos M1–M3 (delta final +2 = pins novos do simplify no spec do core). ## Passos revertidos / diagnóstico - **Sonda temporária** no teste do flake (console.log de `outcome.status` + store) — aplicada para diagnóstico, revertida imediatamente após leitura; nunca commitada. Diagnóstico: isolado o passe 3 cria normalmente (`created: 1`); a falha só existe sob carga → hipótese do orçamento por-hop/pausa registrada em #906. - Nenhuma melhoria precisou ser revertida (nenhum AFTER falhou por causa da edição). ## Flake da família Google Calendar → [#906](https://github.com/fsolla/teqo/issues/906) 7 runs full colhidos nesta noite: 4 falhas distintas dentro da mesma família (sempre verde isolada): reopen-test `created: 0` ×3 (workers default e 4), lock-test `NotFound` cleanup ×1, mirror-test `created: 3≠2` ×1. Hipótese: sync disparado pelo hook concorre com os passes manuais e/ou budget por-hop estoura sob contenção → `paused` fail-closed retorna all-zeros que os asserts leem como contagem errada. Fase 1 do fix: assertar `outcome.status === 'synced'` em toda a família. ## Propostas não executadas (com razão) | proposta | por quê não executou | gatilho/next step | | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ | | Mover `contactPhoneNormalization.int.spec` → unit | hooks exercitados via stub; precisa verificar semântica de `req` antes (risco médio) | verificação de 30min num expediente | | Mover `payloadTransaction.int.spec` → unit | stub puro, mas testa wrapper de transação — preferível provar contra Payload real ou decidir explicitamente | decidir dono no próximo toque no módulo | | Spec dono direto para `campaignSavedFilterStore` | hoje pinado só via 2 wrappers; gap real de cobertura | barato; lote com próximo item de saved filters | | Aposentar `api.int.spec.ts` inteiro | boots Payload completo para um smoke fraco; reforçado hoje, mas custo > valor | avaliar junto de mudança no harness de int | | Migrações e2e (demand-visibility, deliberation, bottom-nav, auth-cookie duplicam pins inferiores; pwa/pixel/admin são HTTP disfarçado; concepts/newsletter/contacts com redundâncias) | edição autônoma de e2e à noite é proibida; exigem trilho OPS87 (HTTP-sem-browser) e medição | cruzar com fila OPS87 e #882; nunca à noite | | Consolidar pins de paginação/q repetidos em 5 `*ListUrl` specs | aceitáveis hoje (barato e local) | deferido: mudança no contrato de `campaignListUrl` ou 6ª lista no padrão | | Extrair parsing de relatório JSON runner para `scripts/lib` | 2ª ocorrência (metrics CLI + measure-e2e-family) | deferido: 3ª ocorrência extrai (regra dos três) | Descartados pela triage (sem valor real): dedup registry×catálogo quick-actions (cobertura legítima em seams distintos); describe.each cosmético em worktree.unit; divergência de numeração de fases impl-vs-SKILL (histórico vs operacional); alternativa --outputFile no CLI (sem problema que a justifique). ## Estado final do PR **PR #907 — MERGIADO às 05:42:41Z pelo auto-merge (fsolla via AUTOMERGE_PAT), CONTRA o critério "sem auto-merge armado".** CI green no SHA final (`checks` ✓ 13m35s); mergeable ✓; conteúdo íntegro em `main`. **Errata do processo (falha real, registrada para as próximas noites):** a sequência do impl plan D5 — "aguardar o run do workflow de automerge do SHA final terminar e então desarmar" — chega exatamente no momento do merge: o GitHub mergea em segundos após o required check ficar verde, e o comando de desarme (05:43Z) falhou com "Can't disable auto-merge" porque o merge já estava em voo. A janela entre CI green e merge foi de segundos; nenhuma verificação humana aconteceu antes do merge, violando o aceite da intenção. **Correção já aplicada na SKILL.md**: desarmar imediatamente após cada push, com o CI ainda PENDENTE (o arm só executa merge com checks verdes — desarmar durante os ~13min de CI elimina a corrida). O humano deve julgar pela manhã se aceita o merged ou reverte (`git revert -m 1` contra o merge de #907 — nota: `main` já avançou com OPS97 logo depois, o que torna o revert mais cirúrgico). Falha também classificada como agent-miss em #909.