Context
While closing out the review round on #293, two new gates (test/hooksembedrecursivecheck.sh,
test/shimselfunverifiedcheck.sh) landed with an ok() missing the house ; return 0; tail —
test/gateexitcheck.sh's own (G1) check caught it, because that omission makes a failed write
inside ok() report the ARM as a shell-level failure to any cmd && ok ... || no ... chain instead
of going through the fail= accumulator.
That's the second time this exact shape of bug has been introduced by hand-copying ok()/no()
into a new gate file. A quick audit turned up two pre-existing instances of the same bug already
live on main, in test/pargatescheck.sh and test/type3clonecheck.sh — unrelated to #293, not
touched here, flagging for visibility.
What we found
ok()/no() are defined inline, per-file, in 624 of test/*.sh. The landscape:
- 591/624 share one byte-identical dominant shape:
ok(){ printf ' PASS %s\n' "$*" || { fail=1; printf ' FAIL could not write the PASS line for: %s\n' "$*"; }; return 0; }
no(){ printf ' FAIL %s\n' "$*"; fail=1; }
- 27 use
echo+$1 instead of printf+$* — spot-checked a sample; every call site passes a
single quoted string, so $1≡$* there. Looks safe to normalize.
- 2 (
test/jsoncheck.sh, test/jsonparitycheck.sh) redirect to stderr deliberately — several
arms capture a verb's raw --json payload from stdout, and PASS/FAIL text there would corrupt it.
A real, intentional variant, not something to collapse away.
- 1 (
test/lb3namecheck.sh) uses FAIL (uppercase) as its accumulator, self-consistently
throughout that file. Also a deliberate override, not a bug.
- 2 (
test/pargatescheck.sh, test/type3clonecheck.sh) already carry the missing-return 0
bug today, live on main.
Why we stopped short of just fixing it
Two existing anti-regression checks hard-assume ok()/no() are defined inline, in the same
file, not sourced from a shared helper:
test/gateexitcheck.sh's (G1) check globs test/*.sh (non-recursive) for literal ok(){...}
definitions and attributes each one's own behavioral contract back to the file containing it. If
the dominant shape moved out of 591 files into one sourced helper, the set of inline definitions
it walks would collapse toward empty — tripping the gate's own "no ok() definition found
anywhere — the sweep is inert" guard. Not a false positive: a real "this scan can no longer see
anything" condition.
test/manifestcheck.sh's (I2) check independently scans every test/*.sh for a call to
ok/no/pass/warn and requires a same-file definition, specifically to catch "calls a
helper it never defines → silent no-op, exits 0 anyway." Every migrated file would trip this too.
Both checks have their own history/comment explaining a real bug they once caught, so weakening
them to special-case a sourcing line is itself a design decision with a real chance of quietly
reopening what they guard against — and neither is a small patch; both need new logic to resolve
. ".../gateresult.sh" and correctly attribute coverage across ~600 files from one shared
definition.
Question for the maintainers
Is this worth doing, and if so, in what shape? Options as we see them:
- (a) Teach
gateexitcheck.sh and manifestcheck.sh to recognize a sourced
test/lib/gateresult.sh (or similar — test/lib/ already holds headbinlib.sh,
unset-agent-env-variables.sh) as equivalent coverage, verified on a couple of hand-migrated
gates first, then do the ~624-file mechanical migration as a second, separate change.
- (b) Leave it as-is — 624 files of controlled duplication with two live static checks against
drift is arguably an acceptable, if unusual, tradeoff, and the actual bug rate (2 known instances
in the project's whole history) may not justify the harness-logic risk.
- (c) Something else — happy to take direction on the shape you'd want if this is worth doing
at all.
Happy to do the work either way once there's a steer; didn't want to force a specific migration
design without checking first, given it touches nearly every test file in the tree.
🤖 Generated with Claude Code
Context
While closing out the review round on #293, two new gates (
test/hooksembedrecursivecheck.sh,test/shimselfunverifiedcheck.sh) landed with anok()missing the house; return 0;tail —test/gateexitcheck.sh's own (G1) check caught it, because that omission makes a failed writeinside
ok()report the ARM as a shell-level failure to anycmd && ok ... || no ...chain insteadof going through the
fail=accumulator.That's the second time this exact shape of bug has been introduced by hand-copying
ok()/no()into a new gate file. A quick audit turned up two pre-existing instances of the same bug already
live on main, in
test/pargatescheck.shandtest/type3clonecheck.sh— unrelated to #293, nottouched here, flagging for visibility.
What we found
ok()/no()are defined inline, per-file, in 624 oftest/*.sh. The landscape:ok(){ printf ' PASS %s\n' "$*" || { fail=1; printf ' FAIL could not write the PASS line for: %s\n' "$*"; }; return 0; }no(){ printf ' FAIL %s\n' "$*"; fail=1; }echo+$1instead ofprintf+$*— spot-checked a sample; every call site passes asingle quoted string, so
$1≡$*there. Looks safe to normalize.test/jsoncheck.sh,test/jsonparitycheck.sh) redirect to stderr deliberately — severalarms capture a verb's raw
--jsonpayload from stdout, and PASS/FAIL text there would corrupt it.A real, intentional variant, not something to collapse away.
test/lb3namecheck.sh) usesFAIL(uppercase) as its accumulator, self-consistentlythroughout that file. Also a deliberate override, not a bug.
test/pargatescheck.sh,test/type3clonecheck.sh) already carry the missing-return 0bug today, live on main.
Why we stopped short of just fixing it
Two existing anti-regression checks hard-assume
ok()/no()are defined inline, in the samefile, not sourced from a shared helper:
test/gateexitcheck.sh's (G1) check globstest/*.sh(non-recursive) for literalok(){...}definitions and attributes each one's own behavioral contract back to the file containing it. If
the dominant shape moved out of 591 files into one sourced helper, the set of inline definitions
it walks would collapse toward empty — tripping the gate's own "no ok() definition found
anywhere — the sweep is inert" guard. Not a false positive: a real "this scan can no longer see
anything" condition.
test/manifestcheck.sh's (I2) check independently scans everytest/*.shfor a call took/no/pass/warnand requires a same-file definition, specifically to catch "calls ahelper it never defines → silent no-op, exits 0 anyway." Every migrated file would trip this too.
Both checks have their own history/comment explaining a real bug they once caught, so weakening
them to special-case a sourcing line is itself a design decision with a real chance of quietly
reopening what they guard against — and neither is a small patch; both need new logic to resolve
. ".../gateresult.sh"and correctly attribute coverage across ~600 files from one shareddefinition.
Question for the maintainers
Is this worth doing, and if so, in what shape? Options as we see them:
gateexitcheck.shandmanifestcheck.shto recognize a sourcedtest/lib/gateresult.sh(or similar —test/lib/already holdsheadbinlib.sh,unset-agent-env-variables.sh) as equivalent coverage, verified on a couple of hand-migratedgates first, then do the ~624-file mechanical migration as a second, separate change.
drift is arguably an acceptable, if unusual, tradeoff, and the actual bug rate (2 known instances
in the project's whole history) may not justify the harness-logic risk.
at all.
Happy to do the work either way once there's a steer; didn't want to force a specific migration
design without checking first, given it touches nearly every test file in the tree.
🤖 Generated with Claude Code