a73x

a3fd1a94

test: e2e — hub scenarios: passivity pin, origin gate, tear/re-attach

a73x   2026-08-13 11:31

Commit message
test: e2e — hub scenarios: passivity pin, origin gate, tear/re-attach

M-web Task 10. Three scenarios, pins 20->23 and 33->35. (a) The 1x1
passivity contract, daemon-side and hub-free on purpose: the geometry
assertion is textual — a marker typed at 80 wide can only appear
contiguous in muxd dump if the grid is still 80 wide — and the 1x1
attacher types a claim-attempt keystroke while provably attached. (b)
The hub pumps a real session: a wsclient wall tile converges
byte-for-byte with muxd dump through the WebSocket leg, with the
cannot-fail control (doctored dump must diverge, the wan.sh rule); a
forged evil Origin exits 4 on a 403 with the daemon's client count
unmoved. (c) muxd stop under a live tile: the hub narrates
reconnecting/up across the restart, and the stand-in's re-attach —
quoting coordinates the new daemon never issued — converges on the new
session's content across the epoch crossing.

wsclient and muxweb ride as artifact args 7 and 8 (build.zig, e2e.sh,
soak.sh). e2e green twice; soak SOAK_N=3 3/3 with the leak gate clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

build.zig
Old New
@@ -550,6 +550,8 @@ pub fn build(b: *std.Build) void {
550 e2e.addArtifactArg(delaypipe_exe); 550 e2e.addArtifactArg(delaypipe_exe);
551 e2e.addArtifactArg(render_exe); 551 e2e.addArtifactArg(render_exe);
552 e2e.addArtifactArg(ptyclient_exe); 552 e2e.addArtifactArg(ptyclient_exe);
553 e2e.addArtifactArg(wsclient_exe);
554 e2e.addArtifactArg(webhub_exe);
553 const e2e_step = b.step("e2e", "Run end-to-end test"); 555 const e2e_step = b.step("e2e", "Run end-to-end test");
554 e2e_step.dependOn(&e2e.step); 556 e2e_step.dependOn(&e2e.step);
555 557
@@ -560,6 +562,8 @@ pub fn build(b: *std.Build) void {
560 soak.addArtifactArg(delaypipe_exe); 562 soak.addArtifactArg(delaypipe_exe);
561 soak.addArtifactArg(render_exe); 563 soak.addArtifactArg(render_exe);
562 soak.addArtifactArg(ptyclient_exe); 564 soak.addArtifactArg(ptyclient_exe);
565 soak.addArtifactArg(wsclient_exe);
566 soak.addArtifactArg(webhub_exe);
563 const soak_step = b.step("soak", "Run the e2e suite SOAK_N times (default 10)"); 567 const soak_step = b.step("soak", "Run the e2e suite SOAK_N times (default 10)");
564 soak_step.dependOn(&soak.step); 568 soak_step.dependOn(&soak.step);
565 569
test/e2e.sh
Old New
@@ -12,6 +12,10 @@ DELAYPIPE="$4"
12 RENDER="$5" 12 RENDER="$5"
13 # M12 pty fixture: runs the client on a real pty (test/ptyclient.zig). 13 # M12 pty fixture: runs the client on a real pty (test/ptyclient.zig).
14 PTYCLIENT="$6" 14 PTYCLIENT="$6"
15 # M-web: the scripted WebSocket replica client and the hub it speaks to
16 # (test/wsclient.zig, src/webhub_main.zig).
17 WSCLIENT="$7"
18 MUXWEB="$8"
15 SOCK="${TMPDIR:-/tmp}/muxd-e2e-$$.sock" 19 SOCK="${TMPDIR:-/tmp}/muxd-e2e-$$.sock"
16 OUT="${TMPDIR:-/tmp}/mux-e2e-out-$$" 20 OUT="${TMPDIR:-/tmp}/mux-e2e-out-$$"
17 # M10: hermetic XDG homes. Key-default scenarios must see OUR key or none, 21 # M10: hermetic XDG homes. Key-default scenarios must see OUR key or none,
@@ -147,6 +151,23 @@ H1PID=""
147 H3PID="" 151 H3PID=""
148 H4PID="" 152 H4PID=""
149 H5PID="" 153 H5PID=""
154 # M-web. Three daemons (the passivity pin's, the hub-basic one, and the
155 # tear scenario's two incarnations share SOCK20), two hub processes, and
156 # a port band of its own following the 5000-spacing convention — INSIDE
157 # the ephemeral range, which is fine for ports we bind (only provably-DEAD
158 # ports must stay below 32768; see HDEADPORT above).
159 SOCK18="${TMPDIR:-/tmp}/muxd-e2e-web-a-$$.sock"
160 SOCK19="${TMPDIR:-/tmp}/muxd-e2e-web-b-$$.sock"
161 SOCK20="${TMPDIR:-/tmp}/muxd-e2e-web-c-$$.sock"
162 WPORT=$(( 41000 + ($$ % 4000) ))
163 WPORT2=$(( 46000 + ($$ % 4000) ))
164 D14PID=""
165 D15PID=""
166 D16PID=""
167 D17PID=""
168 W1PID=""
169 W2PID=""
170 WCLIPID=""
150 171
151 # One counter out of a MUX_PREDICT_STATS line. The client prints exactly one 172 # One counter out of a MUX_PREDICT_STATS line. The client prints exactly one
152 # such line on exit; every field is a key=value pair, so a rename or reorder 173 # such line on exit; every field is a key=value pair, so a rename or reorder
@@ -420,6 +441,30 @@ cleanup() {
420 "$OUT.h1" "$OUT.h1.err" "$OUT.h2" "$OUT.h2.err" "$OUT.h3" "$OUT.h3.err" \ 441 "$OUT.h1" "$OUT.h1.err" "$OUT.h2" "$OUT.h2.err" "$OUT.h3" "$OUT.h3.err" \
421 "$OUT.h4" "$OUT.h4.err" "$OUT.h5" "$OUT.h5.err" 442 "$OUT.h4" "$OUT.h4.err" "$OUT.h5" "$OUT.h5.err"
422 rm -rf "$SSHIM_DIR" "$HRUN" "$HRUN2" 443 rm -rf "$SSHIM_DIR" "$HRUN" "$HRUN2"
444 # M-web: hubs and daemons by tracked pid, then stop-by-socket for any
445 # daemon that died between fork and up-line, then the captures. The
446 # stops must precede the socket rm, like SOCK14-17 above.
447 [ -n "$W1PID" ] && kill "$W1PID" 2>/dev/null || true
448 [ -n "$W2PID" ] && kill "$W2PID" 2>/dev/null || true
449 [ -n "$WCLIPID" ] && kill "$WCLIPID" 2>/dev/null || true
450 [ -n "$D14PID" ] && kill "$D14PID" 2>/dev/null || true
451 [ -n "$D15PID" ] && kill "$D15PID" 2>/dev/null || true
452 [ -n "$D16PID" ] && kill "$D16PID" 2>/dev/null || true
453 [ -n "$D17PID" ] && kill "$D17PID" 2>/dev/null || true
454 [ -S "$SOCK18" ] && "$MUXD" stop --sock "$SOCK18" 2>/dev/null || true
455 [ -S "$SOCK19" ] && "$MUXD" stop --sock "$SOCK19" 2>/dev/null || true
456 [ -S "$SOCK20" ] && "$MUXD" stop --sock "$SOCK20" 2>/dev/null || true
457 rm -f "$SOCK18" "$SOCK19" "$SOCK20" \
458 "$OUT.weba" "$OUT.weba.err" "$OUT.weba.d" \
459 "$OUT.web1x1" "$OUT.web1x1.err" "$OUT.web1x1.log" \
460 "$OUT.webb" "$OUT.webb.err" "$OUT.webb.d" "$OUT.webh" \
461 "$OUT.webws" "$OUT.webws.err" "$OUT.webws.n" "$OUT.webws.dump" \
462 "$OUT.webws.dump.n" "$OUT.webws.diff" "$OUT.webws.doc" \
463 "$OUT.webevil" "$OUT.webevil.err" \
464 "$OUT.webc" "$OUT.webc.err" "$OUT.webc.d" "$OUT.webc2" "$OUT.webc2.err" \
465 "$OUT.webc2.d" "$OUT.webh2" "$OUT.webws2" "$OUT.webws2.err" \
466 "$OUT.webws2.n" "$OUT.webws2.dump" "$OUT.webws2.dump.n" \
467 "$OUT.webws2.diff" "$OUT.webstop" "$OUT.webstop2" "$OUT.webstop3"
423 # The convergence files a FAILING assert_converged leaves behind 468 # The convergence files a FAILING assert_converged leaves behind
424 # (.render/.dump/.rvt/.dvt/.diff for that capture) are deliberately not 469 # (.render/.dump/.rvt/.dvt/.diff for that capture) are deliberately not
425 # chased here: on a failing run they are the evidence. 470 # chased here: on a failing run they are the evidence.
@@ -2817,17 +2862,273 @@ rm -f "$OUT.h1" "$OUT.h1.err" "$OUT.h2" "$OUT.h2.err" "$OUT.h3" "$OUT.h3.err" \
2817 "$OUT.h4" "$OUT.h4.err" "$OUT.h5" "$OUT.h5.err" "$OUT.stop" "$HKEY" "$HCFGBAD" 2862 "$OUT.h4" "$OUT.h4.err" "$OUT.h5" "$OUT.h5.err" "$OUT.stop" "$HKEY" "$HCFGBAD"
2818 rm -rf "$SSHIM_DIR" "$HRUN" "$HRUN2" 2863 rm -rf "$SSHIM_DIR" "$HRUN" "$HRUN2"
2819 2864
2865 # --- M-web (a): a 1x1 attach is refused the grid and can never claim it.
2866 # The wall tile's passivity contract, pinned DAEMON-SIDE and hub-free on
2867 # purpose: the mechanism is applySize's cols<2 refusal plus claimGrid's
2868 # 0x0-slot refusal (server.zig), and it must hold whether or not muxweb
2869 # exists. The probe for "the grid did not move" is textual: a marker
2870 # string typed at 80 wide can only appear CONTIGUOUS in `muxd dump` if
2871 # the grid is still 80 wide — a 1-column grid puts every glyph on its
2872 # own row, so grep itself is the geometry assertion.
2873 "$MUXD" run --sock "$SOCK18" --shell /bin/sh > "$OUT.weba.d" 2>&1 &
2874 D14PID=$!
2875 i=0
2876 while [ ! -S "$SOCK18" ] && [ "$i" -lt 50 ]; do sleep 0.1; i=$((i+1)); done
2877 [ -S "$SOCK18" ] || { echo "e2e FAIL: web passivity daemon never bound"; cat "$OUT.weba.d"; exit 1; }
2878
2879 # The 80x24 client: types the first marker, holds the session open long
2880 # enough for the 1x1 attacher to come and go, types the second marker
2881 # (the geometry probe), detaches.
2882 { printf 'printf "wall-%%s\\n" pin\n'; sleep 4; printf 'printf "zz-%%s\\n" web\n'; sleep 2; printf '\034'; } | \
2883 timeout 40 "$MUX" --sock "$SOCK18" > "$OUT.weba" 2> "$OUT.weba.err" &
2884 WCLIPID=$!
2885 i=0
2886 while [ "$i" -lt 100 ]; do
2887 "$MUXD" dump --sock "$SOCK18" 2>/dev/null | grep -q "wall-pin" && break
2888 sleep 0.1; i=$((i+1))
2889 done
2890 "$MUXD" dump --sock "$SOCK18" | grep -q "wall-pin" || {
2891 echo "e2e FAIL: web passivity: first marker never landed"; exit 1; }
2892
2893 # The 1x1 attacher, on a real 1x1 pty. Its attach is answered with a
2894 # unicast snapshot (the alt-screen enter proves the first frame came),
2895 # its keystroke is the claim attempt, and the 'z' it waits for is the
2896 # second marker echoing — proof it was STILL ATTACHED while the marker
2897 # landed, so the grid it could have wrecked was live the whole time.
2898 set +e
2899 timeout 40 "$PTYCLIENT" --cols 1 --rows 1 --out "$OUT.web1x1" --err "$OUT.web1x1.err" \
2900 -- "$MUX" --sock "$SOCK18" > "$OUT.web1x1.log" 2>&1 <<'EOF'
2901 expect \x1b[?1049h 15000
2902 send \x20
2903 expect z 20000
2904 settle 300 10000
2905 send \x1c
2906 waitexit 10000
2907 EOF
2908 RC=$?
2909 set -e
2910 [ "$RC" -eq 0 ] || {
2911 echo "e2e FAIL: web passivity: 1x1 client exited $RC"
2912 cat "$OUT.web1x1.log"; cat -v "$OUT.web1x1.err" 2>/dev/null; exit 1; }
2913
2914 # The geometry assertion: both markers contiguous AFTER the 1x1 client
2915 # attached, typed, and left. A grid moved to 1 wide cannot hold either.
2916 "$MUXD" dump --sock "$SOCK18" | grep -q "wall-pin" || {
2917 echo "e2e FAIL: web passivity: first marker lost after 1x1 attach:"
2918 "$MUXD" dump --sock "$SOCK18"; exit 1; }
2919 "$MUXD" dump --sock "$SOCK18" | grep -q "zz-web" || {
2920 echo "e2e FAIL: web passivity: the 1x1 attacher moved the grid (marker not contiguous):"
2921 "$MUXD" dump --sock "$SOCK18"; exit 1; }
2922
2923 set +e
2924 wait "$WCLIPID"
2925 RC=$?
2926 set -e
2927 WCLIPID=""
2928 [ "$RC" -eq 0 ] || { echo "e2e FAIL: web passivity: 80x24 client exited $RC"; cat "$OUT.weba.err"; exit 1; }
2929 assert_converged "$OUT.weba" "$SOCK18" "web passivity: the 80x24 client never glitched"
2930
2931 set +e
2932 "$MUXD" stop --sock "$SOCK18" 2> "$OUT.webstop"
2933 RC_STOP=$?
2934 set -e
2935 [ "$RC_STOP" = "0" ] || { echo "e2e FAIL: web passivity stop exited $RC_STOP"; cat "$OUT.webstop"; exit 1; }
2936 grep -q '^muxd: stopped' "$OUT.webstop" || { echo "e2e FAIL: web passivity stop did not report stopped"; cat "$OUT.webstop"; exit 1; }
2937 [ ! -S "$SOCK18" ] || { echo "e2e FAIL: web passivity stop left the socket"; ls -l "$SOCK18"; exit 1; }
2938 wait_pid_gone "$D14PID" "web passivity: stop reported stopped"
2939 D14PID=""
2940 ok "a 1x1 attach is refused the grid and can never claim it"
2941
2942 # --- M-web (b): the hub pumps a real session; wrong Origin refused.
2943 "$MUXD" run --sock "$SOCK19" --shell /bin/sh > "$OUT.webb.d" 2>&1 &
2944 D15PID=$!
2945 i=0
2946 while [ ! -S "$SOCK19" ] && [ "$i" -lt 50 ]; do sleep 0.1; i=$((i+1)); done
2947 [ -S "$SOCK19" ] || { echo "e2e FAIL: web hub daemon never bound"; cat "$OUT.webb.d"; exit 1; }
2948 "$MUXWEB" --sock "$SOCK19" --port "$WPORT" > "$OUT.webh" 2>&1 &
2949 W1PID=$!
2950 wait_for "$OUT.webh" "serving" 10 || {
2951 echo "e2e FAIL: hub never reported serving"; cat "$OUT.webh"; exit 1; }
2952
2953 { printf 'printf "web-%%s\\n" b1\n'; sleep 4; printf '\034'; } | \
2954 timeout 40 "$MUX" --sock "$SOCK19" > "$OUT.webb" 2> "$OUT.webb.err" &
2955 WCLIPID=$!
2956 i=0
2957 while [ "$i" -lt 100 ]; do
2958 "$MUXD" dump --sock "$SOCK19" 2>/dev/null | grep -q "web-b1" && break
2959 sleep 0.1; i=$((i+1))
2960 done
2961
2962 # The browser stand-in: a passive 1x1 wall tile through the hub. Its
2963 # final grid is the daemon's replica as REPLAYED THROUGH the WebSocket
2964 # leg, dumped in muxd dump's own format.
2965 set +e
2966 timeout 40 "$WSCLIENT" --port "$WPORT" --tile 0 --out "$OUT.webws" --err "$OUT.webws.err" <<'EOF'
2967 attach 1 1
2968 expectstate up 10000
2969 expectgrid web-b1 15000
2970 settle 500 10000
2971 dumpexit
2972 EOF
2973 RC=$?
2974 set -e
2975 [ "$RC" -eq 0 ] || {
2976 echo "e2e FAIL: web hub: wsclient exited $RC"
2977 cat -v "$OUT.webws.err" 2>/dev/null; cat "$OUT.webh"; exit 1; }
2978
2979 "$MUXD" dump --sock "$SOCK19" > "$OUT.webws.dump"
2980 sed 's/[[:space:]]*$//' "$OUT.webws" > "$OUT.webws.n"
2981 sed 's/[[:space:]]*$//' "$OUT.webws.dump" > "$OUT.webws.dump.n"
2982 diff -u "$OUT.webws.dump.n" "$OUT.webws.n" > "$OUT.webws.diff" || {
2983 echo "e2e FAIL: web hub: wsclient replica diverges from daemon grid (-daemon +ws):"
2984 head -40 "$OUT.webws.diff"; exit 1; }
2985
2986 # The cannot-fail control (the wan.sh rule, M9): doctor a copy of the
2987 # daemon dump and assert the SAME diff catches it. A convergence check
2988 # that cannot fail proves nothing.
2989 cp "$OUT.webws.dump.n" "$OUT.webws.doc"
2990 printf 'doctored-row\n' >> "$OUT.webws.doc"
2991 if diff -u "$OUT.webws.doc" "$OUT.webws.n" > /dev/null 2>&1; then
2992 echo "e2e FAIL: web hub: the convergence diff cannot fail (doctored dump passed)"; exit 1
2993 fi
2994
2995 # Wrong Origin: refused at HTTP, before any upgrade — the wsclient sees
2996 # a non-101 and exits 4 — and the daemon never sees a client for it.
2997 CLIENTS_BEFORE=$("$MUXD" stats --sock "$SOCK19" | sed -n 's/.*clients=\([0-9]*\).*/\1/p')
2998 set +e
2999 timeout 20 "$WSCLIENT" --port "$WPORT" --tile 0 --origin http://evil.example \
3000 --out "$OUT.webevil" --err "$OUT.webevil.err" < /dev/null
3001 RC=$?
3002 set -e
3003 [ "$RC" -eq 4 ] || {
3004 echo "e2e FAIL: web hub: evil-origin wsclient exited $RC, want 4"
3005 cat -v "$OUT.webevil.err" 2>/dev/null; exit 1; }
3006 grep -q "upgrade refused.*403" "$OUT.webevil.err" || {
3007 echo "e2e FAIL: web hub: refusal was not the 403 it must be:"
3008 cat -v "$OUT.webevil.err"; exit 1; }
3009 CLIENTS_AFTER=$("$MUXD" stats --sock "$SOCK19" | sed -n 's/.*clients=\([0-9]*\).*/\1/p')
3010 [ "$CLIENTS_BEFORE" = "$CLIENTS_AFTER" ] || {
3011 echo "e2e FAIL: web hub: evil origin reached the daemon (clients $CLIENTS_BEFORE -> $CLIENTS_AFTER)"; exit 1; }
3012
3013 set +e
3014 wait "$WCLIPID"
3015 RC=$?
3016 set -e
3017 WCLIPID=""
3018 [ "$RC" -eq 0 ] || { echo "e2e FAIL: web hub: typing client exited $RC"; cat "$OUT.webb.err"; exit 1; }
3019 assert_converged "$OUT.webb" "$SOCK19" "web hub: the typing client"
3020
3021 kill "$W1PID" 2>/dev/null || true
3022 wait_pid_gone "$W1PID" "web hub: killed by tracked pid"
3023 W1PID=""
3024 set +e
3025 "$MUXD" stop --sock "$SOCK19" 2> "$OUT.webstop2"
3026 RC_STOP=$?
3027 set -e
3028 [ "$RC_STOP" = "0" ] || { echo "e2e FAIL: web hub stop exited $RC_STOP"; cat "$OUT.webstop2"; exit 1; }
3029 grep -q '^muxd: stopped' "$OUT.webstop2" || { echo "e2e FAIL: web hub stop did not report stopped"; cat "$OUT.webstop2"; exit 1; }
3030 [ ! -S "$SOCK19" ] || { echo "e2e FAIL: web hub stop left the socket"; ls -l "$SOCK19"; exit 1; }
3031 wait_pid_gone "$D15PID" "web hub: stop reported stopped"
3032 D15PID=""
3033 ok "hub pumps a real session; wrong origin refused"
3034
3035 # --- M-web (c): the hub narrates the tear; the replica re-attaches
3036 # across an epoch. muxd stop kills the daemon under a live tile; the hub
3037 # must say `reconnecting`, redial the restarted daemon (same socket, new
3038 # epoch), say `up`, and the stand-in's re-attach — quoting coordinates
3039 # the NEW daemon has never issued — must be answered with a snapshot
3040 # that converges on the new session's content.
3041 "$MUXD" run --sock "$SOCK20" --shell /bin/sh > "$OUT.webc.d" 2>&1 &
3042 D16PID=$!
3043 i=0
3044 while [ ! -S "$SOCK20" ] && [ "$i" -lt 50 ]; do sleep 0.1; i=$((i+1)); done
3045 [ -S "$SOCK20" ] || { echo "e2e FAIL: web tear daemon never bound"; cat "$OUT.webc.d"; exit 1; }
3046 "$MUXWEB" --sock "$SOCK20" --port "$WPORT2" > "$OUT.webh2" 2>&1 &
3047 W2PID=$!
3048 wait_for "$OUT.webh2" "serving" 10 || {
3049 echo "e2e FAIL: tear hub never reported serving"; cat "$OUT.webh2"; exit 1; }
3050
3051 { printf 'printf "web-%%s\\n" c1\n'; sleep 1.5; printf '\034'; } | \
3052 timeout 30 "$MUX" --sock "$SOCK20" > "$OUT.webc" 2> "$OUT.webc.err"
3053 i=0
3054 while [ "$i" -lt 100 ]; do
3055 "$MUXD" dump --sock "$SOCK20" 2>/dev/null | grep -q "web-c1" && break
3056 sleep 0.1; i=$((i+1))
3057 done
3058
3059 timeout 90 "$WSCLIENT" --port "$WPORT2" --tile 0 --out "$OUT.webws2" --err "$OUT.webws2.err" <<'EOF' &
3060 attach 1 1
3061 expectstate up 10000
3062 expectgrid web-c1 15000
3063 expectstate reconnecting 25000
3064 expectstate up 25000
3065 attach 1 1
3066 expectgrid web-c2 30000
3067 settle 500 10000
3068 dumpexit
3069 EOF
3070 WCLIPID=$!
3071 # Let the stand-in reach its first expectgrid before the tear; the marker
3072 # it waits for is already on the grid, so one settle-length is plenty.
3073 sleep 1
3074
3075 set +e
3076 "$MUXD" stop --sock "$SOCK20" 2> "$OUT.webstop3"
3077 RC_STOP=$?
3078 set -e
3079 [ "$RC_STOP" = "0" ] || { echo "e2e FAIL: web tear stop exited $RC_STOP"; cat "$OUT.webstop3"; exit 1; }
3080 wait_pid_gone "$D16PID" "web tear: first daemon stopped under the tile"
3081 D16PID=""
3082
3083 "$MUXD" run --sock "$SOCK20" --shell /bin/sh > "$OUT.webc2.d" 2>&1 &
3084 D17PID=$!
3085 i=0
3086 while [ ! -S "$SOCK20" ] && [ "$i" -lt 50 ]; do sleep 0.1; i=$((i+1)); done
3087 [ -S "$SOCK20" ] || { echo "e2e FAIL: web tear restart never bound"; cat "$OUT.webc2.d"; exit 1; }
3088
3089 { printf 'printf "web-%%s\\n" c2\n'; sleep 1.5; printf '\034'; } | \
3090 timeout 30 "$MUX" --sock "$SOCK20" > "$OUT.webc2" 2> "$OUT.webc2.err"
3091
3092 set +e
3093 wait "$WCLIPID"
3094 RC=$?
3095 set -e
3096 WCLIPID=""
3097 [ "$RC" -eq 0 ] || {
3098 echo "e2e FAIL: web tear: wsclient exited $RC"
3099 cat -v "$OUT.webws2.err" 2>/dev/null; cat "$OUT.webh2"; exit 1; }
3100
3101 "$MUXD" dump --sock "$SOCK20" > "$OUT.webws2.dump"
3102 sed 's/[[:space:]]*$//' "$OUT.webws2" > "$OUT.webws2.n"
3103 sed 's/[[:space:]]*$//' "$OUT.webws2.dump" > "$OUT.webws2.dump.n"
3104 diff -u "$OUT.webws2.dump.n" "$OUT.webws2.n" > "$OUT.webws2.diff" || {
3105 echo "e2e FAIL: web tear: replica diverges after the epoch crossing (-daemon +ws):"
3106 head -40 "$OUT.webws2.diff"; exit 1; }
3107
3108 kill "$W2PID" 2>/dev/null || true
3109 wait_pid_gone "$W2PID" "web tear: hub killed by tracked pid"
3110 W2PID=""
3111 set +e
3112 "$MUXD" stop --sock "$SOCK20" 2> "$OUT.webstop3"
3113 RC_STOP=$?
3114 set -e
3115 [ "$RC_STOP" = "0" ] || { echo "e2e FAIL: web tear final stop exited $RC_STOP"; cat "$OUT.webstop3"; exit 1; }
3116 [ ! -S "$SOCK20" ] || { echo "e2e FAIL: web tear final stop left the socket"; ls -l "$SOCK20"; exit 1; }
3117 wait_pid_gone "$D17PID" "web tear: restarted daemon stopped"
3118 D17PID=""
3119 ok "hub narrates the tear; the replica re-attaches across an epoch"
3120
2820 # The pins. Literals, not variables set from counting something else — 3121 # The pins. Literals, not variables set from counting something else —
2821 # "assert the literal, never the constant the code under test reads" 3122 # "assert the literal, never the constant the code under test reads"
2822 # (decisions.md, M10). 20 scenario checkpoints; 33 convergence points. 3123 # (decisions.md, M10). 20 scenario checkpoints; 33 convergence points.
2823 # Anyone adding a scenario updates these by hand, on purpose. 3124 # Anyone adding a scenario updates these by hand, on purpose.
2824 [ "$OK_COUNT" = "20" ] || { 3125 [ "$OK_COUNT" = "23" ] || {
2825 echo "e2e FAIL: $OK_COUNT scenario checkpoints ran, the pin says 20 —" 3126 echo "e2e FAIL: $OK_COUNT scenario checkpoints ran, the pin says 23 —"
2826 echo " a scenario was added (update the pin) or silently lost" 3127 echo " a scenario was added (update the pin) or silently lost"
2827 exit 1 3128 exit 1
2828 } 3129 }
2829 [ "$CONV_COUNT" = "33" ] || { 3130 [ "$CONV_COUNT" = "35" ] || {
2830 echo "e2e FAIL: $CONV_COUNT convergence points ran, the pin says 33" 3131 echo "e2e FAIL: $CONV_COUNT convergence points ran, the pin says 35"
2831 exit 1 3132 exit 1
2832 } 3133 }
2833 echo "e2e OK (20 scenarios, 33 convergence points)" 3134 echo "e2e OK (23 scenarios, 35 convergence points)"
test/soak.sh
Old New
@@ -6,7 +6,7 @@
6 # same temp-file patterns while it looks (a concurrently running suite 6 # same temp-file patterns while it looks (a concurrently running suite
7 # would read as a leak). 7 # would read as a leak).
8 set -u 8 set -u
9 MUXD="$1"; MUX="$2"; RAWMODE="$3"; DELAYPIPE="$4"; RENDER="$5"; PTYCLIENT="$6" 9 MUXD="$1"; MUX="$2"; RAWMODE="$3"; DELAYPIPE="$4"; RENDER="$5"; PTYCLIENT="$6"; WSCLIENT="$7"; MUXWEB="$8"
10 E2E="$(dirname "$0")/e2e.sh" 10 E2E="$(dirname "$0")/e2e.sh"
11 N="${SOAK_N:-10}" 11 N="${SOAK_N:-10}"
12 TMP="${TMPDIR:-/tmp}" 12 TMP="${TMPDIR:-/tmp}"
@@ -30,7 +30,7 @@ BASE_STRAYS=$(find "$TMP" -maxdepth 1 \( -name 'muxd-e2e-*' -o -name 'mux-e2e-*'
30 FAILED=0 30 FAILED=0
31 i=1 31 i=1
32 while [ "$i" -le "$N" ]; do 32 while [ "$i" -le "$N" ]; do
33 if "$E2E" "$MUXD" "$MUX" "$RAWMODE" "$DELAYPIPE" "$RENDER" "$PTYCLIENT" > "$LOG" 2>&1; then 33 if "$E2E" "$MUXD" "$MUX" "$RAWMODE" "$DELAYPIPE" "$RENDER" "$PTYCLIENT" "$WSCLIENT" "$MUXWEB" > "$LOG" 2>&1; then
34 echo "soak run $i/$N: PASS" 34 echo "soak run $i/$N: PASS"
35 else 35 else
36 FAILED=$((FAILED + 1)) 36 FAILED=$((FAILED + 1))