From 7fe56b93e9bbe0beecd70a4fe256b45bf02c63ad Mon Sep 17 00:00:00 2001 From: Luiz Carlos Date: Mon, 19 Oct 2026 10:50:52 -0300 Subject: [PATCH 1/2] docs: plan shell harness tests entering the acceptance coverage (LUI-183) Acceptance file docs/tickets/LUI-182/LUI-183/aceite.md (CA1-CA4). No route, contract or schema changes: the ticket only changes harness scripts. Decision: the shell tests write their own report, apps/api/reports/harness/shell-tests.json, in the Vitest format the checker and check-report.cjs already read, and the checker receives it as one more vitest: spec. It is not merged into node-tests.json because the Node reporter writes that one inside the api container, while the shell suites run on the machine (git and jq); two files keep each writer alone and need no change in the checker. Decision: the helpers record the cases, and a new runner assembles the report. check, expect_hook and the new record_case append "suite, status, name" to the file in SH_TEST_LOG when it is set, and the runner turns that log into the report with jq at the end of the run. Suites that print their own ok/FALHOU lines (stop-verify and record-touched) call record_case, so every shell case of the harness is in the report, not only the ones that use check. Without SH_TEST_LOG the helpers behave as today. Decision: the tag comes from the case name with the same pattern as the Node reporter (@-CA), and the title stays as written, so the checker's rule against an ID only in the title is satisfied the same way. The pattern lives in two places (JS and jq), which is accepted: it is one line, and the test of CA1 would catch a drift. Decision: the runner removes the report before anything runs and always writes a new one at the end, even when a suite fails, so a report from a previous run never covers a criterion whose case did not run now (CA3). It keeps going after a failing suite, which the old && chain did not, so the report lists every suite and the Aceite step does not add a missing-report error to a failure already shown. A suite that exits non-zero without a failed case, or that does not exist, becomes one failed case with the exit code, so a crash cannot pass as a report without failures. Decision: harness_tests runs the shell suites even when the Node tests fail, and then runs check_report on the shell report, so zero cases, a failed case or a skipped one fail the "Testes do harness" step as they do for the Node report. Decision: the tests of this ticket are shell tests too and carry the @LUI-183-CA token, so they are in the new report and the ticket proves itself with the mechanism it builds. They run the checker for real, through scripts/node-in-api.sh, on a fake acceptance file and a report written under apps/api/reports/ (git-ignored), the only writable folder the api container shares with the machine, so the checker reads them under /repo. Files and interfaces the implementation will touch: - scripts/test/sh-helpers.sh: record_case (new) and its use in check and expect_hook; the header documents how to name an acceptance test and where the report is (CA4). - scripts/test/shell-tests.sh (new): shell-tests.sh ... removes the report, runs each suite with SH_TEST_LOG set, writes the report with jq, exits 1 if any suite failed. - scripts/test/stop-verify.test.sh and scripts/test/record-touched.test.sh: their own assertions call record_case. - scripts/test/shell-tests.test.sh (new): the tests of CA1 to CA3, with the new suite added to the list the runner receives. - scripts/verify.sh: HARNESS_SHELL_REPORT; harness_tests runs the shell suites through the runner and checks the shell report; acceptance adds it to the checker specs; the header comment of harness_tests says so. - AGENTS.md: the "Cobertura" bullet points to the helpers header for naming a shell acceptance test. Existing tests whose behavior changes: none (the shell suites print the same lines; the two with their own assertions also record them). Data changes: none. Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_013ChFCP7iJd9oEJ81Mc5L2p --- docs/tickets/LUI-182/LUI-183/aceite.md | 37 ++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 docs/tickets/LUI-182/LUI-183/aceite.md diff --git a/docs/tickets/LUI-182/LUI-183/aceite.md b/docs/tickets/LUI-182/LUI-183/aceite.md new file mode 100644 index 0000000..bcd27ba --- /dev/null +++ b/docs/tickets/LUI-182/LUI-183/aceite.md @@ -0,0 +1,37 @@ +# Aceite do LUI-183 + +Ticket de preparação: faz os testes em shell do harness virarem prova de critério. Os CA1 a CA3 são provados por testes de unidade em shell, e o CA4, pela leitura do cabeçalho do auxiliar dos testes em shell. + +Os testes em shell rodam na máquina, porque usam o `git` e o `jq`, e os do próprio ticket são em shell também: eles estão no relatório novo com a tag `@LUI-183-CA`, o que prova o mecanismo por dentro. Cada teste prepara uma suíte de mentira (um script que carrega o auxiliar e roda um ou dois casos com um ID de mentira, `PROJ-9`) e a roda pelo executor novo, com o relatório numa pasta de `apps/api/reports/`. O contêiner `api` monta o repositório, então a pasta é a mesma para o conferidor, que roda no contêiner (`scripts/node-in-api.sh`) sobre um arquivo de aceite de mentira gravado ao lado do relatório. Assim o conferidor é o de verdade, o mesmo do `full`, e o teste não depende de como o relatório foi escrito, só de ele ser lido. + +O relatório novo é um arquivo à parte do dos testes em Node, no mesmo formato, e entra no conferidor como mais um `vitest:`. O ID continua só na tag: o token `@-CA` fica no nome do caso e o relatório o repete em `tags`, como o repórter dos testes em Node faz. + +## LUI-183-CA1 + +**Marca:** teste de unidade + +**Cenário:** Dado um arquivo de aceite com o critério `PROJ-9-CA1` de teste de unidade e uma suíte em shell com um caso que passa e traz `@PROJ-9-CA1` no nome, quando o executor roda a suíte, então o relatório gravado traz esse caso com a tag `@PROJ-9-CA1` e o estado `passed`, o título continua como foi escrito, e o conferidor, lendo o relatório como `vitest`, sai com 0 e diz que `PROJ-9-CA1` está coberto por 1 teste. + +**Teste previsto:** testes dos scripts do harness, `scripts/test/shell-tests.test.sh`, "o caso com o ID no nome sai no relatório com a tag e o conferidor o conta como coberto". + +## LUI-183-CA2 + +**Marca:** teste de unidade + +**Cenário:** Dado o mesmo arquivo de aceite e uma suíte em shell cujo caso com `@PROJ-9-CA1` no nome falha, quando o executor roda a suíte, então ele sai com 1, o relatório traz o caso com a tag e o estado `failed`, e o conferidor sai com 1 dizendo que um teste com o ID `PROJ-9-CA1` falhou. Dado uma suíte que termina com erro sem ter registrado nenhum caso que falhou, o relatório traz um caso `failed` que diz isso, e o executor sai com 1. + +**Teste previsto:** testes dos scripts do harness, `scripts/test/shell-tests.test.sh`, "o caso que falha sai como failed e o conferidor reprova o critério" e "a suíte que termina com erro sem caso que falhou vira um caso que falhou". + +## LUI-183-CA3 + +**Marca:** teste de unidade + +**Cenário:** Dado o relatório de uma execução em que o caso com `@PROJ-9-CA1` passou, quando o executor roda de novo no mesmo caminho de relatório com uma suíte que não tem esse caso, então o relatório novo não traz a tag, e o conferidor sai com 1 dizendo que `PROJ-9-CA1` não tem nenhum teste. + +**Teste previsto:** testes dos scripts do harness, `scripts/test/shell-tests.test.sh`, "o relatório de uma execução anterior não cobre o critério cujo caso não rodou agora". + +## LUI-183-CA4 + +**Marca:** documento + +**Cenário:** Dado o cabeçalho do auxiliar dos testes em shell, quando se lê o diff do PR, então ele diz como nomear um teste de aceite (o token `@-CA` no nome do caso, como no teste em Node) e onde fica o relatório (o caminho dentro de `apps/api/reports/`, apagado e regravado a cada execução do executor). From f53dcdb12d3719190785d23b3fa11b6020bc0952 Mon Sep 17 00:00:00 2001 From: Luiz Carlos Date: Mon, 19 Oct 2026 11:43:31 -0300 Subject: [PATCH 2/2] feat: shell harness tests enter the acceptance coverage (LUI-183) Shell suites now write a report in the Vitest format (apps/api/reports/harness/shell-tests.json) through a new runner, with the @-CA token of a case name as its tag, and the acceptance checker reads it as one more vitest report. The report is rebuilt on every run, so a case that did not run now never covers a criterion. The helpers record each case through pass_case and fail_case, with record_case behind them; the plan named only record_case. Co-Authored-By: Claude Sonnet 5.5 Claude-Session: https://claude.ai/code/session_01ANcDtWf5vRMzv1r42Ax9ce --- AGENTS.md | 2 +- scripts/test/record-touched.test.sh | 9 +- scripts/test/sh-helpers.sh | 54 ++++++++--- scripts/test/shell-tests.sh | 93 ++++++++++++++++++ scripts/test/shell-tests.test.sh | 144 ++++++++++++++++++++++++++++ scripts/test/stop-verify.test.sh | 5 +- scripts/verify.sh | 37 ++++--- 7 files changed, 305 insertions(+), 39 deletions(-) create mode 100755 scripts/test/shell-tests.sh create mode 100755 scripts/test/shell-tests.test.sh diff --git a/AGENTS.md b/AGENTS.md index 1e0e8bb..5295baf 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -232,7 +232,7 @@ O atalho `scripts/verify.sh plan []` chama o s - **Desenho:** o contrato de rotas vai direto para o `apps/api/openapi.json`, e o schema das tabelas implementadas, para o `schema.prisma`: são os donos deles, conferidos por máquina. Como cada item do formato de rota e de tabela da skill se escreve neles está no `docs/lld.md` (seção 3, "Formato de uma rota no snapshot", e seção 2, "Formato de uma tabela no schema"). O LLD fica com as convenções, as regras e o que ainda não existe. A lista de arquivos e interfaces e a mudança de dados (seed, backfill) ficam na mensagem do commit de plano, como diz a skill em `formatos/mensagem.md`. - **Congelamento:** o agente não altera um arquivo de aceite já em commit. Um hook nega a edição pelo agente, e a guarda do harness avisa no PR quando um aceite existente muda. -- **Cobertura:** a CI confere o ID na tag do teste, como descrito em "Aceite no pull request". O ID vai na tag, e não no título, porque os dois relatórios JSON, o do Vitest e o do Playwright, trazem as tags de cada teste. O `strictTags` do Vitest fica desligado nas configs da API e da web, porque quem valida o ID é o conferidor: um ID que não está em nenhum arquivo de aceite reprova. +- **Cobertura:** a CI confere o ID na tag do teste, como descrito em "Aceite no pull request". O ID vai na tag, e não no título, porque os dois relatórios JSON, o do Vitest e o do Playwright, trazem as tags de cada teste. O `strictTags` do Vitest fica desligado nas configs da API e da web, porque quem valida o ID é o conferidor: um ID que não está em nenhum arquivo de aceite reprova. Os testes em shell do harness também provam critério: como nomear o caso e onde fica o relatório está no cabeçalho de `scripts/test/sh-helpers.sh`. ## Convenções de Git diff --git a/scripts/test/record-touched.test.sh b/scripts/test/record-touched.test.sh index 24121a2..e875178 100755 --- a/scripts/test/record-touched.test.sh +++ b/scripts/test/record-touched.test.sh @@ -31,10 +31,9 @@ expect_touched() { local got got="$(touched "$2")" if [[ "$got" == "$3" ]]; then - echo "ok: $1" + pass_case "$1" else - echo "FALHOU: $1 (anotado: ${got:-nada}; esperava: ${3:-nada})" - failed=$((failed + 1)) + fail_case "$1" "anotado: ${got:-nada}; esperava: ${3:-nada}" fi } @@ -43,7 +42,7 @@ run() { local code "$HOOK" <<<"$1" >/dev/null 2>&1 code=$? - [[ $code -eq 0 ]] || { echo "FALHOU: o hook saiu com $code para $1"; failed=$((failed + 1)); } + [[ $code -eq 0 ]] || fail_case "o hook saiu com código diferente de 0" "saída $code para $1" } run "{\"session_id\":\"s1\",\"tool_input\":{\"file_path\":\"$project/apps/api/src/a.ts\"}}" @@ -101,7 +100,7 @@ run_in() { local code CLAUDE_PROJECT_DIR="$1" "$HOOK" <<<"$2" >/dev/null 2>&1 code=$? - [[ $code -eq 0 ]] || { echo "FALHOU: o hook saiu com $code sem a pasta de estado"; failed=$((failed + 1)); } + [[ $code -eq 0 ]] || fail_case "o hook sem a pasta de estado saiu com código diferente de 0" "saída $code" } run_in "$project/nao-existe" "{\"session_id\":\"x1\",\"tool_input\":{\"file_path\":\"$project/apps/api/src/a.ts\"}}" expect_touched "sem a raiz do projeto, o hook não anota e sai com 0" x1 "" diff --git a/scripts/test/sh-helpers.sh b/scripts/test/sh-helpers.sh index 0cfcf4f..63e381d 100644 --- a/scripts/test/sh-helpers.sh +++ b/scripts/test/sh-helpers.sh @@ -1,19 +1,52 @@ -# Auxiliares das suítes em shell do harness (a dos hooks e a da trava do modo completo): o contador de -# casos que falharam e a saída `ok:` ou `FALHOU:` de cada um. Cada suíte os carrega com `source`. +# Auxiliares das suítes em shell do harness (a dos hooks, a da trava do modo completo e as dos modos do +# verify.sh): o contador de casos que falharam e a saída `ok:` ou `FALHOU:` de cada um. Cada suíte os +# carrega com `source`. +# +# Um caso de shell também pode provar um critério de aceite, como um teste em Node. Para isso, ponha o +# token do ID no nome do caso, como `@-CA` (por exemplo, `@LUI-183-CA1`), e o critério tem de +# ser de teste no arquivo de aceite. O nome fica como foi escrito, e o executor +# (scripts/test/shell-tests.sh) repete o token em `tags` no relatório, que o conferidor do aceite lê. +# +# O relatório fica em apps/api/reports/harness/shell-tests.json, no mesmo formato do Vitest que o +# relatório dos testes em Node (node-tests.json, ao lado). O executor o apaga e o grava de novo a cada +# execução, então um caso que não rodou agora não cobre critério nenhum. Quem grava os casos nele são +# estes auxiliares, pelo `record_case`, no arquivo que o executor aponta em SH_TEST_LOG; sem a variável, +# eles só imprimem, como sempre. Uma suíte com asserção própria dá o veredito pelo `pass_case` e pelo +# `fail_case`, que imprimem, registram e contam o caso. failed=0 +# Registra um caso no relatório dos testes em shell, quando o executor o pediu (SH_TEST_LOG). A suíte é +# a que o executor está rodando (SH_TEST_SUITE), ou o nome do script, se a suíte roda sozinha. +# O log tem um caso por linha, separado por TAB: o TAB e a quebra de linha do nome viram espaço. +# record_case +record_case() { + [[ -n "${SH_TEST_LOG:-}" ]] || return 0 + local name="${2//[$'\t\n']/ }" + printf '%s\t%s\t%s\n' "${SH_TEST_SUITE:-${0##*/}}" "$1" "$name" >>"$SH_TEST_LOG" +} + +# O veredito de um caso: imprime, registra no relatório e, se falhou, conta. +# pass_case +pass_case() { + echo "ok: $1" + record_case passed "$1" +} + +# O detalhe só sai na tela: o relatório traz o nome, que é por onde o conferidor liga o caso ao critério. +# fail_case [detalhe] +fail_case() { + echo "FALHOU: $1${2:+ ($2)}" + record_case failed "$1" + failed=$((failed + 1)) +} + # Um caso que passa quando a condição sai com 0. # check check() { local name="$1" shift - if "$@"; then - echo "ok: $name" - else - echo "FALHOU: $name" - failed=$((failed + 1)) - fi + if "$@"; then pass_case "$name"; else fail_case "$name"; fi } # Um caso de hook: roda o hook com a entrada dada e confere o código de saída e um trecho da saída de @@ -24,10 +57,9 @@ expect_hook() { err="$("$hook" <<<"$input" 2>&1 >/dev/null)" got=$? if [[ $got -ne $want || "$err" != *"$text"* ]]; then - echo "FALHOU: $name (saída $got, esperava $want; erro: $err)" - failed=$((failed + 1)) + fail_case "$name" "saída $got, esperava $want; erro: $err" else - echo "ok: $name" + pass_case "$name" fi } diff --git a/scripts/test/shell-tests.sh b/scripts/test/shell-tests.sh new file mode 100755 index 0000000..c745d3f --- /dev/null +++ b/scripts/test/shell-tests.sh @@ -0,0 +1,93 @@ +#!/usr/bin/env bash +# +# Executor das suítes em shell do harness: roda cada suíte e grava o relatório no formato do Vitest, +# o mesmo que o relatório dos testes em Node, para o `check-report.cjs` e o conferidor do aceite (o +# scripts/check-acceptance.cjs) o lerem. O token `@-CA` do nome de um caso vira também uma +# tag dele, como no repórter dos testes em Node (scripts/test/node-test-report.cjs). Como nomear um +# caso de aceite está no cabeçalho do scripts/test/sh-helpers.sh. +# +# scripts/test/shell-tests.sh ... +# +# Apaga o relatório antes de rodar e sempre grava um novo no fim, até com uma suíte que falha: um +# relatório de uma execução anterior nunca cobre um critério cujo caso não rodou agora. Segue depois +# de uma suíte que falha, para o relatório trazer todas. Uma suíte que sai com erro sem ter registrado +# um caso que falhou (ou que não existe) vira um caso que falhou, com o código dela, para o erro dela +# não passar por um relatório sem falha. +# +# Sai com 0 se todas as suítes passaram, 1 se alguma falhou e 2 no uso errado. Roda na máquina, como +# as suítes, e precisa do `jq`. + +set -uo pipefail + +if [[ $# -lt 2 ]]; then + echo 'Uso: scripts/test/shell-tests.sh ...' >&2 + exit 2 +fi +command -v jq >/dev/null || { + echo 'Falta o `jq` na máquina: o executor dos testes em shell precisa dele.' >&2 + exit 2 +} + +report="$1" +shift +suites=("$@") + +log="$(mktemp)" || exit 2 +trap 'rm -f "$log"' EXIT +rm -f "$report" +mkdir -p "$(dirname "$report")" || exit 2 + +# Se a suíte registrou algum caso que falhou. +# has_failed_case +has_failed_case() { + awk -F'\t' -v suite="$1" '$1 == suite && $2 == "failed" { found = 1 } END { exit !found }' "$log" +} + +status=0 +export SH_TEST_LOG="$log" +for suite in "${suites[@]}"; do + export SH_TEST_SUITE="$suite" + if [[ -f "$suite" ]]; then + bash "$suite" + code=$? + else + echo "A suíte $suite não existe." + code=127 + fi + if [[ $code -ne 0 ]]; then + status=1 + # O erro de uma suíte sem um caso que o explique vira um caso que falhou. + has_failed_case "$suite" || + printf '%s\tfailed\t%s\n' "$suite" "a suíte terminou com erro (saída $code) sem registrar um caso que falhou" >>"$log" + fi +done +unset SH_TEST_LOG SH_TEST_SUITE + +# O relatório: um arquivo por suíte, os casos na ordem em que rodaram. O `-R` lê o log linha a linha, +# e o `scan` tira o token do ID do nome (sem grupos, ele devolve o texto inteiro do que casou). A +# expressão do token é a do TOKEN de scripts/test/node-test-report.cjs: mude as duas juntas. +jq -R -n --args ' + [inputs | split("\t") | {suite: .[0], status: .[1], title: .[2]}] as $cases + | [$ARGS.positional[] as $suite + | {name: $suite, + assertionResults: [$cases[] | select(.suite == $suite) | { + title: .title, + fullName: .title, + ancestorTitles: [], + tags: [.title | scan("@[A-Z][A-Z0-9]*-[0-9]+-CA[0-9]+")], + status: .status + }]}] as $files + | [$files[].assertionResults[]] as $all + | { + numTotalTests: ($all | length), + numFailedTests: ($all | map(select(.status == "failed")) | length), + numPendingTests: 0, + numTodoTests: 0, + testResults: $files + }' "${suites[@]}" <"$log" >"$report" || { + echo "Não deu para gravar o relatório $report." >&2 + rm -f "$report" + exit 2 +} + +exit $status diff --git a/scripts/test/shell-tests.test.sh b/scripts/test/shell-tests.test.sh new file mode 100755 index 0000000..39c79ea --- /dev/null +++ b/scripts/test/shell-tests.test.sh @@ -0,0 +1,144 @@ +#!/usr/bin/env bash +# +# Testes do executor das suítes em shell (scripts/test/shell-tests.sh) e do relatório dele: o caso com o +# token de um ID no nome sai com a tag, o conferidor do aceite o conta, e o relatório é refeito a cada +# execução. Cada teste monta suítes de mentira (scripts que carregam o sh-helpers.sh e rodam um caso com +# o ID de mentira `PROJ-9`), roda o executor e confere o relatório com o conferidor de verdade, o mesmo +# do modo completo, pelo scripts/node-in-api.sh. O conferidor roda no contêiner `api`, que monta o +# repositório em /repo, então a pasta de trabalho fica em apps/api/reports/ (ignorada pelo Git), e o +# conferidor lê dali o aceite de mentira e o relatório. Rodam na máquina, como o executor, que precisa +# do `jq`. Quem chama é o scripts/verify.sh. + +set -uo pipefail +ROOT="$(cd "$(dirname "$0")/../.." && pwd)" +# shellcheck source=sh-helpers.sh +source "$ROOT/scripts/test/sh-helpers.sh" + +command -v jq >/dev/null || { echo "Falta o \`jq\` na máquina: o executor e o teste dele precisam dele."; exit 2; } + +RUNNER="$ROOT/scripts/test/shell-tests.sh" +mkdir -p "$ROOT/apps/api/reports" +work="$(mktemp -d -p "$ROOT/apps/api/reports" shell-tests.XXXXXX)" +trap 'rm -rf "$work"' EXIT +# A pasta de trabalho como o contêiner a vê, e relativa à raiz dele, que é onde o conferidor lê os tickets. +in_container="/repo/${work#"$ROOT"/}" +tickets="${work#"$ROOT"/}/tickets" +report="$work/shell-tests.json" + +# O arquivo de aceite de mentira: um critério de teste de unidade, PROJ-9-CA1. +mkdir -p "$work/tickets/PROJ-8/PROJ-9" +cat >"$work/tickets/PROJ-8/PROJ-9/aceite.md" <<'ACCEPTANCE' +# Aceite do PROJ-9 + +## PROJ-9-CA1 + +**Marca:** teste de unidade + +**Cenário:** Dado um caso de mentira, quando ele roda, então o critério está coberto. +ACCEPTANCE + +# suite : uma suíte de mentira, que carrega os auxiliares reais. +suite() { + { + printf '#!/usr/bin/env bash\nsource "%s/scripts/test/sh-helpers.sh"\n' "$ROOT" + printf '%s\n' "$2" + } >"$work/$1" +} + +suite passes.sh 'check "o caso passa @PROJ-9-CA1" true +finish "da suíte de mentira"' +suite fails.sh 'check "o caso falha @PROJ-9-CA1" false +finish "da suíte de mentira"' +suite without-case.sh 'check "um caso sem o ID" true +finish "da suíte de mentira"' +# Termina com erro sem ter registrado um caso que falhou. +suite crashes.sh 'exit 3' + +# run : roda o executor sobre as suítes dadas, no relatório de mentira, e deixa o código em $code. +run() { + local suites=() name + for name in "$@"; do suites+=("$work/$name"); done + "$RUNNER" "$report" "${suites[@]}" >/dev/null 2>&1 + code=$? +} + +# checker: o conferidor do aceite sobre o relatório de mentira, lido como `vitest`, com a saída em $out e +# o código em $checker_code. +checker() { + out="$(scripts/node-in-api.sh scripts/check-acceptance.cjs --optional - "$tickets" "vitest:$in_container/shell-tests.json" 2>&1 /dev/null +} + +cd "$ROOT" || exit 2 + +# --- CA1: o caso com o ID no nome sai com a tag, e o conferidor o conta --- +ca1() { + run passes.sh && + [[ $code -eq 0 ]] && + report_has '.testResults[0].assertionResults[0] + | .title == "o caso passa @PROJ-9-CA1" and .tags == ["@PROJ-9-CA1"] and .status == "passed"' && + report_has '.numTotalTests == 1 and .numFailedTests == 0' && + checker && + [[ $checker_code -eq 0 && "$out" == *"PROJ-9-CA1: coberto por 1 teste(s)."* ]] +} +check "o caso com o ID no nome sai no relatório com a tag e o conferidor o conta como coberto @LUI-183-CA1" ca1 + +# --- CA2: o caso que falha reprova o critério --- +ca2_failed() { + run fails.sh && + [[ $code -eq 1 ]] && + report_has '.testResults[0].assertionResults[0] | .tags == ["@PROJ-9-CA1"] and .status == "failed"' && + checker && + [[ $checker_code -eq 1 && "$out" == *"PROJ-9-CA1: um teste com o ID dele falhou."* ]] +} +check "o caso que falha sai como failed e o conferidor reprova o critério @LUI-183-CA2" ca2_failed + +ca2_crash() { + run crashes.sh && + [[ $code -eq 1 ]] && + report_has '.numTotalTests == 1 and .numFailedTests == 1 + and (.testResults[0].assertionResults[0] | .status == "failed" and (.title | contains("terminou com erro (saída 3)")))' +} +check "a suíte que termina com erro sem caso que falhou vira um caso que falhou @LUI-183-CA2" ca2_crash + +# --- CA3: o relatório de uma execução anterior não cobre o critério --- +ca3() { + # Antes, o relatório traz a tag: sem isso, o teste não provaria que foi a execução nova que a tirou. + run passes.sh && report_has '.testResults[0].assertionResults[0].tags == ["@PROJ-9-CA1"]' && + run without-case.sh && + [[ $code -eq 0 ]] && + report_has '[.testResults[].assertionResults[].tags[]] == []' && + checker && + [[ $checker_code -eq 1 && "$out" == *"PROJ-9-CA1"*"não tem nenhum teste"* ]] +} +check "o relatório de uma execução anterior não cobre o critério cujo caso não rodou agora @LUI-183-CA3" ca3 + +# --- O que o executor garante além dos critérios --- +missing_suite() { + run passes.sh nao-existe.sh && + [[ $code -eq 1 ]] && + report_has '.numFailedTests == 1 and (.testResults[1].assertionResults[0].title | contains("saída 127"))' +} +check "a suíte que não existe vira um caso que falhou, e as outras entram no relatório" missing_suite + +after_failure() { + run fails.sh passes.sh && + [[ $code -eq 1 ]] && + report_has '.testResults | length == 2 and (.[1].assertionResults[0].status == "passed")' +} +check "o executor segue depois da suíte que falha e o relatório traz todas" after_failure + +check "sem suíte, o executor recusa com a saída 2" bash -c '"$0" "$1" 2>/dev/null; [[ $? -eq 2 ]]' "$RUNNER" "$report" + +# Sem o executor (sem SH_TEST_LOG), os auxiliares só imprimem, como sempre. +no_executor() { + (unset SH_TEST_LOG SH_TEST_SUITE && bash "$work/passes.sh" | grep -q '^ok: o caso passa') +} +check "sem o executor, os auxiliares só imprimem" no_executor + +finish "do executor dos testes em shell" diff --git a/scripts/test/stop-verify.test.sh b/scripts/test/stop-verify.test.sh index d58271a..2c9e93e 100755 --- a/scripts/test/stop-verify.test.sh +++ b/scripts/test/stop-verify.test.sh @@ -82,10 +82,9 @@ expect_stop() { local name="$1" want="$2" ran="$3" text="${4:-}" got_ran="não roda" [[ -s "$FAKE_LOG" ]] && got_ran="roda" if [[ $code -ne $want || "$got_ran" != "$ran" || "$err$out" != *"$text"* ]]; then - echo "FALHOU: $name (saída $code, esperava $want; $got_ran, esperava $ran; erro: $err; saída: $out)" - failed=$((failed + 1)) + fail_case "$name" "saída $code, esperava $want; $got_ran, esperava $ran; erro: $err; saída: $out" else - echo "ok: $name" + pass_case "$name" fi } diff --git a/scripts/verify.sh b/scripts/verify.sh index 1e14be4..33a0934 100755 --- a/scripts/verify.sh +++ b/scripts/verify.sh @@ -200,30 +200,29 @@ node_in_api() { # escrita, e o `check_report` e o conferidor o leem como o de uma suíte da API. A pasta dele nasce no # `api_full`, que roda antes, porque o Node não a cria. HARNESS_REPORT=reports/harness/node-tests.json +# O dos testes em shell do harness, na mesma pasta e no mesmo formato. É outro arquivo porque quem o grava +# é o executor, na máquina (scripts/test/shell-tests.sh), e o do Node é gravado no contêiner: cada +# relatório tem um escritor só. +HARNESS_SHELL_REPORT=reports/harness/shell-tests.json # Roda os testes do harness: os do conferidor, do renderizador do contrato e os da skill de plano (o -# parser e a página do plano) no contêiner `api`, e os dos hooks de congelamento e de antes do comando -# e os da trava do modo completo na máquina. +# parser e a página do plano) no contêiner `api`, e as suítes em shell (os hooks, a trava do modo +# completo e os modos do verify.sh) na máquina, pelo executor que grava o relatório delas. Os dois +# relatórios passam pelo `check_report`, e um grupo que falha não impede o outro de rodar. harness_tests() { + local code=0 # O repórter próprio grava o relatório no formato do Vitest, para o `check_report` e o conferidor # lerem; o repórter `spec` segue na saída, para quem lê o log. Os testes são os mesmos caminhos # relativos aqui e no /repo, então a lista sai daqui. node_in_api --test --test-reporter=spec --test-reporter-destination=stdout \ --test-reporter=./scripts/test/node-test-report.cjs --test-reporter-destination="/app/$HARNESS_REPORT" \ scripts/test/*.test.cjs "$PLAN_SKILL_SCRIPTS"/test/*.test.cjs && - check_report api vitest "$HARNESS_REPORT" && - # Os hooks (o do congelamento, o do push, o que anota os arquivos da sessão e o de fim de turno) - # usam o `git` e o `jq`, que o contêiner não tem, e a trava é do verify.sh, que roda na máquina: - # os testes deles rodam aqui. - scripts/test/freeze-acceptance.test.sh && - scripts/test/plan-page.test.sh && - scripts/test/verify-plan.test.sh && - scripts/test/verify-review.test.sh && - scripts/test/harness-guard.test.sh && - scripts/test/push-verify.test.sh && - scripts/test/full-lock.test.sh && - scripts/test/record-touched.test.sh && - scripts/test/stop-verify.test.sh + check_report api vitest "$HARNESS_REPORT" || code=1 + # As suítes em shell usam o `git` e o `jq`, que o contêiner não tem, e a trava é do verify.sh, que + # roda na máquina: elas rodam aqui. O relatório é o que faz um caso delas provar um critério. + scripts/test/shell-tests.sh "apps/api/$HARNESS_SHELL_REPORT" scripts/test/*.test.sh && + check_report api vitest "$HARNESS_SHELL_REPORT" || code=1 + return $code } # Falha, dizendo por quê, quando o ACCEPTANCE_BODY_FILE aponta para um arquivo que não existe. Sem @@ -310,10 +309,10 @@ acceptance() { # O conferidor roda no contêiner `api` (`node_in_api`), na raiz do /repo, e lê de lá os arquivos de # aceite, pela pasta dos tickets relativa a ela, e os relatórios, que as suítes gravam na pasta de - # cada app, e o dos testes do harness, na da API. O corpo do PR, que não está no repositório, vai - # pela entrada padrão. O relatório que falta o conferidor reprova, dizendo qual; aqui sai também o - # que fazer. - specs+=("vitest:/repo/apps/api/$HARNESS_REPORT") + # cada app, e os dos testes do harness (o do Node e o das suítes em shell), na da API. O corpo do PR, + # que não está no repositório, vai pela entrada padrão. O relatório que falta o conferidor reprova, + # dizendo qual; aqui sai também o que fazer. + specs+=("vitest:/repo/apps/api/$HARNESS_REPORT" "vitest:/repo/apps/api/$HARNESS_SHELL_REPORT") for app in "${APPS[@]}"; do ref="${app}_reports[@]" for spec in "${!ref}"; do