diff --git a/plugins/autonomy/CHANGELOG.md b/plugins/autonomy/CHANGELOG.md index 240ba5fdc1..8554915578 100644 --- a/plugins/autonomy/CHANGELOG.md +++ b/plugins/autonomy/CHANGELOG.md @@ -18,6 +18,11 @@ merged work-package PRs (#333, #343, #356, #372, #377, #600, #676). now carries all three (verdict aggregation under the unanimity invariant including the timeout and no-verdict cases, refusing to count two checkers that resolve to one instance, and lens drawing), each stated as a hole until the build trigger fires. +- **`lane-stop-gate.test.sh` test-hygiene fixes from the #2065 verification pass (#2086).** The FIFO + hang case now fails closed when `mkfifo` is unavailable, uses `timeout` instead of `kill -9` on a + subshell that can orphan a grandchild, and `rc0` rejects an empty exit-code argument. The hook + comment records that `umask 077` is advisory on MSYS and documents the mid-lane downgrade over-gate + window. ## [0.16.2] diff --git a/plugins/autonomy/hooks/lane-stop-gate.sh b/plugins/autonomy/hooks/lane-stop-gate.sh index d1f4ccf37c..8fbdfb9181 100755 --- a/plugins/autonomy/hooks/lane-stop-gate.sh +++ b/plugins/autonomy/hooks/lane-stop-gate.sh @@ -193,6 +193,8 @@ gate_arm_owned() { # a symlink aimed elsewhere decides nothing. Leave it untouched and honor. [[ ! -e "$claim" || (-f "$claim" && ! -L "$claim") ]] || return 0 if [[ -n "$me" ]] && ( + # umask 077 is best-effort on MSYS (the claim may still land 644); the + # exclusive-create is what binds the record, not the file mode. umask 077 set -o noclobber printf '%s\n' "$me" >"$claim" @@ -231,7 +233,9 @@ gate_load_arm_record() { # Compatibility read: a record claimed before the sidecar existed carries its # owner in the record itself and has no claim file. That field stays # authoritative so an upgrade cannot let a second session claim a record - # already bound to a live lane; nothing writes it any more. + # already bound to a live lane; nothing writes it any more. Mid-lane downgrade + # (newer hook → older without sidecar support) can leave a sidecar the older + # hook ignores — over-gating only; re-arm after downgrade. claimed=$(printf '%s' "$json" | jq -r '.session_id // empty' 2>/dev/null) if [[ -n "$claimed" ]]; then [[ -n "$SESSION_ID" && "$claimed" == "$SESSION_ID" ]] || return 1 @@ -369,6 +373,7 @@ marker_identity() { # the next alternative, and a host with neither returns the empty identity this # function documents (#1784) stat -c '%Y %s' -- "$1" 2>/dev/null || + # portability-ok: BSD ladder rung paired with GNU `stat -c` above (#1784) stat -f '%m %z' -- "$1" 2>/dev/null || printf '' } diff --git a/plugins/autonomy/hooks/lane-stop-gate.test.sh b/plugins/autonomy/hooks/lane-stop-gate.test.sh index c6250c4090..ddf53f51b1 100755 --- a/plugins/autonomy/hooks/lane-stop-gate.test.sh +++ b/plugins/autonomy/hooks/lane-stop-gate.test.sh @@ -696,7 +696,10 @@ if [[ "$OUT" == *'LANE-STOP-OK'* ]]; then ok "the nudge names the default token, # The claim-ownership cases below assert the exit code alongside the decision. # A Stop hook that starts exiting non-zero — 127 for a helper that stopped # resolving — is a regression a stdout-only assertion passes over in silence. -rc0() { [[ "$1" -eq 0 ]] || fail "$2 exited $1"; } +rc0() { + [[ -n "${1:-}" ]] || fail "$2 had no exit code to check" + [[ "$1" -eq 0 ]] || fail "$2 exited $1" +} # --- Case 41: the PERSISTED owner decides, not the presenting session -------- # Case 32 proves a sequential replay is refused; that passes whether the claim @@ -801,30 +804,38 @@ if is_block "$OUT"; then ok "an ownerless claim still honors the arm (fail direc ARM_ID_10="fedcba9876543210" bash "$ARM" --id "$ARM_ID_10" --cwd "$WORK" 2>/dev/null rm -f "$SETTINGS" -mkfifo "$DATA_DIR/lane-arms/$ARM_ID_10.claim" +if ! mkfifo "$DATA_DIR/lane-arms/$ARM_ID_10.claim" 2>/dev/null; then + ok "mkfifo unavailable; skip non-regular claim path hang case" +elif ! command -v timeout >/dev/null 2>&1 || + ! timeout --help 2>&1 | grep -Fq -- '--kill-after'; then + ok "GNU timeout unavailable; skip non-regular claim path hang case" +else +FIFO_INPUT=$(build_input Stop "no token" false "" "sess-fifo") FIFO_OUT="$WORK/fifo-out" -(cd "$UNRELATED" && printf '%s' "$(build_input Stop "no token" false "" "sess-fifo")" | - env -u CLAUDE_PLUGIN_OPTION_LANE_STOP_GATE_ENABLED \ - -u CLAUDE_PLUGIN_OPTION_LANE_STOP_GATE_SENTINEL \ - -u CLAUDE_PLUGIN_OPTION_LANE_STOP_GATE_MARKER \ - -u CLAUDE_PLUGIN_DATA \ - CLAUDE_PLUGIN_OPTION_LANE_NOTIFY_ENABLED=false \ - CLAUDE_PLUGIN_OPTION_LANE_STOP_GATE_ARM_ID="$ARM_ID_10" \ - bash "$HOOK" 2>/dev/null >"$FIFO_OUT") & -FIFO_PID=$! -for _ in $(seq 1 60); do - kill -0 "$FIFO_PID" 2>/dev/null || break - sleep 0.25 -done -if kill -0 "$FIFO_PID" 2>/dev/null; then - kill -9 "$FIFO_PID" 2>/dev/null +FIFO_RC=0 +# shellcheck disable=SC2016 # bash -c program is single-quoted; \$1..\$4 expand in the child +if timeout 5 bash -c ' + cd "$1" && printf "%s" "$2" | + env -u CLAUDE_PLUGIN_OPTION_LANE_STOP_GATE_ENABLED \ + -u CLAUDE_PLUGIN_OPTION_LANE_STOP_GATE_SENTINEL \ + -u CLAUDE_PLUGIN_OPTION_LANE_STOP_GATE_MARKER \ + -u CLAUDE_PLUGIN_DATA \ + CLAUDE_PLUGIN_OPTION_LANE_NOTIFY_ENABLED=false \ + CLAUDE_PLUGIN_OPTION_LANE_STOP_GATE_ARM_ID="$3" \ + bash "$4" 2>/dev/null +' _ "$UNRELATED" "$FIFO_INPUT" "$ARM_ID_10" "$HOOK" >"$FIFO_OUT" 2>/dev/null; then + FIFO_RC=0 +else + FIFO_RC=$? +fi +if [[ "$FIFO_RC" -eq 124 ]]; then fail "a planted FIFO at the claim path hung the Stop hook" elif is_block "$(cat "$FIFO_OUT" 2>/dev/null)"; then ok "a non-regular file at the claim path neither hangs the hook nor loses the gate" else fail "a planted FIFO at the claim path left the lane ungated" fi -wait "$FIFO_PID" 2>/dev/null +fi echo echo "PASS=$PASS FAIL=$FAIL"