@chrono-meta/fh-gate 1.4.76 → 1.4.78
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/rules/fh_4axis_gate.md +63 -0
- package/.claude-plugin/marketplace.json +2 -2
- package/AGENTS.md +96 -260
- package/docs/codex-compat.md +4 -1
- package/knowledge/shared/harness-core/agents_md_runtime_details.md +233 -0
- package/knowledge/shared/harness-core/multi_model_sidecar_strategy.md +1 -1
- package/knowledge/shared/learnings/subagent_invocations_log.yaml +14 -0
- package/package.json +8 -2
- package/plugins/fh-commons/.claude-plugin/plugin.json +1 -1
- package/plugins/fh-meta/.claude-plugin/plugin.json +1 -1
- package/plugins/fh-meta/skills/install-doctor/SKILL.md +88 -0
- package/plugins/fh-meta/skills/install-wizard/SKILL.md +1 -1
- package/plugins/fh-meta/skills/install-wizard/SKILL_detail.md +117 -3
- package/scripts/fh_node_check.sh +184 -0
- package/scripts/fh_session_load.sh +59 -28
- package/scripts/package_coverage_check.sh +22 -0
- package/scripts/selfcheck.sh +45 -0
- package/scripts/sidecar_calibrate.sh +190 -0
- package/scripts/sidecar_wait.sh +55 -5
- package/scripts/test_node_check_lanes.sh +179 -0
- package/scripts/test_sidecar_calibrate_lanes.sh +218 -0
- package/scripts/test_sidecar_wait_stdin.sh +169 -0
- package/templates/settings.SessionStart.snippet.json +54 -0
package/scripts/sidecar_wait.sh
CHANGED
|
@@ -41,12 +41,59 @@ shift 2
|
|
|
41
41
|
[ "${1:-}" = "--" ] && shift
|
|
42
42
|
[ $# -gt 0 ] || { echo "sidecar_wait: no command given" >&2; exit 2; }
|
|
43
43
|
|
|
44
|
-
|
|
45
|
-
|
|
44
|
+
# Forward stdin to the child with an explicit fd dup.
|
|
45
|
+
#
|
|
46
|
+
# The hole (measured 2026-07-29, known-pair): `"$@" > "$OUT" 2>&1 &` gave the child /dev/null for
|
|
47
|
+
# stdin in a non-interactive shell, so the documented pipe form reached codex with NO prompt, codex
|
|
48
|
+
# answered "No prompt provided via stdin", and this wrapper reported COMPLETE — the exact 0-output
|
|
49
|
+
# misjudgment it exists to prevent, produced by itself.
|
|
50
|
+
#
|
|
51
|
+
# `<&0` is the whole fix: POSIX substitutes /dev/null ONLY when stdin is not explicitly redirected.
|
|
52
|
+
# The first repair spooled stdin to a tempfile instead, and adversarial review (Axis 2) showed that
|
|
53
|
+
# mechanism was both unnecessary AND strictly worse than the bug — reproduced, not argued:
|
|
54
|
+
# - the unbounded `cat` ran BEFORE the child, so an inherited never-EOF stdin hung the wrapper
|
|
55
|
+
# forever and the timeout budget never applied (`-- true` with inherited stdin → rc=124);
|
|
56
|
+
# - it CONSUMED the caller's stdin, so a caller reading 3 lines after the call read 0.
|
|
57
|
+
# A bounded-wait wrapper with an unbounded pre-step, and an input-preserving tool that eats input.
|
|
58
|
+
# The lesson is kept in the file: the simplest correct fix was one token, and the machinery built
|
|
59
|
+
# around it introduced two S-grade defects the original bug did not have.
|
|
60
|
+
: > "$OUT" || { echo "SIDECAR_VERDICT=OUTFILE_UNWRITABLE path=$OUT" >&2; exit 2; }
|
|
61
|
+
set -m # own process group per child, so the TIMEOUT kill can reach grandchildren
|
|
62
|
+
if [ -t 0 ]; then
|
|
63
|
+
# On a controlling tty, a child that READS stdin raises SIGTTIN and stops the whole process
|
|
64
|
+
# group — the wrapper with it — so the budget never fires and no verdict is emitted (measured:
|
|
65
|
+
# `cat` under a tty gave rc=124 and zero verdict lines, while `sleep 30` correctly TIMEOUTed).
|
|
66
|
+
# Interactive callers have no prompt to pipe anyway; the documented pipe form is never a tty.
|
|
67
|
+
"$@" < /dev/null > "$OUT" 2>&1 &
|
|
68
|
+
else
|
|
69
|
+
"$@" <&0 > "$OUT" 2>&1 &
|
|
70
|
+
fi
|
|
46
71
|
PID=$!
|
|
47
72
|
|
|
48
73
|
waited=0
|
|
49
74
|
last_size=0
|
|
75
|
+
# Poll interval is overridable so the regression anchor is not charged the 5s floor per
|
|
76
|
+
# invocation (a 25s anchor is an anchor people skip).
|
|
77
|
+
POLL="${SIDECAR_POLL:-5}"
|
|
78
|
+
# Validate it. Caller-controlled and unvalidated, this knob RESTORED the very failure the wrapper
|
|
79
|
+
# exists to prevent (measured 2026-07-29, budget 3s under an external timeout 10):
|
|
80
|
+
# SIDECAR_POLL=0 -> rc=124, `waited` never advances, TIMEOUT never fires, zero typed verdicts
|
|
81
|
+
# SIDECAR_POLL=0.5 -> rc=124, arithmetic error each iteration, assignment never lands
|
|
82
|
+
# SIDECAR_POLL=abc -> exits 1 (the documented TIMEOUT code) with NO verdict line and a live child
|
|
83
|
+
# The file's own "a 25s anchor is an anchor people skip" comment invites tuning this, and `0.5` is
|
|
84
|
+
# the obvious next step for someone doing that. An unbounded wait must not be reachable by typo.
|
|
85
|
+
# `10#` forces base 10: `test` reads 08 as decimal-8 and PASSES it, then $((waited + 08)) dies with
|
|
86
|
+
# "value too great for base" every iteration, waited never advances, and the wait is unbounded again
|
|
87
|
+
# — the first guard did not close its own finding (measured: 08/09 -> rc=124, zero verdicts).
|
|
88
|
+
# The ceiling matters just as much: the budget is checked at the TOP of the loop, so any POLL above
|
|
89
|
+
# it makes the effective wait POLL, not BUDGET (SIDECAR_POLL=600 -> rc=124). 600 is exactly what
|
|
90
|
+
# someone copying the budget argument into the knob writes.
|
|
91
|
+
_poll_raw="$POLL"
|
|
92
|
+
case "$POLL" in ''|*[!0-9]*) POLL=5 ;; *) POLL=$((10#$POLL)) 2>/dev/null || POLL=5 ;; esac
|
|
93
|
+
{ [ "$POLL" -ge 1 ] && [ "$POLL" -le 60 ]; } 2>/dev/null || POLL=5
|
|
94
|
+
# Never coerce silently on a script whose whole thesis is a typed channel.
|
|
95
|
+
[ "$POLL" = "$_poll_raw" ] || [ -z "${SIDECAR_POLL:-}" ] || \
|
|
96
|
+
echo "sidecar_wait: ignoring SIDECAR_POLL='$_poll_raw' (not an integer in 1..60); using $POLL" >&2
|
|
50
97
|
# Poll rather than `wait`, so a live-but-quiet process is distinguishable from a dead one and the
|
|
51
98
|
# caller can SEE progress. A silent minute on a reasoning model is normal; the earlier misreading
|
|
52
99
|
# happened precisely because silence was treated as termination.
|
|
@@ -54,11 +101,14 @@ while kill -0 "$PID" 2>/dev/null; do
|
|
|
54
101
|
if [ "$waited" -ge "$BUDGET" ]; then
|
|
55
102
|
size=$(wc -c < "$OUT" 2>/dev/null | tr -d ' ')
|
|
56
103
|
echo "SIDECAR_VERDICT=TIMEOUT waited=${BUDGET}s bytes=${size:-0} pid=$PID"
|
|
57
|
-
echo " the process is STILL RUNNING — this is not 'no output'. Raise the budget
|
|
104
|
+
echo " the process is STILL RUNNING — this is not 'no output'. Raise the budget if it needs longer." >&2
|
|
105
|
+
# Kill the GROUP. `kill "$PID"` reaches only the direct child, so `sh -c 'sleep N & wait'`
|
|
106
|
+
# left a live grandchild behind while the lane stayed green (measured wave 4).
|
|
107
|
+
kill -- -"$PID" 2>/dev/null || kill "$PID" 2>/dev/null
|
|
58
108
|
exit 1
|
|
59
109
|
fi
|
|
60
|
-
sleep
|
|
61
|
-
waited=$((waited +
|
|
110
|
+
sleep "$POLL"
|
|
111
|
+
waited=$((waited + POLL))
|
|
62
112
|
size=$(wc -c < "$OUT" 2>/dev/null | tr -d ' ')
|
|
63
113
|
if [ "${size:-0}" -ne "$last_size" ]; then
|
|
64
114
|
echo " … ${waited}s elapsed, ${size} bytes so far (alive)" >&2
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# test_node_check_lanes.sh — regression lanes for scripts/fh_node_check.sh.
|
|
3
|
+
#
|
|
4
|
+
# WHY THIS FILE EXISTS: across three adversarial rounds on the node check, every surviving defect
|
|
5
|
+
# was a NEGATIVE leg nobody was testing — "the floor does not apply here", "those are someone
|
|
6
|
+
# else's hooks", "that file is absent on a fresh clone". Each round's fix reverted a previous
|
|
7
|
+
# round's fix because no lane pinned it. The lanes below are that pin: they encode the *shape* of
|
|
8
|
+
# each defect, not just its instance.
|
|
9
|
+
#
|
|
10
|
+
# Usage: bash scripts/test_node_check_lanes.sh
|
|
11
|
+
# Exit: 0 = all lanes pass; 1 = at least one lane failed (prints which and why).
|
|
12
|
+
|
|
13
|
+
set -uo pipefail
|
|
14
|
+
|
|
15
|
+
FH_REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
16
|
+
CHECK="$FH_REPO/scripts/fh_node_check.sh"
|
|
17
|
+
TMP="$(mktemp -d)"
|
|
18
|
+
trap 'rm -rf "$TMP"' EXIT
|
|
19
|
+
|
|
20
|
+
PASS=0; FAIL=0
|
|
21
|
+
ok() { PASS=$((PASS+1)); printf ' ✅ %s\n' "$1"; }
|
|
22
|
+
bad() { FAIL=$((FAIL+1)); printf ' ❌ %s\n got: %s\n' "$1" "$(printf '%s' "$2" | tr '\n' '|' | cut -c1-220)"; }
|
|
23
|
+
|
|
24
|
+
# run <hubdir> <statefile> [env assignments...] → stdout of one check run
|
|
25
|
+
run() { local hub="$1" st="$2"; shift 2; env "$@" HUB_DIR="$hub" FH_NODE_STATE="$st" bash "$CHECK" 2>&1; }
|
|
26
|
+
|
|
27
|
+
mk_git_hub() { # a git repo with FH-style hooks installed and executable
|
|
28
|
+
local d="$1" fh_sentinel="${2:-yes}"
|
|
29
|
+
mkdir -p "$d" && git -C "$d" init -q && git -C "$d" config user.email t@t && git -C "$d" config user.name t
|
|
30
|
+
echo x > "$d/f" && git -C "$d" add f && git -C "$d" -c commit.gpgsign=false commit -qm init
|
|
31
|
+
mkdir -p "$d/.git/hooks"
|
|
32
|
+
local body='#!/bin/sh\nexit 0\n'
|
|
33
|
+
[ "$fh_sentinel" = "yes" ] && body='#!/bin/sh\n# FH 4-Axis Gate Pre-Commit Hook\nfh-gate\nexit 0\n'
|
|
34
|
+
printf "$body" > "$d/.git/hooks/pre-commit"; printf "$body" > "$d/.git/hooks/pre-push"
|
|
35
|
+
chmod +x "$d/.git/hooks/pre-commit" "$d/.git/hooks/pre-push"
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
echo "── node-check lanes ──"
|
|
39
|
+
|
|
40
|
+
# LANE 1 (S3-1) — NOT a git repo. The git-hook floor cannot be installed here at all, so it is
|
|
41
|
+
# N/A, not missing. Reporting it would be an unfixable notice repeating every session forever.
|
|
42
|
+
mkdir -p "$TMP/nogit"
|
|
43
|
+
out="$(run "$TMP/nogit" "$TMP/s1" FH_MACHINE_ID=nogitbox)"
|
|
44
|
+
# POSITIVE CONTROL: assert what MUST appear alongside what must not. Absence-only lanes are
|
|
45
|
+
# satisfied by a script that prints nothing at all — an `exit 0` stub passed lanes 1, 4 and 7
|
|
46
|
+
# (cross-family mutation 2026-07-30), so each now pins an expected utterance too.
|
|
47
|
+
case "$out" in
|
|
48
|
+
*"Missing mechanical floor"*) bad "lane1 non-git: must NOT claim a missing floor (N/A, unfixable)" "$out" ;;
|
|
49
|
+
*"first session for this clone"*) ok "lane1 non-git: N/A on the git floor, but still reports the node event" ;;
|
|
50
|
+
*) bad "lane1 non-git: silent — a stub would pass this lane" "$out" ;;
|
|
51
|
+
esac
|
|
52
|
+
|
|
53
|
+
# LANE 2 (S3-2) — hooks exist but belong to another framework (husky/pre-commit). Executable ≠ FH's
|
|
54
|
+
# gate. Silence here is the exact accident this check was built for: FH gates absent, machine quiet.
|
|
55
|
+
mk_git_hub "$TMP/husky" no
|
|
56
|
+
out="$(run "$TMP/husky" "$TMP/s2" FH_MACHINE_ID=huskybox)"
|
|
57
|
+
case "$out" in
|
|
58
|
+
*"Missing mechanical floor"*) ok "lane2 foreign hooks: FH gate absence reported" ;;
|
|
59
|
+
*) bad "lane2 foreign hooks: non-FH hooks were accepted as FH floors" "$out" ;;
|
|
60
|
+
esac
|
|
61
|
+
|
|
62
|
+
# LANE 3 (S3-3) — Mode D user on a FRESH clone: companion store present, settings.local.json absent
|
|
63
|
+
# (it is gitignored, so a clone never has it). This is the MEASURED 2026-07-30 incident. Must speak.
|
|
64
|
+
mk_git_hub "$TMP/moded" yes
|
|
65
|
+
mkdir -p "$TMP/companion/.git"
|
|
66
|
+
out="$(run "$TMP/moded" "$TMP/s3" FH_MACHINE_ID=modedbox BE_DIR="$TMP/companion")"
|
|
67
|
+
case "$out" in
|
|
68
|
+
*companion-load*) ok "lane3 fresh Mode D clone: companion-load absence surfaced" ;;
|
|
69
|
+
*) bad "lane3 fresh Mode D clone: SILENT — the measured incident would recur" "$out" ;;
|
|
70
|
+
esac
|
|
71
|
+
|
|
72
|
+
# LANE 4 (M2-4) — public non-Mode-D user: no companion store anywhere. The companion item must not
|
|
73
|
+
# appear, or the majority path gets a false positive for a feature it does not use.
|
|
74
|
+
mk_git_hub "$TMP/public" yes
|
|
75
|
+
out="$(run "$TMP/public" "$TMP/s4" FH_MACHINE_ID=publicbox)"
|
|
76
|
+
case "$out" in
|
|
77
|
+
*companion-load*) bad "lane4a non-Mode-D: companion item shown to a user with no store" "$out" ;;
|
|
78
|
+
*"first session for this clone"*) ok "lane4a non-Mode-D: companion absent, node event still reported" ;;
|
|
79
|
+
*) bad "lane4a non-Mode-D: silent — a stub would pass this lane" "$out" ;;
|
|
80
|
+
esac
|
|
81
|
+
|
|
82
|
+
# LANE 4b (S4-1) — CLAUDE.local.md is Claude Code's STANDARD local-override file; anyone may keep
|
|
83
|
+
# one for any reason. Its mere EXISTENCE must not classify a user as Mode D, or the majority path
|
|
84
|
+
# gets a companion notice every session forever (state-based emission makes it permanent, not
|
|
85
|
+
# one-shot). Only the binding INSIDE the file counts.
|
|
86
|
+
printf '# my local notes\nuse tabs not spaces\n' > "$TMP/public/CLAUDE.local.md"
|
|
87
|
+
out="$(run "$TMP/public" "$TMP/s4b" FH_MACHINE_ID=publicbox)"
|
|
88
|
+
case "$out" in
|
|
89
|
+
*companion-load*) bad "lane4b plain CLAUDE.local.md: existence alone classified the user as Mode D" "$out" ;;
|
|
90
|
+
*) ok "lane4b plain CLAUDE.local.md: not treated as a Mode D signal" ;;
|
|
91
|
+
esac
|
|
92
|
+
|
|
93
|
+
# LANE 4c — the same file WITH a companion binding must classify as Mode D and speak. Without this
|
|
94
|
+
# leg, "never classify as Mode D" would also pass 4b.
|
|
95
|
+
# One fixture PER alternative: a single fixture like `BE_DIR=/some/companion-store` satisfies two
|
|
96
|
+
# alternatives at once, so either could be deleted and the lane would still pass. The store is a
|
|
97
|
+
# ROLE, not a repo layout (install-wizard SKILL.md: Obsidian vault / gbrain ingest target / *-be
|
|
98
|
+
# repo all qualify) — so the vocabulary variants are the documented user base, not hypotheticals.
|
|
99
|
+
i=0
|
|
100
|
+
# `backend: obsidian` carries no other keyword on purpose — with a `vault:` fixture only, the
|
|
101
|
+
# `obsidian` alternative is never exercised and could be deleted with the suite staying green
|
|
102
|
+
# (verified: removing it left 16/16). An untested alternative is an untested branch.
|
|
103
|
+
for binding in 'BE_DIR=/x/store' 'companion store: ~/notes' '컴패니언 스토어: ~/notes' \
|
|
104
|
+
'vault: ~/vaults/notes' 'gbrain ingest target: ~/gbrain' 'backend: obsidian'; do
|
|
105
|
+
i=$((i+1))
|
|
106
|
+
printf '# local\n%s\n' "$binding" > "$TMP/public/CLAUDE.local.md"
|
|
107
|
+
out="$(run "$TMP/public" "$TMP/s4c$i" FH_MACHINE_ID=publicbox)"
|
|
108
|
+
case "$out" in
|
|
109
|
+
*companion-load*) ok "lane4c.$i Mode D detected via: $binding" ;;
|
|
110
|
+
*) bad "lane4c.$i binding present but Mode D not detected: $binding" "$out" ;;
|
|
111
|
+
esac
|
|
112
|
+
done
|
|
113
|
+
rm -f "$TMP/public/CLAUDE.local.md"
|
|
114
|
+
|
|
115
|
+
# LANE 5 — emission model: a healthy machine speaks once (event) then goes silent.
|
|
116
|
+
mk_git_hub "$TMP/healthy" yes
|
|
117
|
+
r1="$(run "$TMP/healthy" "$TMP/s5" FH_MACHINE_ID=healthybox)"
|
|
118
|
+
r2="$(run "$TMP/healthy" "$TMP/s5" FH_MACHINE_ID=healthybox)"
|
|
119
|
+
if [ -n "$r1" ] && [ -z "$r2" ]; then ok "lane5 healthy: event once, then silent"
|
|
120
|
+
else bad "lane5 healthy: expected run1 non-empty and run2 empty" "r1=[$r1] r2=[$r2]"; fi
|
|
121
|
+
|
|
122
|
+
# LANE 6 (S2-4) — a missing floor is a CONDITION: it must be reported on every run, not once.
|
|
123
|
+
mk_git_hub "$TMP/broken" yes
|
|
124
|
+
rm -f "$TMP/broken/.git/hooks/pre-commit"
|
|
125
|
+
n=0
|
|
126
|
+
for i in 1 2 3; do
|
|
127
|
+
o="$(run "$TMP/broken" "$TMP/s6" FH_MACHINE_ID=brokenbox)"
|
|
128
|
+
case "$o" in *"Missing mechanical floor"*) n=$((n+1)) ;; esac
|
|
129
|
+
done
|
|
130
|
+
[ "$n" -eq 3 ] && ok "lane6 broken: reported on all 3 runs (condition, not event)" \
|
|
131
|
+
|| bad "lane6 broken: reported $n/3 runs — a broken machine went quiet" "n=$n"
|
|
132
|
+
|
|
133
|
+
# LANE 7 (M2-2) — linked worktree: .git is a FILE there, so a hand-built "$FH/.git/hooks" does not
|
|
134
|
+
# exist and working hooks read as missing.
|
|
135
|
+
mk_git_hub "$TMP/wt" yes
|
|
136
|
+
git -C "$TMP/wt" worktree add -q "$TMP/wt_linked" -b lane7 2>/dev/null
|
|
137
|
+
out="$(run "$TMP/wt_linked" "$TMP/s7" FH_MACHINE_ID=wtbox)"
|
|
138
|
+
case "$out" in
|
|
139
|
+
*"Missing mechanical floor"*) bad "lane7 worktree: false missing-floor (hooks resolve to the main gitdir)" "$out" ;;
|
|
140
|
+
*"first session for this clone"*) ok "lane7 worktree: hooks resolved correctly, node event reported" ;;
|
|
141
|
+
*) bad "lane7 worktree: silent — a stub would pass this lane" "$out" ;;
|
|
142
|
+
esac
|
|
143
|
+
|
|
144
|
+
# LANE 8 (M3-3) — python3 unavailable: the companion verdict is UNMEASURED, never silently "fine".
|
|
145
|
+
# Simulated by a PATH with no python3, for a Mode D hub (so the check is applicable).
|
|
146
|
+
mk_git_hub "$TMP/nopy" yes
|
|
147
|
+
mkdir -p "$TMP/emptybin" "$TMP/companion2/.git"
|
|
148
|
+
out="$(PATH="$TMP/emptybin:/usr/bin:/bin" run "$TMP/nopy" "$TMP/s8" FH_MACHINE_ID=nopybox BE_DIR="$TMP/companion2")"
|
|
149
|
+
if command -v python3 >/dev/null 2>&1 && [ -x /usr/bin/python3 ]; then
|
|
150
|
+
ok "lane8 skipped: /usr/bin/python3 exists so absence cannot be simulated via PATH"
|
|
151
|
+
else
|
|
152
|
+
case "$out" in
|
|
153
|
+
*UNMEASURED*|*unmeasured*) ok "lane8 no python3: reported UNMEASURED, not silence" ;;
|
|
154
|
+
*) bad "lane8 no python3: absence read as pass" "$out" ;;
|
|
155
|
+
esac
|
|
156
|
+
fi
|
|
157
|
+
|
|
158
|
+
# LANE 9 (S4-2) — the sentinel regex is coupled to PROSE THAT LIVES IN ANOTHER FILE. Every other
|
|
159
|
+
# lane hands it a fixture containing the string it expects, so the suite would stay fully green
|
|
160
|
+
# while a purely cosmetic edit to the real hook headers (which no gate checks) made every FH machine
|
|
161
|
+
# report "not FH's gate" every session. Calibrate the instrument against the shipped article.
|
|
162
|
+
# DERIVE the regex from the script — never retype it. A hardcoded copy is the divergent-copy class
|
|
163
|
+
# this repo already paid for once (SYNC_EXCLUDES in three places, which needed its own parity
|
|
164
|
+
# checker): tighten the script's regex and a duplicated lane keeps validating the SHIPPED hooks
|
|
165
|
+
# against the OLD pattern, staying green while the real check drifts.
|
|
166
|
+
SENT="$(sed -n "s/.*grep -qE '\([^']*\)'.*/\1/p" "$FH_REPO/scripts/fh_node_check.sh" | head -1)"
|
|
167
|
+
if [ -z "$SENT" ]; then
|
|
168
|
+
bad "lane9 sentinel: could not derive the regex from fh_node_check.sh (extraction broke — not a pass)" "empty"
|
|
169
|
+
SENT='__never_matches__'
|
|
170
|
+
fi
|
|
171
|
+
for h in "$FH_REPO"/templates/.git-hooks/pre-commit "$FH_REPO"/templates/.git-hooks/pre-push; do
|
|
172
|
+
if [ ! -f "$h" ]; then bad "lane9 sentinel: shipped hook missing: $h" "absent"; continue; fi
|
|
173
|
+
if grep -qE "$SENT" "$h"; then ok "lane9 sentinel matches shipped $(basename "$h")"
|
|
174
|
+
else bad "lane9 sentinel does NOT match shipped $(basename "$h") — every FH machine would report 'not FH gate'" "$(head -3 "$h")"; fi
|
|
175
|
+
done
|
|
176
|
+
|
|
177
|
+
printf '\nnode-check lanes: %d passed, %d failed\n' "$PASS" "$FAIL"
|
|
178
|
+
[ "$FAIL" -eq 0 ] || exit 1
|
|
179
|
+
exit 0
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# test_sidecar_calibrate_lanes.sh — regression lanes for scripts/sidecar_calibrate.sh.
|
|
3
|
+
#
|
|
4
|
+
# WHY LANES FIRST: the calibrator's whole job is to distinguish states that LOOK the same from the
|
|
5
|
+
# outside — "the sidecar ran" vs "the model I asked for answered", "absent" vs "unmeasured". Those
|
|
6
|
+
# are negative legs, and negative legs are what three adversarial rounds on the node check showed
|
|
7
|
+
# nobody tests until a lane forces it. Each lane below drives the calibrator with a STUB CLI whose
|
|
8
|
+
# behaviour is known, so the calibrator's verdict can be checked against ground truth.
|
|
9
|
+
#
|
|
10
|
+
# No network, no API spend: every sidecar binary is replaced by a stub on PATH.
|
|
11
|
+
#
|
|
12
|
+
# Usage: bash scripts/test_sidecar_calibrate_lanes.sh
|
|
13
|
+
# Exit: 0 = all lanes pass; 1 = at least one failed.
|
|
14
|
+
|
|
15
|
+
set -uo pipefail
|
|
16
|
+
|
|
17
|
+
REPO="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
18
|
+
CAL="$REPO/scripts/sidecar_calibrate.sh"
|
|
19
|
+
TMP="$(mktemp -d)"
|
|
20
|
+
trap 'rm -rf "$TMP"' EXIT
|
|
21
|
+
|
|
22
|
+
PASS=0; FAIL=0
|
|
23
|
+
ok() { PASS=$((PASS+1)); printf ' ✅ %s\n' "$1"; }
|
|
24
|
+
bad() { FAIL=$((FAIL+1)); printf ' ❌ %s\n got: %s\n' "$1" "$(printf '%s' "$2" | tr '\n' '|' | cut -c1-240)"; }
|
|
25
|
+
|
|
26
|
+
# stub <name> <behaviour> — write a fake sidecar CLI onto the lane PATH.
|
|
27
|
+
# honest : echoes back the model it was pinned to (a truthful runtime)
|
|
28
|
+
# silent-fallback: ALWAYS answers as one fixed model, whatever the pin (the agy trap)
|
|
29
|
+
# loud-reject : exits non-zero on an unknown pin (the codex behaviour)
|
|
30
|
+
# chatty : answers with agentic prose instead of the requested token (unparseable verdict)
|
|
31
|
+
# banner-echo : prints a session banner that REPEATS the pinned model, then answers as a
|
|
32
|
+
# different model — the real shape of `codex exec` stdout (measured 2026-07-30)
|
|
33
|
+
# listed-substitute: REJECTS an unknown name (so the bogus control says "rejects-bogus") yet
|
|
34
|
+
# silently serves a different model for a name that IS in its own catalogue —
|
|
35
|
+
# the measured agy shape (`gemini-3.1-pro-high` answered as 3.6 Flash, no error)
|
|
36
|
+
# alias-name : answers with the vendor's PRODUCT name carrying the right version but not every
|
|
37
|
+
# token of the pin slug — measured: pin `gpt-5.6-sol` → "GPT-5.6 Codex"
|
|
38
|
+
# absent : not created at all
|
|
39
|
+
mkstub() {
|
|
40
|
+
local name="$1" mode="$2" bin="$TMP/bin"
|
|
41
|
+
mkdir -p "$bin"
|
|
42
|
+
case "$mode" in
|
|
43
|
+
absent) rm -f "$bin/$name"; return ;;
|
|
44
|
+
esac
|
|
45
|
+
cat > "$bin/$name" <<STUB
|
|
46
|
+
#!/usr/bin/env bash
|
|
47
|
+
mode="$mode"
|
|
48
|
+
pin=""
|
|
49
|
+
prev=""
|
|
50
|
+
for a in "\$@"; do
|
|
51
|
+
case "\$prev" in -m|--model) pin="\$a" ;; esac
|
|
52
|
+
prev="\$a"
|
|
53
|
+
done
|
|
54
|
+
case "\$1" in --version) echo "stub 9.9.9"; exit 0 ;; esac
|
|
55
|
+
# Scan ALL arguments for the verdict prompt — it is NOT always last. codex puts the prompt at the
|
|
56
|
+
# end; agy puts it after -p with `--print-timeout 170s` trailing. A first draft read only the last
|
|
57
|
+
# argument, so the agy-shaped call never looked like a verdict request and lane5b failed against a
|
|
58
|
+
# correct script. An honest runtime ANSWERS THE QUESTION ASKED: a one-token request gets one token.
|
|
59
|
+
is_verdict=0
|
|
60
|
+
for a in "\$@"; do case "\$a" in *"PASS or FAIL"*) is_verdict=1 ;; esac; done
|
|
61
|
+
case "\$mode" in
|
|
62
|
+
loud-reject)
|
|
63
|
+
case "\$pin" in *nonexistent*|*bogus*) echo "ERROR: model '\$pin' is not supported" >&2; exit 1 ;; esac
|
|
64
|
+
if [ "\$is_verdict" -eq 1 ]; then echo "PASS"; else echo "I am \$pin, by StubCorp."; fi ;;
|
|
65
|
+
silent-fallback)
|
|
66
|
+
if [ "\$is_verdict" -eq 1 ]; then echo "PASS"; else echo "I am stub-flash-3.6, by StubCorp."; fi ;;
|
|
67
|
+
alias-name)
|
|
68
|
+
if [ "\$is_verdict" -eq 1 ]; then echo "PASS"; else echo "StubGPT-3.1 Coder"; fi ;;
|
|
69
|
+
listed-substitute)
|
|
70
|
+
case "\$pin" in *nonexistent*|*bogus*) echo "ERROR: model '\$pin' is not supported" >&2; exit 1 ;; esac
|
|
71
|
+
if [ "\$is_verdict" -eq 1 ]; then echo "PASS"; else echo "I am stub-flash-3.6, by StubCorp."; fi ;;
|
|
72
|
+
banner-echo)
|
|
73
|
+
echo "Reading additional input from stdin... CLI v0.0.0"
|
|
74
|
+
echo "--------"
|
|
75
|
+
echo "model: \$pin workdir: /tmp approval: never"
|
|
76
|
+
echo "--------"
|
|
77
|
+
if [ "\$is_verdict" -eq 1 ]; then echo "PASS"; else echo "I am stub-flash-3.6, by StubCorp."; fi ;;
|
|
78
|
+
chatty)
|
|
79
|
+
printf 'Summary of Work\n- considered the request\n- the answer is probably PASS\n' ;;
|
|
80
|
+
honest|*)
|
|
81
|
+
if [ "\$is_verdict" -eq 1 ]; then echo "PASS"; else echo "I am \$pin, by StubCorp."; fi ;;
|
|
82
|
+
esac
|
|
83
|
+
exit 0
|
|
84
|
+
STUB
|
|
85
|
+
chmod +x "$bin/$name"
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
# HERMETIC PATH — system paths only, plus the stub dir. Inheriting $PATH looked harmless and was
|
|
89
|
+
# not: "absent" is simulated by NOT creating a stub, so the real codex/agy further down $PATH were
|
|
90
|
+
# found instead and the lanes fired REAL, billed API calls (measured: a lane run hung past 120s
|
|
91
|
+
# against live runtimes). A test that can reach production is not a test.
|
|
92
|
+
run_cal() { PATH="$TMP/bin:/usr/bin:/bin:/usr/sbin:/sbin" bash "$CAL" --stub-model "stub-pro-3.1" "$@" 2>&1; }
|
|
93
|
+
|
|
94
|
+
echo "── sidecar-calibrate lanes ──"
|
|
95
|
+
|
|
96
|
+
# LANE 1 — absent runtime must read as ABSENT, never as a failure of the panel and never as "fine".
|
|
97
|
+
mkstub codex absent; mkstub agy absent
|
|
98
|
+
out="$(run_cal --only codex)"
|
|
99
|
+
case "$out" in
|
|
100
|
+
*"codex"*ABSENT*) ok "lane1 absent runtime reported ABSENT" ;;
|
|
101
|
+
*) bad "lane1 absent runtime not reported as ABSENT" "$out" ;;
|
|
102
|
+
esac
|
|
103
|
+
|
|
104
|
+
# LANE 2 — THE POINT OF THE WHOLE SCRIPT. A runtime that silently answers as a different model must
|
|
105
|
+
# be reported as UNTRUSTED-PIN, not as reachable. "The sidecar ran" and "the model I pinned answered"
|
|
106
|
+
# are different propositions; agy's slug fallback is the measured instance (2026-07-30).
|
|
107
|
+
mkstub agy silent-fallback
|
|
108
|
+
out="$(run_cal --only agy)"
|
|
109
|
+
case "$out" in
|
|
110
|
+
*UNTRUSTED-PIN*) ok "lane2 silent fallback caught (pinned model did not answer)" ;;
|
|
111
|
+
*) bad "lane2 silent fallback passed as a healthy sidecar" "$out" ;;
|
|
112
|
+
esac
|
|
113
|
+
|
|
114
|
+
# LANE 3 — an honest runtime that echoes its pin is PIN-OK.
|
|
115
|
+
mkstub agy honest
|
|
116
|
+
out="$(run_cal --only agy)"
|
|
117
|
+
case "$out" in
|
|
118
|
+
*PIN-OK*) ok "lane3 honest runtime reported PIN-OK" ;;
|
|
119
|
+
*) bad "lane3 honest runtime not reported PIN-OK" "$out" ;;
|
|
120
|
+
esac
|
|
121
|
+
|
|
122
|
+
# LANE 4 — the negative control must actually control. A runtime that ACCEPTS a bogus model pin has
|
|
123
|
+
# no server-side validation, so "it ran without error" proves nothing about which model answered;
|
|
124
|
+
# that must be visible in the report rather than inferred.
|
|
125
|
+
mkstub codex loud-reject
|
|
126
|
+
out="$(run_cal --only codex)"
|
|
127
|
+
case "$out" in
|
|
128
|
+
*"control: rejects-bogus"*) ok "lane4 bogus-pin control observed (runtime validates pins)" ;;
|
|
129
|
+
*) bad "lane4 bogus-pin control not reported" "$out" ;;
|
|
130
|
+
esac
|
|
131
|
+
mkstub codex honest # honest stub answers ANY pin, including a bogus one
|
|
132
|
+
out="$(run_cal --only codex)"
|
|
133
|
+
case "$out" in
|
|
134
|
+
*"control: accepts-bogus"*) ok "lane4b runtime accepting a bogus pin is flagged" ;;
|
|
135
|
+
*) bad "lane4b runtime accepting a bogus pin was not flagged" "$out" ;;
|
|
136
|
+
esac
|
|
137
|
+
|
|
138
|
+
# LANE 5 — verdict fitness is MEASURED, not assumed. A runtime that answers a one-token request with
|
|
139
|
+
# agentic prose cannot carry a machine-read verdict. The recorded 2026-07-04 finding said exactly
|
|
140
|
+
# this about agy at 1.0.14; the version has moved since, so the answer must be re-measured, never
|
|
141
|
+
# inherited.
|
|
142
|
+
mkstub agy chatty
|
|
143
|
+
out="$(run_cal --only agy)"
|
|
144
|
+
case "$out" in
|
|
145
|
+
*VERDICT-UNPARSEABLE*) ok "lane5 prose-answering runtime flagged VERDICT-UNPARSEABLE" ;;
|
|
146
|
+
*) bad "lane5 prose answer accepted as a usable verdict channel" "$out" ;;
|
|
147
|
+
esac
|
|
148
|
+
mkstub agy honest
|
|
149
|
+
out="$(run_cal --only agy)"
|
|
150
|
+
case "$out" in
|
|
151
|
+
*VERDICT-OK*) ok "lane5b token-answering runtime reported VERDICT-OK" ;;
|
|
152
|
+
*) bad "lane5b clean token answer not reported VERDICT-OK" "$out" ;;
|
|
153
|
+
esac
|
|
154
|
+
|
|
155
|
+
# LANE 8 (measured on the first real run) — BANNER ECHO MUST NOT SATISFY THE IDENTITY PROBE.
|
|
156
|
+
# Real CLIs print a session banner before the answer, and that banner repeats the pinned model name
|
|
157
|
+
# back (`codex exec` prints its version and config header). Matching against whole stdout therefore
|
|
158
|
+
# passes any runtime that merely echoes its own configuration — which is precisely the lie this
|
|
159
|
+
# script exists to catch, re-entering through the transport layer instead of the model.
|
|
160
|
+
mkstub agy banner-echo
|
|
161
|
+
out="$(run_cal --only agy)"
|
|
162
|
+
case "$out" in
|
|
163
|
+
*UNTRUSTED-PIN*) ok "lane8 banner echo rejected (config echo is not a self-report)" ;;
|
|
164
|
+
*) bad "lane8 banner echoing the pin passed the identity probe" "$out" ;;
|
|
165
|
+
esac
|
|
166
|
+
|
|
167
|
+
# LANE 10 (measured 2026-07-30) — a vendor product alias carrying the RIGHT VERSION is not a pin
|
|
168
|
+
# failure. Pinning `gpt-5.6-sol` returned "GPT-5.6 Codex": same family, same version, different
|
|
169
|
+
# product suffix. Requiring every token of the pin slug to appear made the calibrator report
|
|
170
|
+
# UNTRUSTED-PIN on a runtime whose pin had actually held — and that runtime also validates pins
|
|
171
|
+
# server-side, so the corroborating evidence was there to read. The version token is the
|
|
172
|
+
# discriminator that matters; the fallback cases (3.1 asked, 3.6 answered) differ exactly there.
|
|
173
|
+
mkstub agy alias-name
|
|
174
|
+
out="$(run_cal --only agy)"
|
|
175
|
+
case "$out" in
|
|
176
|
+
*PIN-OK*) ok "lane10 product alias with matching version accepted as PIN-OK" ;;
|
|
177
|
+
*) bad "lane10 matching version rejected because a suffix token was absent" "$out" ;;
|
|
178
|
+
esac
|
|
179
|
+
|
|
180
|
+
# LANE 9 (measured 2026-07-30) — "rejects unknown names" does NOT imply "serves known names
|
|
181
|
+
# faithfully", and the gap between them is where the real trap lives. agy rejects a nonsense model
|
|
182
|
+
# yet silently substituted Flash for `gemini-3.1-pro-high`, a slug from its OWN catalogue. A control
|
|
183
|
+
# that only probes nonsense therefore returns reassuring evidence about the wrong question: the
|
|
184
|
+
# identity probe must still decide, and the report must not let `rejects-bogus` read as "pin safe".
|
|
185
|
+
mkstub agy listed-substitute
|
|
186
|
+
out="$(run_cal --only agy)"
|
|
187
|
+
case "$out" in
|
|
188
|
+
*"control: rejects-bogus"*UNTRUSTED-PIN*|*UNTRUSTED-PIN*"control: rejects-bogus"*)
|
|
189
|
+
ok "lane9 validates unknown names yet substitutes a known one → still UNTRUSTED-PIN" ;;
|
|
190
|
+
*) bad "lane9 pin substitution masked by a passing bogus-control" "$out" ;;
|
|
191
|
+
esac
|
|
192
|
+
|
|
193
|
+
# LANE 6 — panel verdict. With no usable different-family sidecar the script must say so explicitly:
|
|
194
|
+
# this is the line a marker's `crossfamily:` leg quotes, and the 2026-07-30 defect was asserting
|
|
195
|
+
# "cross-family unavailable" without ever probing.
|
|
196
|
+
mkstub codex absent; mkstub agy absent
|
|
197
|
+
out="$(run_cal)"
|
|
198
|
+
case "$out" in
|
|
199
|
+
*"PANEL: none"*) ok "lane6 empty panel stated explicitly (not silence)" ;;
|
|
200
|
+
*) bad "lane6 empty panel not stated" "$out" ;;
|
|
201
|
+
esac
|
|
202
|
+
mkstub codex honest
|
|
203
|
+
out="$(run_cal)"
|
|
204
|
+
case "$out" in
|
|
205
|
+
*"PANEL: "*codex*) ok "lane6b populated panel names the usable runtime" ;;
|
|
206
|
+
*) bad "lane6b populated panel not named" "$out" ;;
|
|
207
|
+
esac
|
|
208
|
+
|
|
209
|
+
# LANE 7 — detector, not gate: always exit 0, so a calibration run can never block a caller. The
|
|
210
|
+
# caller reads the verdict; the script does not decide for it.
|
|
211
|
+
mkstub codex absent; mkstub agy absent
|
|
212
|
+
PATH="$TMP/bin:/usr/bin:/bin:/usr/sbin:/sbin" bash "$CAL" >/dev/null 2>&1
|
|
213
|
+
[ $? -eq 0 ] && ok "lane7 exits 0 even with an empty panel (detector, not gate)" \
|
|
214
|
+
|| bad "lane7 non-zero exit on empty panel" "exit=$?"
|
|
215
|
+
|
|
216
|
+
printf '\nsidecar-calibrate lanes: %d passed, %d failed\n' "$PASS" "$FAIL"
|
|
217
|
+
[ "$FAIL" -eq 0 ] || exit 1
|
|
218
|
+
exit 0
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
# test_sidecar_wait_stdin.sh — known-pair anchor for scripts/sidecar_wait.sh's stdin plumbing.
|
|
3
|
+
#
|
|
4
|
+
# WHY (measured 2026-07-29)
|
|
5
|
+
# `sidecar_wait.sh` shipped in v1.4.76 documenting this invocation in its own header:
|
|
6
|
+
# printf '%s' "$prompt" | bash scripts/sidecar_wait.sh out.txt 600 -- codex exec -m gpt-5.5 -
|
|
7
|
+
# It did not work. `"$@" > "$OUT" 2>&1 &` gives the child /dev/null for stdin in a
|
|
8
|
+
# non-interactive shell, so codex received no prompt, answered "No prompt provided via stdin",
|
|
9
|
+
# and the wrapper reported SIDECAR_VERDICT=COMPLETE — a sidecar that never ran, reported as
|
|
10
|
+
# complete. That is the exact misjudgment the script exists to prevent, produced by the script.
|
|
11
|
+
#
|
|
12
|
+
# The FIRST repair was worse than the bug, and adversarial review caught it before it shipped.
|
|
13
|
+
# It spooled stdin to a tempfile; reproduced consequences:
|
|
14
|
+
# - the unbounded `cat` ran BEFORE the child, so an inherited never-EOF stdin hung the wrapper
|
|
15
|
+
# forever and the timeout budget never applied (`-- true` with inherited stdin → rc=124);
|
|
16
|
+
# - it CONSUMED the caller's stdin (a caller reading 3 lines afterwards read 0).
|
|
17
|
+
# The correct fix is one token: `<&0`. POSIX substitutes /dev/null only when stdin is NOT
|
|
18
|
+
# explicitly redirected. Lanes P2/P3 exist because the anchor's first version reported 4/4 green
|
|
19
|
+
# in the same shell where the argv form returned rc=124 — it bound only P1.
|
|
20
|
+
#
|
|
21
|
+
# Lanes
|
|
22
|
+
# P1 piped stdin REACHES the child (the original hole)
|
|
23
|
+
# P2 argv-form with an inherited never-EOF stdin does NOT hang (spool regression)
|
|
24
|
+
# P3 the caller's own stdin is NOT consumed by the wrapper (spool regression)
|
|
25
|
+
# P4 verdict codes: TIMEOUT=1 · EMPTY=0 · COMPLETE=0, and EMPTY/COMPLETE are distinguished
|
|
26
|
+
# P5 an unwritable outfile is exit 2, not "EMPTY" (a false clean on the typed channel)
|
|
27
|
+
#
|
|
28
|
+
# Verdicts are read by running the wrapper DIRECTLY — piping it into `tail` and reading `$?`
|
|
29
|
+
# answers with tail's status and turns a real exit 1 green.
|
|
30
|
+
#
|
|
31
|
+
# Exit 0 = all lanes correct. Exit 1 = the plumbing regressed.
|
|
32
|
+
set -uo pipefail
|
|
33
|
+
ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
|
34
|
+
SW="$ROOT/scripts/sidecar_wait.sh"
|
|
35
|
+
[ -f "$SW" ] || { echo "FAIL: $SW not found"; exit 1; }
|
|
36
|
+
# NOTE: SIDECAR_POLL is set PER LANE, never exported globally. A global export pinned the knob for
|
|
37
|
+
# every lane and thereby MASKED a bad default — a regression to `SIDECAR_POLL:-0` passed 5/5.
|
|
38
|
+
FAST="SIDECAR_POLL=1"
|
|
39
|
+
|
|
40
|
+
pass=0; fail=0
|
|
41
|
+
ok() { printf ' ✅ %s\n' "$1"; pass=$((pass+1)); }
|
|
42
|
+
bad() { printf ' ❌ %s\n' "$1"; fail=$((fail+1)); }
|
|
43
|
+
TD="$(mktemp -d)"; trap 'rm -rf "$TD"' EXIT
|
|
44
|
+
|
|
45
|
+
# P1 — `cat` echoes whatever it receives; an empty outfile means stdin was swallowed, which is
|
|
46
|
+
# exactly what the shipped version did.
|
|
47
|
+
printf 'MARKER_STDIN_ARRIVED' | SIDECAR_POLL=1 bash "$SW" "$TD/p1.out" 20 -- cat >/dev/null 2>&1
|
|
48
|
+
if grep -q 'MARKER_STDIN_ARRIVED' "$TD/p1.out" 2>/dev/null; then
|
|
49
|
+
ok "P1 piped stdin reaches the child process"
|
|
50
|
+
else
|
|
51
|
+
bad "P1 STDIN HOLE IS BACK — child saw no stdin (got: '$(head -c 60 "$TD/p1.out" 2>/dev/null)')"
|
|
52
|
+
fi
|
|
53
|
+
|
|
54
|
+
# P2 — the lane the first anchor lacked. A never-EOF stdin must not block the wrapper: the child is
|
|
55
|
+
# launched immediately and the budget governs. `sleep 20 |` keeps the pipe open with no data.
|
|
56
|
+
( sleep 6 | SIDECAR_POLL=1 timeout 5 bash "$SW" "$TD/p2.out" 3 -- echo ARGV_OK ) >/dev/null 2>&1
|
|
57
|
+
rc_hang=$?
|
|
58
|
+
if [ "$rc_hang" -ne 124 ] && grep -q 'ARGV_OK' "$TD/p2.out" 2>/dev/null; then
|
|
59
|
+
ok "P2 an inherited never-EOF stdin does not hang the wrapper (child still runs)"
|
|
60
|
+
else
|
|
61
|
+
bad "P2 the wrapper HUNG or never ran the child on inherited stdin (rc=$rc_hang, out='$(head -c 40 "$TD/p2.out" 2>/dev/null)')"
|
|
62
|
+
fi
|
|
63
|
+
|
|
64
|
+
# P3 — the wrapper must not eat the caller's stream. `echo` reads nothing, so all three lines must
|
|
65
|
+
# remain readable afterwards. The spool version left zero.
|
|
66
|
+
n=$(printf 'l1\nl2\nl3\n' | { SIDECAR_POLL=1 bash "$SW" "$TD/p3.out" 20 -- echo x >/dev/null 2>&1
|
|
67
|
+
c=0; while read -r _; do c=$((c+1)); done; echo "$c"; })
|
|
68
|
+
if [ "$n" = "3" ]; then
|
|
69
|
+
ok "P3 the caller's own stdin survives the call (3/3 lines still readable)"
|
|
70
|
+
else
|
|
71
|
+
bad "P3 the wrapper consumed the caller's stdin ($n of 3 lines left)"
|
|
72
|
+
fi
|
|
73
|
+
|
|
74
|
+
# P4 — verdict channel. `-- true` writes nothing and is EMPTY, NOT COMPLETE; the first anchor
|
|
75
|
+
# labelled that lane "COMPLETE" and so never exercised COMPLETE at all.
|
|
76
|
+
SIDECAR_POLL=1 bash "$SW" "$TD/p4t.out" 2 -- sleep 30 >"$TD/p4t.txt" 2>&1 </dev/null; rc_t=$?
|
|
77
|
+
SIDECAR_POLL=1 bash "$SW" "$TD/p4e.out" 20 -- true >"$TD/p4e.txt" 2>&1 </dev/null; rc_e=$?
|
|
78
|
+
SIDECAR_POLL=1 bash "$SW" "$TD/p4c.out" 20 -- echo hi >"$TD/p4c.txt" 2>&1 </dev/null; rc_c=$?
|
|
79
|
+
if [ "$rc_t" -eq 1 ] && [ "$rc_e" -eq 0 ] && [ "$rc_c" -eq 0 ] \
|
|
80
|
+
&& grep -q 'SIDECAR_VERDICT=TIMEOUT' "$TD/p4t.txt" \
|
|
81
|
+
&& grep -q 'SIDECAR_VERDICT=EMPTY' "$TD/p4e.txt" \
|
|
82
|
+
&& grep -q 'SIDECAR_VERDICT=COMPLETE' "$TD/p4c.txt"; then
|
|
83
|
+
ok "P4 verdicts intact and distinguished (TIMEOUT=1 · EMPTY=0 · COMPLETE=0)"
|
|
84
|
+
else
|
|
85
|
+
bad "P4 verdict channel drifted (timeout rc=$rc_t, empty rc=$rc_e, complete rc=$rc_c)"
|
|
86
|
+
head -1 "$TD/p4t.txt" "$TD/p4e.txt" "$TD/p4c.txt" 2>/dev/null | sed 's/^/ /'
|
|
87
|
+
fi
|
|
88
|
+
|
|
89
|
+
# P5 — an outfile that cannot be written made `wc -c` fail, `${size:-0}` read 0, and the wrapper
|
|
90
|
+
# announce EMPTY with exit 0. The sidecar had in fact produced output. Same false-clean class as
|
|
91
|
+
# the bug this file anchors, on the typed channel itself.
|
|
92
|
+
SIDECAR_POLL=1 bash "$SW" "$TD/nodir/x.out" 10 -- echo hi >/dev/null 2>&1 </dev/null
|
|
93
|
+
rc_w=$?
|
|
94
|
+
if [ "$rc_w" -eq 2 ]; then
|
|
95
|
+
ok "P5 an unwritable outfile fails closed (exit 2), never 'EMPTY'"
|
|
96
|
+
else
|
|
97
|
+
bad "P5 an unwritable outfile returned rc=$rc_w — a sidecar that ran reported as saying nothing"
|
|
98
|
+
fi
|
|
99
|
+
|
|
100
|
+
# P6 — the knob added while making this anchor cheap re-opened the unbounded wait. Each bad value
|
|
101
|
+
# must still produce a bounded TIMEOUT. Deliberately NOT using $FAST: the point is a bad POLL.
|
|
102
|
+
p6=0
|
|
103
|
+
for badpoll in 0 0.5 abc 08 09 600; do
|
|
104
|
+
SIDECAR_POLL="$badpoll" timeout 12 bash "$SW" "$TD/p6.out" 2 -- sleep 30 >"$TD/p6.txt" 2>&1
|
|
105
|
+
rc6=$?
|
|
106
|
+
if [ "$rc6" -eq 1 ] && grep -q 'SIDECAR_VERDICT=TIMEOUT' "$TD/p6.txt"; then p6=$((p6+1)); fi
|
|
107
|
+
done
|
|
108
|
+
if [ "$p6" -eq 6 ]; then
|
|
109
|
+
ok "P6 a malformed SIDECAR_POLL (0·0.5·abc·08·09·600) still times out — the budget is not disarmable"
|
|
110
|
+
else
|
|
111
|
+
bad "P6 only $p6/6 malformed poll values produced a bounded TIMEOUT (unbounded wait is reachable)"
|
|
112
|
+
fi
|
|
113
|
+
|
|
114
|
+
# P7 — TIMEOUT must not orphan the sidecar, INCLUDING grandchildren: `kill "$PID"` reached only the
|
|
115
|
+
# direct child, so `sh -c 'sleep N & wait'` survived while this lane stayed green. The marker is
|
|
116
|
+
# per-run: a machine-global `pgrep -f 'sleep 25'` false-FAILs on any concurrent sleep and its
|
|
117
|
+
# failure branch would kill unrelated host processes.
|
|
118
|
+
MARK="p7_$$_$(date +%s 2>/dev/null || echo x)"
|
|
119
|
+
SIDECAR_POLL=1 bash "$SW" "$TD/p7.out" 2 -- sh -c "sleep 25 & wait # $MARK" >"$TD/p7.txt" 2>&1 </dev/null
|
|
120
|
+
sleep 1
|
|
121
|
+
if ! pgrep -f "$MARK" >/dev/null 2>&1; then
|
|
122
|
+
ok "P7 a timed-out sidecar is killed with its grandchildren, not orphaned past our exit"
|
|
123
|
+
else
|
|
124
|
+
bad "P7 a grandchild survived TIMEOUT (orphan restored)"; pkill -f "$MARK" 2>/dev/null
|
|
125
|
+
fi
|
|
126
|
+
|
|
127
|
+
# P9 — the tty path. HONEST SCOPE, do not read this lane as more than it is.
|
|
128
|
+
#
|
|
129
|
+
# It asserts that a stdin-reading child under an allocated pty still produces a bounded verdict. It
|
|
130
|
+
# does NOT bind the `if [ -t 0 ]` branch: removing that branch entirely leaves this lane GREEN
|
|
131
|
+
# (measured 2026-07-29). Cross-family review reproduced a SIGTTIN stop (rc=124, no verdict) under a
|
|
132
|
+
# pty before `set -m` was added for the group-kill; this harness cannot reproduce that condition,
|
|
133
|
+
# so whether the branch is still load-bearing is UNMEASURED — the branch is kept as defensive, not
|
|
134
|
+
# as something this anchor proves.
|
|
135
|
+
#
|
|
136
|
+
# A lane that cannot separate a known-positive from a known-negative is not measuring; it is
|
|
137
|
+
# generating. Shipping it as a plain green would manufacture exactly the false confidence this
|
|
138
|
+
# file exists to prevent, so it is labelled instead of silently counted.
|
|
139
|
+
if command -v script >/dev/null 2>&1; then
|
|
140
|
+
# `< /dev/null` on `script` itself: with the harness's inherited stdin it could not allocate a pty
|
|
141
|
+
# at all and wrote no rc file, so the lane failed identically with AND without the fix under test
|
|
142
|
+
# — it discriminated nothing. The pty it allocates for the inner command is what matters.
|
|
143
|
+
# The inner shell writes its rc to a FILE. Parsing `script`'s own stdout fails: it emits ^D and
|
|
144
|
+
# CRLF, and the first version of this lane read rc as empty and reported a defect that did not
|
|
145
|
+
# exist — the target was fine, the instrument was not.
|
|
146
|
+
script -q /dev/null bash -c "SIDECAR_POLL=1 timeout 12 bash '$SW' '$TD/p9.out' 3 -- cat >'$TD/p9.txt' 2>&1; echo \$? > '$TD/p9rc'" >/dev/null 2>&1 < /dev/null
|
|
147
|
+
rc9=$(tr -dc '0-9' < "$TD/p9rc" 2>/dev/null)
|
|
148
|
+
if [ -n "$rc9" ] && [ "$rc9" != "124" ] && grep -q 'SIDECAR_VERDICT=' "$TD/p9.txt" 2>/dev/null; then
|
|
149
|
+
ok "P9 the tty path runs and emits a verdict (UNCALIBRATED — see note; does NOT bind the branch)"
|
|
150
|
+
else
|
|
151
|
+
bad "P9 tty case rc='${rc9:-<unread>}' verdict='$(head -1 "$TD/p9.txt" 2>/dev/null)'"
|
|
152
|
+
fi
|
|
153
|
+
else
|
|
154
|
+
bad "P9 UNCALIBRATED — \`script\` unavailable, the tty branch cannot be exercised here"
|
|
155
|
+
fi
|
|
156
|
+
|
|
157
|
+
# P8 — the child's stderr AND its exit code must reach the typed channel. Dropping `2>&1` or
|
|
158
|
+
# hardcoding rc=0 both passed the earlier anchor: it grepped the verdict WORD and the wrapper's
|
|
159
|
+
# own rc, never the child's.
|
|
160
|
+
SIDECAR_POLL=1 bash "$SW" "$TD/p8.out" 20 -- sh -c 'echo STDERR_MARK >&2; exit 3' >"$TD/p8.txt" 2>&1 </dev/null
|
|
161
|
+
if grep -q 'STDERR_MARK' "$TD/p8.out" 2>/dev/null && grep -q 'exit=3' "$TD/p8.txt"; then
|
|
162
|
+
ok "P8 child stderr is captured and its exit code reaches the verdict line"
|
|
163
|
+
else
|
|
164
|
+
bad "P8 stderr or child exit code lost (out='$(head -c 40 "$TD/p8.out" 2>/dev/null)' verdict='$(head -1 "$TD/p8.txt")')"
|
|
165
|
+
fi
|
|
166
|
+
|
|
167
|
+
echo "----"
|
|
168
|
+
echo "sidecar_wait stdin anchor: $pass passed, $fail failed"
|
|
169
|
+
[ "$fail" -eq 0 ] || exit 1
|