ab09f763
test(e2e): two sessions on one daemon, on the wall and over QUIC
a73x 2026-08-15 12:22
Commit message
Makefile
| Old | New | ||
|---|---|---|---|
| @@ -2,7 +2,7 @@ | |||
| 2 | # default zig is 0.17-dev. Override with ZIG=... if yours lives elsewhere. | 2 | # default zig is 0.17-dev. Override with ZIG=... if yours lives elsewhere. |
| 3 | ZIG ?= $(HOME)/Downloads/zig-x86_64-linux-0.15.2/zig | 3 | ZIG ?= $(HOME)/Downloads/zig-x86_64-linux-0.15.2/zig |
| 4 | 4 | ||
| 5 | .PHONY: build test e2e soak bench deps clean clean-deps | 5 | .PHONY: build test e2e soak bench agent deps clean clean-deps xversion xversion-build |
| 6 | 6 | ||
| 7 | # The QUIC stack (deps/quic) is built on demand by build.zig, so no target | 7 | # The QUIC stack (deps/quic) is built on demand by build.zig, so no target |
| 8 | # here needs to depend on this one. It exists to make the one-time cost | 8 | # here needs to depend on this one. It exists to make the one-time cost |
| @@ -28,6 +28,29 @@ soak: | |||
| 28 | bench: | 28 | bench: |
| 29 | $(ZIG) build bench | 29 | $(ZIG) build bench |
| 30 | 30 | ||
| 31 | agent: | ||
| 32 | $(ZIG) build agent | ||
| 33 | |||
| 34 | # Cross-version gate (test/xversion.sh): this tree's client against a | ||
| 35 | # previous version's daemon and back, each daemon in a container. | ||
| 36 | # | ||
| 37 | # Two steps because only you know what "previous" means. XVER_OLD_REF is | ||
| 38 | # any ref with a checkout to build from — set XVER_OLD_WORKTREE to that | ||
| 39 | # checkout's path. Both sides build STATIC musl: the same binary then runs | ||
| 40 | # in the container AND on this host, so the rig needs one artifact per | ||
| 41 | # version rather than two. | ||
| 42 | # | ||
| 43 | # Installed with `-p` into .xversion/ rather than the default prefix on | ||
| 44 | # purpose: a bare `-Dtarget=` build overwrites zig-out/bin with binaries | ||
| 45 | # for the wrong libc, and the next `make e2e` would silently run those. | ||
| 46 | XVER_OLD_WORKTREE ?= .. | ||
| 47 | xversion-build: | ||
| 48 | $(ZIG) build -Dtarget=x86_64-linux-musl -p $(CURDIR)/.xversion/new | ||
| 49 | cd $(XVER_OLD_WORKTREE) && $(ZIG) build -Dtarget=x86_64-linux-musl -p $(CURDIR)/.xversion/old | ||
| 50 | |||
| 51 | xversion: | ||
| 52 | ./test/xversion.sh | ||
| 53 | |||
| 31 | # Deliberately leaves deps/quic alone: rebuilding it needs the network and | 54 | # Deliberately leaves deps/quic alone: rebuilding it needs the network and |
| 32 | # several minutes, and it is pinned by checksum, so a routine clean should | 55 | # several minutes, and it is pinned by checksum, so a routine clean should |
| 33 | # not cost that. `clean-deps` is the one that does. | 56 | # not cost that. `clean-deps` is the one that does. |
build.zig
| Old | New | ||
|---|---|---|---|
| @@ -671,6 +671,13 @@ pub fn build(b: *std.Build) void { | |||
| 671 | e2e.addArtifactArg(ptyclient_exe); | 671 | e2e.addArtifactArg(ptyclient_exe); |
| 672 | e2e.addArtifactArg(wsclient_exe); | 672 | e2e.addArtifactArg(wsclient_exe); |
| 673 | e2e.addArtifactArg(webhub_exe); | 673 | e2e.addArtifactArg(webhub_exe); |
| 674 | // muxa, for the M18 block alone: it is the one verb family that can ask | ||
| 675 | // about a session WITHOUT attaching to it, and the sessions it asks | ||
| 676 | // about have to be created by `mux` first — which is why the question | ||
| 677 | // is asked here rather than in the agent suite, where there is no `mux` | ||
| 678 | // to create a named session with. test/agent.sh remains muxa's own home | ||
| 679 | // for everything that does not need two sessions to exist. | ||
| 680 | e2e.addArtifactArg(muxa_exe); | ||
| 674 | const e2e_step = b.step("e2e", "Run end-to-end test"); | 681 | const e2e_step = b.step("e2e", "Run end-to-end test"); |
| 675 | e2e_step.dependOn(&e2e.step); | 682 | e2e_step.dependOn(&e2e.step); |
| 676 | 683 | ||
| @@ -696,6 +703,10 @@ pub fn build(b: *std.Build) void { | |||
| 696 | soak.addArtifactArg(ptyclient_exe); | 703 | soak.addArtifactArg(ptyclient_exe); |
| 697 | soak.addArtifactArg(wsclient_exe); | 704 | soak.addArtifactArg(wsclient_exe); |
| 698 | soak.addArtifactArg(webhub_exe); | 705 | soak.addArtifactArg(webhub_exe); |
| 706 | // Same list as the e2e step, muxa included: soak IS the e2e suite run N | ||
| 707 | // times, so an argument added to one and not the other makes every soak | ||
| 708 | // run abort on an unbound variable before its first scenario. | ||
| 709 | soak.addArtifactArg(muxa_exe); | ||
| 699 | const soak_step = b.step("soak", "Run the e2e suite SOAK_N times (default 10)"); | 710 | const soak_step = b.step("soak", "Run the e2e suite SOAK_N times (default 10)"); |
| 700 | soak_step.dependOn(&soak.step); | 711 | soak_step.dependOn(&soak.step); |
| 701 | 712 | ||
test/e2e.sh
| Old | New | ||
|---|---|---|---|
| @@ -16,6 +16,10 @@ PTYCLIENT="$6" | |||
| 16 | # (test/wsclient.zig, src/webhub_main.zig). | 16 | # (test/wsclient.zig, src/webhub_main.zig). |
| 17 | WSCLIENT="$7" | 17 | WSCLIENT="$7" |
| 18 | MUXWEB="$8" | 18 | MUXWEB="$8" |
| 19 | # M18: the agent client, used by one block — the out-of-band verbs that | ||
| 20 | # address a session by name without attaching to it. Everything else muxa | ||
| 21 | # does is test/agent.sh's business. | ||
| 22 | MUXA="$9" | ||
| 19 | SOCK="${TMPDIR:-/tmp}/muxd-e2e-$$.sock" | 23 | SOCK="${TMPDIR:-/tmp}/muxd-e2e-$$.sock" |
| 20 | OUT="${TMPDIR:-/tmp}/mux-e2e-out-$$" | 24 | OUT="${TMPDIR:-/tmp}/mux-e2e-out-$$" |
| 21 | # M10: hermetic XDG homes. Key-default scenarios must see OUR key or none, | 25 | # M10: hermetic XDG homes. Key-default scenarios must see OUR key or none, |
| @@ -168,6 +172,29 @@ D17PID="" | |||
| 168 | W1PID="" | 172 | W1PID="" |
| 169 | W2PID="" | 173 | W2PID="" |
| 170 | WCLIPID="" | 174 | WCLIPID="" |
| 175 | # M18. Two daemons — the CLI multi-session block's and the wall's — and one | ||
| 176 | # more hub on the next 5000-spaced port. Only two sockets for four-and-some | ||
| 177 | # sessions, which is the whole point of the milestone: sessions are named on | ||
| 178 | # one socket, not spread across one socket each. | ||
| 179 | SOCK21="${TMPDIR:-/tmp}/muxd-e2e-m18-$$.sock" | ||
| 180 | SOCK22="${TMPDIR:-/tmp}/muxd-e2e-m18web-$$.sock" | ||
| 181 | WPORT3=$(( 51000 + ($$ % 4000) )) | ||
| 182 | D18PID="" | ||
| 183 | D19PID="" | ||
| 184 | W3PID="" | ||
| 185 | # ...and a third, the only M18 daemon holding a UDP port: it serves the | ||
| 186 | # session socket AND a QUIC listener at once, which is what lets one block | ||
| 187 | # ask whether a session name means the same session on either transport. | ||
| 188 | # Next 5000-spaced band after WPORT3. | ||
| 189 | SOCK23="${TMPDIR:-/tmp}/muxd-e2e-m18quic-$$.sock" | ||
| 190 | QPORT5=$(( 56000 + ($$ % 4000) )) | ||
| 191 | # A key of its own rather than the M8 block's $QKEY: that one is rm_swept | ||
| 192 | # the moment the M8 scenarios finish (see the sweep after the key-source | ||
| 193 | # block), which is hundreds of lines before this one runs. Reaching for a | ||
| 194 | # deleted file would make this block's daemon fail to bind for a reason | ||
| 195 | # that has nothing to do with sessions. | ||
| 196 | M18KEY="${TMPDIR:-/tmp}/mux-e2e-m18key-$$" | ||
| 197 | D20PID="" | ||
| 171 | 198 | ||
| 172 | # One counter out of a MUX_PREDICT_STATS line. The client prints exactly one | 199 | # One counter out of a MUX_PREDICT_STATS line. The client prints exactly one |
| 173 | # such line on exit; every field is a key=value pair, so a rename or reorder | 200 | # such line on exit; every field is a key=value pair, so a rename or reorder |
| @@ -247,20 +274,57 @@ wait_sock() { | |||
| 247 | } | 274 | } |
| 248 | } | 275 | } |
| 249 | 276 | ||
| 250 | # wait_grid SOCK NEEDLE LABEL — poll the daemon's own grid for NEEDLE (10s) | 277 | # dump_session SOCK [NAME] — `muxd dump` against one session (M18). An |
| 251 | # and ASSERT it landed. The assert is the whole point. The bare form of this | 278 | # EMPTY or absent NAME passes no --session flag AT ALL rather than an empty |
| 252 | # loop — poll, `&& break`, carry on — falls out silently when the marker | 279 | # one, and that is the load-bearing part: no tail is the wire's own |
| 253 | # never arrives, so a session that died at birth is discovered by whatever | 280 | # default-session spelling and the pre-M18 compat path (decision 3), so |
| 254 | # runs next, 15s later and in the wrong layer. Three M-web scenarios had | 281 | # every call site here that never heard of sessions keeps sending exactly |
| 255 | # grown exactly that shape. | 282 | # the bytes it always did. One helper, so a scenario asking about a named |
| 283 | # session and one asking about the default cannot drift into two spellings. | ||
| 284 | dump_session() { | ||
| 285 | if [ -n "${2:-}" ]; then | ||
| 286 | "$MUXD" dump --sock "$1" --session "$2" | ||
| 287 | else | ||
| 288 | "$MUXD" dump --sock "$1" | ||
| 289 | fi | ||
| 290 | } | ||
| 291 | |||
| 292 | # wait_grid SOCK NEEDLE LABEL [SESSION] — poll the daemon's own grid for | ||
| 293 | # NEEDLE (10s) and ASSERT it landed. The assert is the whole point. The bare | ||
| 294 | # form of this loop — poll, `&& break`, carry on — falls out silently when | ||
| 295 | # the marker never arrives, so a session that died at birth is discovered by | ||
| 296 | # whatever runs next, 15s later and in the wrong layer. Three M-web | ||
| 297 | # scenarios had grown exactly that shape. | ||
| 298 | # | ||
| 299 | # SESSION is optional and defaults to the default session: a marker typed | ||
| 300 | # into session `a` is not on the default session's grid, so a named-session | ||
| 301 | # scenario that forgot the argument would poll a grid the marker can never | ||
| 302 | # reach and fail 10s later blaming the wrong thing. | ||
| 256 | wait_grid() { | 303 | wait_grid() { |
| 257 | _i=0 | 304 | _i=0 |
| 258 | while [ "$_i" -lt 100 ]; do | 305 | while [ "$_i" -lt 100 ]; do |
| 259 | "$MUXD" dump --sock "$1" 2>/dev/null | grep -q "$2" && return 0 | 306 | dump_session "$1" "${4:-}" 2>/dev/null | grep -q "$2" && return 0 |
| 260 | sleep 0.1; _i=$((_i+1)) | 307 | sleep 0.1; _i=$((_i+1)) |
| 261 | done | 308 | done |
| 262 | echo "e2e FAIL: $3: '$2' never reached the daemon's grid; it holds:" | 309 | echo "e2e FAIL: $3: '$2' never reached the daemon's grid; it holds:" |
| 263 | "$MUXD" dump --sock "$1" || echo "(nothing answers on $1)" | 310 | dump_session "$1" "${4:-}" || echo "(nothing answers on $1)" |
| 311 | exit 1 | ||
| 312 | } | ||
| 313 | |||
| 314 | # wait_sessions SOCK N LABEL — poll until `muxd stats` reports N live | ||
| 315 | # sessions (10s) and ASSERT it. wait_grid's shape for wait_grid's reason: a | ||
| 316 | # session is freed asynchronously — its shell exits, the daemon reaps on a | ||
| 317 | # later pump — so "it died" has to be waited FOR, and a wait that falls out | ||
| 318 | # silently turns "the session never died" into a puzzling failure two | ||
| 319 | # assertions further down. | ||
| 320 | wait_sessions() { | ||
| 321 | _i=0 | ||
| 322 | while [ "$_i" -lt 100 ]; do | ||
| 323 | "$MUXD" stats --sock "$1" 2>/dev/null | grep -q "sessions=$2" && return 0 | ||
| 324 | sleep 0.1; _i=$((_i+1)) | ||
| 325 | done | ||
| 326 | echo "e2e FAIL: $3: stats never reported sessions=$2; it holds:" | ||
| 327 | "$MUXD" stats --sock "$1" || echo "(nothing answers on $1)" | ||
| 264 | exit 1 | 328 | exit 1 |
| 265 | } | 329 | } |
| 266 | 330 | ||
| @@ -391,19 +455,24 @@ assert_converged() { | |||
| 391 | } | 455 | } |
| 392 | } | 456 | } |
| 393 | 457 | ||
| 394 | # assert_ws_converged WSOUT SOCK LABEL — the WebSocket leg's convergence | 458 | # assert_ws_converged WSOUT SOCK LABEL [SESSION] — the WebSocket leg's |
| 395 | # check. WSOUT is the stand-in's `dumpexit` file: its replica's grid, in | 459 | # convergence check. WSOUT is the stand-in's `dumpexit` file: its replica's |
| 396 | # `muxd dump`'s own format, built from frames that crossed the hub. Diff it | 460 | # grid, in `muxd dump`'s own format, built from frames that crossed the hub. |
| 397 | # against the daemon's grid, then doctor a copy of that grid and assert the | 461 | # Diff it against the daemon's grid, then doctor a copy of that grid and |
| 398 | # SAME diff catches the doctored one — the wan.sh rule (M9): a convergence | 462 | # assert the SAME diff catches the doctored one — the wan.sh rule (M9): a |
| 399 | # check that cannot fail proves nothing. | 463 | # convergence check that cannot fail proves nothing. |
| 464 | # | ||
| 465 | # SESSION (M18) names which grid the daemon side of that diff comes from. A | ||
| 466 | # tile attached to session `a` must be diffed against `a`, and pointing it | ||
| 467 | # at the default session instead would compare two unrelated grids — the | ||
| 468 | # check would fail, loudly and for the wrong reason. | ||
| 400 | # | 469 | # |
| 401 | # Deliberately NOT counted in CONV_COUNT. That pin counts assert_converged | 470 | # Deliberately NOT counted in CONV_COUNT. That pin counts assert_converged |
| 402 | # call sites — render-replays-the-client-stream — and this is different | 471 | # call sites — render-replays-the-client-stream — and this is different |
| 403 | # machinery answering a different question. Folding the two counts together | 472 | # machinery answering a different question. Folding the two counts together |
| 404 | # would make either pin's number stop meaning anything. | 473 | # would make either pin's number stop meaning anything. |
| 405 | assert_ws_converged() { | 474 | assert_ws_converged() { |
| 406 | "$MUXD" dump --sock "$2" > "$1.dump" | 475 | dump_session "$2" "${4:-}" > "$1.dump" |
| 407 | # Trailing whitespace is a formatting difference between two correct | 476 | # Trailing whitespace is a formatting difference between two correct |
| 408 | # grids, exactly as in converged_quiet. | 477 | # grids, exactly as in converged_quiet. |
| 409 | sed 's/[[:space:]]*$//' "$1" > "$1.n" | 478 | sed 's/[[:space:]]*$//' "$1" > "$1.n" |
| @@ -632,6 +701,14 @@ cleanup() { | |||
| 632 | [ -n "$D15PID" ] && kill "$D15PID" 2>/dev/null || true | 701 | [ -n "$D15PID" ] && kill "$D15PID" 2>/dev/null || true |
| 633 | [ -n "$D16PID" ] && kill "$D16PID" 2>/dev/null || true | 702 | [ -n "$D16PID" ] && kill "$D16PID" 2>/dev/null || true |
| 634 | [ -n "$D17PID" ] && kill "$D17PID" 2>/dev/null || true | 703 | [ -n "$D17PID" ] && kill "$D17PID" 2>/dev/null || true |
| 704 | # M18's two daemons and its hub. A multi-session daemon holds N shells, | ||
| 705 | # so one leaked pid here is N leaked processes — the kill is by the | ||
| 706 | # tracked pid all the same, and killing the daemon closes every pty | ||
| 707 | # master it owns, which is what ends the shells on the other side. | ||
| 708 | [ -n "$W3PID" ] && kill "$W3PID" 2>/dev/null || true | ||
| 709 | [ -n "$D18PID" ] && kill "$D18PID" 2>/dev/null || true | ||
| 710 | [ -n "$D19PID" ] && kill "$D19PID" 2>/dev/null || true | ||
| 711 | [ -n "$D20PID" ] && kill "$D20PID" 2>/dev/null || true | ||
| 635 | # The stops still precede the socket rm below, like SOCK14-17 above: | 712 | # The stops still precede the socket rm below, like SOCK14-17 above: |
| 636 | # unlinking a socket first would leave a live daemon nothing could reach | 713 | # unlinking a socket first would leave a live daemon nothing could reach |
| 637 | # by path. | 714 | # by path. |
| @@ -648,7 +725,8 @@ cleanup() { | |||
| 648 | reap_briefly "${DPID:-}" "$D2PID" "$D3PID" "$D4PID" "$D5PID" "$D6PID" \ | 725 | reap_briefly "${DPID:-}" "$D2PID" "$D3PID" "$D4PID" "$D5PID" "$D6PID" \ |
| 649 | "$D7PID" "$D9PID" "$D10PID" "$D12PID" "$D13PID" "$SPID" "$TPID" \ | 726 | "$D7PID" "$D9PID" "$D10PID" "$D12PID" "$D13PID" "$SPID" "$TPID" \ |
| 650 | "$GPID" "$APID" "$PAPID" "$HAPID" "$HDPID" \ | 727 | "$GPID" "$APID" "$PAPID" "$HAPID" "$HDPID" \ |
| 651 | "$D14PID" "$D15PID" "$D16PID" "$D17PID" | 728 | "$D14PID" "$D15PID" "$D16PID" "$D17PID" "$D18PID" "$D19PID" \ |
| 729 | "$D20PID" | ||
| 652 | _leak=0 | 730 | _leak=0 |
| 653 | leak_sweep "$_rc" || _leak=1 | 731 | leak_sweep "$_rc" || _leak=1 |
| 654 | 732 | ||
| @@ -715,6 +793,22 @@ cleanup() { | |||
| 715 | "$OUT.webws2.n" "$OUT.webws2.dump" "$OUT.webws2.dump.n" \ | 793 | "$OUT.webws2.n" "$OUT.webws2.dump" "$OUT.webws2.dump.n" \ |
| 716 | "$OUT.webws2.diff" "$OUT.webws2.doc" \ | 794 | "$OUT.webws2.diff" "$OUT.webws2.doc" \ |
| 717 | "$OUT.webstop" "$OUT.webstop2" "$OUT.webstop3" "$OUT.webstop4" | 795 | "$OUT.webstop" "$OUT.webstop2" "$OUT.webstop3" "$OUT.webstop4" |
| 796 | # M18 captures, both blocks, including the files assert_ws_converged | ||
| 797 | # derives from each stand-in's dump (.n/.dump/.dump.n/.diff/.doc). | ||
| 798 | rm -f "$SOCK21" "$SOCK22" \ | ||
| 799 | "$OUT.m18.d" "$OUT.m18a" "$OUT.m18a.err" "$OUT.m18b" "$OUT.m18b.err" \ | ||
| 800 | "$OUT.m18ax" "$OUT.m18ax.err" "$OUT.m18stats" "$OUT.m18zz" \ | ||
| 801 | "$OUT.m18mst" "$OUT.m18mcap" \ | ||
| 802 | "$OUT.m18stop" "$OUT.m18w.d" "$OUT.m18wh" \ | ||
| 803 | "$OUT.m18wa" "$OUT.m18wa.err" "$OUT.m18wb" "$OUT.m18wb.err" \ | ||
| 804 | "$OUT.m18ws0" "$OUT.m18ws0.err" "$OUT.m18ws0.n" "$OUT.m18ws0.dump" \ | ||
| 805 | "$OUT.m18ws0.dump.n" "$OUT.m18ws0.diff" "$OUT.m18ws0.doc" \ | ||
| 806 | "$OUT.m18ws1" "$OUT.m18ws1.err" "$OUT.m18ws1.n" "$OUT.m18ws1.dump" \ | ||
| 807 | "$OUT.m18ws1.dump.n" "$OUT.m18ws1.diff" "$OUT.m18ws1.doc" \ | ||
| 808 | "$OUT.m18wstop" \ | ||
| 809 | "$SOCK23" "$M18KEY" "$OUT.m18q.d" "$OUT.m18qa" "$OUT.m18qa.err" \ | ||
| 810 | "$OUT.m18qb" "$OUT.m18qb.err" "$OUT.m18qx" "$OUT.m18qx.err" \ | ||
| 811 | "$OUT.m18qstop" | ||
| 718 | # The convergence files a FAILING assert_converged leaves behind | 812 | # The convergence files a FAILING assert_converged leaves behind |
| 719 | # (.render/.dump/.rvt/.dvt/.diff for that capture) are deliberately not | 813 | # (.render/.dump/.rvt/.dvt/.diff for that capture) are deliberately not |
| 720 | # chased here: on a failing run they are the evidence. | 814 | # chased here: on a failing run they are the evidence. |
| @@ -3055,6 +3149,327 @@ rm_swept "$OUT.h1" "$OUT.h1.err" "$OUT.h2" "$OUT.h2.err" "$OUT.h3" "$OUT.h3.err" | |||
| 3055 | "$OUT.h4" "$OUT.h4.err" "$OUT.h5" "$OUT.h5.err" "$OUT.stop" "$HKEY" "$HCFGBAD" | 3149 | "$OUT.h4" "$OUT.h4.err" "$OUT.h5" "$OUT.h5.err" "$OUT.stop" "$HKEY" "$HCFGBAD" |
| 3056 | rm -rf "$SSHIM_DIR" "$HRUN" "$HRUN2" | 3150 | rm -rf "$SSHIM_DIR" "$HRUN" "$HRUN2" |
| 3057 | 3151 | ||
| 3152 | # --- M18: two sessions on one daemon are two shells --------------------- | ||
| 3153 | # The milestone, observed from outside: ONE daemon, one socket path, one | ||
| 3154 | # key — and two sessions that share nothing. A connection IS a session | ||
| 3155 | # (decision 6), so the two clients below differ in exactly one flag, and | ||
| 3156 | # every instrument here is asked to PROVE the separation rather than | ||
| 3157 | # assumed to have it: for each session both a positive grep (its own | ||
| 3158 | # marker is here) and a negative one (the other's is not), because "a | ||
| 3159 | # holds a's marker" is satisfied just as well by a single shared shell. | ||
| 3160 | # | ||
| 3161 | # Ordered ahead of the M-web block on the legible-catch rule. The wall | ||
| 3162 | # scenarios below now attach BY NAME, and a regression in name resolution | ||
| 3163 | # would hang one of them with nothing printed — fifteen seconds into a | ||
| 3164 | # scenario whose label says "WebSocket". This block reaches the same | ||
| 3165 | # resolver with a CLI in front of it, and fails there first. | ||
| 3166 | "$MUXD" run --sock "$SOCK21" --shell /bin/sh > "$OUT.m18.d" 2>&1 & | ||
| 3167 | D18PID=$! | ||
| 3168 | wait_sock "$SOCK21" "$OUT.m18.d" "M18 multi-session daemon never bound" | ||
| 3169 | |||
| 3170 | # Two clients, two names, one socket. Each plants a marker its own shell | ||
| 3171 | # has to EXPAND — the typed line reads `printf "m18a-%s\n" pin` and only | ||
| 3172 | # the output reads `m18a-pin`, so a grep that hits is the shell's work and | ||
| 3173 | # not an echo of our keystrokes (the M-web passivity block's trick). | ||
| 3174 | # | ||
| 3175 | # Both run in the foreground and DETACH (\034, Ctrl-\) rather than staying | ||
| 3176 | # up: the session has to outlive its client for every assertion below to | ||
| 3177 | # mean anything. A session ends when its shell exits, not when its last | ||
| 3178 | # client leaves (decision 8) — so these greps run against two sessions | ||
| 3179 | # with nobody attached, which is also why none of their answers can be | ||
| 3180 | # explained by a live client holding something open. | ||
| 3181 | { printf 'printf "m18a-%%s\\n" pin\n'; sleep 2; printf '\034'; } | \ | ||
| 3182 | timeout 40 "$MUX" --sock "$SOCK21" --session a > "$OUT.m18a" 2> "$OUT.m18a.err" | ||
| 3183 | wait_grid "$SOCK21" "m18a-pin" "M18: session a's marker" a | ||
| 3184 | |||
| 3185 | { printf 'printf "m18b-%%s\\n" pin\n'; sleep 2; printf '\034'; } | \ | ||
| 3186 | timeout 40 "$MUX" --sock "$SOCK21" --session b > "$OUT.m18b" 2> "$OUT.m18b.err" | ||
| 3187 | wait_grid "$SOCK21" "m18b-pin" "M18: session b's marker" b | ||
| 3188 | |||
| 3189 | # Each session's grid holds its own marker and NOT the other's. | ||
| 3190 | if dump_session "$SOCK21" a | grep -q "m18b-pin"; then | ||
| 3191 | echo "e2e FAIL: M18: b's marker is on a's grid — one shell, not two:" | ||
| 3192 | dump_session "$SOCK21" a; exit 1 | ||
| 3193 | fi | ||
| 3194 | if dump_session "$SOCK21" b | grep -q "m18a-pin"; then | ||
| 3195 | echo "e2e FAIL: M18: a's marker is on b's grid — one shell, not two:" | ||
| 3196 | dump_session "$SOCK21" b; exit 1 | ||
| 3197 | fi | ||
| 3198 | |||
| 3199 | # The default session was created at daemon init and nobody ever typed | ||
| 3200 | # into it. It is the third session, and its emptiness is the assertion | ||
| 3201 | # that `--session` ROUTES rather than decorating one shared grid. | ||
| 3202 | if dump_session "$SOCK21" | grep -q -e "m18a-pin" -e "m18b-pin"; then | ||
| 3203 | echo "e2e FAIL: M18: a named session's marker reached the default grid:" | ||
| 3204 | dump_session "$SOCK21"; exit 1 | ||
| 3205 | fi | ||
| 3206 | |||
| 3207 | # stats names all three, and names them one segment each. `sessions=3` on | ||
| 3208 | # its own would survive a daemon that counted right and routed wrong, so | ||
| 3209 | # the per-session segments are grepped by name too. | ||
| 3210 | "$MUXD" stats --sock "$SOCK21" > "$OUT.m18stats" 2>&1 | ||
| 3211 | grep -q 'sessions=3' "$OUT.m18stats" || { | ||
| 3212 | echo "e2e FAIL: M18: stats does not see three sessions:" | ||
| 3213 | cat "$OUT.m18stats"; exit 1; } | ||
| 3214 | for _s in a b; do | ||
| 3215 | grep -q "session $_s clients=" "$OUT.m18stats" || { | ||
| 3216 | echo "e2e FAIL: M18: stats has no segment for session $_s:" | ||
| 3217 | cat "$OUT.m18stats"; exit 1; } | ||
| 3218 | done | ||
| 3219 | |||
| 3220 | # An unknown name is ANSWERED, in words, on the dump's own channel — not a | ||
| 3221 | # hang and not a fabricated "connection lost" (decision 9). The exit code | ||
| 3222 | # is 0 because the daemon replied; the text is the whole point. | ||
| 3223 | dump_session "$SOCK21" zz > "$OUT.m18zz" 2>&1 | ||
| 3224 | grep -q 'muxd: no such session: zz' "$OUT.m18zz" || { | ||
| 3225 | echo "e2e FAIL: M18: an unknown session did not say so; dump answered:" | ||
| 3226 | cat "$OUT.m18zz"; exit 1; } | ||
| 3227 | |||
| 3228 | # muxa addresses a session by name on the verbs that never attach at all. | ||
| 3229 | # status_req and debug_dump grew the same name tail attach did (decision | ||
| 3230 | # 14), and that is what lets an agent ask about one session of several | ||
| 3231 | # without claiming a grid — muxa attaches at 0x0 or not at all, so without | ||
| 3232 | # the tail it could only ever have answered for the default session. | ||
| 3233 | set +e | ||
| 3234 | timeout 20 "$MUXA" status --sock "$SOCK21" --session b > "$OUT.m18mst" 2>&1 | ||
| 3235 | RC=$? | ||
| 3236 | set -e | ||
| 3237 | [ "$RC" -eq 0 ] || { | ||
| 3238 | echo "e2e FAIL: M18: muxa status --session b exited $RC:" | ||
| 3239 | cat "$OUT.m18mst"; exit 1; } | ||
| 3240 | # Not the exit code alone: a silently empty object would pass that. `cols` | ||
| 3241 | # is a field only a decoded StatusReply can put there (agent.sh's shape). | ||
| 3242 | grep -q '"cols"' "$OUT.m18mst" || { | ||
| 3243 | echo "e2e FAIL: M18: muxa status --session b answered without a status reply:" | ||
| 3244 | cat "$OUT.m18mst"; exit 1; } | ||
| 3245 | |||
| 3246 | set +e | ||
| 3247 | timeout 20 "$MUXA" capture --sock "$SOCK21" --session a > "$OUT.m18mcap" 2>&1 | ||
| 3248 | RC=$? | ||
| 3249 | set -e | ||
| 3250 | [ "$RC" -eq 0 ] || { | ||
| 3251 | echo "e2e FAIL: M18: muxa capture --session a exited $RC:" | ||
| 3252 | cat "$OUT.m18mcap"; exit 1; } | ||
| 3253 | grep -q 'm18a-pin' "$OUT.m18mcap" || { | ||
| 3254 | echo "e2e FAIL: M18: muxa capture --session a did not return a's grid:" | ||
| 3255 | cat "$OUT.m18mcap"; exit 1; } | ||
| 3256 | if grep -q 'm18b-pin' "$OUT.m18mcap"; then | ||
| 3257 | echo "e2e FAIL: M18: muxa capture --session a returned b's grid too:" | ||
| 3258 | cat "$OUT.m18mcap"; exit 1 | ||
| 3259 | fi | ||
| 3260 | |||
| 3261 | # Lifetime: a session ends when ITS shell exits, and the daemon outlives | ||
| 3262 | # every death but the last (decision 8). This client attaches to a session | ||
| 3263 | # that ALREADY EXISTS — attach-or-create's join arm, the other half of the | ||
| 3264 | # two creations above — and types the exit that ends it. | ||
| 3265 | set +e | ||
| 3266 | { printf 'exit\n'; sleep 3; } | \ | ||
| 3267 | timeout 40 "$MUX" --sock "$SOCK21" --session a > "$OUT.m18ax" 2> "$OUT.m18ax.err" | ||
| 3268 | RC=$? | ||
| 3269 | set -e | ||
| 3270 | [ "$RC" -eq 0 ] || { | ||
| 3271 | echo "e2e FAIL: M18: the client of an exiting session exited $RC, want 0" | ||
| 3272 | cat "$OUT.m18ax.err"; exit 1; } | ||
| 3273 | |||
| 3274 | wait_sessions "$SOCK21" 2 "M18: session a's shell exited" | ||
| 3275 | |||
| 3276 | # The daemon is still serving, and b is untouched by its neighbour's | ||
| 3277 | # death: the death freed ONE session, which at N=1 would have been the | ||
| 3278 | # daemon's own exit. | ||
| 3279 | if dump_session "$SOCK21" a | grep -q "m18a-pin"; then | ||
| 3280 | echo "e2e FAIL: M18: session a still answers after its shell exited:" | ||
| 3281 | dump_session "$SOCK21" a; exit 1 | ||
| 3282 | fi | ||
| 3283 | dump_session "$SOCK21" b | grep -q "m18b-pin" || { | ||
| 3284 | echo "e2e FAIL: M18: session b lost its grid when session a died:" | ||
| 3285 | dump_session "$SOCK21" b; exit 1; } | ||
| 3286 | |||
| 3287 | assert_stopped "$SOCK21" "$D18PID" "M18 multi-session" "$OUT.m18stop" | ||
| 3288 | D18PID="" | ||
| 3289 | ok "two sessions on one daemon are two shells; one dies without the other" | ||
| 3290 | |||
| 3291 | # --- M18 on the wall: one socket, two tiles, two sessions --------------- | ||
| 3292 | # What the milestone was FOR (decision 2): the wall showing the same host | ||
| 3293 | # twice. Both tiles name the same socket path and differ only in the | ||
| 3294 | # `#NAME` suffix, so the hub dials it twice and the two connections are | ||
| 3295 | # two sessions — the browser's spelling of everything the block above | ||
| 3296 | # proved with a CLI. | ||
| 3297 | "$MUXD" run --sock "$SOCK22" --shell /bin/sh > "$OUT.m18w.d" 2>&1 & | ||
| 3298 | D19PID=$! | ||
| 3299 | wait_sock "$SOCK22" "$OUT.m18w.d" "M18 wall daemon never bound" | ||
| 3300 | "$MUXWEB" --sock "$SOCK22#a" --sock "$SOCK22#b" --port "$WPORT3" > "$OUT.m18wh" 2>&1 & | ||
| 3301 | W3PID=$! | ||
| 3302 | wait_for "$OUT.m18wh" "serving" 10 || { | ||
| 3303 | echo "e2e FAIL: M18 wall hub never reported serving"; cat "$OUT.m18wh"; exit 1; } | ||
| 3304 | |||
| 3305 | # Content for each session, planted by a CLI client and left behind: the | ||
| 3306 | # tiles are passive 1x1 wall tiles and can never type anything themselves. | ||
| 3307 | { printf 'printf "wall-a-%%s\\n" pin\n'; sleep 2; printf '\034'; } | \ | ||
| 3308 | timeout 40 "$MUX" --sock "$SOCK22" --session a > "$OUT.m18wa" 2> "$OUT.m18wa.err" | ||
| 3309 | wait_grid "$SOCK22" "wall-a-pin" "M18 wall: session a's marker" a | ||
| 3310 | { printf 'printf "wall-b-%%s\\n" pin\n'; sleep 2; printf '\034'; } | \ | ||
| 3311 | timeout 40 "$MUX" --sock "$SOCK22" --session b > "$OUT.m18wb" 2> "$OUT.m18wb.err" | ||
| 3312 | wait_grid "$SOCK22" "wall-b-pin" "M18 wall: session b's marker" b | ||
| 3313 | |||
| 3314 | # Tile 0 is session a's; tile 1 is session b's. The stand-in spells the | ||
| 3315 | # name the way mux.js does — bytes appended after the fixed 20 — and | ||
| 3316 | # attaches at 1x1, the passivity contract: both sessions already exist, so | ||
| 3317 | # these are joins, and applySize refuses to move an 80-wide grid to 1. | ||
| 3318 | for _t in 0 1; do | ||
| 3319 | case "$_t" in | ||
| 3320 | 0) _sn=a; _mine=wall-a-pin; _theirs=wall-b-pin ;; | ||
| 3321 | *) _sn=b; _mine=wall-b-pin; _theirs=wall-a-pin ;; | ||
| 3322 | esac | ||
| 3323 | set +e | ||
| 3324 | timeout 40 "$WSCLIENT" --port "$WPORT3" --tile "$_t" \ | ||
| 3325 | --out "$OUT.m18ws$_t" --err "$OUT.m18ws$_t.err" <<EOF | ||
| 3326 | attach 1 1 $_sn | ||
| 3327 | expectstate up 10000 | ||
| 3328 | expectgrid $_mine 15000 | ||
| 3329 | settle 500 10000 | ||
| 3330 | dumpexit | ||
| 3331 | EOF | ||
| 3332 | RC=$? | ||
| 3333 | set -e | ||
| 3334 | [ "$RC" -eq 0 ] || { | ||
| 3335 | echo "e2e FAIL: M18 wall: tile $_t (session $_sn) wsclient exited $RC" | ||
| 3336 | cat -v "$OUT.m18ws$_t.err" 2>/dev/null; cat "$OUT.m18wh"; exit 1; } | ||
| 3337 | |||
| 3338 | # The replica converges on ITS session's grid... | ||
| 3339 | assert_ws_converged "$OUT.m18ws$_t" "$SOCK22" "M18 wall: tile $_t" "$_sn" | ||
| 3340 | # ...and the other session's content never crossed into it. The | ||
| 3341 | # convergence check above is a diff against one grid and would pass on | ||
| 3342 | # a hub that fanned every session's frames to every tile only if that | ||
| 3343 | # grid happened to match; this grep is the direct question. | ||
| 3344 | if grep -q "$_theirs" "$OUT.m18ws$_t"; then | ||
| 3345 | echo "e2e FAIL: M18 wall: tile $_t (session $_sn) received the other session's content:" | ||
| 3346 | cat "$OUT.m18ws$_t"; exit 1 | ||
| 3347 | fi | ||
| 3348 | done | ||
| 3349 | |||
| 3350 | kill "$W3PID" 2>/dev/null || true | ||
| 3351 | wait_pid_gone "$W3PID" "M18 wall: hub killed by tracked pid" | ||
| 3352 | W3PID="" | ||
| 3353 | assert_stopped "$SOCK22" "$D19PID" "M18 wall" "$OUT.m18wstop" | ||
| 3354 | D19PID="" | ||
| 3355 | ok "the wall shows one host twice: two tiles, two sessions, one socket" | ||
| 3356 | |||
| 3357 | # --- M18 over QUIC: two dials are two sessions, and a name means one ----- | ||
| 3358 | # --- session whichever transport carried the attach ---------------------- | ||
| 3359 | # Decision 6 — a connection IS a session on EVERY transport — and QUIC is | ||
| 3360 | # where that claim is least obvious. A QUIC client is promoted to a client | ||
| 3361 | # slot when its HANDSHAKE completes, before its attach has arrived, so its | ||
| 3362 | # slot starts session-less and the name on the attach is the only thing | ||
| 3363 | # that ever binds it. Two dials to one port therefore have to come out as | ||
| 3364 | # two shells, and the block above proves nothing about this one: it went | ||
| 3365 | # through a unix socket, where the slot and the attach arrive together. | ||
| 3366 | # | ||
| 3367 | # The second half is the operator's question rather than the protocol's. | ||
| 3368 | # The session table belongs to the DAEMON, not to the listener that | ||
| 3369 | # accepted a connection — so `a` must name the same shell whether it was | ||
| 3370 | # reached over QUIC or over the socket. Nothing else in this suite asks a | ||
| 3371 | # daemon anything with two transports open at once. | ||
| 3372 | head -c 32 /dev/urandom > "$M18KEY" | ||
| 3373 | chmod 600 "$M18KEY" | ||
| 3374 | # The port is an OBSERVATION, not a derivation (decisions.md, hygiene kit). | ||
| 3375 | # This band sits INSIDE /proc/sys/net/ipv4/ip_local_port_range, and the | ||
| 3376 | # scenarios above open plenty of outbound QUIC sockets — one of them can | ||
| 3377 | # already hold the candidate by the time this daemon asks for it. Soak | ||
| 3378 | # caught exactly that, once in ten runs: `muxd: a daemon is already | ||
| 3379 | # listening on udp 127.0.0.1:57943`, from a client socket, not a daemon. | ||
| 3380 | # | ||
| 3381 | # So step on the daemon's own refusal until one binds, the way agent.sh's | ||
| 3382 | # start_quic does. Anything OTHER than that refusal is a real failure and | ||
| 3383 | # is reported with the daemon's own words; only the taken-port case walks. | ||
| 3384 | M18_TRIES=0 | ||
| 3385 | while : ; do | ||
| 3386 | "$MUXD" run --sock "$SOCK23" --quic "127.0.0.1:$QPORT5" --key "$M18KEY" \ | ||
| 3387 | --quic-idle-ms 15000 --shell /bin/sh > "$OUT.m18q.d" 2>&1 & | ||
| 3388 | D20PID=$! | ||
| 3389 | _i=0 | ||
| 3390 | while [ ! -S "$SOCK23" ] && kill -0 "$D20PID" 2>/dev/null && [ "$_i" -lt 60 ]; do | ||
| 3391 | sleep 0.1; _i=$((_i + 1)) | ||
| 3392 | done | ||
| 3393 | [ -S "$SOCK23" ] && break | ||
| 3394 | grep -q 'already listening on udp' "$OUT.m18q.d" || { | ||
| 3395 | echo "e2e FAIL: M18 quic daemon never bound its session socket" | ||
| 3396 | cat "$OUT.m18q.d"; exit 1; } | ||
| 3397 | wait "$D20PID" 2>/dev/null || true | ||
| 3398 | D20PID="" | ||
| 3399 | M18_TRIES=$((M18_TRIES + 1)) | ||
| 3400 | [ "$M18_TRIES" -lt 8 ] || { | ||
| 3401 | echo "e2e FAIL: M18 quic: 8 candidate udp ports were taken, last $QPORT5" | ||
| 3402 | exit 1; } | ||
| 3403 | # A step that is not a multiple of any other band's spacing, so a walk | ||
| 3404 | # from here cannot march into the ports another scenario reserved. | ||
| 3405 | QPORT5=$((QPORT5 + 137)) | ||
| 3406 | done | ||
| 3407 | |||
| 3408 | set +e | ||
| 3409 | { printf 'printf "q18a-%%s\\n" pin\n'; sleep 2; printf '\034'; } | \ | ||
| 3410 | timeout 40 "$MUX" "quic://127.0.0.1:$QPORT5" --key "$M18KEY" \ | ||
| 3411 | --quic-idle-ms 15000 --session a > "$OUT.m18qa" 2> "$OUT.m18qa.err" | ||
| 3412 | RC=$? | ||
| 3413 | set -e | ||
| 3414 | [ "$RC" -eq 0 ] || { | ||
| 3415 | echo "e2e FAIL: M18 quic: the --session a dial exited $RC" | ||
| 3416 | cat "$OUT.m18qa" "$OUT.m18qa.err" 2>/dev/null; exit 1; } | ||
| 3417 | wait_grid "$SOCK23" "q18a-pin" "M18 quic: session a's marker" a | ||
| 3418 | |||
| 3419 | set +e | ||
| 3420 | { printf 'printf "q18b-%%s\\n" pin\n'; sleep 2; printf '\034'; } | \ | ||
| 3421 | timeout 40 "$MUX" "quic://127.0.0.1:$QPORT5" --key "$M18KEY" \ | ||
| 3422 | --quic-idle-ms 15000 --session b > "$OUT.m18qb" 2> "$OUT.m18qb.err" | ||
| 3423 | RC=$? | ||
| 3424 | set -e | ||
| 3425 | [ "$RC" -eq 0 ] || { | ||
| 3426 | echo "e2e FAIL: M18 quic: the --session b dial exited $RC" | ||
| 3427 | cat "$OUT.m18qb" "$OUT.m18qb.err" 2>/dev/null; exit 1; } | ||
| 3428 | wait_grid "$SOCK23" "q18b-pin" "M18 quic: session b's marker" b | ||
| 3429 | |||
| 3430 | if dump_session "$SOCK23" a | grep -q "q18b-pin"; then | ||
| 3431 | echo "e2e FAIL: M18 quic: b's marker is on a's grid — one session, not two:" | ||
| 3432 | dump_session "$SOCK23" a; exit 1 | ||
| 3433 | fi | ||
| 3434 | if dump_session "$SOCK23" b | grep -q "q18a-pin"; then | ||
| 3435 | echo "e2e FAIL: M18 quic: a's marker is on b's grid — one session, not two:" | ||
| 3436 | dump_session "$SOCK23" b; exit 1 | ||
| 3437 | fi | ||
| 3438 | "$MUXD" stats --sock "$SOCK23" | grep -q 'sessions=3' || { | ||
| 3439 | echo "e2e FAIL: M18 quic: two dials did not make two sessions beside the default:" | ||
| 3440 | "$MUXD" stats --sock "$SOCK23"; exit 1; } | ||
| 3441 | |||
| 3442 | # The cross-transport join. Same name, different door: this must land in | ||
| 3443 | # the shell the QUIC client left behind, not spawn a second one. | ||
| 3444 | set +e | ||
| 3445 | { printf 'printf "x18a-%%s\\n" pin\n'; sleep 2; printf '\034'; } | \ | ||
| 3446 | timeout 40 "$MUX" --sock "$SOCK23" --session a > "$OUT.m18qx" 2> "$OUT.m18qx.err" | ||
| 3447 | RC=$? | ||
| 3448 | set -e | ||
| 3449 | [ "$RC" -eq 0 ] || { | ||
| 3450 | echo "e2e FAIL: M18 quic: the socket client of session a exited $RC" | ||
| 3451 | cat "$OUT.m18qx" "$OUT.m18qx.err" 2>/dev/null; exit 1; } | ||
| 3452 | wait_grid "$SOCK23" "x18a-pin" "M18 quic: the socket client's marker in a" a | ||
| 3453 | |||
| 3454 | # Both markers on one grid is the whole assertion: the QUIC client's | ||
| 3455 | # output and the socket client's, in the session they both named. | ||
| 3456 | dump_session "$SOCK23" a | grep -q "q18a-pin" || { | ||
| 3457 | echo "e2e FAIL: M18 quic: joining 'a' over the socket did not find the QUIC dial's shell:" | ||
| 3458 | dump_session "$SOCK23" a; exit 1; } | ||
| 3459 | # ...and it JOINED rather than created: a fourth session here would mean | ||
| 3460 | # the name resolved per-transport, which is the bug this pair exists for. | ||
| 3461 | "$MUXD" stats --sock "$SOCK23" | grep -q 'sessions=3' || { | ||
| 3462 | echo "e2e FAIL: M18 quic: a socket join of an existing name made a new session:" | ||
| 3463 | "$MUXD" stats --sock "$SOCK23"; exit 1; } | ||
| 3464 | if dump_session "$SOCK23" b | grep -q "x18a-pin"; then | ||
| 3465 | echo "e2e FAIL: M18 quic: the socket client's marker leaked into b:" | ||
| 3466 | dump_session "$SOCK23" b; exit 1 | ||
| 3467 | fi | ||
| 3468 | |||
| 3469 | assert_stopped "$SOCK23" "$D20PID" "M18 quic multi-session" "$OUT.m18qstop" | ||
| 3470 | D20PID="" | ||
| 3471 | ok "quic: two dials are two sessions, and one name is one session on either transport" | ||
| 3472 | |||
| 3058 | # --- M-web (a): a 1x1 attach is refused the grid and can never claim it. | 3473 | # --- M-web (a): a 1x1 attach is refused the grid and can never claim it. |
| 3059 | # The wall tile's passivity contract, pinned DAEMON-SIDE and hub-free on | 3474 | # The wall tile's passivity contract, pinned DAEMON-SIDE and hub-free on |
| 3060 | # purpose: the mechanism is applySize's cols<2 refusal plus claimGrid's | 3475 | # purpose: the mechanism is applySize's cols<2 refusal plus claimGrid's |
| @@ -3263,10 +3678,16 @@ DPID="" | |||
| 3263 | 3678 | ||
| 3264 | # The pins. Literals, not variables set from counting something else — | 3679 | # The pins. Literals, not variables set from counting something else — |
| 3265 | # "assert the literal, never the constant the code under test reads" | 3680 | # "assert the literal, never the constant the code under test reads" |
| 3266 | # (decisions.md, M10). 20 scenario checkpoints; 33 convergence points. | 3681 | # (decisions.md, M10). 26 scenario checkpoints; 35 convergence points. |
| 3267 | # Anyone adding a scenario updates these by hand, on purpose. | 3682 | # Anyone adding a scenario updates these by hand, on purpose. |
| 3268 | [ "$OK_COUNT" = "23" ] || { | 3683 | # |
| 3269 | echo "e2e FAIL: $OK_COUNT scenario checkpoints ran, the pin says 23 —" | 3684 | # M18 added three checkpoints and no convergence points: its wall block |
| 3685 | # asserts through assert_ws_converged, which is deliberately outside | ||
| 3686 | # CONV_COUNT (see its comment) — that pin counts assert_converged call | ||
| 3687 | # sites, and folding the two together would make either number stop | ||
| 3688 | # meaning anything. | ||
| 3689 | [ "$OK_COUNT" = "26" ] || { | ||
| 3690 | echo "e2e FAIL: $OK_COUNT scenario checkpoints ran, the pin says 26 —" | ||
| 3270 | echo " a scenario was added (update the pin) or silently lost" | 3691 | echo " a scenario was added (update the pin) or silently lost" |
| 3271 | exit 1 | 3692 | exit 1 |
| 3272 | } | 3693 | } |
| @@ -3274,4 +3695,4 @@ DPID="" | |||
| 3274 | echo "e2e FAIL: $CONV_COUNT convergence points ran, the pin says 35" | 3695 | echo "e2e FAIL: $CONV_COUNT convergence points ran, the pin says 35" |
| 3275 | exit 1 | 3696 | exit 1 |
| 3276 | } | 3697 | } |
| 3277 | echo "e2e OK (23 scenarios, 35 convergence points)" | 3698 | echo "e2e OK (26 scenarios, 35 convergence points)" |
test/soak.sh
| Old | New | ||
|---|---|---|---|
| @@ -7,6 +7,9 @@ | |||
| 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"; WSCLIENT="$7"; MUXWEB="$8" | 9 | MUXD="$1"; MUX="$2"; RAWMODE="$3"; DELAYPIPE="$4"; RENDER="$5"; PTYCLIENT="$6"; WSCLIENT="$7"; MUXWEB="$8" |
| 10 | # M18: e2e.sh takes muxa as $9. This list is that list — see the note in | ||
| 11 | # build.zig; the two must be handed the same binaries in the same order. | ||
| 12 | MUXA="$9" | ||
| 10 | E2E="$(dirname "$0")/e2e.sh" | 13 | E2E="$(dirname "$0")/e2e.sh" |
| 11 | N="${SOAK_N:-10}" | 14 | N="${SOAK_N:-10}" |
| 12 | TMP="${TMPDIR:-/tmp}" | 15 | TMP="${TMPDIR:-/tmp}" |
| @@ -30,7 +33,7 @@ BASE_STRAYS=$(find "$TMP" -maxdepth 1 \( -name 'muxd-e2e-*' -o -name 'mux-e2e-*' | |||
| 30 | FAILED=0 | 33 | FAILED=0 |
| 31 | i=1 | 34 | i=1 |
| 32 | while [ "$i" -le "$N" ]; do | 35 | while [ "$i" -le "$N" ]; do |
| 33 | if "$E2E" "$MUXD" "$MUX" "$RAWMODE" "$DELAYPIPE" "$RENDER" "$PTYCLIENT" "$WSCLIENT" "$MUXWEB" > "$LOG" 2>&1; then | 36 | if "$E2E" "$MUXD" "$MUX" "$RAWMODE" "$DELAYPIPE" "$RENDER" "$PTYCLIENT" "$WSCLIENT" "$MUXWEB" "$MUXA" > "$LOG" 2>&1; then |
| 34 | echo "soak run $i/$N: PASS" | 37 | echo "soak run $i/$N: PASS" |
| 35 | else | 38 | else |
| 36 | FAILED=$((FAILED + 1)) | 39 | FAILED=$((FAILED + 1)) |