16 KiB
Testing audit — 2026-08-25 (run 2)
Segunda execução da skill /testing-audit na mesma data da primeira noite (2026-08-25.md, PR #907). Como o arquivo <YYYY-MM-DD>.md já existia commitado com o relatório da run 1, esta noite usa o sufixo -run2 para preservar os dois relatórios — única divergência do template (anotada aqui por transparência).
Sumário executivo
- 4 movimentos int→unit aplicados (29 testes) com conservação exata: unit+int = 3361 testes em todos os passos. Nenhum teste removido, nenhum enfraquecido.
payloadTransaction(10) — stub puro do wrapper de transação, zero Payload/DB.contactPhoneNormalization(4) — hook do Contact exercitado via stub.municipalityUpdateCreateSchema(2) — pins puros de zod extraídos do spec int que boota Payload.campaignAuthIdentity(13) — helpers de identidade (telefone/schema/whatsapp/guarda de migração) extraídos docampaignAuth.int.spec; RBAC do arquivo intacto.
- 1 fix no tooling first-party:
parseVitestStdoutno core do metrics — suite vermelha agora imprime o retrato (contrato do próprio script), tolerando o sufixoELIFECYCLEdo pnpm. +5 testes unit no spec do core. - Recidiva do flake #906 com nova assinatura: após 7 runs full verdes no início da noite, a família Google Calendar passou a falhar de forma persistente sob a mesma carga; com
VITEST_MAX_WORKERS=1o fracasso colapsa em UM teste determinístico (googleCalendarSyncAction"no-op…not-configured") causado por ~38 linhas de resíduo emgoogle_calendar_syncdeixadas pelo spec C114 num único run full. Reset completo do DB de teste (db:reset, OPS88) não resolveu — a família recria o resíduo dentro do próprio run. Evidência em #906 via relatório (sem escrita no tracker). - Nenhuma edição em: intocáveis absolutos (LGPD/RBAC/lockdown de liderança), intocáveis estruturais (ci-scope, e2e-manifest, workflows, husky), specs e2e, e nenhuma remoção autônoma de teste.
- 0 passos revertidos.
Metodologia e tetos respeitados
Fases: baseline (13:43) → inventário paralelo (3 leitores Task read-only, ~25min) → rubrica com validação de cada achado contra o fonte (~25min) → loop de melhorias sequencial (5 passos) → fecho. Tetos: budget ~6h ✓ (uso real ≈ 2h); ≤6 melhorias ✓ (5); ≤45min inventário ✓; ≤30min/passo ✓ (passo mais longo: ~8min); debug de AFTER ≤10min ✓ (nenhum AFTER falhou — as falhas posteriores são do flake conhecido, não das edições).
Retrato da suíte
Baseline (13:43–13:44, SHA 8199e2ee)
| camada | arquivos | testes | parede |
|---|---|---|---|
| unit | 277 | 2620 | 30,9s |
| int | 80 | 741 | 51,4s |
| e2e | 44 | 208 | não medido |
Final (SHA deste PR, 008b1ee7)
| camada | arquivos | testes | parede |
|---|---|---|---|
| unit | 281 (+4) | 2654 (+34)* | 40,8s |
| int | 78 (−2) | 712 (−29) | ~184s† |
| e2e | 44 (=) | 208 (=) | não medido |
* +29 movidos +5 novos do parseVitestStdout. Conservação global nos passos de movimento: unit+int = 3361 constante.
† parede int na medição vermelha (flake); a parede verde do mesmo SHA foi 51,4s.
Top-8 unit mais lentas no SHA final (tabela completa: node scripts/testing-audit-metrics.mjs unit --top 15):
| arquivo | testes | duração | teste mais lento |
|---|---|---|---|
| tests/unit/activityOverlay.unit.spec.tsx | 14 | 4,7s | troca dia no calendário e hora/minuto nos selects inline… (2,0s) |
| tests/unit/petitionLead.unit.spec.tsx | 4 | 2,6s | fires exactly one Lead… (1,2s) |
| tests/unit/municipalityRelationEditor.unit.spec.tsx | 9 | 2,4s | serializes same-cell mutations… (0,7s) |
| tests/unit/campaignCellEditOverlay.unit.spec.ts | 19 | 2,0s | opens 'tendência' in a Drawer… (0,3s) |
| tests/unit/eslintConventions.unit.spec.ts | 12 | 2,0s | enforces PascalCase… (1,7s) |
| tests/unit/campaignQuickActionsDrawer.unit.spec.tsx | 15 | 1,8s | posts home-search… (0,4s) |
| tests/unit/campaignColumnPicker.unit.spec.ts | 8 | 1,4s | batches a whole editing session… (0,4s) |
| tests/unit/codebaseConventions.unit.spec.ts | 25 | 1,3s | keeps conflict markers out… (0,9s) |
Cobertura diagnóstica disponível sob flag (nunca regra): pnpm vitest run --config ./vitest.unit.config.mts --coverage <filtro>.
Disposições
| id | camada | candidato | disposição | prioridade | prova/decisão |
|---|---|---|---|---|---|
| M1 | int→unit | payloadTransaction.int.spec (10 testes, stub de Payload['db'], zero DB) |
mover ✓ | alta | tests/int/payloadTransaction.int.spec.ts:23-43 stubs; conservação +10/−10 |
| M2 | int→unit | contactPhoneNormalization.int.spec (hook do Contact via stub, importOriginal) |
mover ✓ | alta | tests/int/contactPhoneNormalization.int.spec.ts:24-35; conservação +4/−4 |
| M3 | int→unit | 2 pins zod de municipalityUpdateCreateSchema dentro de spec que boota Payload | mover ✓ | alta | tests/int/campaignMunicipalityUpdate.int.spec.ts:71-140 puros; Local API (:142+) permanece int |
| M4 | int→unit | helpers puros de identidade no campaignAuth.int.spec (telefone/schema/whatsapp/migração) | mover ✓ | alta | blocos :441-452 e :551-603 sem payload; RBAC :454-549 intocado; conservação +13/−13 |
| F1 | tooling | metrics CLI morria em suite vermelha (sufixo ELIFECYCLE do pnpm quebrava o JSON.parse) | reforçar ✓ | média | contrato do próprio script ("a red suite still prints its portrait"); parseVitestStdout no core + 5 tests |
| P1 | unit | catálogos quick-action repinados (registry × domínio × v2) | proposta | baixa | U1/U3: seams distintos como na run 1 (K1) — duplicata não comprovada |
| P2 | unit | scan de conflict-markers pinado em codebaseConventions + conflictMarkers.unit + CI | proposta | baixa | U2: o describe do codebaseConventions roda git ls-files inteiro a cada run |
| P3 | unit | 6 specs do agent-pool sobre scripts dormentes (OPS65) | proposta | baixa | U6: decisão de produto (remover ou suíte "dormant" separada) |
| P4 | int | origin policy pinada 2× (campaignInvite.int :393-550 × campaignInviteOrigin.unit) | proposta | média | I6: casos únicos em cada lado — merge exige toque em arquivo com conteúdo consent (369-391 intocável) |
| P5 | int | campaignInvite.int :298-607 blocos puros (token/whatsapp/schema) | proposta | média | I5: split parcial em arquivo com consent e RBAC — risco > valor à noite |
| P6 | int | cleanup de fixtures pinado 2× (campaignFixtures × campaignFixturesRowCounts) | proposta | baixa | I7: variante row-count estritamente mais forte; remoção exige prova forte |
| P7 | unit | campaignLoginAction asserta mock contra mock (objectContaining({ login: mocks.login })) |
manter | baixa | U4: pin fraco, inofensivo; os pins reais (mapeamento/TTL) têm valor; remover asserção não é reforçar |
| P8 | unit | campaignAuthMemoization reimplementa react.cache num Map caseiro |
manter | baixa | U5: asserção de identidade de promise (:69) é o sinal real; limitação documentada |
| P9 | unit | eslintConventions/codebaseConventions/campaignCompositionCleanup pesados (ESLint real, scans full-tree) | manter | baixa | U7/U8/U10: locks de convenção com valor alto; otimização é proposta, não melhoria noturna |
| E1–E12 | e2e | inventário: duplicatas com int (C113 agenda feed, C130 party facet, TTL×3, C143, C88), http-disfarçados, setup.e2e prewarm morto | proposta | variada | leitor e2e; e2e nunca editado/executado à noite — trilho OPS87/medição para as duplicatas HTTP |
Intocáveis varridos e intactos: consentimento/LGPD (hash do consent no campaignInvite mantido no lugar), RBAC (campaignAuth :454-549, lockdown de liderança, I9 triple-pin mantido), intocáveis estruturais sem diff.
Evidências verde antes/depois
| passo | antes | depois | commit |
|---|---|---|---|
| M1 | 13:54:34 unit 277/2620 ✓ · int 80/741 ✓ | 13:56:04 unit 278/2630 ✓ · int 79/731 ✓ (conservação 3361) | 7de9822f |
| M2 | = anterior | 13:57:42 unit 279/2634 ✓ · int 78/727 ✓ (conservação 3361) | adcfb39a |
| M3 | = anterior | 13:59:40 unit 280/2636 ✓ · int 78/725 ✓ (conservação 3361) | d8866d90 |
| M4 | = anterior | 14:01:47 unit 281/2649 ✓ · int 78/712 ✓ (conservação 3361) | 62a7dab8 |
| F1 | spec core isolado 10/10 ✓ | 14:09:29 spec core 15/15 ✓ · unit full 281/2654 ✓ | 008b1ee7 |
Provas executadas bare (nunca piped). A partir de 14:05 os runs full de int passaram a falhar APENAS na família #906 (ver abaixo) — as provas AFTER de M1–M4 são anteriores ao surto e verdes.
Flake #906 — recidiva com nova assinatura (evidência desta noite)
- 7 runs full verdes (13:43–14:03) → 8 runs seguintes com falhas na família Google Calendar: 25 falhas com workers default (14:05, 14:07, 14:12), 25 com
VITEST_MAX_WORKERS=4(14:10), 1 falha comVITEST_MAX_WORKERS=1(14:14, 14:18, 14:22, 14:36). - A família isolada segue verde (14:13:45
googleCalendarSync25/25 em 7,8s) — mesma assinatura "verde isolada, vermelho full" da noite anterior. - Nova evidência causal:
google_calendar_syncacumulou 38 linhas de resíduo após um único run full; o testegoogleCalendarSyncAction.int.spec.ts:121-146("no-op…not-configured") apaga só 1 doc stale (limit: 1no :124-131) e o action então lê outro doc residual →expected 'synced' to be 'not-configured'(:145). A hipótese: o afterChange do engine recria docs de config depois do cleanup do spec (corrida hook × passes), e o resíduo faz a falha persistir entre runs. db:resetcompleto (OPS88, 14:12) NÃO curou — a família recria o resíduo dentro do próprio run.- Conclusão: recidiva do #906 com evidência de resíduo nova; os fixes pertencem ao trilho da Issue (Fase 1 planejada: assertar
outcome.status === 'synced'na família; a esta evidência soma-se: cleanup do no-op deve apagar TODOS os docs stale, e a família precisa de tear-down resiliente ao hook). Nada editado nesta família à noite.
Propostas não executadas (com razão)
| proposta | por quê não executou |
|---|---|
| P1/P2/P3 (unit: catálogos re-pinados, scan de markers, agent-pool dormente) | decisão de produto/consolidação exige humano; scans de convenção têm valor de lock |
| P4/P5 (int: origin policy 2×, blocos puros do campaignInvite) | split parcial em arquivo com consent/RBAC — risco > valor no turno noturno |
| P6 (int: cleanup de fixtures 2×) | remoção exige prova forte; variante row-count é mais forte, mas duplicata parcial |
| E1–E12 (e2e) | e2e é inventariado, nunca executado/editado à noite; trilho OPS87 + medição para as HTTP |
| Otimizações de latência (U7/U8/U10: cache de scans, tier de conventions) | refactor de specs de convenção sem ganho de cobertura — proposta, não melhoria segura |
Descartados pela triage (sem valor real): P7/P8/P9 (manter — ver disposições); dedup de sugestões wizard×home-search (I8, seams distintos); triple-pin de lockdown (I9, intocável).
Estado final do PR
PR #919 — branch agent/testing-audit-2026-08-25-run2, base main. CI checks green no SHA final (3337ef03, success ~15:39Z); mergeStateStatus: CLEAN, mergeable.
Automerge: desarmado e verificado (exit 0) às 15:25:10. Nas re-verificações seguintes, a rede à API do GitHub instabilizou (timeouts intermitentes no GraphQL) e o fallback fail-closed da skill converteu o PR para draft (~15:42Z, autoMergeRequest: null mantido) — veto estrutural do safety-net. Estado final protegido: DRAFT + CI green + mergeable. Passo humano pela manhã: gh pr ready 919 (o safety-net re-arma pelo fluxo normal) e decisão humana de merge, conforme o contrato "sem auto-merge armado" desta skill.
Logística do fecho (transparência):
- HTTPS a
github.comficou inacessível da máquina (~14:50–15:17, conexão ao 443 timeout;api.github.comok) — o push foi feito por SSH (ssh://git@github.com/fsolla/teqo.git), 15:17:23. - Após 3 gates
gate:pushconsecutivos barrados APENAS pelo flake int (#906, assinaturas rotativas), o push usou o escape documentadogit push --no-verify(o mesmo gate já havia passado verde 2× neste SHA; o CI do PR é o gate real — e ficou green).
Fase 5 — desarme do auto-merge
Protocolo determinístico desta skill: criação ready → desarme → verificação autoMergeRequest === null && isDraft === false num ÚNICO comando (scripts/testing-audit-disarm.mjs), repetido imediatamente após cada push, nunca "aguardar o CI" para desarmar. Execução desta noite:
- 15:17:33 — criação+desarme em um comando: PR #919 criado Ready, mas a verificação e a conversão para draft falharam por rede (
fetch failedintermitente, mesma janela do bloqueio HTTPS); exit 1 com aviso de intervenção. - 15:18:39 — retry imediato
--pr 919 --draft-on-failure: status verificado e impresso —{"number":919,"isDraft":false,"mergeable":"MERGEABLE","mergeStateStatus":"BLOCKED","autoMergeRequest":null}— desarmado e Ready, exit 0. - 15:22 — push do relatório final (SHA
3337ef03,synchronizerearma) → re-desarme imediato verificado exit 0 (15:22:22); re-verificação às 15:25:10 exit 0 (o workflowPR Ready + auto-mergedo novo SHA já havia concluídosuccess). - 15:41–15:44 — re-verificações do fecho sob rede instável: leituras GraphQL falharam 4×; o
--draft-on-failureconverteu a PR para draft (fail-closed) mantendoautoMergeRequest: null— fallback §4 da Fase 5 aplicado: humano vira ready pela manhã (gh pr ready 919).