diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3f952226695..45848367cc5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -106,7 +106,7 @@ EOF tmp=$(mktemp -d) && printf 'done: smoke\n' > "$tmp/smoke.status" && FM_STATE_OVERRIDE="$tmp" FM_SIGNAL_GRACE=1 FM_POLL=1 FM_HEARTBEAT=999999 bin/fm-watch-arm.sh # watcher re-arm smoke test (prints arm status, then an actionable signal) ``` -`bin/fm-test-run.sh` is the single owner of behavior-suite selection, portable CI lane composition, bounded concurrency admission, per-script timing markers, family totals, the coverage guard, and the optional JSON timing artifact. +`bin/fm-test-run.sh` is the single owner of behavior-suite selection, portable CI lane composition, bounded concurrency admission, owned-fixture cleanup, per-script timing markers, family totals, the coverage guard, and the optional JSON timing artifact. Its header and `--help` own the flags, family labels, lanes, and changed-file map; this section only documents the entry points. `bin/fm-test-isolation-proof.sh` remains the single owner of the portable candidate proof and reusable family proof harness; see `docs/fm-test-isolation-proof.md`. Portable shard balance evidence lives in `docs/fm-test-portable-shards.md`. @@ -119,7 +119,7 @@ Those sleeps look like recoverable overhead - `fm-watch-triage.test.sh` alone is Sampling less often does not remove that wait, it only delays detection: raising the interval to 0.5s and charging each sample proportionally measured `fm-watch-triage.test.sh` at 435s and 440s against 390s and 393s for the unchanged script, back to back on 2026-09-03, because each of its ~40 poll-cycle waits and ~73 process-exit waits paid up to half a second more. Some of those loops are also catching a transient rather than waiting for a settled condition, so a coarser sample can step over the state they assert on. Discover tests by listing `tests/*.test.sh`: each is a self-contained bash script named `.test.sh`, and its header comment describes what it covers, so pass one to `bin/fm-test-run.sh` to focus on a subject with canonical timing output. -Shared test helpers live in `tests/lib.sh` (reporters, temp roots, git fixtures), `tests/fixtures.sh` (fake toolchain and spawn-world builders), `tests/wake-helpers.sh`, and `tests/secondmate-helpers.sh`. +Shared test helpers live in `tests/lib.sh` (reporters, temp roots, owned-fixture cleanup, git fixtures), `tests/fixtures.sh` (fake toolchain and spawn-world builders), `tests/wake-helpers.sh`, and `tests/secondmate-helpers.sh`. Source those instead of copying a fake toolchain into a new suite. A fixture may shorten a production timeout to keep a failure path prompt, but never below what the real work inside that window costs on a loaded machine: a fork, an exec, a lock acquisition, a beacon publication, or a first-poll check. Where a case's assertion is not about the timeout itself, give that window headroom over the measured loaded cost, and bound the test's own waiting with iteration-counted poll loops, which stretch under load where a wall-clock budget does not. diff --git a/bin/fm-test-run.sh b/bin/fm-test-run.sh index 649b807bb90..23e6cd392ca 100755 --- a/bin/fm-test-run.sh +++ b/bin/fm-test-run.sh @@ -1,7 +1,8 @@ #!/usr/bin/env bash # fm-test-run.sh - single owner of Firstmate's behavior-test runner, lane # composition for portable CI shards, local --jobs for proven-concurrent work, -# timing markers, and the complete-regression coverage guard. +# owned-fixture cleanup, timing markers, and the complete-regression coverage +# guard. # # Selection modes (exactly one of: --all, --family, --changed, --lane, # --proven-isolated, or script paths): @@ -1997,12 +1998,19 @@ declare -a WORKER_SCRIPTS=() # Invoked indirectly by the EXIT trap below. # shellcheck disable=SC2329 cleanup_run() { + if [ "$RUN_CLEANUP_FAILED" -eq 1 ] || + compgen -G "$RUN_TMP/cleanup-failed.*" >/dev/null 2>&1 || + compgen -G "$RUN_TMP/owned.*/child.*" >/dev/null 2>&1; then + log "preserving owned-child cleanup evidence at $RUN_TMP" + return 0 + fi rm -rf "$RUN_TMP" } trap cleanup_run EXIT RUN_ID="fm-test-run-${RUN_STARTED_MS}-$$" +RUN_CLEANUP_FAILED=0 TOTAL=0 FAILED=0 SKIPPED_GATE=0 @@ -2078,27 +2086,35 @@ record_script_result() { # positive, a script that outruns it is terminated and reported as exit 124: a # hung script must become a bounded failure rather than an unbounded suite, # because an unbounded suite is what silently outruns its caller's budget. +# Each script also receives a private owned-child registry, which is cleaned +# after the script exits; an ambiguous or surviving registered group fails the +# script and keeps the run directory as evidence for a safe follow-up. run_script_bounded() { #