Skip to content

fix(gate): run-tests.sh could not check its own most important precondition - #310

Merged
ZacxDev merged 2 commits into
mainfrom
gate/run-tests-honest-failure
Aug 3, 2026
Merged

fix(gate): run-tests.sh could not check its own most important precondition#310
ZacxDev merged 2 commits into
mainfrom
gate/run-tests-honest-failure

Conversation

@ZacxDev

@ZacxDev ZacxDev commented Aug 3, 2026

Copy link
Copy Markdown
Contributor

1. REQUIRED_TOOLS could not express its own most important precondition

It is a list of binaries checked with command -v. pytest is not a binary this runner calls — it is a module (python -m pytest) — so the one guard whose entire job is "the thing that runs the tests is present" was structurally unable to check pytest. It also asserted python3 while the runner actually invokes python.

Measured on the dev host with every REQUIRED_TOOLS binary present but no pytest importable — all 17 targets printed:

run-tests: ERROR — could not parse pytest's summary for <dir>.

and the run ended TOTAL collected=0 … RESULT: FAIL, exit 1.

Correction to the briefed severity

The gate did not go green, and the briefed "reports per-target PASS with collected=0" does not exist on this revision — GUARD 4 (unparseable summary) and GUARD 3 (the collected floor) both fire. The defect is diagnostic: seventeen copies of a message blaming pytest's output format for a missing dependency, pointing at the wrong subsystem entirely. That is the #276 shape — a real finding that reads like an environment fault.

Now a single named FATAL, exit 2, before any suite starts.

2. declare -a + set -u — a guard's own error path was broken

