feat(ops96): protocolo completo na SKILL.md + ignores do artefato coverage

This commit is contained in:
Francisco Solla 2026-08-25 01:00:34 -03:00
parent c13174b8aa
commit 54fd059d9f
4 changed files with 135 additions and 24 deletions

View file

@ -5,35 +5,144 @@ description: Auditoria autônoma noturna da suíte de testes do Teqo — retrato
# Skill: testing-audit # Skill: testing-audit
Auditoria autônoma da suíte de testes com implementação das melhorias seguras. Disparo humano à noite (`/testing-audit`); pela manhã existe UM relatório que explica toda a noite e um PR ready revisável em pedaços lógicos. Auditoria autônoma da suíte de testes com implementação das melhorias seguras. Disparo humano à noite (`/testing-audit`) e a máquina trabalha sozinha; pela manhã existe UM relatório que explica toda a noite — cada ação tomada e o porquê — e um diff revisável em pedaços lógicos.
## Contrato de entrega (inviolável) ## Contrato de entrega (inviolável)
- **Um único PR ready** (não draft) contra `main`, **CI green**, **mergeable** (sem conflitos; se houver conflito com `main`, NÃO rebasar depois do desarme — registrar no topo do relatório que rebasar é passo humano). - **Um único PR ready** (não draft) contra `main`, **CI green**, **mergeable** (sem conflitos). Conflito com `main` ao amanhecer → NÃO rebasar depois do desarme (rearmaria o automerge): registrar no topo do relatório que rebasar é passo humano.
- **SEM auto-merge armado.** Esta skill NÃO herda o contrato auto-merge de `engineering-audit`. O safety-net do repo (`agent-pr-ready-automerge.yml`) arma automerge em TODO PR ready contra main e REARMA a cada push — o desarme é sempre o ÚLTIMO passo da noite (ver Fase 5). - **SEM auto-merge armado.** Esta skill NÃO herda o contrato "PR Ready + auto-merge" de `engineering-audit` — aqui o humano decide o merge pela manhã. O safety-net do repo (`agent-pr-ready-automerge.yml`) arma automerge em TODO PR ready contra main e REARMA a cada push (`synchronize`); por isso o desarme é SEMPRE o último passo da noite (Fase 5).
- A **descrição do PR é o relatório final**; o mesmo relatório vive como artefato commitado em `docs/testing-audits/<YYYY-MM-DD>.md` (ou condensado com ponteiro para o artefato). - A **descrição do PR é o relatório final** (integral ou condensado com ponteiro para o artefato); o relatório vive como artefato commitado em `docs/testing-audits/<YYYY-MM-DD>.md`.
- Fixes em commits separados quando possível (preferência, não requisito duro). - Melhorias em commits separados quando possível (preferência, não requisito duro).
- **Não registrar Issues no tracker**: propostas vivem só no relatório; a decisão humana de capturar débitos vem depois.
## Intocáveis absolutos ## Intocáveis absolutos
Nunca remover nem enfraquecer autonomamente: Nunca remover nem enfraquecer autonomamente (nem com prova forte):
1. Testes de consentimento/LGPD fail-closed (`Consent`, chave estável, falha fechada). 1. Testes de consentimento/LGPD fail-closed (`Consent`, chave estável, falha fechada).
2. Access control/RBAC (`src/utilities/access/*`). 2. Access control/RBAC (`src/utilities/access/*`).
3. Lockdown de liderança (`leader`; `estimatedVotes` nunca visível à liderança). 3. Lockdown de liderança (`leader`; `estimatedVotes` nunca visível à liderança).
Duplicata aparente envolvendo intocável vira proposta no relatório, sempre. Duplicata aparente envolvendo intocável vira proposta no relatório, sempre.
Intocáveis estruturais (nenhum diff toca): `scripts/ci-scope.mjs`, `scripts/lib/e2e-affected-manifest.mjs`, `.github/workflows/`, `.husky/`, gates e a política de seleção e2e (OPS72/OPS86). Zero edições em arquivos `tests/e2e/`. Intocáveis estruturais (nenhum diff toca): `scripts/ci-scope.mjs`, `scripts/lib/e2e-affected-manifest.mjs`, `.github/workflows/`, `.husky/`, qualquer gate ou política de seleção e2e (OPS72/OPS86). Zero edições em arquivos `tests/e2e/` — e2e é inventariado, não executado nem editado à noite.
## Fases (teto total da noite ~6h) ## Critérios de nível (aponte, não redefina)
0. **Baseline** — validar DB do worktree (`teqo_wt<slot>_test`) e registrar contagens/durações verdes de `pnpm test:unit` + `pnpm test:int`. A pirâmide das três camadas já está codificada: `.agents/skills/test-driven-development/SKILL.md` §144–198 e `docs/TESTING.md`. As disposições da auditoria julgam contra esses critérios existentes — nunca criem critérios novos paralelos.
1. **Inventário paralelo** — 3 leitores Task read-only por camada (unit/int/e2e estático), ≤45min.
2. **Rubrica** — disposições por candidato (mover/reforçar/remover/manter/proposta) ≤30min; auditoria TERMINA aqui antes de implementar.
3. **Loop de melhorias** — ≤6 passos, ≤30min cada, prova verde antes/depois por passo, 1 commit por passo.
4. **Fecho** — relatório + changelog + push único + PR ready + CI green.
5. **Desarme do auto-merge** — SEMPRE o último passo da noite.
*(As fases 0–5 são detalhadas nas seções abaixo; este arquivo é autossuficiente para executar a noite inteira sem contexto externo.)* ## Tetos duros da noite
<!-- FASES-DETALHE: placeholder do tracer — protocolo completo entra na fase 3 do impl plan OPS96 --> | Teto | Valor |
|---|---|
| Budget global | ~6h |
| Inventário paralelo (Fase 1) | ≤45min |
| Rubrica (Fase 2) | ≤30min |
| Melhorias aplicadas (Fase 3) | ≤6 passos, ≤30min cada |
| Debug de AFTER falho | ≤10min, 1 retry, senão revert |
Estourou teto → o candidato vira proposta no relatório; faltou tempo no fim → pular direto ao fecho com o que há. **A auditoria TERMINA antes de implementar** — nada de ler até amanhecer.
## Ferramentas (first-party apenas)
- Retrato unit/int: `node scripts/testing-audit-metrics.mjs unit|int [--top N]` (reporters JSON nativos; imprime totais + tabela markdown dos arquivos mais lentos; propaga exit code).
- Inventário e2e estático: `node scripts/testing-audit-metrics.mjs e2e-inventory` (contagem de describe/test por arquivo; duração "não medido").
- Cobertura diagnóstica sob flag: `pnpm vitest run --config ./vitest.unit.config.mts --coverage <filtro>` (plugin @vitest/coverage-v8; opcional, sem meta percentual, nunca regra dura).
- Proibido: instalar outras ferramentas de análise de suíte, mutation testing na rotina, dashboards.
## Fase 0 — Baseline (antes de editar qualquer arquivo)
1. Validar ambiente do worktree: `.env.test.local` aponta para `teqo_wt<slot>_test` (nunca produção; guards locais intactos) e Postgres local acessível.
2. Rodar `pnpm test:unit` e `pnpm test:int` (bare, nunca piped) — ambos devem estar verdes.
3. Registrar no rascunho do relatório: contagens de arquivos/testes e durações por camada (baseline da noite).
## Fase 1 — Inventário paralelo (≤45min)
Despache **três leitores Task read-only em paralelo**, um por camada. Cada leitor recebe: a lista de intocáveis acima, os critérios de nível (§144–198 + `docs/TESTING.md`) e esta instrução fixa:
> "Leia os specs de `<tests/unit|tests/int|tests/e2e>` e devolva candidatos com evidência `caminho:linha`: (a) duplicatas prováveis — mesmo comportamento pinado duas vezes, citando as assertions gêmeas; (b) teste de integração que não toca DB/rede/Payload e poderia ser unitário (ou vice-versa, teste unitário que mocka tanto que testa mocks); (e para e2e) fluxo que camadas inferiores já garantem. Para cada candidato: prova literal citada, por quê, risco. Read-only: não escreva código nem planos."
Regras dos leitores: read-only obrigatório; todo achado cita caminho:linha; achado sem citação é descartado.
## Fase 2 — Rubrica de disposições (≤30min)
Consolide os achados validando CADA um contra o fonte antes de virar disposição (leitor pode alucinar; implementação nunca é delegada). Cada candidato recebe UMA disposição:
| Disposição | Exige | Vira |
|---|---|---|
| **mover** | prova de que a camada atual está errada pelos critérios OPS90 | melhoria aplicável |
| **reforçar** | assertion faltante dentro de escopo JÁ testado | melhoria aplicável |
| **remover** | PROVA FORTE: duplicata demonstrada (comportamento idêntico, diff das assertions) ou teste morto de feature removida (referência verificada morta no codebase) | melhoria aplicável |
| **manter** | valor claro / intocável / custo > benefício | linha no retrato |
| **proposta** | dúvida, prova insuficiente ou toque em intocável/e2e | seção de propostas do relatório |
Na dúvida entre remover e proposta → proposta. Priorize por: risco coberto × custo de execução × confiança da prova.
## Fase 3 — Loop de melhorias (≤6 passos × 30min)
Para cada melhoria aplicável, EM SEQUÊNCIA (nunca paralelo):
1. **Prova ANTES:** rodar `pnpm test:unit` + `pnpm test:int` (passo confinado a arquivos unit pode provar unit-only) — registrar contagens literais ("Test Files X passed", "Tests Y passed") no relatório.
2. **Editar** o mínimo necessário (uma melhoria = um commit; sem misturar melhorias).
3. **Prova DEPOIS:** mesma suíte verde com contagens coerentes (ex.: mover não muda totais; remover-com-prova-forte reduz exatamente o número documentado).
4. **Commit próprio** com mensagem que nomeia a disposição (ex.: `test(ops96-audit): mover X para unit — puro, sem Payload`).
5. **AFTER falhou** → debug ≤10min, 1 retry; persistindo → `git reset --hard HEAD~1`, registrar como REVERTIDO no relatório com o motivo da falha. Nada fica quebrado.
Evidência por passo no relatório: timestamp, hash curto, linhas-resumo antes/depois. JSON bruto é efêmero (fora do git).
## Fase 4 — Fecho (relatório + PR)
1. Finalizar `docs/testing-audits/<YYYY-MM-DD>.md` no template abaixo.
2. Escrever `docs/changelog/<data>-ops96-testing-audit.md` (entrada append-only curta; o agregado é gitignored — nunca commitar `docs/CHANGELOG-AGENTS.md`).
3. Gate completo no SHA final, bare: `pnpm lint`, `pnpm format:check`, `pnpm typecheck` (`tsc --noEmit`), `pnpm exec knip`, `pnpm check:cycles`, `pnpm test:unit`, `pnpm test:int`.
4. Push ÚNICO via `pnpm push -u origin HEAD` (o hook roda `gate:push`; minimiza rearms do automerge).
5. Abrir PR **ready** base `main` (`GITHUB_TOKEN=<PAT> node scripts/github-pr.mjs --head <branch> --title "<data> — testing-audit" --body-file <relatório>`), body = relatório, com `Closes #898` se a Issue desta entrega ainda estiver aberta.
6. Aguardar CI green no SHA final: flake aparente → 1 rerun (`gh run rerun --failed`); quebra real → fix-forward ≤2 ciclos (cada fix é novo push e REARMA o automerge → repetir Fase 5 depois). Esgotado → CI pendente registrado como pendência conhecida (main não corre risco: automerge desarmado na Fase 5).
### Template do relatório (seções fixas)
```markdown
# Testing audit <YYYY-MM-DD>
## Sumário executivo
(números da noite: baseline → final; melhorias aplicadas/revertidas/propostas)
## Metodologia e tetos respeitados
(fases executadas, horários, tetos: 6h / 6 melhorias / 30min-passo)
## Retrato da suíte
(unit/int medidos pelo script; e2e inventário estático, duração "não medido";
tabelas dos arquivos mais lentos; duplicatas prováveis)
## Disposições
(id, camada, arquivo, tipo mover/reforçar/remover/manter/proposta,
prioridade alta/média/baixa, prova, decisão aplicada/proposta/revertida)
## Evidências verde antes/depois
(uma linha por passo: timestamp, hash, contagens antes → depois)
## Passos revertidos
(motivo da falha de cada revert)
## Propostas não executadas
(cada uma com a razão — prova insuficiente / intocável / teto)
## Estado final do PR
(CI, mergeable, automerge desarmado às HH:MM)
```
## Fase 5 — Desarme do auto-merge (SEMPRE o último passo da noite)
O safety-net arma o automerge no evento de abertura e REARMA a cada push. Sequência obrigatória:
1. Confirmar que nenhum push novo acontecerá (todo o trabalho já foi empurrado na Fase 4).
2. Aguardar o run do workflow `PR Ready + auto-merge` **do SHA final** terminar (foi ele que armou): `gh run list --workflow agent-pr-ready-automerge.yml --limit 1`.
3. Desarmar: `gh pr merge <N> --disable-auto`.
4. Verificar: `gh pr view <N> --json isDraft,autoMergeRequest,mergeable,mergeStateStatus` → esperado `{isDraft: false, autoMergeRequest: null, mergeable: MERGEABLE}`.
5. **Fallback fail-closed:** desarme falhou ou `autoMergeRequest` persistiu não-nulo → converter para draft (`gh pr ready <N> --undo`; draft fora de `cursor/*` é veto estrutural do safety-net) e registrar no relatório que o humano vira ready pela manhã. Draft protegido é preferível a main mergeado sem querer.
6. Gravar no relatório (seção estado final): hora do desarme + saída da verificação.
Depois do desarme: **nenhum push, rebase ou merge** — tudo isso rearma.
## Done when
Baseline verde registrado; inventário com achados citados por caminho:linha; toda disposição justificada; melhorias aplicadas com provas antes/depois e commits separados; reverts registrados; relatório completo commitado + changelog; PR ready, mergeable, CI green no SHA final; **automerge desarmado e verificado** (ou fallback draft registrado). Nada quebrado pela manhã.

