fix: fail suites that skip assertions; document bare-name helper calls #431
No reviewers
Labels
No labels
Agent
Compat/Breaking
Kind/Bug
Kind/Documentation
Kind/Enhancement
Kind/Feature
Kind/Security
Kind/Testing
Priority
Critical
Priority
High
Priority
Low
Priority
Medium
Reviewed
Confirmed
Reviewed
Duplicate
Reviewed
Invalid
Reviewed
Won't Fix
Status
Abandoned
Status
Blocked
Status
Need More Info
No milestone
No assignees
2 participants
Notifications
Total time spent: 7 minutes 47 seconds
Due date
igor
7 minutes 47 seconds
No due date set.
Dependencies
No dependencies set
Reference
joshtronic/igor!431
Loading…
Reference in a new issue
No description provided.
Delete branch "fix/430-suite-guard-and-bare-name-helpers"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
What this PR does
Both halves of #430.
bin/test-*.shsuite that reported a pass while skipping an assertionagent-*.shhelpers must be called by bare name1. The dropped work item
The suites run under
set -uo pipefail-- deliberately without-e, since assertions invoke failing commands on purpose -- and verdict from aFAILcounter each helper increments. A line that never executes cannot increment it, so a mistyped helper name is a skipped assertion the suite reports as a pass.That is not hypothetical. Three assertions in
bin/test-forgejo.shwere written ashas ...in a suite defining onlyeq. They died ashas: command not foundon stderr while the suite printedall checks passedand exited 0, and CI was green on a third of the block they were added for.Fixed at the runner, not per-suite, so it covers all 25 existing suites and every future one without touching any of them.
check-sync.shcaptures each suite's combined output and fails it when the shell reported a line it refused to run:check-syncexits 1 on that, where before it exited 0.The predicate lives in
lib/suite-guard.shwith its own unit tests. Those fixtures are real captured bash output from real sub-shells, not hand-written strings, so the patterns stay pinned to what bash actually emits rather than to what the test assumes. The suite also asserts its own premise -- that the fixture really does exit 0 and really does print its success banner -- so it can't quietly stop reproducing the bug it guards.The match is anchored to bash's
<file>: line N: <cmd>: ...diagnostic shape, so a suite whose assertion TEXT contains "command not found" (one testing error handling) does not trip it. There are explicit negative tests for that.2. Why the follow-up was lost
The ticket diagnoses this as a capability gap. It is not one, and I could not find a denial anywhere in the journal -- only the session's own prose. The evidence is in what it typed:
agent-settings.jsonalready allowsBash(agent-ask.sh:*), andtick.shputs the harnessbin/onPATHprecisely so these are callable by name. The grant matches the bare name; abin/prefix does not match it, andtick.sheven carries a comment warning about this exact failure mode. The session used the path form, was refused, concluded the capability was unavailable, and its drafted ticket evaporated.That requirement lived only in a
tick.shcode comment -- nowhere a session ever reads.AGENTS.mdnow states it, with the failure mode and an instruction to retry by bare name before concluding a capability is missing, and to surface an unfinished side-effect as an explicit unfinished item rather than burying it in prose.No permission changes. Nothing here grants the agent anything it did not already have -- deliberately, since a guardrail change is not mine to self-authorize.
Test plan
bash bin/test-suite-guard.shpasses -- 12 assertionsmake testpasses (25 suites)make lintexits 0shellcheck lib/suite-guard.sh bin/test-suite-guard.sh bin/check-sync.shcleancheck-sync.shnow fails it and names the line (output above). Seeded file removed.Left for you
The one thing I did not do is add path-form entries (
Bash(bin/agent-ask.sh:*)) to the allowlist. It would be defence in depth against the same mistake, but it is a permissions edit and that is your call, not mine to make and merge.Closes #430
🤖 Review —
REQUEST_CHANGES(automated)CI for
e840658f: successRunner-level guard that fails a
bin/test-*.shsuite which exited 0 while bash refused to run one of its lines, plus an AGENTS.md note about bare-name helper calls. The idea and the guard library are good; the runner integration introduces a regression on the failing-suite path.Blocking
bin/check-sync.shruns underset -euo pipefail, and the new capture is no longer in a condition context.A plain assignment whose RHS is a command substitution takes the substitution's exit status, and
set -efires on it. So when a suite genuinely fails (rc != 0):suite_rc=$?never runs;printf '%s\n' "$suite_out"never runs, so the failing suite's output is discarded entirely — the human sees check-sync exit nonzero with no indication of which suite failed or why;x $t failedbranch is now dead code;The old
if bash "$t"; then ... else ... fiwasset -e-exempt precisely because it was a condition. This is a strict regression in the failure path — the path this PR exists to make more trustworthy.CI is green only because every suite passes today, so the branch is never taken. The description's end-to-end check ("seeded a suite with a typo'd helper") exercised only the exit-0 path, which is why it wasn't caught.
Fix, safe under
set -e:Please also add coverage that a suite exiting nonzero (a) still prints its output, (b) is reported as
x $t failed, and (c) does not abort the runner — otherwise the same hole reopens. Notebin/test-suite-guard.shitself exits 1 on assertion failure, so it hits this path too.Non-blocking observations
lib/suite-guard.shpatterns cover onlycommand not foundandunbound variable. Other shell diagnostics that mean "a line didn't run" —syntax error near unexpected token,No such file or directory,Permission denied,bad substitution— slip through. Worth a comment stating the deliberate scope, or extending the alternation.printf '%s\n' "$suite_out"emits a stray blank line for a silent suite. Cosmetic.line N:shape holds up. Also verified the guard suite doesn't print fixture text on its success path, so it can't trip the runner guard on itself.Contract
Independent review by the harness on
claude-opus-5(effort: high). The human reviewer is requested once Igor has reviewed; a human still merges.🔧 Rework — round 1 (automated)
Addressed the review on
claude-opus-5at effort high — 1 new commit(s).🤖 Review —
COMMENT(automated)CI for
c8b2e3dc: successRunner-level guard (
lib/suite-guard.sh) that fails abin/test-*.shsuite which exits 0 while bash reported a line it refused to run, wired intobin/check-sync.sh, plus an AGENTS.md note about calling harness helpers by bare name. CI is green and I found no defect I'd block on — but two things I can't verify from the diff keep me off APPROVE.Things I could not verify (the reason this isn't an approve)
bin/check-sync.sh, but theMakefileisn't in the diff. Ifmake testhas its own loop overbin/test-*.shrather than delegating tocheck-sync.sh, the exact hole described in the issue (CI green onbin/test-forgejo.sh) stays open in the place it was observed. Please confirmmake test/CI goes throughcheck-sync.sh.bash bin/test-suite-guard.shpasses -- 12 assertions": I count ~18 discrete pass/fail checks inbin/test-suite-guard.sh(typo trip, premise rc==0, banner, unbound, clean, empty, 3 negatives, offender named, count==1, failing rc, failing output preserved,x ... failedverdict, loop survivesset -e, passing rc,+ ... passedverdict, silent-suite single line). And "make testpasses (25 suites)" — this PR adds a 26thbin/test-*.sh. Both look like stale numbers rather than missing work (the work is all present in the diff), but the human trusts these lines, so they should be corrected.Non-blocking findings
lib/suite-guard.sh:39— false-positive surface. Any suite that captures a sub-shell's stderr and echoes it verbatim (bash: line 1: foo: command not found) will now be failed even though nothing was skipped. The negative tests inbin/test-suite-guard.shonly cover text lacking theline N:prefix, so they don't pin this case. Currently harmless (25 suites pass), but it's a trap for a future suite testing error handling; worth a sentence in the lib comment.lib/suite-guard.sh:73-80— when a suite both exits nonzero and emitted a skipped-line diagnostic, the earlyreturn 1onrc -ne 0means the offending line is never named. That's the diagnostics-poor case; consider reporting skipped lines regardless ofrc.lib/suite-guard.sh:62-68— the comment justifies|| rc=$?by "the runner sources this underset -e". Incheck-sync.shthe call issuite_run_report "$t" || FAIL=1, i.e. a condition context, where errexit is already suppressed for the entire call chain. The defensive form is still correct (bin/test-suite-guard.shcalls it bare), but the stated rationale is off.Positives
<file>: line N: <cmd>: ...diagnostic shape rather than a bare substring, with explicit negative tests — the right call.set -eloop-abort case is explicitly tested (LOOP_OUT), which is the subtle part of moving the capture into a function.bash "$suite"so the missing exec bit doesn't matter;mktemp -d+trap ... EXITcleanup is correct.Independent review by the harness on
claude-opus-5(effort: high). The human reviewer is requested once Igor has reviewed; a human still merges.