diff --git a/plans/agent-briefings/sx-gate-loop.md b/plans/agent-briefings/sx-gate-loop.md index 1649a08e..4afc8933 100644 --- a/plans/agent-briefings/sx-gate-loop.md +++ b/plans/agent-briefings/sx-gate-loop.md @@ -52,7 +52,8 @@ Pin each confirmed-and-fixed finding with a minimal repro. Add suites to is K16 [W8], still OPEN — not a W14 pin target until its fix lands - [x] crit-2 [W1] — signal-return kont pinned NON-VACUOUSLY (side-effect sentinel across two tests; a plain assert would inherit the vacuity) -- [ ] C1/C1b [W3] — HTTP-mode concurrency fixes, pin +- [x] C1/C1b [W3] — command-channel crash guards pinned + (`scripts/test-protocol-gate.sh`, seed for section E's fuzz suite) - [ ] S4 [conformance] — housekeeping repro, pin ### B. Runner/production env unification @@ -80,6 +81,16 @@ Pin each confirmed-and-fixed finding with a minimal repro. Add suites to ## Progress log (newest first) +- 2026-07-04 — **C1/C1b command-channel pins (item A.6)**. These are + protocol-level, not .sx-suite pins: authored + `scripts/test-protocol-gate.sh` — each case spawns its OWN timeout-bounded + `sx_server.exe` (no shared process touched) and asserts three things: an + `(error N "Malformed command line: ...")` response is emitted, the + follow-up epoch still evaluates (process survived), and no `Fatal error` + escapes / exit is clean. Cases: C1 unterminated list (exact review repro), + C1 plain-garbage line, C1b non-ASCII byte (`café`), plus a well-formed + control session. 4/4 green. The script is deliberately structured to grow + into section E's fuzz suite (C3–C7). Test-only. - 2026-07-04 — **crit-2 non-vacuous pin (item A.5)**. The original bug's signature — handler value becomes the WHOLE program result, discarding every outer frame *including the covering test's own assert* — means a diff --git a/scripts/test-protocol-gate.sh b/scripts/test-protocol-gate.sh new file mode 100755 index 00000000..9ac7355b --- /dev/null +++ b/scripts/test-protocol-gate.sh @@ -0,0 +1,89 @@ +#!/bin/bash +# test-protocol-gate.sh — W14 pins for the epoch/command-channel protocol. +# +# Pins C1/C1b (review, plans/sx-review/hosts.md): a malformed or non-ASCII +# line on the top-level command channel used to raise an uncaught +# Sx_types.Parse_error and KILL the whole sx_server process (the shared +# channel used by bridges and conformance runners). Fixed in dc7aa709: +# the server now answers `(error N "Malformed command line: ...")` and +# keeps serving. +# +# Each case spawns its OWN timeout-bounded sx_server.exe subprocess — +# no shared/sibling process is ever touched. Designed to grow into the +# W14 section-E protocol fuzz suite (C3-C7). +# +# Usage: bash scripts/test-protocol-gate.sh +# Exit: 0 = all pins green; 1 = a pin failed (fix regressed). +set -uo pipefail + +cd "$(dirname "$0")/.." +SERVER=hosts/ocaml/_build/default/bin/sx_server.exe + +if [[ ! -x "$SERVER" ]]; then + echo "SKIP: $SERVER not built (run sx_build target=ocaml first)" >&2 + exit 2 +fi + +pass=0 +fail=0 + +# run_case NAME INPUT EXPECT_SENTINEL +# Feeds INPUT to a fresh server. Asserts: +# 1. an (error ... "Malformed command line: ...") response is emitted +# 2. the follow-up epoch still evaluates (EXPECT_SENTINEL in output) +# 3. the process exits cleanly (no Fatal error, exit 0 on stdin EOF) +run_case() { + local name="$1" input="$2" sentinel="$3" + local out rc + out=$(printf '%b' "$input" | timeout 60 "$SERVER" 2>&1) + rc=$? + local ok=1 + if ! grep -q 'Malformed command line' <<<"$out"; then + echo "FAIL: $name — no malformed-line error response"; ok=0 + fi + if ! grep -q "^${sentinel}\$" <<<"$out"; then + echo "FAIL: $name — follow-up epoch did not run (process died?)"; ok=0 + fi + if grep -q 'Fatal error' <<<"$out"; then + echo "FAIL: $name — Fatal error escaped to the top level"; ok=0 + fi + if [[ $rc -ne 0 ]]; then + echo "FAIL: $name — nonzero exit ($rc)"; ok=0 + fi + if [[ $ok -eq 1 ]]; then + echo "PASS: $name" + pass=$((pass+1)) + else + echo " --- output ---"; sed 's/^/ /' <<<"$out"; echo " --------------" + fail=$((fail+1)) + fi +} + +# C1: unterminated list on the command channel (exact review repro) +run_case "C1 unterminated list survives" \ + '(epoch 2)\n(eval "(+ 1 2"\n(epoch 3)\n(eval "99")\n' \ + '99' + +# C1: plain-garbage line (second C1 repro shape) +run_case "C1 garbage line survives" \ + '(epoch 1)\nnot an s-expr ]]] {{{\n(epoch 2)\n(eval "42")\n' \ + '42' + +# C1b: non-ASCII byte on the command channel (exact review repro; \xc3\xa9 = é) +run_case "C1b non-ASCII line survives" \ + '(epoch 1)\n(eval (quote caf\xc3\xa9))\n(epoch 2)\n(eval "99")\n' \ + '99' + +# Control: a well-formed session still works end to end +ctrl=$(printf '(epoch 1)\n(eval "(+ 40 2)")\n' | timeout 60 "$SERVER" 2>&1) +if grep -q '^42$' <<<"$ctrl"; then + echo "PASS: control well-formed session" + pass=$((pass+1)) +else + echo "FAIL: control well-formed session"; sed 's/^/ /' <<<"$ctrl" + fail=$((fail+1)) +fi + +echo +echo "protocol-gate: $pass passed, $fail failed" +[[ $fail -eq 0 ]]