a73x

2518c30b

test: convergence on every transport scenario that ends with a live daemon

a73x   2026-08-09 17:27

Commit message
test: convergence on every transport scenario that ends with a live daemon

test/e2e.sh
Old New
@@ -199,6 +199,8 @@ cleanup() {
199 rm -f "$SOCK8" "$SOCK8T" "$SOCK11" "$OUT.start" "$OUT.start2" "$OUT.s8" \ 199 rm -f "$SOCK8" "$SOCK8T" "$SOCK11" "$OUT.start" "$OUT.start2" "$OUT.s8" \
200 "$OUT.ra" "$OUT.rb" "$OUT.goal" "$OUT.g9" "$OUT.dead" \ 200 "$OUT.ra" "$OUT.rb" "$OUT.goal" "$OUT.g9" "$OUT.dead" \
201 "$SOCK9" "$SOCK10" "$OUT.env1" "$OUT.env2" \ 201 "$SOCK9" "$SOCK10" "$OUT.env1" "$OUT.env2" \
202 "$OUT.s8.err" "$OUT.race" "$OUT.g9.err" "$OUT.env1.err" "$OUT.env2.err" \
203 "$OUT.m7.err" "$OUT.m7b.err" "$OUT.qc.err" "$OUT.qr.err" "$OUT.qk.err" \
202 "$SOCK5" "$SOCK6" "$SOCK7" "$PWSH" \ 204 "$SOCK5" "$SOCK6" "$SOCK7" "$PWSH" \
203 "$OUT.p1" "$OUT.p1.early" "$OUT.pb" "$OUT.pw" "$OUT.rw" "$OUT.pr" \ 205 "$OUT.p1" "$OUT.p1.early" "$OUT.pb" "$OUT.pw" "$OUT.rw" "$OUT.pr" \
204 "$SOCK" "$SOCK2" "$SOCK3" "$SOCK4" "$SOCK4.second" "$QKEY" "$QKEY.bad" \ 206 "$SOCK" "$SOCK2" "$SOCK3" "$SOCK4" "$SOCK4.second" "$QKEY" "$QKEY.bad" \
@@ -217,6 +219,7 @@ cleanup() {
217 trap cleanup EXIT INT TERM 219 trap cleanup EXIT INT TERM
218 220
219 # --- M10: --version answers "did the scp land" without a daemon anywhere. 221 # --- M10: --version answers "did the scp land" without a daemon anywhere.
222 # No convergence: no daemon and no client, so there is no grid on either side.
220 "$MUXD" --version | grep -q '^muxd 0\.' || { echo "e2e FAIL: muxd --version"; exit 1; } 223 "$MUXD" --version | grep -q '^muxd 0\.' || { echo "e2e FAIL: muxd --version"; exit 1; }
221 "$MUX" --version | grep -q '^mux 0\.' || { echo "e2e FAIL: mux --version"; exit 1; } 224 "$MUX" --version | grep -q '^mux 0\.' || { echo "e2e FAIL: mux --version"; exit 1; }
222 # Both must report the SAME version: the greps above pass just as happily for 225 # Both must report the SAME version: the greps above pass just as happily for
@@ -227,6 +230,7 @@ trap cleanup EXIT INT TERM
227 echo "e2e OK: --version on both binaries" 230 echo "e2e OK: --version on both binaries"
228 231
229 # --- M10: keygen writes 0600, prints the path, refuses a second run. 232 # --- M10: keygen writes 0600, prints the path, refuses a second run.
233 # No convergence: keygen touches the filesystem, never a session.
230 KEYOUT=$("$MUXD" keygen) 234 KEYOUT=$("$MUXD" keygen)
231 [ "$KEYOUT" = "$XDG_CONFIG_HOME/mux/key" ] || { 235 [ "$KEYOUT" = "$XDG_CONFIG_HOME/mux/key" ] || {
232 echo "e2e FAIL: keygen printed '$KEYOUT'"; exit 1; } 236 echo "e2e FAIL: keygen printed '$KEYOUT'"; exit 1; }
@@ -283,6 +287,8 @@ rm -f "$OUT.doctored" "$OUT.doctored.render" "$OUT.doctored.dump" \
283 echo "e2e OK: convergence control fires on a doctored stream" 287 echo "e2e OK: convergence control fires on a doctored stream"
284 288
285 # --- M3: kill a client mid-run; daemon survives; reattach lands correctly. 289 # --- M3: kill a client mid-run; daemon survives; reattach lands correctly.
290 # No convergence on $OUT.kill: kill -9 truncates the stream mid-paint, possibly
291 # mid-escape-sequence, so a half-drawn capture is the expected shape here.
286 { printf 'seq 1 60\n'; sleep 2; } | "$MUX" --sock "$SOCK" > "$OUT.kill" & 292 { printf 'seq 1 60\n'; sleep 2; } | "$MUX" --sock "$SOCK" > "$OUT.kill" &
287 CPID=$! 293 CPID=$!
288 sleep 1 294 sleep 1
@@ -294,6 +300,7 @@ kill -0 "$DPID" || { echo "e2e FAIL: daemon died after client kill -9"; exit 1;
294 grep -q "60" "$OUT.re" || { 300 grep -q "60" "$OUT.re" || {
295 echo "e2e FAIL: reattach after kill missing state"; cat "$OUT.re"; exit 1; 301 echo "e2e FAIL: reattach after kill missing state"; cat "$OUT.re"; exit 1;
296 } 302 }
303 assert_converged "$OUT.re" "$SOCK" "reattach after kill"
297 rm -f "$OUT.kill" "$OUT.re" 304 rm -f "$OUT.kill" "$OUT.re"
298 305
299 # --- M5: two clients on one session. Output typed in A reaches both; then A 306 # --- M5: two clients on one session. Output typed in A reaches both; then A
@@ -315,6 +322,10 @@ grep -q "m5-after-a-left" "$OUT.b" || {
315 echo "e2e FAIL: client B lost its input path after A detached"; exit 1; 322 echo "e2e FAIL: client B lost its input path after A detached"; exit 1;
316 } 323 }
317 kill -0 "$DPID" || { echo "e2e FAIL: daemon died in two-client scenario"; exit 1; } 324 kill -0 "$DPID" || { echo "e2e FAIL: daemon died in two-client scenario"; exit 1; }
325 # B's capture, not A's: A detached before B's marker landed, so A's stream ends
326 # on a grid the daemon has since moved past. B saw both, and its convergence
327 # covers the same grid.
328 assert_converged "$OUT.b" "$SOCK" "two clients"
318 rm -f "$OUT.a" "$OUT.b" 329 rm -f "$OUT.a" "$OUT.b"
319 330
320 # --- M6: the same protocol over an arbitrary byte pipe. `muxd proxy` is a 331 # --- M6: the same protocol over an arbitrary byte pipe. `muxd proxy` is a
@@ -334,10 +345,12 @@ grep -q "m6-via-pipe" "$OUT.via" || {
334 echo "e2e FAIL: --via transport"; cat "$OUT.via"; exit 1; 345 echo "e2e FAIL: --via transport"; cat "$OUT.via"; exit 1;
335 } 346 }
336 kill -0 "$DPID" || { echo "e2e FAIL: daemon died in --via scenario"; exit 1; } 347 kill -0 "$DPID" || { echo "e2e FAIL: daemon died in --via scenario"; exit 1; }
348 assert_converged "$OUT.via" "$SOCK" "via transport"
337 rm -f "$OUT.via" 349 rm -f "$OUT.via"
338 350
339 # --- M10: a --via command that dies before the first frame stops claiming 351 # --- M10: a --via command that dies before the first frame stops claiming
340 # a connection existed. ssh's own stderr still passes through untouched. 352 # a connection existed. ssh's own stderr still passes through untouched.
353 # No convergence: no session was ever established, so there is no grid to match.
341 set +e 354 set +e
342 "$MUX" --via "sh -c 'exit 127'" > "$OUT.via" 2>&1 355 "$MUX" --via "sh -c 'exit 127'" > "$OUT.via" 2>&1
343 VRC=$? 356 VRC=$?
@@ -355,6 +368,7 @@ echo "e2e OK: --via failure says what happened"
355 # an unkillable client. With piped stdin there is no Ctrl-\ to rescue it, so 368 # an unkillable client. With piped stdin there is no Ctrl-\ to rescue it, so
356 # a regression here hangs forever: exit 124 below is the timeout, and it is 369 # a regression here hangs forever: exit 124 below is the timeout, and it is
357 # the failure this test exists to catch. 370 # the failure this test exists to catch.
371 # No convergence: the transport died first, so no session and no grid.
358 set +e 372 set +e
359 timeout 10 "$MUX" --via "exit 7" < /dev/null > "$OUT.dead" 2>&1 373 timeout 10 "$MUX" --via "exit 7" < /dev/null > "$OUT.dead" 2>&1
360 RC=$? 374 RC=$?
@@ -376,6 +390,8 @@ rm -f "$OUT.dead"
376 # session (so reconnect is allowed), its daemon is then killed under it, and 390 # session (so reconnect is allowed), its daemon is then killed under it, and
377 # Ctrl-\ arrives while it is retrying. Exit 0 and the message; a double-close 391 # Ctrl-\ arrives while it is retrying. Exit 0 and the message; a double-close
378 # of the transport would abort here instead (SIGABRT = 134). 392 # of the transport would abort here instead (SIGABRT = 134).
393 # No convergence: the client exits while reconnecting and its daemon is dead,
394 # so there is nothing left to dump against.
379 "$MUXD" run --sock "$SOCK2" --shell /bin/sh & 395 "$MUXD" run --sock "$SOCK2" --shell /bin/sh &
380 D2PID=$! 396 D2PID=$!
381 i=0 397 i=0
@@ -414,14 +430,15 @@ set +e
414 { sleep 0.5; printf 'printf "m7-%%s\\n" before\n'; sleep 8; \ 430 { sleep 0.5; printf 'printf "m7-%%s\\n" before\n'; sleep 8; \
415 printf 'printf "m7-%%s\\n" after\n'; sleep 2.5; printf '\034'; } | \ 431 printf 'printf "m7-%%s\\n" after\n'; sleep 2.5; printf '\034'; } | \
416 XDG_RUNTIME_DIR=/nonexistent-mux-e2e timeout 40 \ 432 XDG_RUNTIME_DIR=/nonexistent-mux-e2e timeout 40 \
417 "$MUX" --via "$MUXD proxy --sock $SOCK" > "$OUT.m7" 2>&1 & 433 "$MUX" --via "$MUXD proxy --sock $SOCK" > "$OUT.m7" 2> "$OUT.m7.err" &
418 M7PID=$! 434 M7PID=$!
419 set -e 435 set -e
420 436
421 # Tear only once the session is provably live and idle: the marker is the 437 # Tear only once the session is provably live and idle: the marker is the
422 # shell's own output, so seeing it means attached, drained, mid-sleep. 438 # shell's own output, so seeing it means attached, drained, mid-sleep.
423 wait_for "$OUT.m7" "m7-before" 20 || { 439 wait_for "$OUT.m7" "m7-before" 20 || {
424 echo "e2e FAIL: m7 pre-tear output never arrived"; cat "$OUT.m7"; exit 1; 440 echo "e2e FAIL: m7 pre-tear output never arrived"
441 cat "$OUT.m7" "$OUT.m7.err" 2>/dev/null; exit 1;
425 } 442 }
426 SNAPS_BEFORE=$("$MUXD" stats --sock "$SOCK" | sed -n 's/.*snapshots=\([0-9]*\).*/\1/p') 443 SNAPS_BEFORE=$("$MUXD" stats --sock "$SOCK" | sed -n 's/.*snapshots=\([0-9]*\).*/\1/p')
427 PPID_PROXY=$(proxy_pid "$SOCK") 444 PPID_PROXY=$(proxy_pid "$SOCK")
@@ -437,12 +454,12 @@ RC=$?
437 set -e 454 set -e
438 [ "$RC" -eq 0 ] || { 455 [ "$RC" -eq 0 ] || {
439 echo "e2e FAIL: m7 client exited $RC after a transport tear (want 0)" 456 echo "e2e FAIL: m7 client exited $RC after a transport tear (want 0)"
440 cat "$OUT.m7"; exit 1; 457 cat "$OUT.m7" "$OUT.m7.err" 2>/dev/null; exit 1;
441 } 458 }
442 grep -q "m7-before" "$OUT.m7" || { echo "e2e FAIL: m7 pre-tear output missing"; exit 1; } 459 grep -q "m7-before" "$OUT.m7" || { echo "e2e FAIL: m7 pre-tear output missing"; exit 1; }
443 grep -q "m7-after" "$OUT.m7" || { 460 grep -q "m7-after" "$OUT.m7" || {
444 echo "e2e FAIL: m7 client did not resume after the transport was killed" 461 echo "e2e FAIL: m7 client did not resume after the transport was killed"
445 cat "$OUT.m7"; exit 1; 462 cat "$OUT.m7" "$OUT.m7.err" 2>/dev/null; exit 1;
446 } 463 }
447 SNAPS_AFTER=$("$MUXD" stats --sock "$SOCK" | sed -n 's/.*snapshots=\([0-9]*\).*/\1/p') 464 SNAPS_AFTER=$("$MUXD" stats --sock "$SOCK" | sed -n 's/.*snapshots=\([0-9]*\).*/\1/p')
448 [ -n "$SNAPS_BEFORE" ] && [ -n "$SNAPS_AFTER" ] || { 465 [ -n "$SNAPS_BEFORE" ] && [ -n "$SNAPS_AFTER" ] || {
@@ -452,7 +469,8 @@ SNAPS_AFTER=$("$MUXD" stats --sock "$SOCK" | sed -n 's/.*snapshots=\([0-9]*\).*/
452 echo "e2e FAIL: reconnect was served a snapshot ($SNAPS_BEFORE -> $SNAPS_AFTER), not a delta" 469 echo "e2e FAIL: reconnect was served a snapshot ($SNAPS_BEFORE -> $SNAPS_AFTER), not a delta"
453 exit 1; 470 exit 1;
454 } 471 }
455 rm -f "$OUT.m7" 472 assert_converged "$OUT.m7" "$SOCK" "delta resume"
473 rm -f "$OUT.m7" "$OUT.m7.err"
456 474
457 # --- M7 Scenario B: kill the DAEMON under an attached client and start a new 475 # --- M7 Scenario B: kill the DAEMON under an attached client and start a new
458 # one on the same path. The seq the client holds belongs to a session that no 476 # one on the same path. The seq the client holds belongs to a session that no
@@ -467,11 +485,12 @@ while [ ! -S "$SOCK3" ] && [ "$i" -lt 50 ]; do sleep 0.1; i=$((i+1)); done
467 set +e 485 set +e
468 { sleep 0.5; printf 'printf "m7b-%%s\\n" pre-restart\n'; sleep 8; \ 486 { sleep 0.5; printf 'printf "m7b-%%s\\n" pre-restart\n'; sleep 8; \
469 printf 'printf "m7b-%%s\\n" post-restart\n'; sleep 3; printf '\034'; } | \ 487 printf 'printf "m7b-%%s\\n" post-restart\n'; sleep 3; printf '\034'; } | \
470 timeout 40 "$MUX" --sock "$SOCK3" > "$OUT.m7b" 2>&1 & 488 timeout 40 "$MUX" --sock "$SOCK3" > "$OUT.m7b" 2> "$OUT.m7b.err" &
471 M7BPID=$! 489 M7BPID=$!
472 set -e 490 set -e
473 wait_for "$OUT.m7b" "m7b-pre-restart" 20 || { 491 wait_for "$OUT.m7b" "m7b-pre-restart" 20 || {
474 echo "e2e FAIL: m7b pre-restart output never arrived"; cat "$OUT.m7b"; exit 1; 492 echo "e2e FAIL: m7b pre-restart output never arrived"
493 cat "$OUT.m7b" "$OUT.m7b.err" 2>/dev/null; exit 1;
475 } 494 }
476 495
477 # SIGKILL leaves the socket file behind; the new daemon's stale-socket 496 # SIGKILL leaves the socket file behind; the new daemon's stale-socket
@@ -490,11 +509,11 @@ RC=$?
490 set -e 509 set -e
491 [ "$RC" -eq 0 ] || { 510 [ "$RC" -eq 0 ] || {
492 echo "e2e FAIL: m7b client exited $RC across a daemon restart (want 0)" 511 echo "e2e FAIL: m7b client exited $RC across a daemon restart (want 0)"
493 cat "$OUT.m7b"; exit 1; 512 cat "$OUT.m7b" "$OUT.m7b.err" 2>/dev/null; exit 1;
494 } 513 }
495 grep -q "m7b-post-restart" "$OUT.m7b" || { 514 grep -q "m7b-post-restart" "$OUT.m7b" || {
496 echo "e2e FAIL: m7b client did not resume into the restarted daemon" 515 echo "e2e FAIL: m7b client did not resume into the restarted daemon"
497 cat "$OUT.m7b"; exit 1; 516 cat "$OUT.m7b" "$OUT.m7b.err" 2>/dev/null; exit 1;
498 } 517 }
499 # The fresh session really is fresh: a new daemon means a new shell, so the 518 # The fresh session really is fresh: a new daemon means a new shell, so the
500 # pre-restart marker cannot be in its grid. If it were, we would be looking 519 # pre-restart marker cannot be in its grid. If it were, we would be looking
@@ -515,7 +534,8 @@ SNAPS_NEW=$("$MUXD" stats --sock "$SOCK3" | sed -n 's/.*snapshots=\([0-9]*\).*/\
515 echo "e2e FAIL: restarted daemon served no snapshot ($SNAPS_NEW); the stale seq was honoured" 534 echo "e2e FAIL: restarted daemon served no snapshot ($SNAPS_NEW); the stale seq was honoured"
516 exit 1; 535 exit 1;
517 } 536 }
518 rm -f "$OUT.m7b" 537 assert_converged "$OUT.m7b" "$SOCK3" "epoch resync"
538 rm -f "$OUT.m7b" "$OUT.m7b.err"
519 539
520 # --- M8: the --quic flags. Every refusal must cost nothing — no session 540 # --- M8: the --quic flags. Every refusal must cost nothing — no session
521 # socket, no shell, no stack trace — and the accepted case must leave a 541 # socket, no shell, no stack trace — and the accepted case must leave a
@@ -526,6 +546,7 @@ rm -f "$OUT.m7b"
526 546
527 # A refusal that leaves a socket behind has already started a session, which 547 # A refusal that leaves a socket behind has already started a session, which
528 # is the failure this ordering exists to prevent. 548 # is the failure this ordering exists to prevent.
549 # No convergence anywhere in refuse(): no daemon survives one, by construction.
529 # Takes the config home to run under as an explicit second argument, rather 550 # Takes the config home to run under as an explicit second argument, rather
530 # than relying on a `XDG_CONFIG_HOME=... refuse ...` prefix: POSIX says an 551 # than relying on a `XDG_CONFIG_HOME=... refuse ...` prefix: POSIX says an
531 # assignment prefixing a FUNCTION call may outlive it, so on a dash /bin/sh 552 # assignment prefixing a FUNCTION call may outlive it, so on a dash /bin/sh
@@ -645,6 +666,7 @@ grep -q "quic-flags-ok" "$OUT.q" || {
645 echo "e2e FAIL: --quic daemon's grid missing output"; exit 1; 666 echo "e2e FAIL: --quic daemon's grid missing output"; exit 1;
646 } 667 }
647 kill -0 "$D4PID" || { echo "e2e FAIL: --quic daemon died"; exit 1; } 668 kill -0 "$D4PID" || { echo "e2e FAIL: --quic daemon died"; exit 1; }
669 assert_converged "$OUT.q" "$SOCK4" "quic daemon serves sockets"
648 670
649 # --- M8 Task 3: the client speaks quic://. Same daemon, new transport. 671 # --- M8 Task 3: the client speaks quic://. Same daemon, new transport.
650 672
@@ -654,18 +676,21 @@ kill -0 "$D4PID" || { echo "e2e FAIL: --quic daemon died"; exit 1; }
654 set +e 676 set +e
655 { printf 'printf "quic-%%s\\n" attach-ok\n'; sleep 2; printf '\034'; } | \ 677 { printf 'printf "quic-%%s\\n" attach-ok\n'; sleep 2; printf '\034'; } | \
656 timeout 30 "$MUX" "quic://127.0.0.1:$QPORT" --key "$QKEY" \ 678 timeout 30 "$MUX" "quic://127.0.0.1:$QPORT" --key "$QKEY" \
657 --quic-idle-ms 15000 > "$OUT.qc" 2>&1 679 --quic-idle-ms 15000 > "$OUT.qc" 2> "$OUT.qc.err"
658 RC=$? 680 RC=$?
659 set -e 681 set -e
660 [ "$RC" -eq 0 ] || { 682 [ "$RC" -eq 0 ] || {
661 echo "e2e FAIL: quic:// client exited $RC (want 0)"; cat "$OUT.qc"; exit 1; 683 echo "e2e FAIL: quic:// client exited $RC (want 0)"
684 cat "$OUT.qc" "$OUT.qc.err" 2>/dev/null; exit 1;
662 } 685 }
663 grep -q "quic-attach-ok" "$OUT.qc" || { 686 grep -q "quic-attach-ok" "$OUT.qc" || {
664 echo "e2e FAIL: quic:// client render missing output"; cat "$OUT.qc"; exit 1; 687 echo "e2e FAIL: quic:// client render missing output"
688 cat "$OUT.qc" "$OUT.qc.err" 2>/dev/null; exit 1;
665 } 689 }
666 "$MUXD" dump --sock "$SOCK4" | grep -q "quic-attach-ok" || { 690 "$MUXD" dump --sock "$SOCK4" | grep -q "quic-attach-ok" || {
667 echo "e2e FAIL: daemon grid missing the quic:// client's output"; exit 1; 691 echo "e2e FAIL: daemon grid missing the quic:// client's output"; exit 1;
668 } 692 }
693 assert_converged "$OUT.qc" "$SOCK4" "quic attach"
669 694
670 # 1b. Reattach. A detach leaves the session running, so coming back must 695 # 1b. Reattach. A detach leaves the session running, so coming back must
671 # find it — and must be served as a fresh attach (its own snapshot), 696 # find it — and must be served as a fresh attach (its own snapshot),
@@ -675,20 +700,22 @@ SNAPS_RA=$("$MUXD" stats --sock "$SOCK4" | sed -n 's/.*snapshots=\([0-9]*\).*/\1
675 set +e 700 set +e
676 { printf 'printf "quic-%%s\\n" reattached\n'; sleep 2; printf '\034'; } | \ 701 { printf 'printf "quic-%%s\\n" reattached\n'; sleep 2; printf '\034'; } | \
677 timeout 30 "$MUX" "quic://127.0.0.1:$QPORT" --key "$QKEY" \ 702 timeout 30 "$MUX" "quic://127.0.0.1:$QPORT" --key "$QKEY" \
678 --quic-idle-ms 15000 > "$OUT.qc" 2>&1 703 --quic-idle-ms 15000 > "$OUT.qc" 2> "$OUT.qc.err"
679 RC=$? 704 RC=$?
680 set -e 705 set -e
681 [ "$RC" -eq 0 ] || { 706 [ "$RC" -eq 0 ] || {
682 echo "e2e FAIL: quic:// reattach exited $RC (want 0)"; cat "$OUT.qc"; exit 1; 707 echo "e2e FAIL: quic:// reattach exited $RC (want 0)"
708 cat "$OUT.qc" "$OUT.qc.err" 2>/dev/null; exit 1;
683 } 709 }
684 # The earlier marker is still on the grid this client was handed, which is 710 # The earlier marker is still on the grid this client was handed, which is
685 # the session having survived the detach rather than a new shell. 711 # the session having survived the detach rather than a new shell.
686 grep -q "quic-attach-ok" "$OUT.qc" || { 712 grep -q "quic-attach-ok" "$OUT.qc" || {
687 echo "e2e FAIL: quic:// reattach did not land in the existing session" 713 echo "e2e FAIL: quic:// reattach did not land in the existing session"
688 cat "$OUT.qc"; exit 1; 714 cat "$OUT.qc" "$OUT.qc.err" 2>/dev/null; exit 1;
689 } 715 }
690 grep -q "quic-reattached" "$OUT.qc" || { 716 grep -q "quic-reattached" "$OUT.qc" || {
691 echo "e2e FAIL: quic:// reattach could not run a command"; cat "$OUT.qc"; exit 1; 717 echo "e2e FAIL: quic:// reattach could not run a command"
718 cat "$OUT.qc" "$OUT.qc.err" 2>/dev/null; exit 1;
692 } 719 }
693 SNAPS_RA2=$("$MUXD" stats --sock "$SOCK4" | sed -n 's/.*snapshots=\([0-9]*\).*/\1/p') 720 SNAPS_RA2=$("$MUXD" stats --sock "$SOCK4" | sed -n 's/.*snapshots=\([0-9]*\).*/\1/p')
694 [ "$((SNAPS_RA2 - SNAPS_RA))" -eq 1 ] || { 721 [ "$((SNAPS_RA2 - SNAPS_RA))" -eq 1 ] || {
@@ -700,10 +727,12 @@ CLIENTS=$("$MUXD" stats --sock "$SOCK4" | sed -n 's/.*clients=\([0-9]*\).*/\1/p'
700 [ "$CLIENTS" = "0" ] || { 727 [ "$CLIENTS" = "0" ] || {
701 echo "e2e FAIL: $CLIENTS client slots still held after a clean detach (want 0)"; exit 1; 728 echo "e2e FAIL: $CLIENTS client slots still held after a clean detach (want 0)"; exit 1;
702 } 729 }
730 assert_converged "$OUT.qc" "$SOCK4" "quic reattach"
703 731
704 # 2. The key is checked, and a wrong one is refused loudly rather than 732 # 2. The key is checked, and a wrong one is refused loudly rather than
705 # retried forever. Nothing was ever established, so the reconnect loop 733 # retried forever. Nothing was ever established, so the reconnect loop
706 # must not engage — that is the never-established gate, over QUIC. 734 # must not engage — that is the never-established gate, over QUIC.
735 # No convergence: a refused client painted no grid.
707 head -c 32 /dev/urandom > "$QKEY.wrong" 736 head -c 32 /dev/urandom > "$QKEY.wrong"
708 chmod 600 "$QKEY.wrong" 737 chmod 600 "$QKEY.wrong"
709 set +e 738 set +e
@@ -725,6 +754,7 @@ grep -q "did not answer" "$OUT.qc" || {
725 # the handshake bound allows — and during a reconnect the terminal is in 754 # the handshake bound allows — and during a reconnect the terminal is in
726 # raw mode, where Ctrl-\ is the only way out. A deliberately unreachable 755 # raw mode, where Ctrl-\ is the only way out. A deliberately unreachable
727 # port makes the handshake run its full length if nothing interrupts it. 756 # port makes the handshake run its full length if nothing interrupts it.
757 # No convergence: the client aborts before it ever attaches, so it paints nothing.
728 QDEAD=$(( QPORT + 1 )) 758 QDEAD=$(( QPORT + 1 ))
729 # A fifo rather than a pipeline, so what is timed is the CLIENT's exit and 759 # A fifo rather than a pipeline, so what is timed is the CLIENT's exit and
730 # not how long the writer happened to hang around afterwards. 760 # not how long the writer happened to hang around afterwards.
@@ -765,12 +795,12 @@ SNAPS_BEFORE=$("$MUXD" stats --sock "$SOCK4" | sed -n 's/.*snapshots=\([0-9]*\).
765 set +e 795 set +e
766 { printf 'printf "quic-%%s\\n" pre-tear\n'; sleep 9; printf '\034'; } | \ 796 { printf 'printf "quic-%%s\\n" pre-tear\n'; sleep 9; printf '\034'; } | \
767 timeout 40 "$MUX" "quic://127.0.0.1:$QPORT" --key "$QKEY" \ 797 timeout 40 "$MUX" "quic://127.0.0.1:$QPORT" --key "$QKEY" \
768 --quic-idle-ms 1500 > "$OUT.qr" 2>&1 & 798 --quic-idle-ms 1500 > "$OUT.qr" 2> "$OUT.qr.err" &
769 QRPID=$! 799 QRPID=$!
770 set -e 800 set -e
771 wait_for "$OUT.qr" "quic-pre-tear" 20 || { 801 wait_for "$OUT.qr" "quic-pre-tear" 20 || {
772 echo "e2e FAIL: quic reconnect client never got its pre-tear marker" 802 echo "e2e FAIL: quic reconnect client never got its pre-tear marker"
773 cat "$OUT.qr"; exit 1; 803 cat "$OUT.qr" "$OUT.qr.err" 2>/dev/null; exit 1;
774 } 804 }
775 805
776 # Held well past the 1500ms idle timeout, so the client cannot mistake it 806 # Held well past the 1500ms idle timeout, so the client cannot mistake it
@@ -786,10 +816,12 @@ wait "$QRPID"
786 RC=$? 816 RC=$?
787 set -e 817 set -e
788 [ "$RC" -eq 0 ] || { 818 [ "$RC" -eq 0 ] || {
789 echo "e2e FAIL: quic client exited $RC across a tear (want 0)"; cat "$OUT.qr"; exit 1; 819 echo "e2e FAIL: quic client exited $RC across a tear (want 0)"
820 cat "$OUT.qr" "$OUT.qr.err" 2>/dev/null; exit 1;
790 } 821 }
791 grep -q "quic-pre-tear" "$OUT.qr" || { 822 grep -q "quic-pre-tear" "$OUT.qr" || {
792 echo "e2e FAIL: quic client lost its session across the tear"; cat "$OUT.qr"; exit 1; 823 echo "e2e FAIL: quic client lost its session across the tear"
824 cat "$OUT.qr" "$OUT.qr.err" 2>/dev/null; exit 1;
793 } 825 }
794 826
795 # The counter is the only witness to HOW the resume was served: a snapshot 827 # The counter is the only witness to HOW the resume was served: a snapshot
@@ -803,6 +835,7 @@ SNAPS_AFTER=$("$MUXD" stats --sock "$SOCK4" | sed -n 's/.*snapshots=\([0-9]*\).*
803 echo " the attach only; the resume itself must be delta-served)" 835 echo " the attach only; the resume itself must be delta-served)"
804 exit 1; 836 exit 1;
805 } 837 }
838 assert_converged "$OUT.qr" "$SOCK4" "quic delta resume"
806 839
807 840
808 # 5. The daemon is killed outright and started again on the SAME paths — unix 841 # 5. The daemon is killed outright and started again on the SAME paths — unix
@@ -817,12 +850,12 @@ set +e
817 { printf 'printf "quic-%%s\\n" pre-restart\n'; sleep 3; \ 850 { printf 'printf "quic-%%s\\n" pre-restart\n'; sleep 3; \
818 printf 'printf "quic-%%s\\n" post-restart\n'; sleep 3; printf '\034'; } | \ 851 printf 'printf "quic-%%s\\n" post-restart\n'; sleep 3; printf '\034'; } | \
819 timeout 40 "$MUX" "quic://127.0.0.1:$QPORT" --key "$QKEY" \ 852 timeout 40 "$MUX" "quic://127.0.0.1:$QPORT" --key "$QKEY" \
820 --quic-idle-ms 1500 > "$OUT.qk" 2>&1 & 853 --quic-idle-ms 1500 > "$OUT.qk" 2> "$OUT.qk.err" &
821 QKPID=$! 854 QKPID=$!
822 set -e 855 set -e
823 wait_for "$OUT.qk" "quic-pre-restart" 20 || { 856 wait_for "$OUT.qk" "quic-pre-restart" 20 || {
824 echo "e2e FAIL: quic restart client never got its pre-restart marker" 857 echo "e2e FAIL: quic restart client never got its pre-restart marker"
825 cat "$OUT.qk"; exit 1; 858 cat "$OUT.qk" "$OUT.qk.err" 2>/dev/null; exit 1;
826 } 859 }
827 860
828 kill -9 "$D4PID" 861 kill -9 "$D4PID"
@@ -851,11 +884,11 @@ RC=$?
851 set -e 884 set -e
852 [ "$RC" -eq 0 ] || { 885 [ "$RC" -eq 0 ] || {
853 echo "e2e FAIL: quic client exited $RC across a daemon restart (want 0)" 886 echo "e2e FAIL: quic client exited $RC across a daemon restart (want 0)"
854 cat "$OUT.qk"; exit 1; 887 cat "$OUT.qk" "$OUT.qk.err" 2>/dev/null; exit 1;
855 } 888 }
856 grep -q "quic-post-restart" "$OUT.qk" || { 889 grep -q "quic-post-restart" "$OUT.qk" || {
857 echo "e2e FAIL: quic client did not resume into the restarted daemon" 890 echo "e2e FAIL: quic client did not resume into the restarted daemon"
858 cat "$OUT.qk"; exit 1; 891 cat "$OUT.qk" "$OUT.qk.err" 2>/dev/null; exit 1;
859 } 892 }
860 # A new daemon means a new shell, so the old marker cannot be in its grid. 893 # A new daemon means a new shell, so the old marker cannot be in its grid.
861 # If it were, we would be looking at a client that resumed off a seq 894 # If it were, we would be looking at a client that resumed off a seq
@@ -874,6 +907,7 @@ SNAPS_NQ=$("$MUXD" stats --sock "$SOCK4" | sed -n 's/.*snapshots=\([0-9]*\).*/\1
874 echo "e2e FAIL: restarted daemon served no snapshot ($SNAPS_NQ); the stale seq was honoured" 907 echo "e2e FAIL: restarted daemon served no snapshot ($SNAPS_NQ); the stale seq was honoured"
875 exit 1; 908 exit 1;
876 } 909 }
910 assert_converged "$OUT.qk" "$SOCK4" "quic epoch resync"
877 911
878 kill "$D4PID" 2>/dev/null || true 912 kill "$D4PID" 2>/dev/null || true
879 D4PID="" 913 D4PID=""
@@ -894,15 +928,17 @@ while [ ! -S "$SOCK9" ] && [ "$i" -lt 50 ]; do sleep 0.1; i=$((i+1)); done
894 set +e 928 set +e
895 { printf 'printf "envkey-%%s\\n" ok\n'; sleep 2; printf '\034'; } | \ 929 { printf 'printf "envkey-%%s\\n" ok\n'; sleep 2; printf '\034'; } | \
896 timeout 30 "$MUX" "quic://127.0.0.1:$QPORT2" --key "$QKEY" \ 930 timeout 30 "$MUX" "quic://127.0.0.1:$QPORT2" --key "$QKEY" \
897 --quic-idle-ms 15000 > "$OUT.env1" 2>&1 931 --quic-idle-ms 15000 > "$OUT.env1" 2> "$OUT.env1.err"
898 RC=$? 932 RC=$?
899 set -e 933 set -e
900 [ "$RC" -eq 0 ] || { 934 [ "$RC" -eq 0 ] || {
901 echo "e2e FAIL: quic attach to a MUX_KEY_FILE daemon exited $RC (want 0)" 935 echo "e2e FAIL: quic attach to a MUX_KEY_FILE daemon exited $RC (want 0)"
902 cat "$OUT.env1"; exit 1; 936 cat "$OUT.env1" "$OUT.env1.err" 2>/dev/null; exit 1;
903 } 937 }
904 grep -q "envkey-ok" "$OUT.env1" || { 938 grep -q "envkey-ok" "$OUT.env1" || {
905 echo "e2e FAIL: MUX_KEY_FILE daemon served no session"; cat "$OUT.env1"; exit 1; } 939 echo "e2e FAIL: MUX_KEY_FILE daemon served no session"
940 cat "$OUT.env1" "$OUT.env1.err" 2>/dev/null; exit 1; }
941 assert_converged "$OUT.env1" "$SOCK9" "env key"
906 kill "$D9PID" 2>/dev/null || true 942 kill "$D9PID" 2>/dev/null || true
907 D9PID="" 943 D9PID=""
908 944
@@ -922,20 +958,23 @@ while [ ! -S "$SOCK10" ] && [ "$i" -lt 50 ]; do sleep 0.1; i=$((i+1)); done
922 set +e 958 set +e
923 { printf 'printf "flagwins-%%s\\n" ok\n'; sleep 2; printf '\034'; } | \ 959 { printf 'printf "flagwins-%%s\\n" ok\n'; sleep 2; printf '\034'; } | \
924 timeout 30 "$MUX" "quic://127.0.0.1:$QPORT3" --key "$QKEY" \ 960 timeout 30 "$MUX" "quic://127.0.0.1:$QPORT3" --key "$QKEY" \
925 --quic-idle-ms 15000 > "$OUT.env2" 2>&1 961 --quic-idle-ms 15000 > "$OUT.env2" 2> "$OUT.env2.err"
926 RC=$? 962 RC=$?
927 set -e 963 set -e
928 [ "$RC" -eq 0 ] || { 964 [ "$RC" -eq 0 ] || {
929 echo "e2e FAIL: --key did not beat MUX_KEY_FILE: attach exited $RC (want 0)" 965 echo "e2e FAIL: --key did not beat MUX_KEY_FILE: attach exited $RC (want 0)"
930 cat "$OUT.env2"; exit 1; 966 cat "$OUT.env2" "$OUT.env2.err" 2>/dev/null; exit 1;
931 } 967 }
932 grep -q "flagwins-ok" "$OUT.env2" || { 968 grep -q "flagwins-ok" "$OUT.env2" || {
933 echo "e2e FAIL: --key-beats-env daemon served no session"; cat "$OUT.env2"; exit 1; } 969 echo "e2e FAIL: --key-beats-env daemon served no session"
970 cat "$OUT.env2" "$OUT.env2.err" 2>/dev/null; exit 1; }
971 assert_converged "$OUT.env2" "$SOCK10" "flag beats env"
934 kill "$D10PID" 2>/dev/null || true 972 kill "$D10PID" 2>/dev/null || true
935 D10PID="" 973 D10PID=""
936 echo "e2e OK: daemon honours MUX_KEY_FILE, and --key beats it" 974 echo "e2e OK: daemon honours MUX_KEY_FILE, and --key beats it"
937 975
938 rm -f "$OUT.q" "$OUT.qc" "$OUT.qr" "$OUT.qa" "$OUT.qk" "$QKEY" "$QKEY.bad" "$QKEY.wrong" 976 rm -f "$OUT.q" "$OUT.qc" "$OUT.qr" "$OUT.qa" "$OUT.qk" "$QKEY" "$QKEY.bad" "$QKEY.wrong" \
977 "$OUT.qc.err" "$OUT.qr.err" "$OUT.qk.err"
939 978
940 # --- M10: muxd start — detached spawn, no-op rerun, race, pinned lines. 979 # --- M10: muxd start — detached spawn, no-op rerun, race, pinned lines.
941 # 980 #
@@ -962,9 +1001,11 @@ kill -0 "$SPID" || { echo "e2e FAIL: started daemon not alive"; exit 1; }
962 1001
963 # The daemon it started serves a session (marker in, marker in dump). 1002 # The daemon it started serves a session (marker in, marker in dump).
964 { printf 'printf "start-%%s\\n" works\n'; sleep 2; printf '\034'; } | \ 1003 { printf 'printf "start-%%s\\n" works\n'; sleep 2; printf '\034'; } | \
965 "$MUX" --sock "$SOCK8" > "$OUT.s8" 2>&1 1004 "$MUX" --sock "$SOCK8" > "$OUT.s8" 2> "$OUT.s8.err"
966 "$MUXD" dump --sock "$SOCK8" | grep -q "start-works" || { 1005 "$MUXD" dump --sock "$SOCK8" | grep -q "start-works" || {
967 echo "e2e FAIL: auto-started daemon lost the marker"; cat "$OUT.s8"; exit 1; } 1006 echo "e2e FAIL: auto-started daemon lost the marker"
1007 cat "$OUT.s8" "$OUT.s8.err" 2>/dev/null; exit 1; }
1008 assert_converged "$OUT.s8" "$SOCK8" "started daemon"
968 1009
969 # The log the failure path names really is there, and is no more readable 1010 # The log the failure path names really is there, and is no more readable
970 # than the key is: it carries whatever the daemon says about its own start. 1011 # than the key is: it carries whatever the daemon says about its own start.
@@ -1007,7 +1048,7 @@ set -e
1007 { [ "$RCA" = "0" ] && [ "$RCB" = "0" ]; } || { 1048 { [ "$RCA" = "0" ] && [ "$RCB" = "0" ]; } || {
1008 echo "e2e FAIL: race: exits $RCA/$RCB"; cat "$OUT.ra" "$OUT.rb"; exit 1; } 1049 echo "e2e FAIL: race: exits $RCA/$RCB"; cat "$OUT.ra" "$OUT.rb"; exit 1; }
1009 { printf 'printf "race-%%s\\n" one\n'; sleep 2; printf '\034'; } | \ 1050 { printf 'printf "race-%%s\\n" one\n'; sleep 2; printf '\034'; } | \
1010 "$MUX" --sock "$SOCK8" > /dev/null 2>&1 1051 "$MUX" --sock "$SOCK8" > "$OUT.race" 2> /dev/null
1011 "$MUXD" dump --sock "$SOCK8" | grep -q "race-one" || { 1052 "$MUXD" dump --sock "$SOCK8" | grep -q "race-one" || {
1012 echo "e2e FAIL: race: session unusable"; exit 1; } 1053 echo "e2e FAIL: race: session unusable"; exit 1; }
1013 SPID=$(cat "$OUT.ra" "$OUT.rb" | sed -n 's/.* pid=\([0-9]*\).*/\1/p' | while read -r p; do 1054 SPID=$(cat "$OUT.ra" "$OUT.rb" | sed -n 's/.* pid=\([0-9]*\).*/\1/p' | while read -r p; do
@@ -1030,6 +1071,9 @@ grep -q "already running" "$OUT.rb" || {
1030 # ...and the session the winner was serving is undisturbed. 1071 # ...and the session the winner was serving is undisturbed.
1031 "$MUXD" dump --sock "$SOCK8" | grep -q "race-one" || { 1072 "$MUXD" dump --sock "$SOCK8" | grep -q "race-one" || {
1032 echo "e2e FAIL: the refused daemon disturbed the live session"; exit 1; } 1073 echo "e2e FAIL: the refused daemon disturbed the live session"; exit 1; }
1074 # The race client's own capture, checked here rather than above: the refused
1075 # `muxd run` between the two touches no session, so the grid has not moved.
1076 assert_converged "$OUT.race" "$SOCK8" "start race"
1033 1077
1034 kill "$SPID" 2>/dev/null || true 1078 kill "$SPID" 2>/dev/null || true
1035 SPID="" 1079 SPID=""
@@ -1041,6 +1085,7 @@ echo "e2e OK: muxd start — spawn, no-op rerun, log truncation, race"
1041 # child exits on the missing key, the poll loop reaps it, and polling 1085 # child exits on the missing key, the poll loop reaps it, and polling
1042 # continues to the deadline; a second waitpid there gets ECHILD, which the 1086 # continues to the deadline; a second waitpid there gets ECHILD, which the
1043 # stdlib answers with `unreachable`, i.e. exit 134 and a stack trace. 1087 # stdlib answers with `unreachable`, i.e. exit 134 and a stack trace.
1088 # No convergence: the daemon under test is the one that died, so nothing to dump.
1044 DEADCFG="${TMPDIR:-/tmp}/mux-e2e-deadchild-$$" 1089 DEADCFG="${TMPDIR:-/tmp}/mux-e2e-deadchild-$$"
1045 set +e 1090 set +e
1046 env XDG_CONFIG_HOME="$DEADCFG" timeout 30 "$MUXD" start --sock "$SOCK8T" \ 1091 env XDG_CONFIG_HOME="$DEADCFG" timeout 30 "$MUXD" start --sock "$SOCK8T" \
@@ -1075,10 +1120,11 @@ echo "e2e OK: a start whose daemon dies young says so, with the log path"
1075 GPID=$(sed -n 's/.* pid=\([0-9]*\).*/\1/p' "$OUT.goal") 1120 GPID=$(sed -n 's/.* pid=\([0-9]*\).*/\1/p' "$OUT.goal")
1076 [ -n "$GPID" ] || { echo "e2e FAIL: goal start reported no pid"; cat "$OUT.goal"; exit 1; } 1121 [ -n "$GPID" ] || { echo "e2e FAIL: goal start reported no pid"; cat "$OUT.goal"; exit 1; }
1077 { printf 'printf "goal-%%s\\n" quic\n'; sleep 2; printf '\034'; } | \ 1122 { printf 'printf "goal-%%s\\n" quic\n'; sleep 2; printf '\034'; } | \
1078 timeout 30 "$MUX" "quic://127.0.0.1:$QPORT4" > "$OUT.g9" 2>&1 1123 timeout 30 "$MUX" "quic://127.0.0.1:$QPORT4" > "$OUT.g9" 2> "$OUT.g9.err"
1079 "$MUXD" dump --sock "$SOCK11" | grep -q "goal-quic" || { 1124 "$MUXD" dump --sock "$SOCK11" | grep -q "goal-quic" || {
1080 echo "e2e FAIL: no-key-flag QUIC attach did not reach the session" 1125 echo "e2e FAIL: no-key-flag QUIC attach did not reach the session"
1081 cat "$OUT.g9"; exit 1; } 1126 cat "$OUT.g9" "$OUT.g9.err" 2>/dev/null; exit 1; }
1127 assert_converged "$OUT.g9" "$SOCK11" "goal commands"
1082 kill "$GPID" 2>/dev/null || true 1128 kill "$GPID" 2>/dev/null || true
1083 GPID="" 1129 GPID=""
1084 echo "e2e OK: keygen + start --quic + mux quic:// with no --key anywhere" 1130 echo "e2e OK: keygen + start --quic + mux quic:// with no --key anywhere"