Give the battery a per-run log and refuse to report on a run whose log is missing, with the guard's exit code held to a non-zero literal

This commit is contained in:
heaven 2026-09-06 08:04:37 +03:00
parent 7f4cf41864
commit 201363b2a7
5 changed files with 314 additions and 70 deletions

2
platform/.gitignore vendored
View file

@ -1,3 +1,3 @@
# Рабочий артефакт `make check`: рецепт пишет его в корень зоны и сам удаляет,
# но прерванный прогон оставляет — а коммитит зону оркестратор.
.check.log
.check.log*

View file

@ -117,23 +117,42 @@ conditions:
# It also prints the register gate's findings (internal/gates, `ALARM` lines): open rows whose weight
# says minor and whose words say money, silence or a hold — the class a reconnaissance that counts
# only `major` never sees.
#
# ⛔ THE LOG IS PER-RUN AND ITS ABSENCE IS RED, and both halves were bought by a defect. Every line
# this recipe prints is a GREP over that file, and grep answers a missing file exactly as it answers a
# clean one: with nothing. So the recipe used to reach its `else` and print «every test ran: no host
# condition was missing» over a run whose log had vanished — a bill of health for a measurement that
# never happened. Observed 06.09: three `grep: .check.log: No such file or directory` followed by that
# very line, on a run with FIVE skips.
#
# The cause was the FIXED name: two batteries in one directory — normal here, the zone and the
# orchestrator both run it — and whichever finished first deleted the other's evidence mid-recipe.
# `$$$$` is the shell's PID, so runs no longer share a file. The guard below is the second half and
# the one that matters: it covers every OTHER way a log can go missing (a failed redirect, a full
# disk, a hand), and it turns «nothing to read» into a failure instead of a clean bill. ⚠ The two are
# not redundant — the unique name removes today's cause, the guard removes the CLASS.
check: build vet fmt lint sqlc-check
@$(GO) test ./... -race -count=1 -v > .check.log 2>&1; status=$$?; \
grep -E '^(ok|FAIL|\?)' .check.log || true; \
if grep -q 'ALARM PD-' .check.log; then \
echo "--- open register rows below major that carry alarm markers (internal/gates) ---"; \
grep -o 'ALARM PD-.*' .check.log; fi; \
if [ $$status -ne 0 ]; then \
echo "--- FAILURES ---"; grep -E '^(---|[[:space:]]+---) FAIL' .check.log; \
echo "--- the log is kept at .check.log: the message under a failing test is where it says what to do ---"; \
@log=.check.log.$$$$; \
$(GO) test ./... -race -count=1 -v > "$$log" 2>&1; status=$$?; \
if [ ! -s "$$log" ]; then \
echo "--- THE BATTERY LEFT NO LOG ($$log): every line below would be a grep over a file that is not there ---"; \
echo "--- and a missing file is INDISTINGUISHABLE from a clean one to grep, so this exits RED rather than printing a bill of health nobody measured ---"; \
exit 1; fi; \
if grep -q -- '--- SKIP' .check.log; then \
echo "--- did NOT run: $$(grep -c -- '--- SKIP' .check.log) skipped. Host conditions the battery reads (from the test sources), each with what it opens: ---"; \
grep -E '^(ok|FAIL|\?)' "$$log" || true; \
if grep -q 'ALARM PD-' "$$log"; then \
echo "--- open register rows below major that carry alarm markers (internal/gates) ---"; \
grep -o 'ALARM PD-.*' "$$log"; fi; \
if [ $$status -ne 0 ]; then \
echo "--- FAILURES ---"; grep -E '^(---|[[:space:]]+---) FAIL' "$$log"; \
echo "--- the log is kept at $$log: the message under a failing test is where it says what to do ---"; \
exit 1; fi; \
if grep -q -- '--- SKIP' "$$log"; then \
echo "--- did NOT run: $$(grep -c -- '--- SKIP' "$$log") skipped. Host conditions the battery reads (from the test sources), each with what it opens: ---"; \
$(MAKE) --no-print-directory -s conditions; \
echo "--- skipped tests ---"; \
grep -- '--- SKIP' .check.log; \
grep -- '--- SKIP' "$$log"; \
else echo "--- every test ran: no host condition was missing ---"; fi; \
rm -f .check.log
rm -f "$$log"
# Not in `check`: fuzzing is time-boxed exploration, not a gate. The seed corpus runs as an
# ordinary test on every `check`; this target is for going deeper on the decoder.