declare -a RESULTS / declare -a SKIP_LINES leave the arrays declared but unset. Under set -u the first ${#arr[@]} on a still-empty array aborts the command with unbound variable (measured, bash 5.3.15). With zero skips this printed a raw

scripts/run-tests.sh: line 479: SKIP_LINES: unbound variable

where GUARD 2's skip list belonged, and the unpinned-skip loop below it never executed. There is no set -e, so the script continued and the skip-total accounting still fired — the damage was confined to the diagnostic path, at exactly the moment someone is reading why the gate is red. Fixed with NAME=().

Not in the brief; found while reproducing hole 1.

3. "printed RESULT: FAIL and exited 0" — did NOT reproduce

Measured: exit 1. The structure forbids it — RESULT: FAIL is printed only when fail != 0, and the very next statement is exit "$fail", whose only non-zero value is 1. The most likely origin is reading the status through a pipeline (| tail), which yields the last command's status rather than the runner's — the trap the brief itself warns about.

Pinned end-to-end anyway by test_a_failing_run_never_exits_zero, which forces a red run and asserts the two can never disagree — and pytest.fail()s if it could not force one, so it cannot pass vacuously.

Known-bad states — proven LOUD by before/after, not by reading code

state before after
no pytest module 17 misleading "could not parse pytest's summary", exit 1 one named FATAL, exit 2, no suite started
target dir exists but is empty per-directory collected 0 tests floor → RESULT: FAIL unchanged (proven reachable)
typo'd target entry GUARD 5 aborts naming the entry + "does not exist" unchanged (proven reachable)

The last two were already guarded before this PR — nothing had ever proven those paths could fire. Labelled REACHABILITY proofs, not regression coverage.

Measured — both tiers

tier command result
authoritative nix build .#checks.x86_64-linux.pytests 5800 collected / 5799 passed / 1 skipped / 0 failed, RESULT: PASS, exit 0
dev host scripts/tests/test_run_tests_preconditions.py + test_runtime_shebangs.py under nix-shell 15 passed

Delta accounted exactly: baseline 5792 (origin/main 13bc8bd, sandbox) + 8 new tests = 5800. Skips unchanged at 1.

The sandbox caught a defect the dev host could not

First sandbox run was RED (5800 collected / 1 failed). The repo-wide runtime-shebang scanner added by #306 rejected this file: the shim helper wrote its own #!{shutil.which("bash")}. My dev-host run was green because I ran this one file, and the scanner lives in another — a per-file run structurally cannot see a repo-wide guard. Fixed by routing the shim through scripts/testlib/mockbin.write_exec (ec63d11), which is exactly the helper #306 created for this.

Recording it because it is the two-tier lesson landing on the PR that is about two-tier honesty, and because the green I would otherwise have reported was a claim about a subset.

Red/green matrix

scripts/tests/test_run_tests_preconditions.py, base ref origin/main (13bc8bd), measured by swapping origin/main's runner into place:

revision result
origin/main runner 3 failed / 5 passed
this branch 0 failed / 8 passed

The 3 reds are exactly the three regression tests (test_python_is_a_required_tool, test_missing_pytest_module_is_named, test_empty_arrays_are_initialised_assigned). The 5 greens at base are the reachability proofs and controls, which should pass at base because those guards pre-existed — labelled as such in the module docstring rather than counted as coverage for this change.

Control pairs

  • test_the_runner_copy_helper_actually_works — positive control on the mutation helper. Every known-bad-state test rewrites HERMETIC_TARGETS in a copy; if that regex stopped matching, the copies would silently be the unmodified runner and each test would exercise the real 17-target list instead. It asserts the rewrite both applies and takes effect (all 1 hermetic target(s) resolve).
  • test_set_u_empty_array_mechanism_holds — positive control on the bash behaviour the source pin describes. Asserts both halves: declare -a errors, =() succeeds. Without it the pin could guard a non-issue on a bash where neither form errors.
  • test_a_failing_run_never_exits_zero fails loudly if it cannot force a red run, rather than passing on an absence.

Also found, deliberately NOT fixed

scripts/claude-hooks/tests/test_bash_guard.py prints its own verdict line as RESULT: all good, which collides with the runner's own RESULT: PASS/RESULT: FAIL. Any grep -E "RESULT:" over a gate log returns two different subsystems' verdicts interleaved, and the hook test's line appears before the runner's. It cost real confusion while measuring this PR. Left alone because renaming a hook test's output is orthogonal to this change and belongs with whoever owns those scripts — flagging rather than silently widening the diff.

Audit of the remaining exit paths

Asked the wider question — what else can this runner do that produces reassuring output without having tested anything? Findings beyond the above:

  • run_pytest is called in the parent shell (not a subshell), so fail=1 genuinely persists; no lost-status hazard there.
  • The HOOK_TESTS loop's || continue was already converted to a loud failure by a prior PR; verified still loud.
  • _count_of returns 0 for an absent word, so a summary naming only failed still yields collected >= 1 and cannot silently pass the per-directory floor.
  • --check-targets deliberately zeroes missing_tools and now also bypasses the pytest precondition — correct (it runs no tests), and pinned by test_check_targets_is_cheap_and_runs_no_tests from the earlier PR.

Files touched

  • scripts/run-tests.sh
  • scripts/tests/test_run_tests_preconditions.py (new)

No overlap with nix/home.nix, scripts/bar-status-poll, or scripts/collector/**.

🤖 Generated with Claude Code

ZacxDev and others added 2 commits August 3, 2026 00:36
…dition

Two measured holes, plus an honest correction to a third.

1. REQUIRED_TOOLS could not express the precondition that matters most.
   It is a list of BINARIES checked with `command -v`. pytest is not a binary
   this runner calls — it is a MODULE (`python -m pytest`) — so the one guard
   whose entire job is "the thing that runs the tests is present" was
   structurally unable to check pytest. It also asserted `python3` while the
   runner actually invokes `python`.

   MEASURED on the dev host with every REQUIRED_TOOLS binary present but no
   pytest importable: all 17 targets printed

       run-tests: ERROR — could not parse pytest's summary for <dir>.

   and the run ended `TOTAL collected=0 … RESULT: FAIL`, exit 1.

   CORRECTION, stated because it changes the severity: the gate did NOT go
   green, and the briefed "reports per-target PASS with collected=0" does not
   exist on this revision — GUARD 4 (unparseable summary) and GUARD 3 (the
   collected floor) both fire. The defect is DIAGNOSTIC: seventeen copies of a
   message blaming pytest's OUTPUT FORMAT for a missing dependency, pointing at
   the wrong subsystem. That is the #276 shape — a real finding that reads like
   an environment fault. Now one named FATAL, exit 2, before any suite runs.

2. `declare -a RESULTS` / `declare -a SKIP_LINES` leave the arrays DECLARED BUT
   UNSET. Under `set -u` the first `${#arr[@]}` on a still-empty array aborts
   the command with "unbound variable" (measured, bash 5.3.15). With zero skips
   this printed a raw

       scripts/run-tests.sh: line 479: SKIP_LINES: unbound variable

   where GUARD 2's skip list belonged, and the unpinned-skip loop below it never
   executed. No `set -e`, so the script continued and the skip-TOTAL accounting
   still fired — the damage was confined to the DIAGNOSTIC path, at exactly the
   moment someone is reading why the gate is red. Fixed with `NAME=()`.

3. "printed RESULT: FAIL and exited 0" did NOT reproduce.
   Measured: exit 1. The structure forbids it — `RESULT: FAIL` is printed only
   when `fail != 0` and the next statement is `exit "$fail"`, whose only
   non-zero value is 1. The likely origin is reading the status through a
   pipeline (`| tail`), which yields the last command's status rather than the
   runner's. Pinned end-to-end anyway by
   `test_a_failing_run_never_exits_zero`, which forces a red run and asserts the
   two can never disagree — and pytest.fail()s if it could not force one, so it
   cannot pass vacuously.

Known-bad states, each proven LOUD by before/after rather than by reading code:

  no pytest module   before: 17 misleading errors, exit 1
                     after:  one named FATAL, exit 2, no suite started
  empty target dir   fails on the per-directory `collected 0 tests` floor
  typo'd target      GUARD 5 aborts naming the entry and saying "does not exist"

The last two were already guarded before this PR; nothing had ever proven those
paths could fire, so they are labelled REACHABILITY proofs, not regression
coverage.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ed shebang

The repo-wide runtime-shebang scanner (#306, scripts/tests/
test_runtime_shebangs.py) failed this file IN THE SANDBOX ONLY. The dev-host
run was green because I ran this ONE file, and the scanner lives in another —
a per-file run structurally cannot see a repo-wide guard.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@ZacxDev

ZacxDev commented Aug 3, 2026

Copy link
Copy Markdown
Contributor Author

Gated on the MERGED tree, not just this branch

Per RULES.md — a PR green on its own branch proves nothing about the tree its merge creates. Built integration/309-310 off origin/main (13bc8bd), merged both #309 and #310, and ran the full authoritative gate there:

check result
nix build .#checks.x86_64-linux.pytests 5810 collected / 5809 passed / 1 skipped / 0 failedRESULT: PASS
nix build .#checks.x86_64-linux.nodetests 997 tests / 997 pass / 0 fail (3 suites, 29 files) — RESULT: PASS

Combined exit 0.

Delta accounted exactly: 5792 (baseline) + 10 (#309's test_run_node_tests_suites.py) + 8 (#310's test_run_tests_preconditions.py) = 5810. Node: 468 (baseline) + 508 + 21 = 997. No unexplained gap on either.

git merge-tree --write-tree between the two branches exits 0 (branched on the exit code, not a marker grep — merge-tree prints only a tree OID on success and emits no conflict markers either way). The branches also share no files: #309 touches scripts/run-node-tests.sh, flake.nix, scripts/tests/test_run_node_tests_suites.py; #310 touches scripts/run-tests.sh, scripts/tests/test_run_tests_preconditions.py. The only semantic coupling is that both add a file under scripts/tests/, which is a HERMETIC_TARGETS entry — and that is what the 5810 total confirms.

@ZacxDev
ZacxDev merged commit 181e23a into main Aug 3, 2026
ZacxDev added a commit that referenced this pull request Aug 3, 2026
… 0 on failure (#312)

#308 asserted that scripts/run-tests.sh "can exit 0 while printing RESULT: FAIL
/ collected=0". That does not reproduce and is structurally impossible on this
revision: `RESULT: FAIL` prints only inside `if [ "$fail" -ne 0 ]`, and the very
next statement is `exit "$fail"`. Measured on 13bc8bd, a run without pytest
importable prints 17 x "could not parse pytest's summary", RESULT: FAIL, and
exits 1 (2 in a second probe). The original report most likely read a status
through a pipeline -- the rc=$? trap RULES.md already names.

The real defect, fixed in #310, is narrower: REQUIRED_TOOLS checks BINARIES via
`command -v`, but pytest is a MODULE, so a missing pytest yielded a diagnostic
blaming pytest's output format instead of a clean precondition failure. It also
asserted `python3` while the runner calls `python`. Bad diagnostics, not a false
green.

This was relayed from a subagent report and repeated to the operator twice
without being tested -- the same class of error as the four corrections the doc
itself records, except this one reached a merged document before anyone checked
it. Left visible as a strikethrough rather than deleted, because the doc's
purpose is recording what was believed and then disproved.

Also updates three items now closed:
  - #309 gated the node suites: 529 ungated tests, not the 508 first reported
    (scripts/collector/browser-ext/tests was ungated too). Node gate 468 -> 997.
  - #311 shipped the telemetry deadman, including the verified property that
    "cannot tell" cannot render as "all healthy" -- deadman returns count=0 for
    both ok and unreachable, so a consumer reading count alone would be wrong.
  - session-create's cause is now known: `session.created` is a bus EVENT TYPE,
    not a plugin hook name, on opencode 1.18.4. Consequence: 2,736 of 2,799
    tool-call rows carry session=''. Not fixed -- that file's last edit killed
    11 hours of telemetry.

Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant