13e13cc1
test: 05_session moves to a file of its own
a73x 2026-08-26 18:54
Commit message
test/e2e.sh
| Old | New | ||
|---|---|---|---|
| @@ -102,7 +102,7 @@ done | |||
| 102 | E2E_DIR=$(dirname "$0") | 102 | E2E_DIR=$(dirname "$0") |
| 103 | . "$E2E_DIR/e2e_lib.sh" | 103 | . "$E2E_DIR/e2e_lib.sh" |
| 104 | 104 | ||
| 105 | E2E_GROUPS='01_boot 02_predict 03_side 04_handoff' | 105 | E2E_GROUPS='01_boot 02_predict 03_side 04_handoff 05_session' |
| 106 | # The groups that cannot stand alone, and what they need. E2E_ONLY refuses | 106 | # The groups that cannot stand alone, and what they need. E2E_ONLY refuses |
| 107 | # these by name rather than running them: a group whose fixtures are | 107 | # these by name rather than running them: a group whose fixtures are |
| 108 | # missing does not fail, it passes having tested something else — and a | 108 | # missing does not fail, it passes having tested something else — and a |
| @@ -145,340 +145,6 @@ for _g in $E2E_GROUPS; do | |||
| 145 | . "$E2E_DIR/e2e_$_g.sh" | 145 | . "$E2E_DIR/e2e_$_g.sh" |
| 146 | done | 146 | done |
| 147 | 147 | ||
| 148 | # M18. Two daemons — the CLI multi-session block's and the wall's — and one | ||
| 149 | # more hub on the next 5000-spaced port. Only two sockets for four-and-some | ||
| 150 | # sessions, which is the whole point of the milestone: sessions are named on | ||
| 151 | # one socket, not spread across one socket each. | ||
| 152 | SOCK21="${TMPDIR:-/tmp}/muxd-e2e-m18-$$.sock" | ||
| 153 | defer_sock "$SOCK21" | ||
| 154 | SOCK22="${TMPDIR:-/tmp}/muxd-e2e-m18web-$$.sock" | ||
| 155 | defer_sock "$SOCK22" | ||
| 156 | WPORT3=$(( 51000 + ($$ % 4000) )) | ||
| 157 | # ...and a third, the only M18 daemon holding a UDP port: it serves the | ||
| 158 | # session socket AND a QUIC listener at once, which is what lets one block | ||
| 159 | # ask whether a session name means the same session on either transport. | ||
| 160 | # Next 5000-spaced band after WPORT3. | ||
| 161 | SOCK23="${TMPDIR:-/tmp}/muxd-e2e-m18quic-$$.sock" | ||
| 162 | defer_sock "$SOCK23" | ||
| 163 | QPORT5=$(( 56000 + ($$ % 4000) )) | ||
| 164 | # A key of its own rather than the M8 block's $QKEY: that one is rm_swept | ||
| 165 | # the moment the M8 scenarios finish (see the sweep after the key-source | ||
| 166 | # block), which is hundreds of lines before this one runs. Reaching for a | ||
| 167 | # deleted file would make this block's daemon fail to bind for a reason | ||
| 168 | # that has nothing to do with sessions. | ||
| 169 | M18KEY="${TMPDIR:-/tmp}/mux-e2e-m18key-$$" | ||
| 170 | defer_rm "$M18KEY" | ||
| 171 | |||
| 172 | # --- M18: two sessions on one daemon are two shells --------------------- | ||
| 173 | # The milestone, observed from outside: ONE daemon, one socket path, one | ||
| 174 | # key — and two sessions that share nothing. A connection IS a session | ||
| 175 | # (decision 6), so the two clients below differ in exactly one flag, and | ||
| 176 | # every instrument here is asked to PROVE the separation rather than | ||
| 177 | # assumed to have it: for each session both a positive grep (its own | ||
| 178 | # marker is here) and a negative one (the other's is not), because "a | ||
| 179 | # holds a's marker" is satisfied just as well by a single shared shell. | ||
| 180 | # | ||
| 181 | # Ordered ahead of the M-web block on the legible-catch rule. The wall | ||
| 182 | # scenarios below now attach BY NAME, and a regression in name resolution | ||
| 183 | # would hang one of them with nothing printed — fifteen seconds into a | ||
| 184 | # scenario whose label says "WebSocket". This block reaches the same | ||
| 185 | # resolver with a CLI in front of it, and fails there first. | ||
| 186 | start_daemon "$SOCK21" "$OUT.m18.d" "M18 multi-session daemon never bound" --shell /bin/sh | ||
| 187 | D18PID=$DPID | ||
| 188 | |||
| 189 | # Two clients, two names, one socket. Each plants a marker its own shell | ||
| 190 | # has to EXPAND — the typed line reads `printf "m18a-%s\n" pin` and only | ||
| 191 | # the output reads `m18a-pin`, so a grep that hits is the shell's work and | ||
| 192 | # not an echo of our keystrokes (the tiny-attach block's trick). | ||
| 193 | # | ||
| 194 | # Both run in the foreground and DETACH (\034\034, the Ctrl-\ chord) rather than staying | ||
| 195 | # up: the session has to outlive its client for every assertion below to | ||
| 196 | # mean anything. A session ends when its shell exits, not when its last | ||
| 197 | # client leaves (decision 8) — so these greps run against two sessions | ||
| 198 | # with nobody attached, which is also why none of their answers can be | ||
| 199 | # explained by a live client holding something open. | ||
| 200 | pipe_mux "$OUT.m18a" "$OUT.m18a.err" timeout 40 "$MUX" --sock "$SOCK21" --session a | ||
| 201 | pipe_send 'printf "m18a-%%s\\n" pin\n' | ||
| 202 | await_out "$OUT.m18a" "m18a-pin" "m18a-pin never reached the client" | ||
| 203 | pipe_detach | ||
| 204 | wait_grid "$SOCK21" "m18a-pin" "M18: session a's marker" a | ||
| 205 | |||
| 206 | pipe_mux "$OUT.m18b" "$OUT.m18b.err" timeout 40 "$MUX" --sock "$SOCK21" --session b | ||
| 207 | pipe_send 'printf "m18b-%%s\\n" pin\n' | ||
| 208 | await_out "$OUT.m18b" "m18b-pin" "m18b-pin never reached the client" | ||
| 209 | pipe_detach | ||
| 210 | wait_grid "$SOCK21" "m18b-pin" "M18: session b's marker" b | ||
| 211 | |||
| 212 | # Each session's grid holds its own marker and NOT the other's. | ||
| 213 | if dump_session "$SOCK21" a | grep -q "m18b-pin"; then | ||
| 214 | echo "e2e FAIL: M18: b's marker is on a's grid — one shell, not two:" | ||
| 215 | dump_session "$SOCK21" a; exit 1 | ||
| 216 | fi | ||
| 217 | if dump_session "$SOCK21" b | grep -q "m18a-pin"; then | ||
| 218 | echo "e2e FAIL: M18: a's marker is on b's grid — one shell, not two:" | ||
| 219 | dump_session "$SOCK21" b; exit 1 | ||
| 220 | fi | ||
| 221 | |||
| 222 | # The default session was created at daemon init and nobody ever typed | ||
| 223 | # into it. It is the third session, and its emptiness is the assertion | ||
| 224 | # that `--session` ROUTES rather than decorating one shared grid. | ||
| 225 | if dump_session "$SOCK21" | grep -q -e "m18a-pin" -e "m18b-pin"; then | ||
| 226 | echo "e2e FAIL: M18: a named session's marker reached the default grid:" | ||
| 227 | dump_session "$SOCK21"; exit 1 | ||
| 228 | fi | ||
| 229 | |||
| 230 | # stats names all three, and names them one segment each. `sessions=3` on | ||
| 231 | # its own would survive a daemon that counted right and routed wrong, so | ||
| 232 | # the per-session segments are grepped by name too. | ||
| 233 | "$MUXD" stats --sock "$SOCK21" > "$OUT.m18stats" 2>&1 | ||
| 234 | grep -q 'sessions=3' "$OUT.m18stats" || { | ||
| 235 | echo "e2e FAIL: M18: stats does not see three sessions:" | ||
| 236 | cat "$OUT.m18stats"; exit 1; } | ||
| 237 | for _s in a b; do | ||
| 238 | grep -q "session $_s clients=" "$OUT.m18stats" || { | ||
| 239 | echo "e2e FAIL: M18: stats has no segment for session $_s:" | ||
| 240 | cat "$OUT.m18stats"; exit 1; } | ||
| 241 | done | ||
| 242 | |||
| 243 | # An unknown name is ANSWERED, in words, on the dump's own channel — not a | ||
| 244 | # hang and not a fabricated "connection lost" (decision 9). The exit code | ||
| 245 | # is 0 because the daemon replied; the text is the whole point. | ||
| 246 | dump_session "$SOCK21" zz > "$OUT.m18zz" 2>&1 | ||
| 247 | grep -q 'muxd: no such session: zz' "$OUT.m18zz" || { | ||
| 248 | echo "e2e FAIL: M18: an unknown session did not say so; dump answered:" | ||
| 249 | cat "$OUT.m18zz"; exit 1; } | ||
| 250 | |||
| 251 | # muxa addresses a session by name on the verbs that never attach at all. | ||
| 252 | # status_req and debug_dump grew the same name tail attach did (decision | ||
| 253 | # 14), and that is what lets an agent ask about one session of several | ||
| 254 | # without claiming a grid — muxa attaches at 0x0 or not at all, so without | ||
| 255 | # the tail it could only ever have answered for the default session. | ||
| 256 | set +e | ||
| 257 | timeout 20 "$MUXA" status --sock "$SOCK21" --session b > "$OUT.m18mst" 2>&1 | ||
| 258 | RC=$? | ||
| 259 | set -e | ||
| 260 | [ "$RC" -eq 0 ] || { | ||
| 261 | echo "e2e FAIL: M18: muxa status --session b exited $RC:" | ||
| 262 | cat "$OUT.m18mst"; exit 1; } | ||
| 263 | # Not the exit code alone: a silently empty object would pass that. `cols` | ||
| 264 | # is a field only a decoded StatusReply can put there (agent.sh's shape). | ||
| 265 | grep -q '"cols"' "$OUT.m18mst" || { | ||
| 266 | echo "e2e FAIL: M18: muxa status --session b answered without a status reply:" | ||
| 267 | cat "$OUT.m18mst"; exit 1; } | ||
| 268 | |||
| 269 | set +e | ||
| 270 | timeout 20 "$MUXA" capture --sock "$SOCK21" --session a > "$OUT.m18mcap" 2>&1 | ||
| 271 | RC=$? | ||
| 272 | set -e | ||
| 273 | [ "$RC" -eq 0 ] || { | ||
| 274 | echo "e2e FAIL: M18: muxa capture --session a exited $RC:" | ||
| 275 | cat "$OUT.m18mcap"; exit 1; } | ||
| 276 | grep -q 'm18a-pin' "$OUT.m18mcap" || { | ||
| 277 | echo "e2e FAIL: M18: muxa capture --session a did not return a's grid:" | ||
| 278 | cat "$OUT.m18mcap"; exit 1; } | ||
| 279 | if grep -q 'm18b-pin' "$OUT.m18mcap"; then | ||
| 280 | echo "e2e FAIL: M18: muxa capture --session a returned b's grid too:" | ||
| 281 | cat "$OUT.m18mcap"; exit 1 | ||
| 282 | fi | ||
| 283 | |||
| 284 | # Lifetime: a session ends when ITS shell exits, and the daemon outlives | ||
| 285 | # every death but the last (decision 8). This client attaches to a session | ||
| 286 | # that ALREADY EXISTS — attach-or-create's join arm, the other half of the | ||
| 287 | # two creations above — and types the exit that ends it. | ||
| 288 | pipe_mux "$OUT.m18ax" "$OUT.m18ax.err" timeout 40 "$MUX" --sock "$SOCK21" --session a | ||
| 289 | pipe_send 'exit\n' | ||
| 290 | pipe_waitexit "M18: the client of an exiting session" | ||
| 291 | |||
| 292 | wait_sessions "$SOCK21" 2 "M18: session a's shell exited" | ||
| 293 | |||
| 294 | # The daemon is still serving, and b is untouched by its neighbour's | ||
| 295 | # death: the death freed ONE session, which at N=1 would have been the | ||
| 296 | # daemon's own exit. | ||
| 297 | if dump_session "$SOCK21" a | grep -q "m18a-pin"; then | ||
| 298 | echo "e2e FAIL: M18: session a still answers after its shell exited:" | ||
| 299 | dump_session "$SOCK21" a; exit 1 | ||
| 300 | fi | ||
| 301 | dump_session "$SOCK21" b | grep -q "m18b-pin" || { | ||
| 302 | echo "e2e FAIL: M18: session b lost its grid when session a died:" | ||
| 303 | dump_session "$SOCK21" b; exit 1; } | ||
| 304 | |||
| 305 | assert_stopped "$SOCK21" "$D18PID" "M18 multi-session" "$OUT.m18stop" | ||
| 306 | D18PID="" | ||
| 307 | ok "two sessions on one daemon are two shells; one dies without the other" | ||
| 308 | |||
| 309 | # --- M18 on the wall: one socket, two tiles, two sessions --------------- | ||
| 310 | # What the milestone was FOR (decision 2): the wall showing the same host | ||
| 311 | # twice. Both tiles name the same socket path and differ only in the | ||
| 312 | # `#NAME` suffix, so the hub dials it twice and the two connections are | ||
| 313 | # two sessions — the browser's spelling of everything the block above | ||
| 314 | # proved with a CLI. | ||
| 315 | start_daemon "$SOCK22" "$OUT.m18w.d" "M18 wall daemon never bound" --shell /bin/sh | ||
| 316 | D19PID=$DPID | ||
| 317 | "$MUXWEB" --sock "$SOCK22#a" --sock "$SOCK22#b" --port "$WPORT3" > "$OUT.m18wh" 2>&1 & | ||
| 318 | W3PID=$! | ||
| 319 | defer_kill "$W3PID" | ||
| 320 | wait_for "$OUT.m18wh" "serving" 10 || { | ||
| 321 | echo "e2e FAIL: M18 wall hub never reported serving"; cat "$OUT.m18wh"; exit 1; } | ||
| 322 | |||
| 323 | # Content for each session, planted by a CLI client and left behind: the | ||
| 324 | # tiles are passive 0x0 wall tiles and can never type anything themselves. | ||
| 325 | pipe_mux "$OUT.m18wa" "$OUT.m18wa.err" timeout 40 "$MUX" --sock "$SOCK22" --session a | ||
| 326 | pipe_send 'printf "wall-a-%%s\\n" pin\n' | ||
| 327 | await_out "$OUT.m18wa" "wall-a-pin" "M18 wall: session a's marker never reached the client" | ||
| 328 | pipe_detach | ||
| 329 | wait_grid "$SOCK22" "wall-a-pin" "M18 wall: session a's marker" a | ||
| 330 | pipe_mux "$OUT.m18wb" "$OUT.m18wb.err" timeout 40 "$MUX" --sock "$SOCK22" --session b | ||
| 331 | pipe_send 'printf "wall-b-%%s\\n" pin\n' | ||
| 332 | await_out "$OUT.m18wb" "wall-b-pin" "M18 wall: session b's marker never reached the client" | ||
| 333 | pipe_detach | ||
| 334 | wait_grid "$SOCK22" "wall-b-pin" "M18 wall: session b's marker" b | ||
| 335 | |||
| 336 | # Tile 0 is session a's; tile 1 is session b's. The stand-in spells the | ||
| 337 | # name the way mux.js does — bytes appended after the fixed 20 — and | ||
| 338 | # attaches at 0x0, the passivity contract: both sessions already exist, so | ||
| 339 | # these are joins, and a slot that claimed no size can never move the grid. | ||
| 340 | for _t in 0 1; do | ||
| 341 | case "$_t" in | ||
| 342 | 0) _sn=a; _mine=wall-a-pin; _theirs=wall-b-pin ;; | ||
| 343 | *) _sn=b; _mine=wall-b-pin; _theirs=wall-a-pin ;; | ||
| 344 | esac | ||
| 345 | set +e | ||
| 346 | timeout 40 "$WSCLIENT" --port "$WPORT3" --tile "$_t" \ | ||
| 347 | --out "$OUT.m18ws$_t" --err "$OUT.m18ws$_t.err" <<EOF | ||
| 348 | attach 0 0 $_sn | ||
| 349 | expectstate up 10000 | ||
| 350 | expectgrid $_mine 15000 | ||
| 351 | settle 500 10000 | ||
| 352 | dumpexit | ||
| 353 | EOF | ||
| 354 | RC=$? | ||
| 355 | set -e | ||
| 356 | [ "$RC" -eq 0 ] || { | ||
| 357 | echo "e2e FAIL: M18 wall: tile $_t (session $_sn) wsclient exited $RC" | ||
| 358 | cat -v "$OUT.m18ws$_t.err" 2>/dev/null; cat "$OUT.m18wh"; exit 1; } | ||
| 359 | |||
| 360 | # The replica converges on ITS session's grid... | ||
| 361 | assert_ws_converged "$OUT.m18ws$_t" "$SOCK22" "M18 wall: tile $_t" "$_sn" | ||
| 362 | # ...and the other session's content never crossed into it. The | ||
| 363 | # convergence check above is a diff against one grid and would pass on | ||
| 364 | # a hub that fanned every session's frames to every tile only if that | ||
| 365 | # grid happened to match; this grep is the direct question. | ||
| 366 | if grep -q "$_theirs" "$OUT.m18ws$_t"; then | ||
| 367 | echo "e2e FAIL: M18 wall: tile $_t (session $_sn) received the other session's content:" | ||
| 368 | cat "$OUT.m18ws$_t"; exit 1 | ||
| 369 | fi | ||
| 370 | done | ||
| 371 | |||
| 372 | softkill "$W3PID" || true | ||
| 373 | wait_pid_gone "$W3PID" "M18 wall: hub killed by tracked pid" | ||
| 374 | W3PID="" | ||
| 375 | assert_stopped "$SOCK22" "$D19PID" "M18 wall" "$OUT.m18wstop" | ||
| 376 | D19PID="" | ||
| 377 | ok "the wall shows one host twice: two tiles, two sessions, one socket" | ||
| 378 | |||
| 379 | # --- M18 over QUIC: two dials are two sessions, and a name means one ----- | ||
| 380 | # --- session whichever transport carried the attach ---------------------- | ||
| 381 | # Decision 6 — a connection IS a session on EVERY transport — and QUIC is | ||
| 382 | # where that claim is least obvious. A QUIC client is promoted to a client | ||
| 383 | # slot when its HANDSHAKE completes, before its attach has arrived, so its | ||
| 384 | # slot starts session-less and the name on the attach is the only thing | ||
| 385 | # that ever binds it. Two dials to one port therefore have to come out as | ||
| 386 | # two shells, and the block above proves nothing about this one: it went | ||
| 387 | # through a unix socket, where the slot and the attach arrive together. | ||
| 388 | # | ||
| 389 | # The second half is the operator's question rather than the protocol's. | ||
| 390 | # The session table belongs to the DAEMON, not to the listener that | ||
| 391 | # accepted a connection — so `a` must name the same shell whether it was | ||
| 392 | # reached over QUIC or over the socket. Nothing else in this suite asks a | ||
| 393 | # daemon anything with two transports open at once. | ||
| 394 | head -c 32 /dev/urandom > "$M18KEY" | ||
| 395 | chmod 600 "$M18KEY" | ||
| 396 | # The port is an OBSERVATION, not a derivation (decisions.md, hygiene kit). | ||
| 397 | # This band sits INSIDE /proc/sys/net/ipv4/ip_local_port_range, and the | ||
| 398 | # scenarios above open plenty of outbound QUIC sockets — one of them can | ||
| 399 | # already hold the candidate by the time this daemon asks for it. Soak | ||
| 400 | # caught exactly that, once in ten runs: `muxd: a daemon is already | ||
| 401 | # listening on udp 127.0.0.1:57943`, from a client socket, not a daemon. | ||
| 402 | # | ||
| 403 | # So step on the daemon's own refusal until one binds, the way agent.sh's | ||
| 404 | # start_quic does. Anything OTHER than that refusal is a real failure and | ||
| 405 | # is reported with the daemon's own words; only the taken-port case walks. | ||
| 406 | M18_TRIES=0 | ||
| 407 | while : ; do | ||
| 408 | "$MUXD" run --sock "$SOCK23" --quic "127.0.0.1:$QPORT5" --key "$M18KEY" \ | ||
| 409 | --quic-idle-ms 15000 --shell /bin/sh > "$OUT.m18q.d" 2>&1 & | ||
| 410 | D20PID=$! | ||
| 411 | defer_kill "$D20PID" | ||
| 412 | _i=0 | ||
| 413 | while [ ! -S "$SOCK23" ] && kill -0 "$D20PID" 2>/dev/null && [ "$_i" -lt 60 ]; do | ||
| 414 | sleep 0.1; _i=$((_i + 1)) | ||
| 415 | done | ||
| 416 | [ -S "$SOCK23" ] && break | ||
| 417 | grep -q 'already listening on udp' "$OUT.m18q.d" || { | ||
| 418 | echo "e2e FAIL: M18 quic daemon never bound its session socket" | ||
| 419 | cat "$OUT.m18q.d"; exit 1; } | ||
| 420 | wait "$D20PID" 2>/dev/null || true | ||
| 421 | D20PID="" | ||
| 422 | M18_TRIES=$((M18_TRIES + 1)) | ||
| 423 | [ "$M18_TRIES" -lt 8 ] || { | ||
| 424 | echo "e2e FAIL: M18 quic: 8 candidate udp ports were taken, last $QPORT5" | ||
| 425 | exit 1; } | ||
| 426 | # A step that is not a multiple of any other band's spacing, so a walk | ||
| 427 | # from here cannot march into the ports another scenario reserved. | ||
| 428 | QPORT5=$((QPORT5 + 137)) | ||
| 429 | done | ||
| 430 | |||
| 431 | pipe_mux "$OUT.m18qa" "$OUT.m18qa.err" timeout 40 "$MUX" "quic://127.0.0.1:$QPORT5" --key "$M18KEY" --quic-idle-ms 15000 --session a | ||
| 432 | pipe_send 'printf "q18a-%%s\\n" pin\n' | ||
| 433 | await_out "$OUT.m18qa" "q18a-pin" "q18a-pin never reached the client" | ||
| 434 | pipe_detach "M18 quic: the --session a dial" | ||
| 435 | wait_grid "$SOCK23" "q18a-pin" "M18 quic: session a's marker" a | ||
| 436 | |||
| 437 | pipe_mux "$OUT.m18qb" "$OUT.m18qb.err" timeout 40 "$MUX" "quic://127.0.0.1:$QPORT5" --key "$M18KEY" --quic-idle-ms 15000 --session b | ||
| 438 | pipe_send 'printf "q18b-%%s\\n" pin\n' | ||
| 439 | await_out "$OUT.m18qb" "q18b-pin" "q18b-pin never reached the client" | ||
| 440 | pipe_detach "M18 quic: the --session b dial" | ||
| 441 | wait_grid "$SOCK23" "q18b-pin" "M18 quic: session b's marker" b | ||
| 442 | |||
| 443 | if dump_session "$SOCK23" a | grep -q "q18b-pin"; then | ||
| 444 | echo "e2e FAIL: M18 quic: b's marker is on a's grid — one session, not two:" | ||
| 445 | dump_session "$SOCK23" a; exit 1 | ||
| 446 | fi | ||
| 447 | if dump_session "$SOCK23" b | grep -q "q18a-pin"; then | ||
| 448 | echo "e2e FAIL: M18 quic: a's marker is on b's grid — one session, not two:" | ||
| 449 | dump_session "$SOCK23" b; exit 1 | ||
| 450 | fi | ||
| 451 | "$MUXD" stats --sock "$SOCK23" | grep -q 'sessions=3' || { | ||
| 452 | echo "e2e FAIL: M18 quic: two dials did not make two sessions beside the default:" | ||
| 453 | "$MUXD" stats --sock "$SOCK23"; exit 1; } | ||
| 454 | |||
| 455 | # The cross-transport join. Same name, different door: this must land in | ||
| 456 | # the shell the QUIC client left behind, not spawn a second one. | ||
| 457 | pipe_mux "$OUT.m18qx" "$OUT.m18qx.err" timeout 40 "$MUX" --sock "$SOCK23" --session a | ||
| 458 | pipe_send 'printf "x18a-%%s\\n" pin\n' | ||
| 459 | await_out "$OUT.m18qx" "x18a-pin" "x18a-pin never reached the client" | ||
| 460 | pipe_detach "M18 quic: the socket client of session a" | ||
| 461 | wait_grid "$SOCK23" "x18a-pin" "M18 quic: the socket client's marker in a" a | ||
| 462 | |||
| 463 | # Both markers on one grid is the whole assertion: the QUIC client's | ||
| 464 | # output and the socket client's, in the session they both named. | ||
| 465 | dump_session "$SOCK23" a | grep -q "q18a-pin" || { | ||
| 466 | echo "e2e FAIL: M18 quic: joining 'a' over the socket did not find the QUIC dial's shell:" | ||
| 467 | dump_session "$SOCK23" a; exit 1; } | ||
| 468 | # ...and it JOINED rather than created: a fourth session here would mean | ||
| 469 | # the name resolved per-transport, which is the bug this pair exists for. | ||
| 470 | "$MUXD" stats --sock "$SOCK23" | grep -q 'sessions=3' || { | ||
| 471 | echo "e2e FAIL: M18 quic: a socket join of an existing name made a new session:" | ||
| 472 | "$MUXD" stats --sock "$SOCK23"; exit 1; } | ||
| 473 | if dump_session "$SOCK23" b | grep -q "x18a-pin"; then | ||
| 474 | echo "e2e FAIL: M18 quic: the socket client's marker leaked into b:" | ||
| 475 | dump_session "$SOCK23" b; exit 1 | ||
| 476 | fi | ||
| 477 | |||
| 478 | assert_stopped "$SOCK23" "$D20PID" "M18 quic multi-session" "$OUT.m18qstop" | ||
| 479 | D20PID="" | ||
| 480 | ok "quic: two dials are two sessions, and one name is one session on either transport" | ||
| 481 | |||
| 482 | # The pipe_mux client in flight, and its FIFO. Both are registered by | 148 | # The pipe_mux client in flight, and its FIFO. Both are registered by |
| 483 | # pipe_mux itself; its `timeout` stays the primary guarantee and the trap | 149 | # pipe_mux itself; its `timeout` stays the primary guarantee and the trap |
| 484 | # is the backstop for a scenario that exits between pipe_mux and | 150 | # is the backstop for a scenario that exits between pipe_mux and |
test/e2e_05_session.sh
| Old | New | ||
|---|---|---|---|
| @@ -0,0 +1,337 @@ | |||
| 1 | # shellcheck shell=sh | ||
| 2 | # e2e_05_session.sh — sourced by test/e2e.sh after e2e_lib.sh. Scenarios run in | ||
| 3 | # the order they stand in; see the lib's header for what this file may | ||
| 4 | # assume and what it must register. | ||
| 5 | # M18. Two daemons — the CLI multi-session block's and the wall's — and one | ||
| 6 | # more hub on the next 5000-spaced port. Only two sockets for four-and-some | ||
| 7 | # sessions, which is the whole point of the milestone: sessions are named on | ||
| 8 | # one socket, not spread across one socket each. | ||
| 9 | SOCK21="${TMPDIR:-/tmp}/muxd-e2e-m18-$$.sock" | ||
| 10 | defer_sock "$SOCK21" | ||
| 11 | SOCK22="${TMPDIR:-/tmp}/muxd-e2e-m18web-$$.sock" | ||
| 12 | defer_sock "$SOCK22" | ||
| 13 | WPORT3=$(( 51000 + ($$ % 4000) )) | ||
| 14 | # ...and a third, the only M18 daemon holding a UDP port: it serves the | ||
| 15 | # session socket AND a QUIC listener at once, which is what lets one block | ||
| 16 | # ask whether a session name means the same session on either transport. | ||
| 17 | # Next 5000-spaced band after WPORT3. | ||
| 18 | SOCK23="${TMPDIR:-/tmp}/muxd-e2e-m18quic-$$.sock" | ||
| 19 | defer_sock "$SOCK23" | ||
| 20 | QPORT5=$(( 56000 + ($$ % 4000) )) | ||
| 21 | # A key of its own rather than the M8 block's $QKEY: that one is rm_swept | ||
| 22 | # the moment the M8 scenarios finish (see the sweep after the key-source | ||
| 23 | # block), which is hundreds of lines before this one runs. Reaching for a | ||
| 24 | # deleted file would make this block's daemon fail to bind for a reason | ||
| 25 | # that has nothing to do with sessions. | ||
| 26 | M18KEY="${TMPDIR:-/tmp}/mux-e2e-m18key-$$" | ||
| 27 | defer_rm "$M18KEY" | ||
| 28 | |||
| 29 | # --- M18: two sessions on one daemon are two shells --------------------- | ||
| 30 | # The milestone, observed from outside: ONE daemon, one socket path, one | ||
| 31 | # key — and two sessions that share nothing. A connection IS a session | ||
| 32 | # (decision 6), so the two clients below differ in exactly one flag, and | ||
| 33 | # every instrument here is asked to PROVE the separation rather than | ||
| 34 | # assumed to have it: for each session both a positive grep (its own | ||
| 35 | # marker is here) and a negative one (the other's is not), because "a | ||
| 36 | # holds a's marker" is satisfied just as well by a single shared shell. | ||
| 37 | # | ||
| 38 | # Ordered ahead of the M-web block on the legible-catch rule. The wall | ||
| 39 | # scenarios below now attach BY NAME, and a regression in name resolution | ||
| 40 | # would hang one of them with nothing printed — fifteen seconds into a | ||
| 41 | # scenario whose label says "WebSocket". This block reaches the same | ||
| 42 | # resolver with a CLI in front of it, and fails there first. | ||
| 43 | start_daemon "$SOCK21" "$OUT.m18.d" "M18 multi-session daemon never bound" --shell /bin/sh | ||
| 44 | D18PID=$DPID | ||
| 45 | |||
| 46 | # Two clients, two names, one socket. Each plants a marker its own shell | ||
| 47 | # has to EXPAND — the typed line reads `printf "m18a-%s\n" pin` and only | ||
| 48 | # the output reads `m18a-pin`, so a grep that hits is the shell's work and | ||
| 49 | # not an echo of our keystrokes (the tiny-attach block's trick). | ||
| 50 | # | ||
| 51 | # Both run in the foreground and DETACH (\034\034, the Ctrl-\ chord) rather than staying | ||
| 52 | # up: the session has to outlive its client for every assertion below to | ||
| 53 | # mean anything. A session ends when its shell exits, not when its last | ||
| 54 | # client leaves (decision 8) — so these greps run against two sessions | ||
| 55 | # with nobody attached, which is also why none of their answers can be | ||
| 56 | # explained by a live client holding something open. | ||
| 57 | pipe_mux "$OUT.m18a" "$OUT.m18a.err" timeout 40 "$MUX" --sock "$SOCK21" --session a | ||
| 58 | pipe_send 'printf "m18a-%%s\\n" pin\n' | ||
| 59 | await_out "$OUT.m18a" "m18a-pin" "m18a-pin never reached the client" | ||
| 60 | pipe_detach | ||
| 61 | wait_grid "$SOCK21" "m18a-pin" "M18: session a's marker" a | ||
| 62 | |||
| 63 | pipe_mux "$OUT.m18b" "$OUT.m18b.err" timeout 40 "$MUX" --sock "$SOCK21" --session b | ||
| 64 | pipe_send 'printf "m18b-%%s\\n" pin\n' | ||
| 65 | await_out "$OUT.m18b" "m18b-pin" "m18b-pin never reached the client" | ||
| 66 | pipe_detach | ||
| 67 | wait_grid "$SOCK21" "m18b-pin" "M18: session b's marker" b | ||
| 68 | |||
| 69 | # Each session's grid holds its own marker and NOT the other's. | ||
| 70 | if dump_session "$SOCK21" a | grep -q "m18b-pin"; then | ||
| 71 | echo "e2e FAIL: M18: b's marker is on a's grid — one shell, not two:" | ||
| 72 | dump_session "$SOCK21" a; exit 1 | ||
| 73 | fi | ||
| 74 | if dump_session "$SOCK21" b | grep -q "m18a-pin"; then | ||
| 75 | echo "e2e FAIL: M18: a's marker is on b's grid — one shell, not two:" | ||
| 76 | dump_session "$SOCK21" b; exit 1 | ||
| 77 | fi | ||
| 78 | |||
| 79 | # The default session was created at daemon init and nobody ever typed | ||
| 80 | # into it. It is the third session, and its emptiness is the assertion | ||
| 81 | # that `--session` ROUTES rather than decorating one shared grid. | ||
| 82 | if dump_session "$SOCK21" | grep -q -e "m18a-pin" -e "m18b-pin"; then | ||
| 83 | echo "e2e FAIL: M18: a named session's marker reached the default grid:" | ||
| 84 | dump_session "$SOCK21"; exit 1 | ||
| 85 | fi | ||
| 86 | |||
| 87 | # stats names all three, and names them one segment each. `sessions=3` on | ||
| 88 | # its own would survive a daemon that counted right and routed wrong, so | ||
| 89 | # the per-session segments are grepped by name too. | ||
| 90 | "$MUXD" stats --sock "$SOCK21" > "$OUT.m18stats" 2>&1 | ||
| 91 | grep -q 'sessions=3' "$OUT.m18stats" || { | ||
| 92 | echo "e2e FAIL: M18: stats does not see three sessions:" | ||
| 93 | cat "$OUT.m18stats"; exit 1; } | ||
| 94 | for _s in a b; do | ||
| 95 | grep -q "session $_s clients=" "$OUT.m18stats" || { | ||
| 96 | echo "e2e FAIL: M18: stats has no segment for session $_s:" | ||
| 97 | cat "$OUT.m18stats"; exit 1; } | ||
| 98 | done | ||
| 99 | |||
| 100 | # An unknown name is ANSWERED, in words, on the dump's own channel — not a | ||
| 101 | # hang and not a fabricated "connection lost" (decision 9). The exit code | ||
| 102 | # is 0 because the daemon replied; the text is the whole point. | ||
| 103 | dump_session "$SOCK21" zz > "$OUT.m18zz" 2>&1 | ||
| 104 | grep -q 'muxd: no such session: zz' "$OUT.m18zz" || { | ||
| 105 | echo "e2e FAIL: M18: an unknown session did not say so; dump answered:" | ||
| 106 | cat "$OUT.m18zz"; exit 1; } | ||
| 107 | |||
| 108 | # muxa addresses a session by name on the verbs that never attach at all. | ||
| 109 | # status_req and debug_dump grew the same name tail attach did (decision | ||
| 110 | # 14), and that is what lets an agent ask about one session of several | ||
| 111 | # without claiming a grid — muxa attaches at 0x0 or not at all, so without | ||
| 112 | # the tail it could only ever have answered for the default session. | ||
| 113 | set +e | ||
| 114 | timeout 20 "$MUXA" status --sock "$SOCK21" --session b > "$OUT.m18mst" 2>&1 | ||
| 115 | RC=$? | ||
| 116 | set -e | ||
| 117 | [ "$RC" -eq 0 ] || { | ||
| 118 | echo "e2e FAIL: M18: muxa status --session b exited $RC:" | ||
| 119 | cat "$OUT.m18mst"; exit 1; } | ||
| 120 | # Not the exit code alone: a silently empty object would pass that. `cols` | ||
| 121 | # is a field only a decoded StatusReply can put there (agent.sh's shape). | ||
| 122 | grep -q '"cols"' "$OUT.m18mst" || { | ||
| 123 | echo "e2e FAIL: M18: muxa status --session b answered without a status reply:" | ||
| 124 | cat "$OUT.m18mst"; exit 1; } | ||
| 125 | |||
| 126 | set +e | ||
| 127 | timeout 20 "$MUXA" capture --sock "$SOCK21" --session a > "$OUT.m18mcap" 2>&1 | ||
| 128 | RC=$? | ||
| 129 | set -e | ||
| 130 | [ "$RC" -eq 0 ] || { | ||
| 131 | echo "e2e FAIL: M18: muxa capture --session a exited $RC:" | ||
| 132 | cat "$OUT.m18mcap"; exit 1; } | ||
| 133 | grep -q 'm18a-pin' "$OUT.m18mcap" || { | ||
| 134 | echo "e2e FAIL: M18: muxa capture --session a did not return a's grid:" | ||
| 135 | cat "$OUT.m18mcap"; exit 1; } | ||
| 136 | if grep -q 'm18b-pin' "$OUT.m18mcap"; then | ||
| 137 | echo "e2e FAIL: M18: muxa capture --session a returned b's grid too:" | ||
| 138 | cat "$OUT.m18mcap"; exit 1 | ||
| 139 | fi | ||
| 140 | |||
| 141 | # Lifetime: a session ends when ITS shell exits, and the daemon outlives | ||
| 142 | # every death but the last (decision 8). This client attaches to a session | ||
| 143 | # that ALREADY EXISTS — attach-or-create's join arm, the other half of the | ||
| 144 | # two creations above — and types the exit that ends it. | ||
| 145 | pipe_mux "$OUT.m18ax" "$OUT.m18ax.err" timeout 40 "$MUX" --sock "$SOCK21" --session a | ||
| 146 | pipe_send 'exit\n' | ||
| 147 | pipe_waitexit "M18: the client of an exiting session" | ||
| 148 | |||
| 149 | wait_sessions "$SOCK21" 2 "M18: session a's shell exited" | ||
| 150 | |||
| 151 | # The daemon is still serving, and b is untouched by its neighbour's | ||
| 152 | # death: the death freed ONE session, which at N=1 would have been the | ||
| 153 | # daemon's own exit. | ||
| 154 | if dump_session "$SOCK21" a | grep -q "m18a-pin"; then | ||
| 155 | echo "e2e FAIL: M18: session a still answers after its shell exited:" | ||
| 156 | dump_session "$SOCK21" a; exit 1 | ||
| 157 | fi | ||
| 158 | dump_session "$SOCK21" b | grep -q "m18b-pin" || { | ||
| 159 | echo "e2e FAIL: M18: session b lost its grid when session a died:" | ||
| 160 | dump_session "$SOCK21" b; exit 1; } | ||
| 161 | |||
| 162 | assert_stopped "$SOCK21" "$D18PID" "M18 multi-session" "$OUT.m18stop" | ||
| 163 | D18PID="" | ||
| 164 | ok "two sessions on one daemon are two shells; one dies without the other" | ||
| 165 | |||
| 166 | # --- M18 on the wall: one socket, two tiles, two sessions --------------- | ||
| 167 | # What the milestone was FOR (decision 2): the wall showing the same host | ||
| 168 | # twice. Both tiles name the same socket path and differ only in the | ||
| 169 | # `#NAME` suffix, so the hub dials it twice and the two connections are | ||
| 170 | # two sessions — the browser's spelling of everything the block above | ||
| 171 | # proved with a CLI. | ||
| 172 | start_daemon "$SOCK22" "$OUT.m18w.d" "M18 wall daemon never bound" --shell /bin/sh | ||
| 173 | D19PID=$DPID | ||
| 174 | "$MUXWEB" --sock "$SOCK22#a" --sock "$SOCK22#b" --port "$WPORT3" > "$OUT.m18wh" 2>&1 & | ||
| 175 | W3PID=$! | ||
| 176 | defer_kill "$W3PID" | ||
| 177 | wait_for "$OUT.m18wh" "serving" 10 || { | ||
| 178 | echo "e2e FAIL: M18 wall hub never reported serving"; cat "$OUT.m18wh"; exit 1; } | ||
| 179 | |||
| 180 | # Content for each session, planted by a CLI client and left behind: the | ||
| 181 | # tiles are passive 0x0 wall tiles and can never type anything themselves. | ||
| 182 | pipe_mux "$OUT.m18wa" "$OUT.m18wa.err" timeout 40 "$MUX" --sock "$SOCK22" --session a | ||
| 183 | pipe_send 'printf "wall-a-%%s\\n" pin\n' | ||
| 184 | await_out "$OUT.m18wa" "wall-a-pin" "M18 wall: session a's marker never reached the client" | ||
| 185 | pipe_detach | ||
| 186 | wait_grid "$SOCK22" "wall-a-pin" "M18 wall: session a's marker" a | ||
| 187 | pipe_mux "$OUT.m18wb" "$OUT.m18wb.err" timeout 40 "$MUX" --sock "$SOCK22" --session b | ||
| 188 | pipe_send 'printf "wall-b-%%s\\n" pin\n' | ||
| 189 | await_out "$OUT.m18wb" "wall-b-pin" "M18 wall: session b's marker never reached the client" | ||
| 190 | pipe_detach | ||
| 191 | wait_grid "$SOCK22" "wall-b-pin" "M18 wall: session b's marker" b | ||
| 192 | |||
| 193 | # Tile 0 is session a's; tile 1 is session b's. The stand-in spells the | ||
| 194 | # name the way mux.js does — bytes appended after the fixed 20 — and | ||
| 195 | # attaches at 0x0, the passivity contract: both sessions already exist, so | ||
| 196 | # these are joins, and a slot that claimed no size can never move the grid. | ||
| 197 | for _t in 0 1; do | ||
| 198 | case "$_t" in | ||
| 199 | 0) _sn=a; _mine=wall-a-pin; _theirs=wall-b-pin ;; | ||
| 200 | *) _sn=b; _mine=wall-b-pin; _theirs=wall-a-pin ;; | ||
| 201 | esac | ||
| 202 | set +e | ||
| 203 | timeout 40 "$WSCLIENT" --port "$WPORT3" --tile "$_t" \ | ||
| 204 | --out "$OUT.m18ws$_t" --err "$OUT.m18ws$_t.err" <<EOF | ||
| 205 | attach 0 0 $_sn | ||
| 206 | expectstate up 10000 | ||
| 207 | expectgrid $_mine 15000 | ||
| 208 | settle 500 10000 | ||
| 209 | dumpexit | ||
| 210 | EOF | ||
| 211 | RC=$? | ||
| 212 | set -e | ||
| 213 | [ "$RC" -eq 0 ] || { | ||
| 214 | echo "e2e FAIL: M18 wall: tile $_t (session $_sn) wsclient exited $RC" | ||
| 215 | cat -v "$OUT.m18ws$_t.err" 2>/dev/null; cat "$OUT.m18wh"; exit 1; } | ||
| 216 | |||
| 217 | # The replica converges on ITS session's grid... | ||
| 218 | assert_ws_converged "$OUT.m18ws$_t" "$SOCK22" "M18 wall: tile $_t" "$_sn" | ||
| 219 | # ...and the other session's content never crossed into it. The | ||
| 220 | # convergence check above is a diff against one grid and would pass on | ||
| 221 | # a hub that fanned every session's frames to every tile only if that | ||
| 222 | # grid happened to match; this grep is the direct question. | ||
| 223 | if grep -q "$_theirs" "$OUT.m18ws$_t"; then | ||
| 224 | echo "e2e FAIL: M18 wall: tile $_t (session $_sn) received the other session's content:" | ||
| 225 | cat "$OUT.m18ws$_t"; exit 1 | ||
| 226 | fi | ||
| 227 | done | ||
| 228 | |||
| 229 | softkill "$W3PID" || true | ||
| 230 | wait_pid_gone "$W3PID" "M18 wall: hub killed by tracked pid" | ||
| 231 | W3PID="" | ||
| 232 | assert_stopped "$SOCK22" "$D19PID" "M18 wall" "$OUT.m18wstop" | ||
| 233 | D19PID="" | ||
| 234 | ok "the wall shows one host twice: two tiles, two sessions, one socket" | ||
| 235 | |||
| 236 | # --- M18 over QUIC: two dials are two sessions, and a name means one ----- | ||
| 237 | # --- session whichever transport carried the attach ---------------------- | ||
| 238 | # Decision 6 — a connection IS a session on EVERY transport — and QUIC is | ||
| 239 | # where that claim is least obvious. A QUIC client is promoted to a client | ||
| 240 | # slot when its HANDSHAKE completes, before its attach has arrived, so its | ||
| 241 | # slot starts session-less and the name on the attach is the only thing | ||
| 242 | # that ever binds it. Two dials to one port therefore have to come out as | ||
| 243 | # two shells, and the block above proves nothing about this one: it went | ||
| 244 | # through a unix socket, where the slot and the attach arrive together. | ||
| 245 | # | ||
| 246 | # The second half is the operator's question rather than the protocol's. | ||
| 247 | # The session table belongs to the DAEMON, not to the listener that | ||
| 248 | # accepted a connection — so `a` must name the same shell whether it was | ||
| 249 | # reached over QUIC or over the socket. Nothing else in this suite asks a | ||
| 250 | # daemon anything with two transports open at once. | ||
| 251 | head -c 32 /dev/urandom > "$M18KEY" | ||
| 252 | chmod 600 "$M18KEY" | ||
| 253 | # The port is an OBSERVATION, not a derivation (decisions.md, hygiene kit). | ||
| 254 | # This band sits INSIDE /proc/sys/net/ipv4/ip_local_port_range, and the | ||
| 255 | # scenarios above open plenty of outbound QUIC sockets — one of them can | ||
| 256 | # already hold the candidate by the time this daemon asks for it. Soak | ||
| 257 | # caught exactly that, once in ten runs: `muxd: a daemon is already | ||
| 258 | # listening on udp 127.0.0.1:57943`, from a client socket, not a daemon. | ||
| 259 | # | ||
| 260 | # So step on the daemon's own refusal until one binds, the way agent.sh's | ||
| 261 | # start_quic does. Anything OTHER than that refusal is a real failure and | ||
| 262 | # is reported with the daemon's own words; only the taken-port case walks. | ||
| 263 | M18_TRIES=0 | ||
| 264 | while : ; do | ||
| 265 | "$MUXD" run --sock "$SOCK23" --quic "127.0.0.1:$QPORT5" --key "$M18KEY" \ | ||
| 266 | --quic-idle-ms 15000 --shell /bin/sh > "$OUT.m18q.d" 2>&1 & | ||
| 267 | D20PID=$! | ||
| 268 | defer_kill "$D20PID" | ||
| 269 | _i=0 | ||
| 270 | while [ ! -S "$SOCK23" ] && kill -0 "$D20PID" 2>/dev/null && [ "$_i" -lt 60 ]; do | ||
| 271 | sleep 0.1; _i=$((_i + 1)) | ||
| 272 | done | ||
| 273 | [ -S "$SOCK23" ] && break | ||
| 274 | grep -q 'already listening on udp' "$OUT.m18q.d" || { | ||
| 275 | echo "e2e FAIL: M18 quic daemon never bound its session socket" | ||
| 276 | cat "$OUT.m18q.d"; exit 1; } | ||
| 277 | wait "$D20PID" 2>/dev/null || true | ||
| 278 | D20PID="" | ||
| 279 | M18_TRIES=$((M18_TRIES + 1)) | ||
| 280 | [ "$M18_TRIES" -lt 8 ] || { | ||
| 281 | echo "e2e FAIL: M18 quic: 8 candidate udp ports were taken, last $QPORT5" | ||
| 282 | exit 1; } | ||
| 283 | # A step that is not a multiple of any other band's spacing, so a walk | ||
| 284 | # from here cannot march into the ports another scenario reserved. | ||
| 285 | QPORT5=$((QPORT5 + 137)) | ||
| 286 | done | ||
| 287 | |||
| 288 | pipe_mux "$OUT.m18qa" "$OUT.m18qa.err" timeout 40 "$MUX" "quic://127.0.0.1:$QPORT5" --key "$M18KEY" --quic-idle-ms 15000 --session a | ||
| 289 | pipe_send 'printf "q18a-%%s\\n" pin\n' | ||
| 290 | await_out "$OUT.m18qa" "q18a-pin" "q18a-pin never reached the client" | ||
| 291 | pipe_detach "M18 quic: the --session a dial" | ||
| 292 | wait_grid "$SOCK23" "q18a-pin" "M18 quic: session a's marker" a | ||
| 293 | |||
| 294 | pipe_mux "$OUT.m18qb" "$OUT.m18qb.err" timeout 40 "$MUX" "quic://127.0.0.1:$QPORT5" --key "$M18KEY" --quic-idle-ms 15000 --session b | ||
| 295 | pipe_send 'printf "q18b-%%s\\n" pin\n' | ||
| 296 | await_out "$OUT.m18qb" "q18b-pin" "q18b-pin never reached the client" | ||
| 297 | pipe_detach "M18 quic: the --session b dial" | ||
| 298 | wait_grid "$SOCK23" "q18b-pin" "M18 quic: session b's marker" b | ||
| 299 | |||
| 300 | if dump_session "$SOCK23" a | grep -q "q18b-pin"; then | ||
| 301 | echo "e2e FAIL: M18 quic: b's marker is on a's grid — one session, not two:" | ||
| 302 | dump_session "$SOCK23" a; exit 1 | ||
| 303 | fi | ||
| 304 | if dump_session "$SOCK23" b | grep -q "q18a-pin"; then | ||
| 305 | echo "e2e FAIL: M18 quic: a's marker is on b's grid — one session, not two:" | ||
| 306 | dump_session "$SOCK23" b; exit 1 | ||
| 307 | fi | ||
| 308 | "$MUXD" stats --sock "$SOCK23" | grep -q 'sessions=3' || { | ||
| 309 | echo "e2e FAIL: M18 quic: two dials did not make two sessions beside the default:" | ||
| 310 | "$MUXD" stats --sock "$SOCK23"; exit 1; } | ||
| 311 | |||
| 312 | # The cross-transport join. Same name, different door: this must land in | ||
| 313 | # the shell the QUIC client left behind, not spawn a second one. | ||
| 314 | pipe_mux "$OUT.m18qx" "$OUT.m18qx.err" timeout 40 "$MUX" --sock "$SOCK23" --session a | ||
| 315 | pipe_send 'printf "x18a-%%s\\n" pin\n' | ||
| 316 | await_out "$OUT.m18qx" "x18a-pin" "x18a-pin never reached the client" | ||
| 317 | pipe_detach "M18 quic: the socket client of session a" | ||
| 318 | wait_grid "$SOCK23" "x18a-pin" "M18 quic: the socket client's marker in a" a | ||
| 319 | |||
| 320 | # Both markers on one grid is the whole assertion: the QUIC client's | ||
| 321 | # output and the socket client's, in the session they both named. | ||
| 322 | dump_session "$SOCK23" a | grep -q "q18a-pin" || { | ||
| 323 | echo "e2e FAIL: M18 quic: joining 'a' over the socket did not find the QUIC dial's shell:" | ||
| 324 | dump_session "$SOCK23" a; exit 1; } | ||
| 325 | # ...and it JOINED rather than created: a fourth session here would mean | ||
| 326 | # the name resolved per-transport, which is the bug this pair exists for. | ||
| 327 | "$MUXD" stats --sock "$SOCK23" | grep -q 'sessions=3' || { | ||
| 328 | echo "e2e FAIL: M18 quic: a socket join of an existing name made a new session:" | ||
| 329 | "$MUXD" stats --sock "$SOCK23"; exit 1; } | ||
| 330 | if dump_session "$SOCK23" b | grep -q "x18a-pin"; then | ||
| 331 | echo "e2e FAIL: M18 quic: the socket client's marker leaked into b:" | ||
| 332 | dump_session "$SOCK23" b; exit 1 | ||
| 333 | fi | ||
| 334 | |||
| 335 | assert_stopped "$SOCK23" "$D20PID" "M18 quic multi-session" "$OUT.m18qstop" | ||
| 336 | D20PID="" | ||
| 337 | ok "quic: two dials are two sessions, and one name is one session on either transport" | ||