#!/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 # --------------------------------------------------------------------------- # S4 (review, hosts.md): soft error pages must NOT be stored in the HTTP # response cache. Pre-fix, a routing-failure page was cached as HTTP 200 and # served byte-identically from cache to every later visitor (cold 2s → warm # 0.0005s, ONE render line). Post-fix (dc7aa709), http_render_page returns # (html, is_error) and cache insertion is gated on `not is_err` (the skip is # logged as "[cache] → error page, not cached"). # # Pin: GET the same nonexistent path twice against a fresh --http server and # assert BOTH requests re-render (two [sx-http] render lines) plus the # is_err gate line appearing in the log. NB: in a standalone worktree all # docs pages render as soft error pages (no content), so a positive # "real page IS cached" control is not assertable here. # --------------------------------------------------------------------------- s4_case() { local port=$((18000 + RANDOM % 2000)) local log; log=$(mktemp) timeout 90 "$SERVER" --http "$port" >"$log" 2>&1 & local srv=$! local up=0 for _ in $(seq 1 40); do if curl -s -o /dev/null "http://localhost:$port/" 2>/dev/null; then up=1; break; fi sleep 1 done if [[ $up -ne 1 ]]; then echo "FAIL: S4 — http server did not come up on :$port" kill "$srv" 2>/dev/null; rm -f "$log" fail=$((fail+1)); return fi local miss="/sx/gate-pin-missing-$$-$RANDOM" curl -s -o /dev/null "http://localhost:$port$miss" curl -s -o /dev/null "http://localhost:$port$miss" sleep 1 local renders renders=$(grep -c "sx-http\] $miss " "$log") local ok=1 if [[ "$renders" -ne 2 ]]; then echo "FAIL: S4 — expected 2 renders of $miss (not cache-served), got $renders" ok=0 fi if ! grep -q 'error page, not cached' "$log"; then echo "FAIL: S4 — is_err cache gate line absent from server log" ok=0 fi if [[ $ok -eq 1 ]]; then echo "PASS: S4 soft error page not cached (both GETs re-rendered)" pass=$((pass+1)) else echo " --- log tail ---"; tail -12 "$log" | sed 's/^/ /'; echo " ---------------" fail=$((fail+1)) fi kill "$srv" 2>/dev/null rm -f "$log" } s4_case echo echo "protocol-gate: $pass passed, $fail failed" [[ $fail -eq 0 ]]