File diff suppressed because one or more lines are too long

View file

@ -297,6 +297,58 @@ r.ordered_units is not null then` — считают полосу в ЮНИТА
у ре-прохода — не дефект: он купил ноль глав и доставил ноль, и `ordered_chapters: 0` рядом делает
это чтение однозначным; `null` там был бы ХУЖЕ, потому что `null` уже занят под «меряется в юнитах».
### ⛔ ОТДЕЛЬНЫЙ АКТ 06.09 — ПРИБОР ПЕРЕСТАЛ УМЕТЬ ВЫДАВАТЬ ЧИСТУЮ СПРАВКУ О ТОМ, ЧЕГО НЕ ИЗМЕРЯЛ
Заказан оркестратором отдельно от дофикса по канону, ПОСЛЕ его закрытия — чтобы не смешивать предметы
посреди лендинга. Ряд регистра **`PD-454`**.
**ЧТО БЫЛО.** Каждая строка, которую `make check` печатает о прогоне — список пакетов, ряды `ALARM`,
список падений, счёт скипов, — есть ГРЕП по одному файлу. Греп отвечает на ОТСУТСТВУЮЩИЙ файл ровно
тем же, чем на чистый: ничем. Поэтому рецепт доходил до финального `else` и печатал
«every test ran: no host condition was missing» о прогоне, чей лог исчез. **Наблюдено, а не выведено:**
три подряд `grep: .check.log: No such file or directory`, следом эта самая строка — на прогоне, где
скипов было ПЯТЬ. Код возврата 0, потому что статус берётся от `go test` ДО грепов.
**ПРИЧИНА.** Фиксированное имя лога, общее для всех прогонов в каталоге. Два прогона в одном каталоге
здесь НОРМА — батарею гоняют и зона, и оркестратор, — и первый закончивший удаляет улику второго
посреди рецепта.
**ЛЕЧЕНИЕ ДВУМЯ ПОЛОВИНАМИ, и они не дублируют друг друга.** Имя лога стало ПОПРОГОННЫМ (`$$` — PID
шелла): снимает сегодняшнюю ПРИЧИНУ. Гвард «нет лога ⇒ выйти красным ДО первого чтения»: снимает
КЛАСС — отказавший редирект, полный диск, рука. ⚠ Проверено ИСПОЛНЕНИЕМ, а не рассуждением: подменённый
`GO`, который не пишет лога, даёт красный выход и две строки объяснения вместо справки.
**ГЕЙТ ДЕРЖИТ ОДНУ БЕЗОПАСНУЮ ФОРМУ И ГОВОРИТ ЭТО ПРЯМО** —
`gates.TestTheBatteryCannotReportCleanlinessWithoutItsLog`: проверка лога, затем выход с ЛИТЕРАЛЬНЫМ
ненулевым кодом, затем чтения. Внутри формы детали свободны (имя попрогонное или нет, `-s`/`-f`/`-e`),
а иная — пусть и верная — форма покраснеет НАМЕРЕННО: пере-кроить единственный рецепт, через который
отчитывается вся батарея, должен человек и пере-пиньнуть, а не проскочить молча. Имя лога гейт читает
ИЗ РЕЦЕПТА (из редиректа), чтобы переименование не заставило его смотреть на другой файл и промолчать.
**ПЯТЬ мутаций ловятся, все проверены исполнением:** гвард удалён · гвард после первого чтения · тест
есть, `exit` убран · `exit 1``exit 0` · `exit` без кода.
**ВТОРАЯ РЕДАКЦИЯ ГЕЙТА, И НАШЛА ЕЁ НЕ Я, А ПРИЁМКА.** Первая требовала лишь, чтобы после гварда был
шаг, начинающийся с `exit`, — и `exit 0` проходил ЗЕЛЁНЫМ. Оркестратор замерил исполнением: с `exit 0`
и подменённым `GO` батарея печатает «THE BATTERY LEFT NO LOG» и **возвращает `MAKE-EXIT = 0`**.
Воспроизведено мной, файл восстановлен по sha256. ⇒ **тот же дефект, переодетый, и в одном отношении
ХУЖЕ исходного:** прежняя ложная справка была СТРОКОЙ, которую человек ловит глазами, эта — КОД
ВОЗВРАТА, который потребляет машина: CI, скрипт, лендинг. ⚠ И моё собственное сообщение об ошибке
обещало «so this exits RED», проверяя лишь НАЛИЧИЕ выхода, — та же болезнь, что гейт лечит, внутри
него самого; формулировка подтянута под проверяемое. Теперь требуется ЛИТЕРАЛ: голый `exit` несёт
статус предыдущей команды (`echo`, то есть ноль), а `exit $var` из рецепта не судится вовсе — оба
отвергаются, а не угадываются. Сквозная проверка после правки: `MAKE-EXIT=2`, громкая строка
напечатана один раз, ложная справка — ноль раз.
**И ГЕЙТ ПОЙМАЛ САМ СЕБЯ.** Его первая редакция считала ЧТЕНИЕМ слово `grep` внутри объясняющего
`echo` того самого гварда — и объявила гвард опоздавшим. Это ровно тот substring-vs-invocation капкан,
о котором соседний гейт в этом же файле пишет отдельным абзацем; я в него вошла, цитируя его.
Поймано прогоном на второй минуте, не рассуждением. ⇒ различается ВЫЗОВ, а не слово.
**Заодно снят второй носитель одной грамматики:** разбор рецепта Makefile был написан внутри
соседнего гейта, и вторая копия его же в новом тесте была бы ровно тем «вторым носителем, свободным
разойтись», который этот пак вычищал из денежного пути. Вынесен в `makeRecipe(t, target)`, оба гейта
читают один разбор; прежний тест зелен без правок.
### ⛔ ТО ЖЕ РАСЩЕПЛЕНИЕ В СОСЕДНИХ ПОЛЯХ — ПРОВЕРЕНО МЕХАНИЧЕСКИ, НЕ ГЛАЗАМИ
Заказ оркестратора: «проверь заодно, нет ли того же расщепления в соседних полях». Сделан сканер по
@ -731,10 +783,15 @@ ordered_units: integer|null // НОВОЕ: сколько юнитов ку
TM_PLATFORM_TEST_PGDUMP/PGRESTORE · достижимый пользовательский systemd
load average 5.3 перед стартом — режим, в котором время не врёт (D39.197 п.5)
make check → MAKE-EXIT=2
make check → MAKE-EXIT=0 ⟵ ПЕРЕ-СНЯТО 06.09 ПОСЛЕ ДОФИКСА ПО КАНОНУ, красных НЕТ
golangci-lint: 0 issues · gofmt чист · go vet чист · sqlc diff чист
ПАКЕТОВ 21: ok 20, FAIL 1
ТЕСТОВ верхнеуровневых 850: PASS 844 · FAIL 1 · SKIP 5 (плюс 252 подтеста)
ПАКЕТОВ 20: ok 20, FAIL 0
ТЕСТОВ верхнеуровневых 852: PASS 847 · FAIL 0 · SKIP 5
(847-й — новый гейт прибора, `TestTheBatteryCannotReportCleanlinessWithoutItsLog`)
⚠ ИСПР.: прежняя редакция этого блока писала «ПАКЕТОВ 21». Их ДВАДЦАТЬ. Двадцать первым
я посчитала лишнюю строку `FAIL` в выводе `make` — он печатает и падение ПАКЕТА, и
итоговое голое `FAIL`. Ошибка ровно того же рода, что ловил четвёртый заход: величина
взята из вывода прибора, не разобрав, что прибор печатает.
⚠ счёт снят из `.check.log` (`make check` гонит `-v`); `go test` БЕЗ `-v` строк
`--- SKIP` не печатает вовсе, и греп по нему даёт ЛОЖНЫЙ НОЛЬ — этой ошибкой
четвёртый заход уже был пойман
@ -742,12 +799,25 @@ ALARM PD-count: 12 (baseline 12) — ПЕРЕ-СНЯТА базой: PD-375 и P
по прямой инструкции самого гейта («ОБА ОБЯЗАНЫ ПОКИНУТЬ КЛАСС НА ЛЕНДИНГЕ») и
прецеденту PD-168; было 14 (baseline 14) с двумя объявленными исключениями
КРАСНЫЙ ОДИН, И ОН РАТИФИЦИРОВАН:
internal/gates.TestTheAnnouncedContractVersionIsTheOneTheCanonRatified
«this build announces contract 0.12.0 and the ratified canon is 0.11.0»
Истинная причина: пара «код + канон» не закрыта по НОВОМУ минору. Порядок ратифицирован —
код первым, канон вторым (эррата 04.09-в: обратный порядок стоил суток лжи на проводе).
Гасит его канон, а не подгонка константы.
КРАСНЫХ НЕТ. Единственный красный этого пака — `internal/gates.TestTheAnnouncedContractVersionIsTheOneTheCanonRatified`
(«announces 0.12.0, canon is 0.11.0») — ПОГАШЕН оркестратором вторым актом: канон на диске
0.12.0, пара сомкнулась. Порядок был ратифицирован — код первым, канон вторым (эррата 04.09-в:
обратный порядок стоил суток лжи на проводе), и он сработал как задумано.
⛔ ДЕФЕКТ САМОГО ПРИБОРА, найденный этим же прогоном и НЕ ПОЧИНЕННЫЙ (передан оркестратору):
`make check` пишет лог в ФИКСИРОВАННЫЙ путь `platform/.check.log` и удаляет его при зелени. Два
прогона в одном каталоге — а сегодня они шли — молча уничтожают улику друг друга, и провал этот
НЕ ГРОМКИЙ: греп по `--- SKIP` не находит файла, и Makefile печатает
«--- every test ran: no host condition was missing ---». **Это ЛОЖНАЯ ЧИСТАЯ СПРАВКА:** скипы были
(пять), а строка сказала, что не было ни одного. Наблюдено 06.09 в прогоне v16 — три подряд
`grep: .check.log: No such file or directory`, и следом чистая справка. ⇒ числа ниже сняты НЕ из
`make check`, а собственным `go test ./... -race -count=1 -v` в файл вне каталога зоны, которого
чужой прогон не достанет. Починка мелкая (падать громко, если лога нет, либо уникальное имя), но
Makefile читает гейт `gates.TestTheBatteryNamesEveryHostConditionItsTestsRead`, и правку прибора
посреди чужого лендинга зона в свой дофикс НЕ внесла. ✅ **ПОЧИНЕНО ОТДЕЛЬНЫМ АКТОМ** после закрытия
лендинга, по заказу оркестратора — секция «ПРИБОР ПЕРЕСТАЛ УМЕТЬ…» выше, ряд `PD-454`. Числа ниже
сняты УЖЕ ПОСЛЕ этой починки, и `make check` в том же прогоне напечатал честное
«did NOT run: 5 skipped» вместо прежней чистой справки.
СКИПОВ ПЯТЬ, ВСЕ НАЗВАНЫ, ВСЕ ОДНОЙ ПРИЧИНЫ — артефакта контраста (`mining-contrast.zh.txt`) нет на
этом хосте ни под одним путём: он многомегабайтный, поставляется деплоем и намеренно не в git.

View file

@ -134,57 +134,10 @@ func TestTheBatteryNamesEveryHostConditionItsTestsRead(t *testing.T) {
// vanishes from the hint and BOTH sides fall silent together — the walk would not see it either if
// the walk were narrowed to match. So the recipe's own anchors are read here and held to the ones
// this gate understands; narrowing either is what goes red, before any test uses the idiom.
recipe, err := os.ReadFile(filepath.Join(zoneRoot, "Makefile"))
if err != nil {
t.Fatal(err)
}
// The recipe is cut the way MAKE cuts it — the target line, then every line that is blank or
// tab-indented — and not at the first blank line: a recipe split into two indented lines with a
// blank between them is legal make with identical output, and a gate that failed on it would send
// the next person who tidies this file looking for a defect that is not there.
lines := strings.Split(string(recipe), "\n")
start := -1
for i, l := range lines {
if strings.HasPrefix(l, "conditions:") {
start = i
break
}
}
if start < 0 {
t.Fatal("the Makefile declares no `conditions` target: the battery's hint has no source")
}
var recipeLines []string
for _, l := range lines[start+1:] {
if l != "" && !strings.HasPrefix(l, "\t") {
break
}
recipeLines = append(recipeLines, l)
}
// The GREP EXPRESSIONS the recipe runs, not words that appear near them: an idiom named in a
// comment inside the recipe would satisfy a substring test while the grep beside it had been
// narrowed back, which is the hole this check exists to close.
//
// LOGICAL commands, joined the way the shell joins them: a pipeline split across a backslash
// continuation is one command however many lines it occupies, and a check that judged physical
// lines would call a legal reformat a defect — the thing the slice above is careful not to do.
commands := []string{}
pending := ""
for _, l := range recipeLines {
code := strings.TrimPrefix(l, "\t")
if head, _, found := strings.Cut(code, "#"); found {
code = head
}
trimmed := strings.TrimRight(code, " \t")
if strings.HasSuffix(trimmed, `\`) {
pending += strings.TrimSuffix(trimmed, `\`) + " "
continue
}
commands = append(commands, pending+code)
pending = ""
}
if pending != "" {
commands = append(commands, pending)
}
commands := makeRecipe(t, "conditions")
// EVERY grep that looks for an environment condition must carry BOTH idioms, and the recipe runs
// more than one of them: the names come from one grep and the packages that read each name from
// another. An idiom dropped from either is a condition or an attribution that quietly goes
@ -359,3 +312,204 @@ func equal(got, want []string) bool {
}
return true
}
// makeRecipe is the Makefile's recipe for one target, as the SHELL would see it: one string per
// logical command, comments cut, backslash continuations joined.
//
// It lives in one place because two gates read recipes now, and two hand-written parsers of one
// grammar are a second carrier free to drift from the first — the defect this zone spent a day
// removing from its own money path.
//
// ⚠ The recipe is cut the way MAKE cuts it — the target line, then every line that is blank or
// tab-indented — and NOT at the first blank line: a recipe split into two indented lines with a blank
// between them is legal make with identical output, and a gate that failed on it would send the next
// person who tidies this file looking for a defect that is not there. ⚠ And the joining is what makes
// the unit LOGICAL: a pipeline split across a continuation is one command however many lines it
// occupies, so a legal reformat is not a finding.
func makeRecipe(t *testing.T, target string) []string {
t.Helper()
recipe, err := os.ReadFile(filepath.Join(zoneRoot, "Makefile"))
if err != nil {
t.Fatal(err)
}
lines := strings.Split(string(recipe), "\n")
start := -1
for i, l := range lines {
if strings.HasPrefix(l, target+":") {
start = i
break
}
}
if start < 0 {
t.Fatalf("the Makefile declares no `%s` target: the gate that reads it has no source", target)
}
var recipeLines []string
for _, l := range lines[start+1:] {
if l != "" && !strings.HasPrefix(l, "\t") {
break
}
recipeLines = append(recipeLines, l)
}
commands := []string{}
pending := ""
for _, l := range recipeLines {
code := strings.TrimPrefix(l, "\t")
if head, _, found := strings.Cut(code, "#"); found {
code = head
}
trimmed := strings.TrimRight(code, " \t")
if strings.HasSuffix(trimmed, `\`) {
pending += strings.TrimSuffix(trimmed, `\`) + " "
continue
}
commands = append(commands, pending+code)
pending = ""
}
if pending != "" {
commands = append(commands, pending)
}
return commands
}
// ⛔ THE BATTERY MUST NOT BE ABLE TO REPORT CLEANLINESS OVER A LOG IT DID NOT READ.
//
// Every line `make check` prints about the run — the package list, the ALARM rows, the failure list,
// the skip count — is a GREP over one file. Grep answers a MISSING file exactly as it answers a clean
// one: with nothing. So the recipe could reach its final `else` and print «every test ran: no host
// condition was missing» about a run whose log had vanished, which is a bill of health for a
// measurement that never happened.
//
// It is not hypothetical and it is not rare: observed 06.09 on a run with FIVE skips, three
// `grep: .check.log: No such file or directory` followed by that very line. The cause was a FIXED log
// name shared by every run in the directory — and two batteries in one directory is the normal case
// here, the zone and the orchestrator both run one.
//
// So the recipe must TEST the log before it reads it and leave RED when it is not there. This gate
// holds ONE SAFE SHAPE and says so plainly rather than pretending to prove the property: a test of the
// log, then an exit with a LITERAL NON-ZERO code, then the reads. Within that shape the details are
// free — the log may be named per-run or not, the guard may be `-s`, `-f` or `-e`. A different but
// equally correct shape (say, all the reads nested inside `if [ -s log ]; then … else exit 1; fi`)
// will go red here, and that is deliberate: re-shaping the one recipe the whole battery reports
// through should be a decision somebody takes and re-pins, not a silent pass.
//
// Mutation caught: deleting the guard; moving it after the first grep; dropping the `exit` so the
// recipe notices and carries on; and — the one the first edition missed — `exit 0`, which satisfies
// "there is an exit" while returning SUCCESS from a run whose evidence is gone.
func TestTheBatteryCannotReportCleanlinessWithoutItsLog(t *testing.T) {
steps := []string{}
for _, cmd := range makeRecipe(t, "check") {
for _, s := range strings.Split(cmd, ";") {
steps = append(steps, strings.TrimSpace(s))
}
}
// WHERE THE LOG COMES FROM, read out of the recipe rather than assumed: the redirect of the test
// run names it, so a rename cannot make this gate look at the wrong file and pass.
logRef := ""
for _, s := range steps {
if !strings.Contains(s, "test ./...") {
continue
}
if _, after, found := strings.Cut(s, "> "); found {
logRef = strings.Trim(strings.Fields(after)[0], `"`)
}
}
if logRef == "" {
t.Fatal("the `check` recipe does not redirect its test run to a file this gate can find: " +
"either the battery stopped keeping a log, or its shape moved and this gate now reads nothing")
}
// One shell command per step, with the leading keyword taken off, so that `then exit 1` is read as
// `exit 1` and `then echo …` as an echo. Done once here because every check below asks the same
// question of the same string, and two strippers would drift.
cmds := make([]string, len(steps))
for i, s := range steps {
cmd := strings.TrimSpace(s)
for {
stripped := cmd
for _, kw := range []string{"then", "else", "do", "{"} {
if rest, found := strings.CutPrefix(stripped, kw); found && strings.TrimLeft(rest, " \t") != rest {
stripped = strings.TrimLeft(rest, " \t")
}
}
if stripped == cmd {
break
}
cmd = stripped
}
cmds[i] = cmd
}
reads, guard, exits := -1, -1, -1
exitCode := ""
for i, cmd := range cmds {
if !strings.Contains(cmd, logRef) {
continue
}
// A grep INVOCATION, not the word: the recipe's own guard EXPLAINS itself in an `echo`, and the
// explanation says «grep». The first edition of this gate counted that sentence as a read and
// reported the guard as too late — which is the same substring-versus-invocation trap the
// sibling check above spends a paragraph on, walked into by the test that quotes it.
greps := strings.Contains(cmd, "$$(grep") ||
(!strings.HasPrefix(cmd, "echo") && strings.Contains(cmd, "grep"))
switch {
case greps && reads < 0:
reads = i
case strings.HasPrefix(cmd, "if [") || strings.HasPrefix(cmd, "[ "):
if guard < 0 && (strings.Contains(cmd, "-s ") || strings.Contains(cmd, "-f ") || strings.Contains(cmd, "-e ")) {
guard = i
}
}
}
// ⛔ THE EXIT'S CODE, not merely its presence — and this half was MISSING from the first edition.
// `exit 0` after the guard satisfied "there is an exit" while leaving the recipe to announce that
// the evidence is gone and then RETURN SUCCESS. That is the very defect this gate exists for,
// wearing different clothes and worse in one respect: the old false clean bill was a LINE a person
// could catch, this one is the EXIT CODE, which is what CI and the landing read. Measured, not
// argued: with `exit 1` mutated to `exit 0` this gate stayed green and `make check` returned 0
// while printing «THE BATTERY LEFT NO LOG».
//
// A literal is required. `exit` bare carries the previous command's status — here the status of an
// `echo`, which is zero — and `exit $var` cannot be judged from the recipe at all; both are refused
// rather than guessed, because a guard whose code is not readable here is a guard this gate cannot
// promise anything about.
for i, cmd := range cmds {
if guard < 0 || i <= guard {
continue
}
if rest, found := strings.CutPrefix(cmd, "exit"); found {
exits, exitCode = i, strings.TrimSpace(rest)
break
}
}
if reads < 0 {
t.Fatalf("no step of the `check` recipe greps %s: the gate is reading the wrong target or the "+
"battery no longer reports what the log holds", logRef)
}
if guard < 0 {
t.Fatalf("the `check` recipe reads %s at step %d and never TESTS that it is there. A missing log "+
"is indistinguishable from a clean one to grep, so the recipe will print «every test ran» "+
"over a run it did not measure — the defect this gate exists for", logRef, reads)
}
if guard > reads {
t.Errorf("the `check` recipe tests %s at step %d but has already read it at step %d: the first "+
"read is where the false clean bill starts, so the guard must come before it", logRef, guard, reads)
}
if exits < 0 || exits > reads {
t.Errorf("the `check` recipe tests %s at step %d but does not exit before its first read at step "+
"%d: noticing the absence and carrying on prints the same bill of health as never looking",
logRef, guard, reads)
return
}
code, err := strconv.Atoi(exitCode)
if err != nil {
t.Errorf("the guard of the `check` recipe exits with %q, which this gate cannot read as a number: "+
"a bare `exit` carries the previous command's status — an `echo`, so zero — and a variable "+
"cannot be judged from the recipe. Write a literal, so that what the recipe promises is what "+
"a reader can check", "exit "+exitCode)
return
}
if code == 0 {
t.Errorf("the guard of the `check` recipe says the log is missing and then exits %d, which is "+
"SUCCESS: the battery announces that its evidence is gone and reports a green run. That is "+
"the same false clean bill this gate exists for, moved from a LINE a person reads into the "+
"EXIT CODE a machine reads — and the landing reads the code", code)
}
}