View file

@ -7,6 +7,7 @@ out/
build/ build/
playwright-report/ playwright-report/
test-results/ test-results/
coverage/
.playwright-cli/ .playwright-cli/
.aider.chat.history.md .aider.chat.history.md
.aider.input.history .aider.input.history

View file

@ -224,6 +224,8 @@ const eslintConfig = [
// Playwright artifacts (generated on e2e failures/reports): // Playwright artifacts (generated on e2e failures/reports):
'playwright-report/**', 'playwright-report/**',
'test-results/**', 'test-results/**',
// Vitest coverage output (generated under the --coverage flag):
'coverage/**',
// Vendored agent material (same carve-out as .prettierignore): // Vendored agent material (same carve-out as .prettierignore):
'.agents/**', '.agents/**',
]), ]),

View file

@ -21,7 +21,8 @@ const slowestAssertion = (entry) =>
(entry.assertionResults ?? []) (entry.assertionResults ?? [])
.filter((result) => typeof result.duration === 'number') .filter((result) => typeof result.duration === 'number')
.reduce( .reduce(
(slowest, result) => (slowest === null || result.duration > slowest.duration ? result : slowest), (slowest, result) =>
slowest === null || result.duration > slowest.duration ? result : slowest,
null, null,
) )
@ -68,10 +69,7 @@ export const summarizeVitestReport = (report) => {
* @param {{ top?: number }} [options] * @param {{ top?: number }} [options]
*/ */
export const renderSlowestFilesTable = (summary, { top = DEFAULT_TOP } = {}) => { export const renderSlowestFilesTable = (summary, { top = DEFAULT_TOP } = {}) => {
const lines = [ const lines = ['| arquivo | testes | duração | teste mais lento |', '|---|---:|---:|---|']
'| arquivo | testes | duração | teste mais lento |',
'|---|---:|---:|---|',
]
for (const row of summary.rows.slice(0, Math.max(0, top))) { for (const row of summary.rows.slice(0, Math.max(0, top))) {
const slowest = const slowest =
row.slowestTest === null ? '—' : `${row.slowestTest} (${ms(row.slowestMs ?? 0)})` row.slowestTest === null ? '—' : `${row.slowestTest} (${ms(row.slowestMs ?? 0)})`
@ -83,8 +81,8 @@ export const renderSlowestFilesTable = (summary, { top = DEFAULT_TOP } = {}) =>
/** /**
* Static inventory of an e2e spec source — counting only, never execution. * Static inventory of an e2e spec source — counting only, never execution.
* *
* @param {{ file: string, content: string }} sources * @param {{ file: string, content: string }} source
* @returns {Array<{ file: string, describes: number, tests: number }>} * @returns {{ file: string, describes: number, tests: number }}
*/ */
export const inventoryE2ESource = ({ file, content }) => ({ export const inventoryE2ESource = ({ file, content }) => ({
file, file,
@ -98,7 +96,8 @@ export const renderE2EInventoryTable = (rows) => {
'| arquivo | describes | testes | duração |', '| arquivo | describes | testes | duração |',
'|---|---:|---:|---|', '|---|---:|---:|---|',
...rows.map( ...rows.map(
(row) => `| ${row.file} | ${row.describes} | ${row.tests} | não medido — execução noturna proibida |`, (row) =>
`| ${row.file} | ${row.describes} | ${row.tests} | não medido — execução noturna proibida |`,
), ),
] ]
return lines.join('\n') return lines.join('\n')