Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`.
Expand All @@ -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 `<subject>.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.
Expand Down
46 changes: 39 additions & 7 deletions bin/fm-test-run.sh
Original file line number Diff line number Diff line change
@@ -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):
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -2078,34 +2086,58 @@ 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() { # <script> <out> <stream> <id>
local script=$1 out=$2 stream=$3 id=$4
local rc
: "$id"
local rc cleanup_rc registry="$RUN_TMP/owned.$id"
if ! mkdir -p "$registry"; then
printf 'not ok - %s could not create its owned-child registry\n' "$script" >>"$out"
return 1
fi
set +e
if [ "$stream" -eq 1 ]; then
if [ "$PER_SCRIPT_TIMEOUT_SECS" -gt 0 ]; then
# Expansion is intentionally deferred to the child bash passed to -c.
# shellcheck disable=SC2016
fm_run_timed "$PER_SCRIPT_TIMEOUT_SECS" bash -c \
'bash "$1" 2>&1 | tee "$2"; exit "${PIPESTATUS[0]}"' _ "$script" "$out"
'FM_TEST_OWNED_CHILD_REGISTRY=$1 bash "$2" 2>&1 | tee "$3"; exit "${PIPESTATUS[0]}"' \
_ "$registry" "$script" "$out"
rc=$?
else
bash "$script" 2>&1 | tee "$out"
FM_TEST_OWNED_CHILD_REGISTRY="$registry" bash "$script" 2>&1 | tee "$out"
rc=${PIPESTATUS[0]}
fi
elif [ "$PER_SCRIPT_TIMEOUT_SECS" -gt 0 ]; then
fm_run_timed "$PER_SCRIPT_TIMEOUT_SECS" bash "$script" >"$out" 2>&1
fm_run_timed "$PER_SCRIPT_TIMEOUT_SECS" env \
FM_TEST_OWNED_CHILD_REGISTRY="$registry" bash "$script" >"$out" 2>&1
rc=$?
else
bash "$script" >"$out" 2>&1
FM_TEST_OWNED_CHILD_REGISTRY="$registry" bash "$script" >"$out" 2>&1
rc=$?
fi
if [ "$PER_SCRIPT_TIMEOUT_SECS" -gt 0 ] && [ "$rc" -eq 124 ]; then
printf 'not ok - %s exceeded the per-script bound of %ss and was terminated\n' \
"$script" "$PER_SCRIPT_TIMEOUT_SECS" >>"$out"
[ "$stream" -eq 1 ] && tail -1 "$out"
fi
if [ -r "$ROOT/tests/lib.sh" ]; then
bash "$ROOT/tests/lib.sh" owned-children-cleanup "$registry" >>"$out" 2>&1
cleanup_rc=$?
elif compgen -G "$registry/child.*" >/dev/null; then
printf 'fm-test: owned-child registry is populated but tests/lib.sh is unavailable\n' >>"$out"
cleanup_rc=1
else
cleanup_rc=0
fi
if [ "$cleanup_rc" -ne 0 ]; then
RUN_CLEANUP_FAILED=1
: > "$RUN_TMP/cleanup-failed.$id"
printf 'not ok - %s left a registered fixture process or process group alive\n' "$script" >>"$out"
[ "$stream" -eq 1 ] && tail -1 "$out"
[ "$rc" -ne 0 ] || rc=1
fi
return "$rc"
}

Expand Down
2 changes: 1 addition & 1 deletion docs/scripts.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ The shared no-mistakes gate refusal for fleet lifecycle entrypoints is summarize
| `fm-install-herdr.sh` | Install CI's exact-version Herdr pin with official asset URL, SHA-256, and protocol checks |
| `fm-install-treehouse.sh`| Install CI's exact-version Treehouse pin for real-Herdr E2E that needs spawn worktrees |
| `fm-herdr-ci-cleanup.sh` | Snapshot and tear down only job-owned `fm-lab-*` sessions in the Herdr CI lane |
| `fm-test-run.sh` | Behavior-test runner: selection, portable lanes, bounded concurrency, budgets, coverage guard, timing/JSON |
| `fm-test-run.sh` | Behavior-test runner: selection, portable lanes, bounded concurrency, budgets, owned-fixture cleanup, coverage guard, timing/JSON |
| `fm-test-isolation-proof.sh` | Concurrent isolation harness and portable candidate set owner |
| `fm-ensure-agents-md.sh` | Ensure a project's real `AGENTS.md`, its `CLAUDE.md` `@AGENTS.md` pointer, and the canonical self-governance section |
| `fm-guard.sh` | Warn on primary-checkout tangles, main-session pending wakes, and unhealthy supervision |
Expand Down
Loading
Loading