refactor(scripts): unify test-harness scaffolding in scripts/lib/test-harness.sh - #3248
Conversation
…-harness.sh Add a sourced-only library that owns ok/fail/report and the exit contract, then migrate the 23 PASS/FAIL-dialect suites onto it. TAP-ish suites stay on their own dialect this pass. The harness suite asserts a recorded failure cannot exit 0 when report is the last command. Closes #3160 Co-authored-by: Kyle Sexton <kyle-sexton@users.noreply.github.com>
|
Claude finished @kyle-sexton's task in 1m 2s —— View job Security review
No security issues found. This PR is a pure test-scaffolding refactor: it consolidates duplicated
No CI-internal, non-production test tooling with no attacker-controlled inputs and no workflow permission/trigger changes — nothing in this diff crosses a trust boundary. |
|
Last security-reviewed head: |
|
Claude finished @kyle-sexton's task in 3m 47s —— View job Code review complete
No blocking findings. This is a clean, mechanical refactor. What I checked
Not independently verifiedThe PR description's |
|
Claude has reviewed this PR 1 time. The lane skips further automatic reviews after 5; deleting this comment resets the count. |
Closes #3268 ## Summary `scripts/lib/test-harness.sh` owns the pass/fail exit contract for the `scripts/*.test.sh` suites. Its self-test did not cover EXIT-trap interaction, which is why the report is called rather than installed as a trap, and re-sourcing silently zeroed the counters so a recorded failure could exit 0. ## Fix - Add a `TEST_HARNESS_SOURCED` idempotency guard so a second source (direct or transitive) returns without resetting `_test_harness_pass` / `_test_harness_fail`. - Extend `scripts/lib/test-harness.test.sh` to pin: pre-source and post-source cleanup traps still run and cannot turn a recorded failure into exit 0; a failing cleanup does not turn a clean suite red; a succeeding cleanup does not mask a failure; the summary prints before the caller's EXIT trap; printf format specifiers (`%s`, `%d`, `%q`) print verbatim; the summary prints once despite subshells and subshells do not disturb status; counters accumulate across repeated calls; an empty suite exits 0; re-sourcing preserves a recorded failure. ## Verification - `bash scripts/lib/test-harness.test.sh` — PASS=16 FAIL=0 - `bash scripts/check-silent-revert.test.sh` — PASS=130 FAIL=0 (installs an EXIT trap and sources the harness) - `bash scripts/check-skill-portability.test.sh` — PASS=92 FAIL=0 (installs an EXIT trap and sources the harness) - `shellcheck --rcfile=.shellcheckrc -x` and `shfmt -d` clean on both files - `bash -n` clean on both files ## Related - Refs #3248 — the shared harness this holds to its EXIT-trap design - Refs #3160 — original unification brief Co-authored-by: Kyle Sexton <kyle-sexton@users.noreply.github.com>
Closes #3283 ## Summary `scripts/lib/test-harness.test.sh` sources the library it tests, so a break in `test_harness::report`'s return path or in `fail()` counter arithmetic is detected by the suite's assertions and then cannot be reported to CI. The process exit status is produced by the code under test. Two mutations against a throwaway copy of the harness demonstrate it: deleting `return 1` from report, and removing the increment from `fail()`. Both print `FAIL:` lines and still exited 0. Checking `_test_harness_fail` after report catches the first mutation but not the second. ## Fix - Parent assertions go through `self_ok`/`self_fail`, which keep an independent pass/fail tally the harness functions do not write. - After `test_harness::report`, a non-zero independent tally forces exit 1 even if the harness counters or return were sabotaged. - The last-line discipline still requires every other sourcer to end on `test_harness::report`; this self-test is the one file allowed to follow the call. - A throwaway-copy mutation regression runs the self-test against both defects and asserts it still exits non-zero. ## Verification - `bash scripts/lib/test-harness.test.sh` — PASS=18 FAIL=0 (includes both mutation cases) - `bash scripts/check-silent-revert.test.sh` — PASS=130 FAIL=0 (installs an EXIT trap and sources the harness) - `bash scripts/check-skill-portability.test.sh` — PASS=92 FAIL=0 (installs an EXIT trap and sources the harness) - `shellcheck --rcfile=.shellcheckrc -x` and `shfmt -d` clean on `scripts/lib/test-harness.test.sh` - `bash -n` clean on `scripts/lib/test-harness.test.sh` ## Related - Refs #3268 — EXIT-trap and re-source gaps that cannot report a sabotaged report/counter - Refs #3271 — those cases landed; they detect both mutations and still cannot report them - Refs #3248 — the shared harness this self-test holds to its exit contract Co-authored-by: Kyle Sexton <kyle-sexton@users.noreply.github.com>
Closes #3160
Summary
The shell suites under
scripts/re-declaredPASS/FAILcounters,ok()/fail(), and a trailing summary in eight spellings. The copies were free to disagree on the exit contract: a suite that printed failures could still exit 0, and CI would treat that as green.Fix
scripts/lib/test-harness.shis a sourced-only library (no shebang, mode100644) that ownsok,fail,test_harness::report, and the exit status. The shape is called, not anEXITtrap — bash has one trap slot, and the suites with fixture cleanup already use it. Each migrated suite's last line istest_harness::report.This pass migrates the 23 suites that already shared the
ok:/FAIL:/PASS/FAILdialect. TAP-ish suites stay on their own dialect; incremental coexistence is in the brief. Plugin*.test.shhelpers stay duplicated on purpose (docs/conventions/shell-test-helpers/README.md).scripts/lib/test-harness.test.shasserts that a recorded failure plus report cannot exit 0, and that every suite sourcing the harness ends with the report call. The suite is wired intoplugin-gatenext to the otherscripts/libtests.Fixture
stage_libshelpers are unchanged: the gates they copy do not source the harness.Verification
bash scripts/lib/test-harness.test.shshellcheck --rcfile=.shellcheckrc -xandshfmt -dclean on the new library and its suite.bash -nclean across every migrated file.Related
stage_libsfixture pattern this pass does not widen.