a73x

c3830637

test: e2e gates the switch by what moved and what did not

a73x   2026-08-19 17:17

Commit message
test: e2e gates the switch by what moved and what did not

A chord that did nothing at all would still produce a client that stays
alive and a log with both markers in it, so the leg alone cannot tell a
switch from a no-op. The two muxa probes are what separate them: session
"1" has to exist, and the default session's grid has to hold the first
marker and not the second.

The re-attach is waited for by the client's second alternate-screen entry
rather than a sleep — a switch tears the terminal down and builds it back
up, so that byte sequence IS the new session arriving.

test/e2e.sh
Old New
@@ -252,6 +252,12 @@ D23PID=""
252 SOCK27="${TMPDIR:-/tmp}/muxd-e2e-prefix-$$.sock" 252 SOCK27="${TMPDIR:-/tmp}/muxd-e2e-prefix-$$.sock"
253 D24PID="" 253 D24PID=""
254 254
255 # Ctrl-\ c (create a session and switch to it): its own daemon, because the
256 # assertion is about which sessions EXIST — another block's session on the
257 # same daemon would change the name the chord picks.
258 SOCK28="${TMPDIR:-/tmp}/muxd-e2e-newsess-$$.sock"
259 D25PID=""
260
255 # One counter out of a MUX_PREDICT_STATS line. The client prints exactly one 261 # One counter out of a MUX_PREDICT_STATS line. The client prints exactly one
256 # such line on exit; every field is a key=value pair, so a rename or reorder 262 # such line on exit; every field is a key=value pair, so a rename or reorder
257 # in the client shows up here as an empty read rather than a wrong number. 263 # in the client shows up here as an empty read rather than a wrong number.
@@ -773,6 +779,7 @@ cleanup() {
773 [ -n "$D22PID" ] && kill "$D22PID" 2>/dev/null || true 779 [ -n "$D22PID" ] && kill "$D22PID" 2>/dev/null || true
774 [ -n "$D23PID" ] && kill "$D23PID" 2>/dev/null || true 780 [ -n "$D23PID" ] && kill "$D23PID" 2>/dev/null || true
775 [ -n "$D24PID" ] && kill "$D24PID" 2>/dev/null || true 781 [ -n "$D24PID" ] && kill "$D24PID" 2>/dev/null || true
782 [ -n "$D25PID" ] && kill "$D25PID" 2>/dev/null || true
776 # The stops still precede the socket rm below, like SOCK14-17 above: 783 # The stops still precede the socket rm below, like SOCK14-17 above:
777 # unlinking a socket first would leave a live daemon nothing could reach 784 # unlinking a socket first would leave a live daemon nothing could reach
778 # by path. 785 # by path.
@@ -782,6 +789,7 @@ cleanup() {
782 [ -S "$SOCK25" ] && "$MUXD" stop --sock "$SOCK25" 2>/dev/null || true 789 [ -S "$SOCK25" ] && "$MUXD" stop --sock "$SOCK25" 2>/dev/null || true
783 [ -S "$SOCK26" ] && "$MUXD" stop --sock "$SOCK26" 2>/dev/null || true 790 [ -S "$SOCK26" ] && "$MUXD" stop --sock "$SOCK26" 2>/dev/null || true
784 [ -S "$SOCK27" ] && "$MUXD" stop --sock "$SOCK27" 2>/dev/null || true 791 [ -S "$SOCK27" ] && "$MUXD" stop --sock "$SOCK27" 2>/dev/null || true
792 [ -S "$SOCK28" ] && "$MUXD" stop --sock "$SOCK28" 2>/dev/null || true
785 793
786 # ---- the leak sweep (hygiene kit, 6a) ---- 794 # ---- the leak sweep (hygiene kit, 6a) ----
787 # Here rather than at the bottom of the file, which `set -e` reaches only 795 # Here rather than at the bottom of the file, which `set -e` reaches only
@@ -793,7 +801,7 @@ cleanup() {
793 "$D7PID" "$D9PID" "$D10PID" "$D12PID" "$D13PID" "$SPID" "$TPID" \ 801 "$D7PID" "$D9PID" "$D10PID" "$D12PID" "$D13PID" "$SPID" "$TPID" \
794 "$GPID" "$APID" "$PAPID" "$HAPID" "$HDPID" \ 802 "$GPID" "$APID" "$PAPID" "$HAPID" "$HDPID" \
795 "$D14PID" "$D15PID" "$D16PID" "$D17PID" "$D18PID" "$D19PID" \ 803 "$D14PID" "$D15PID" "$D16PID" "$D17PID" "$D18PID" "$D19PID" \
796 "$D20PID" "$D21PID" "$D22PID" "$D23PID" "$D24PID" 804 "$D20PID" "$D21PID" "$D22PID" "$D23PID" "$D24PID" "$D25PID"
797 _leak=0 805 _leak=0
798 leak_sweep "$_rc" || _leak=1 806 leak_sweep "$_rc" || _leak=1
799 807
@@ -4466,6 +4474,114 @@ assert_stopped "$SOCK27" "$D24PID" "prefix" "$OUT.pfxstop"
4466 D24PID="" 4474 D24PID=""
4467 ok "Ctrl-\\ prefix: an unknown chord is swallowed, Ctrl-\\ d detaches" 4475 ok "Ctrl-\\ prefix: an unknown chord is swallowed, Ctrl-\\ d detaches"
4468 4476
4477 # ---- M2: Ctrl-\ c creates a session and switches to it -------------------
4478 #
4479 # The claim is a MOVE, which takes two witnesses: the client ends up
4480 # somewhere new, and the place it left is still there and unchanged.
4481 #
4482 # * one pty client attaches to the default session, runs a marker, types
4483 # `\x1cc`, and runs a second marker. Both markers reach a shell — the
4484 # second one proves the client is attached to a session that works, not
4485 # merely alive after the chord.
4486 # * `muxa status --session 1` proves the daemon really created the name
4487 # the client picked (lowest free integer beside the default "0").
4488 # * the default session's capture still holds the FIRST marker and not the
4489 # second. Without that pair the same log would be produced by a chord
4490 # that did nothing at all.
4491 #
4492 # The leg then keeps pressing until the daemon runs out of sessions, because
4493 # the refusal is the branch with the interesting answer: `.refused` is the
4494 # one attach failure the client recovers from instead of exiting, and
4495 # nothing else in the suite reaches it. See the block after the leg for the
4496 # three things "recovered" has to mean.
4497 #
4498 # The re-attach is waited for by its alternate-screen entry rather than a
4499 # fixed sleep: the switch tears the terminal down and builds it back up, so
4500 # the second `?1049h` is the client saying it has a new session on screen.
4501 "$MUXD" run --sock "$SOCK28" --shell /bin/sh > "$OUT.nsw.d" 2>&1 &
4502 D25PID=$!
4503 wait_sock "$SOCK28" "$OUT.nsw.d" "new-session daemon never bound"
4504
4505 set +e
4506 timeout 40 "$PTYCLIENT" --cols 80 --rows 24 --out "$OUT.nsw" --err "$OUT.nsw.err" \
4507 -- "$MUX" --sock "$SOCK28" > "$OUT.nsw.log" 2>&1 <<'EOF'
4508 expect \x1b[?1049h 15000
4509 settle 400 15000
4510 send printf 'm2a-%s\\n' pin\n
4511 expect m2a-pin 15000
4512 settle 400 15000
4513 send \x1cc
4514 expect \x1b[?1049h 15000
4515 settle 400 15000
4516 send printf 'm2b-%s\\n' pin\n
4517 expect m2b-pin 15000
4518 settle 400 15000
4519 send \x1cc
4520 expect \x1b[?1049h 15000
4521 settle 400 15000
4522 send \x1cc
4523 expect \x1b[?1049h 15000
4524 settle 400 15000
4525 send printf 'm2c-%s\\n' pin\n
4526 expect m2c-pin 15000
4527 settle 400 15000
4528 send \x1cc
4529 expect \x1b[?1049h 15000
4530 settle 400 15000
4531 send printf 'm2d-%s\\n' pin\n
4532 expect m2d-pin 15000
4533 settle 400 15000
4534 send \x1c\x1c
4535 waitexit 10000
4536 EOF
4537 RC=$?
4538 set -e
4539 [ "$RC" -eq 0 ] || {
4540 echo "e2e FAIL: new session: ptyclient leg exited $RC (did \\x1cc switch?):"
4541 cat "$OUT.nsw.log"; exit 1; }
4542 "$MUXA" status --sock "$SOCK28" --session 1 > "$OUT.nswst" 2>&1 || {
4543 echo "e2e FAIL: new session: the chord did not create session 1:"
4544 cat "$OUT.nswst"; exit 1; }
4545 # The session the client LEFT: still live, still holding what it was shown
4546 # before the chord, and innocent of everything typed after it.
4547 "$MUXA" capture --sock "$SOCK28" > "$OUT.nswcap" 2>&1
4548 grep -q "m2a-pin" "$OUT.nswcap" || {
4549 echo "e2e FAIL: new session: the default session lost its own marker:"
4550 cat "$OUT.nswcap"; exit 1; }
4551 grep -q "m2b-pin" "$OUT.nswcap" && {
4552 echo "e2e FAIL: new session: the second marker ran in the OLD session"
4553 cat "$OUT.nswcap"; exit 1; }
4554 # The fourth chord had nowhere to go: max_sessions is 4 and the leg filled
4555 # the table (0, 1, 2, 3), so the daemon answered the attach with an
4556 # exit_status before a single frame of state. That refusal is NOT fatal —
4557 # the switch's arrival knows the name it left — and the three claims below
4558 # are what "fell back" means, none of which the log shows on its own.
4559 #
4560 # * the client said so, once, in its own words. Its stderr is ptyclient's
4561 # --err file, not the pty, so the message is read there.
4562 grep -q "cannot create a new session" "$OUT.nsw.err" || {
4563 echo "e2e FAIL: new session: the refused switch said nothing:"
4564 cat "$OUT.nsw.err"; exit 1; }
4565 # * it is back in session 3, the one it typed the chord in — proved
4566 # POSITIVELY, by a marker run AFTER the refusal landing in that
4567 # session's grid beside the one from before it. A capture that held
4568 # only m2c-pin would equally describe a client that died there.
4569 "$MUXA" capture --sock "$SOCK28" --session 3 > "$OUT.nswcap3" 2>&1
4570 grep -q "m2c-pin" "$OUT.nswcap3" || {
4571 echo "e2e FAIL: new session: session 3 lost the marker from before the refusal:"
4572 cat "$OUT.nswcap3"; exit 1; }
4573 grep -q "m2d-pin" "$OUT.nswcap3" || {
4574 echo "e2e FAIL: new session: the refused switch did not come back to session 3:"
4575 cat "$OUT.nswcap3"; exit 1; }
4576 # * and the name it was reaching for was never created. Without this the
4577 # leg would pass on a daemon that quietly grew a fifth slot.
4578 "$MUXA" status --sock "$SOCK28" --session 4 > "$OUT.nswst4" 2>&1 && {
4579 echo "e2e FAIL: new session: the daemon created a fifth session:"
4580 cat "$OUT.nswst4"; exit 1; }
4581 assert_stopped "$SOCK28" "$D25PID" "new session" "$OUT.nswstop"
4582 D25PID=""
4583 ok "Ctrl-\\ c: a new session is created and switched to, the old one intact; a full daemon refuses and the client stays where it was"
4584
4469 # The long-lived daemon has served every scenario that wanted it; stop it 4585 # The long-lived daemon has served every scenario that wanted it; stop it
4470 # NOW so its allocator verdict is written while the suite is still running 4586 # NOW so its allocator verdict is written while the suite is still running
4471 # and can say so. SIGTERM runs the clean-shutdown path, so the defer chain 4587 # and can say so. SIGTERM runs the clean-shutdown path, so the defer chain
@@ -4481,7 +4597,7 @@ DPID=""
4481 4597
4482 # The pins. Literals, not variables set from counting something else — 4598 # The pins. Literals, not variables set from counting something else —
4483 # "assert the literal, never the constant the code under test reads" 4599 # "assert the literal, never the constant the code under test reads"
4484 # (decisions.md, M10). 33 scenario checkpoints; 35 convergence points. 4600 # (decisions.md, M10). 35 scenario checkpoints; 35 convergence points.
4485 # Anyone adding a scenario updates these by hand, on purpose. 4601 # Anyone adding a scenario updates these by hand, on purpose.
4486 # 4602 #
4487 # M18 added three checkpoints and no convergence points: its wall block 4603 # M18 added three checkpoints and no convergence points: its wall block
@@ -4507,9 +4623,12 @@ DPID=""
4507 # — bytes a wall painted to a terminal — which no grid dump can witness. 4623 # — bytes a wall painted to a terminal — which no grid dump can witness.
4508 # The 34th is the Ctrl-\ prefix, and no convergence point for that same 4624 # The 34th is the Ctrl-\ prefix, and no convergence point for that same
4509 # reason: its subject is what the client REFUSED to forward, which a grid 4625 # reason: its subject is what the client REFUSED to forward, which a grid
4510 # that never saw the bytes cannot testify to either way. 4626 # that never saw the bytes cannot testify to either way. The 35th is
4511 [ "$OK_COUNT" = "34" ] || { 4627 # Ctrl-\ c, and no convergence point either: what it compares is TWO
4512 echo "e2e FAIL: $OK_COUNT scenario checkpoints ran, the pin says 34 —" 4628 # sessions' grids against each other — one holding a marker, one refusing
4629 # to — and assert_converged speaks about one session at a time.
4630 [ "$OK_COUNT" = "35" ] || {
4631 echo "e2e FAIL: $OK_COUNT scenario checkpoints ran, the pin says 35 —"
4513 echo " a scenario was added (update the pin) or silently lost" 4632 echo " a scenario was added (update the pin) or silently lost"
4514 exit 1 4633 exit 1
4515 } 4634 }
@@ -4517,4 +4636,4 @@ DPID=""
4517 echo "e2e FAIL: $CONV_COUNT convergence points ran, the pin says 35" 4636 echo "e2e FAIL: $CONV_COUNT convergence points ran, the pin says 35"
4518 exit 1 4637 exit 1
4519 } 4638 }
4520 echo "e2e OK (34 scenarios, 35 convergence points)" 4639 echo "e2e OK (35 scenarios, 35 convergence points)"