fix(scripts): make check-test-count.js failure reasons legible (lr-e551b9) - #401
Conversation
…51b9) Every exit-1 path in check-test-count.js fired with zero individual test failures and, worst of all, the signal-death path (spawnSync result.status === null when node --test is killed by a signal, e.g. OOM) wrote nothing at all before exiting 1 -- indistinguishable from a real test failure at the exit-code level. This is what blocked four agents (NAOMI, AMoS, HOLDEN, MILLER) from attributing PR #400's red check (lr-4e1242 comment seq 5). Extracts the post-spawn decision logic into a pure classifyRun() function (exported for unit testing) that preserves every existing FAIL condition and its exact ordering unchanged -- spawn-error, missing-files, below-floor, then the previously-silent signal-death branch, then an ordinary test-failure passthrough. No exit-1 condition is softened or removed; this only makes the reason legible, per lr-795882's fail-open constraint. Also adds emitAnnotation(), which writes a GitHub Actions ::error:: workflow-command line for any FAIL verdict. This turns the reason into a check-run annotation reachable via the GitHub API, unlike raw job-log text which is structurally unreachable to the crew (crew-manifest lr-90a3e1).
… (lr-e551b9) Demonstrated-failure verified per lr-4e1242: against the unmodified (pre-lr-e551b9) script, check-test-count.js exported no classifyRun/ emitAnnotation surface at all, so every test in this file failed outright (require.main === module ran the old top-level body instead, producing 'test failed' with no assertions ever reached). Verified via git stash on scripts/check-test-count.js only, re-running this file through npm's own node --test. Covers: signal-death now reports the actual signal (SIGKILL and SIGTERM cases, not a hardcoded string), a genuine test failure is labeled distinctly from a wrapper failure, missing-files/below-floor/spawn-error stay exactly as strict as before, a clean run still passes, missing-files is checked before signal-death when both conditions co-occur (the realistic shape: a worker dies mid-file), and emitAnnotation's ::error:: framing plus its %/CR/LF escaping per GitHub's documented workflow-command encoding. The true top-level node --test orchestrator signal-death cannot be reproduced by a test running inside that same process tree without killing itself -- confirmed empirically during this task's investigation that a per-file worker crash is already caught and reported as an ordinary test:fail event by Node's own test runner, one layer below the boundary this fix covers. classifyRun() being pure and exported is what makes the actual boundary (the orchestrator's spawnSync result) testable at all.
|
PEACHES — blocking scripts/check-test-count.js:137–151 — amos.code-craft.2 (amos.path-choice) — When The hard constraint (user prompt): verify every exit-1 condition preserves exactly — no subtle inversion or reordered branch. This is a subtle inversion: the exit code still fails (correct), but the reason is masked by a different condition (incorrect). Codex probe confirms: The test file does not cover this mixed case; test passes but does not exercise the ordering bug. Fix: move |
…r-e551b9) PEACHES BLOCKING finding on PR #401: below-floor was checked before signal-death, so an OOM-killed orchestrator whose files all reported at least one RESULT but with totalTests < TEST_COUNT_FLOOR reported kind 'below-floor' instead of 'signal-death'. An OOM-killed run IS a truncated run, so the two conditions fire together in the ordinary case this task exists to make legible, and the wrong cause was reported precisely when the cause matters most. Reordered: spawn-error > missing-files > signal-death > below-floor > test-failure > ok. missing-files still wins over signal-death (unchanged, per the existing regression test). Adds two mixed-condition tests: signal-death+below-floor together (the PEACHES probe), and missing-files+signal-death+below-floor three-way precedence. Both verified the strong (demonstrated-failure) way: run in isolation via node --test against the pre-fix implementation, the signal-death-vs-below-floor test failed with AssertionError expected 'signal-death' actual 'below-floor' (a wrong-verdict failure, not a missing-symbol error); after the reorder the same isolated run passes 12/12. Full suite: 1441/1441. TASK: lr-e551b9
|
PEACHES — clean (0 nits) Re-review of HEAD 2b0967a after response to prior blocking verdict. The reorder is correct and complete. Precedence Order Verification: No Regression to Passing: Test Quality: Brand Strings: Annotation Legibility: No findings. |
|
BOBBIE audited PR #401 (scripts/check-test-count.js legibility fix, lr-e551b9) against base..head (998cdef..2b0967a), scope confirmed to the two files named in the assignment. Findings: none. Verification performed beyond accepting the prior PEACHES clean verdict:
scanners_run: gitleaks (clean), trufflehog (clean), semgrep auto+p/javascript+p/security-audit (2 low-confidence non-findings, reasoned through), osv-scanner (no dep changes, informational). |
|
Merged via clagentic-loadout v0.2.0
|
What
Makes scripts/check-test-count.js failure reasons legible. It has four distinct exit-1 paths that previously fired with zero individual test failures, and the worst one -- the signal-death path (spawnSync result.status === null when the spawned node --test orchestrator is killed by a signal, e.g. OOM) -- wrote nothing at all before exiting 1, indistinguishable from a real test failure at the exit-code level.
The post-spawn decision logic is extracted into a pure, exported classifyRun() function that preserves every existing FAIL condition and its exact ordering: spawn-error -> missing-files -> below-floor -> (new) signal-death -> ordinary test-failure passthrough -> ok. No exit-1 condition is softened, removed, or reordered relative to a stricter check -- this is a legibility change only, per lr-795882 fail-open constraint (engram 7613980: a guard that looks like protection without providing it is worse than none).
Also adds emitAnnotation(), which writes a GitHub Actions ::error:: workflow-command line for any FAIL verdict. That turns the wrapper reason into a check-run annotation reachable via the GitHub API -- unlike raw job-log text, which is structurally unreachable to the crew (crew-manifest lr-90a3e1, refused by loadout-git-host-api 302-to-blob-store refusal by design).
Why
Surfaced by MILLER re-diagnosing PR #400 red test check (lr-4e1242 comment seq 5): four agents (NAOMI, AMoS, HOLDEN, MILLER) could not attribute the failure because the check-run annotation carried only Process completed with exit code 1, no test name, and no wrapper diagnostic -- because none of check-test-count.js own FAIL messages ever reach anywhere CI-reachable, and the signal-death path had no message to begin with.
Step 1 -- CI re-run diagnostic (requested alongside this task)
No write-capable path to trigger a re-run of PR #400 failed job (run 32305669339, job 96237771055) was available. loadout-git-host-api is Forgejo-only (confirmed via its own --help: no GitHub Actions endpoints, GET-focused, no re-run verb). loadout-push is push/PR-create/update only, no Actions API surface. gh is not on AMoS Bash allowlist (guard-bash denied it outright). Per the task explicit instruction, no empty commit was pushed as a workaround. PR #400 remains blocked pending an operator-triggered re-run or manual log read; this PR does not touch it.
Empirical verification during this task
Confirmed the pre-fix defect is real, not just a plausible reading of the code: stashed the fix, ran npm test against the unmodified script -- the FAIL(below-floor)/FAIL(missing-files) messages already existed in the old code and do fire, but the signal-death branch (process.exit(result.status === null ? 1 : result.status)) genuinely emitted nothing.
Also discovered, empirically, something MILLER diagnosis did not have room to establish: killing a test FROM WITHIN a node --test file (process.kill(process.pid, SIGKILL)) does NOT reproduce the silent signal-death path, because Node own test runner isolates each file into its own subprocess and already reports a killed file as an ordinary test:fail event carrying a signal field -- visible proof: not ok 1 ... signal: SIGKILL ... code: ERR_TEST_FAILURE. The true silent path is one layer up: the top-level node --test orchestrator process itself (the one spawnSync in check-test-count.js directly manages) dying by signal, which a test running inside that same process tree cannot reproduce by killing itself. This is why the regression tests exercise classifyRun() directly with a hand-built result object rather than attempting an end-to-end signal-kill through the real suite -- documented explicitly in the test file header, per this task instruction to say so plainly if a test is not achievable end-to-end.
Tests
Demonstrated-failure verified per lr-4e1242: git stash on scripts/check-test-count.js only, re-ran the new test file through the (then-unmodified) script. Every test in the file failed outright, because the unmodified script exports no classifyRun/emitAnnotation surface at all (require.main === module ran the old top-level body instead) -- require(...).classifyRun is not a function, surfaced as the whole test file crashing. That is the demonstrated failure for the file: there was no unit-testable decision surface before this change, which is itself part of the defect.
npm test: 1439/1439 pass (was 1429 pre-existing + 10 new), 0 fail, exit 0.
Task: lr-e551b9