Bound shell test suites so a hang reads as a failure - #110
Merged
b-macker merged 1 commit intoJul 31, 2026
Conversation
run-all-tests.sh invokes 94 nested suites as bare `bash <script>`. Unbounded, one hung suite stalls the entire run until the CI runner gives up: the Windows job spent ~50 minutes in `CLI tests` and produced no logs at all, because a killed runner never uploads them. The failure was indistinguishable from a runner fault, which is how it got misread as one. All 94 sites now go through run_shell_test(), which passes arguments to bash unchanged and returns the suite's exit status unchanged — every pass/fail branch behaves exactly as before, only the bound is new. SIGTERM first, SIGKILL after a grace period, in that order deliberately. The suites install EXIT traps that kill stub servers and restore the trust store, and TERM lets those run; verified directly, a trap-installing suite prints its cleanup before dying. -k exists for the case that motivated this — a native Windows binary under MSYS2 that never sees TERM — and a TERM-ignoring suite dies at the grace deadline rather than running to completion. The bound is 600s, ~6x the measured ceiling. Timing all 99 suites over a full run put the median under 1s and the tail almost entirely on one suite: test_prescan_canaries.sh at 95s, then 29s, 21s, 19s. Sampling before the canary suite runs shows a ceiling of 29s and invites a 300s bound that a slower runner would trip on a healthy run; the comment records why not to tighten it there. test_absorption_degenerate.sh is deliberately excluded. It already carries its own 120s bound and its own policy of SKIPPING on timeout rather than failing — routing it through the wrapper would convert a knowingly tolerated flake into a red build. Full suite: 441 tests, 0 unexpected failures. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01ELUfjXZvx8kzXo1UJjrAhC
NAAb Governance Report
All governance checks passed! Generated by NAAb Governance Engine v4.0 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
run-all-tests.shinvokes 94 nested test suites as barebash <script>. Unbounded, one hung suite stalls the entire run until the CI runner gives up — the Windows job on #109 spent ~50 minutes inCLI testsand produced no logs at all, because a killed runner never uploads them.That is the worst shape a CI failure can take: expensive, and indistinguishable from a runner fault. It got misread as a real regression in the change under test, and only a re-run settled it. A hang is a test failure and should read like one.
Changes
run-all-tests.sh— newrun_shell_test()wrapper; all 94if bash "$X_SCRIPT"call sites converted mechanically toif run_shell_test "$X_SCRIPT". Arguments pass tobashunchanged and the suite's exit status returns unchanged, so every pass/fail branch behaves exactly as before — only the bound is new.-kexists for the case that motivated this: a native Windows binary under MSYS2 that never sees TERM, where plaintimeoutwaits forever.timeoutpresence and-ksupport are probed once, with graceful fallback.timeoutis already a hard dependency ofrun_test()for.naabtests, but-kis not universal (older busybox), and this wrapper should not be the thing that breaks an exotic platform.SHELL_TEST_TIMEOUT/SHELL_TEST_KILL_AFTER.Choosing the bound
Timed all 99 suites over a full run rather than guessing. Median under 1s, 29 suites above 10s, and a tail set almost entirely by one suite:
600s is ~6x the 95s ceiling, which holds even if a Windows runner is 2–3x slower than the machine measured. A partial sample taken before the canary suite runs shows a ceiling of 29s and invites a 300s bound that a slow runner would trip on a perfectly healthy run — the comment in the file records why not to tighten it there, since a bound that fails on healthy runs just teaches everyone to raise it.
Deliberately excluded
test_absorption_degenerate.shalready carries its own tighter 120s bound and its own policy of SKIPPING on timeout rather than failing — a knowingly tolerated flake. Routing it through this wrapper would silently convert it into a red build, so it is left alone and the reason is recorded.Test Plan
bash run-all-tests.shwith no new failures — 441 tests, 0 unexpected failures (376 passed, 52 error-behavior, 1 missing-executor, 12 needs-tree-walk)The timeout path was exercised rather than assumed. Driving the real wrapper (extracted verbatim) against synthetic suites at a 3s bound:
-k, not at 300sThe last two carry the design claims: the trap case is what keeps stub servers from leaking, and the TERM-ignoring case is the MSYS2 scenario this change exists for.
Related Issues
Follow-up to #109, whose Windows job surfaced the gap.
Generated by Claude Code