a73x

d6523df1

test: e2e walls two sessions from inside one, and comes back

a73x   2026-08-19 17:17

Commit message
test: e2e walls two sessions from inside one, and comes back

Scenario 37. The money assertion is a stripe per session in ONE client's
capture, asserted on the label bars rather than the markers: a label names
the spelling the chord built out of the target this client was dialling,
and nothing an attached session paints looks like one — so unlike a marker
grep it cannot be satisfied by the paints from before the wall.

The return is witnessed by m4b-pin arriving again after a fresh
`\x1b[?1049h`: a new alternate screen is a new attach, and the marker in it
says the parent came back to the session it left rather than to the default
or to a new one.

The expects inside the wall ride an injection, for scenario 33's reason and
learned here the hard way: the tile threads race, and matching the later
marker first buries the earlier one behind the stream cursor, so a wall
that painted perfectly timed out.

test/e2e.sh
Old New
@@ -265,6 +265,13 @@ D25PID=""
265 SOCK29="${TMPDIR:-/tmp}/muxd-e2e-ring-$$.sock" 265 SOCK29="${TMPDIR:-/tmp}/muxd-e2e-ring-$$.sock"
266 D26PID="" 266 D26PID=""
267 267
268 # Ctrl-\ w (the wall from inside a session): its own daemon for the SOCK29
269 # reason exactly — the wall shows every session this daemon has, so a stray
270 # session from another block would appear as an extra stripe and could push
271 # the two this block asserts on off a 30-row terminal.
272 SOCK30="${TMPDIR:-/tmp}/muxd-e2e-wallchord-$$.sock"
273 D27PID=""
274
268 # One counter out of a MUX_PREDICT_STATS line. The client prints exactly one 275 # One counter out of a MUX_PREDICT_STATS line. The client prints exactly one
269 # such line on exit; every field is a key=value pair, so a rename or reorder 276 # such line on exit; every field is a key=value pair, so a rename or reorder
270 # in the client shows up here as an empty read rather than a wrong number. 277 # in the client shows up here as an empty read rather than a wrong number.
@@ -788,6 +795,7 @@ cleanup() {
788 [ -n "$D24PID" ] && kill "$D24PID" 2>/dev/null || true 795 [ -n "$D24PID" ] && kill "$D24PID" 2>/dev/null || true
789 [ -n "$D25PID" ] && kill "$D25PID" 2>/dev/null || true 796 [ -n "$D25PID" ] && kill "$D25PID" 2>/dev/null || true
790 [ -n "$D26PID" ] && kill "$D26PID" 2>/dev/null || true 797 [ -n "$D26PID" ] && kill "$D26PID" 2>/dev/null || true
798 [ -n "$D27PID" ] && kill "$D27PID" 2>/dev/null || true
791 # The stops still precede the socket rm below, like SOCK14-17 above: 799 # The stops still precede the socket rm below, like SOCK14-17 above:
792 # unlinking a socket first would leave a live daemon nothing could reach 800 # unlinking a socket first would leave a live daemon nothing could reach
793 # by path. 801 # by path.
@@ -799,6 +807,7 @@ cleanup() {
799 [ -S "$SOCK27" ] && "$MUXD" stop --sock "$SOCK27" 2>/dev/null || true 807 [ -S "$SOCK27" ] && "$MUXD" stop --sock "$SOCK27" 2>/dev/null || true
800 [ -S "$SOCK28" ] && "$MUXD" stop --sock "$SOCK28" 2>/dev/null || true 808 [ -S "$SOCK28" ] && "$MUXD" stop --sock "$SOCK28" 2>/dev/null || true
801 [ -S "$SOCK29" ] && "$MUXD" stop --sock "$SOCK29" 2>/dev/null || true 809 [ -S "$SOCK29" ] && "$MUXD" stop --sock "$SOCK29" 2>/dev/null || true
810 [ -S "$SOCK30" ] && "$MUXD" stop --sock "$SOCK30" 2>/dev/null || true
802 811
803 # ---- the leak sweep (hygiene kit, 6a) ---- 812 # ---- the leak sweep (hygiene kit, 6a) ----
804 # Here rather than at the bottom of the file, which `set -e` reaches only 813 # Here rather than at the bottom of the file, which `set -e` reaches only
@@ -811,7 +820,7 @@ cleanup() {
811 "$GPID" "$APID" "$PAPID" "$HAPID" "$HDPID" \ 820 "$GPID" "$APID" "$PAPID" "$HAPID" "$HDPID" \
812 "$D14PID" "$D15PID" "$D16PID" "$D17PID" "$D18PID" "$D19PID" \ 821 "$D14PID" "$D15PID" "$D16PID" "$D17PID" "$D18PID" "$D19PID" \
813 "$D20PID" "$D21PID" "$D22PID" "$D23PID" "$D24PID" "$D25PID" \ 822 "$D20PID" "$D21PID" "$D22PID" "$D23PID" "$D24PID" "$D25PID" \
814 "$D26PID" 823 "$D26PID" "$D27PID"
815 _leak=0 824 _leak=0
816 leak_sweep "$_rc" || _leak=1 825 leak_sweep "$_rc" || _leak=1
817 826
@@ -4672,6 +4681,109 @@ assert_stopped "$SOCK29" "$D26PID" "session ring" "$OUT.ringstop"
4672 D26PID="" 4681 D26PID=""
4673 ok "Ctrl-\\ n / Ctrl-\\ p: the ring steps both ways and wraps at both ends" 4682 ok "Ctrl-\\ n / Ctrl-\\ p: the ring steps both ways and wraps at both ends"
4674 4683
4684 # ---- M4: Ctrl-\ w shows the wall, and `q` gives the session back --------
4685 #
4686 # Scenario 33 proved `mux wall` paints several sessions on one terminal.
4687 # This is that wall reached from INSIDE a session, which is a different
4688 # claim in two places, and the leg is built around both.
4689 #
4690 # The money assertion is that ONE client's capture holds BOTH markers at
4691 # once: m4a-pin is the default session's, m4b-pin belongs to the session
4692 # `\x1cc` created, and no single attached session can ever paint the two
4693 # together. Only the wall can, so seeing both is seeing the wall — the
4694 # chord built the spellings out of the target this client was dialling
4695 # (`--sock PATH#0`, `--sock PATH#1`) and the child painted them.
4696 #
4697 # The second claim is the return: `q` leaves the wall and the parent
4698 # re-attaches to the session it was in, NOT to the default and not to a
4699 # new one. Its witness is m4b-pin arriving again after the next
4700 # `\x1b[?1049h` — a fresh alternate screen, so a fresh attach, and the
4701 # marker in it says session 1. The re-attach is what repaints the terminal
4702 # the wall child painted over; an expect that times out here is a session
4703 # that came back blank.
4704 #
4705 # The markers are shell-EXPANDED, the M18 trick, so a hit is the shell's
4706 # work and never an echo of what this script typed.
4707 #
4708 # The expect ORDER inside the wall leans on an injection, deliberately and
4709 # for scenario 33's reason, learned here the hard way: the tile threads
4710 # race, so session 0's stripe may paint after session 1's, and `expect` is
4711 # a stream cursor — matching the LATER marker first buries the earlier one
4712 # behind the cursor for good, and the leg times out on a wall that painted
4713 # perfectly. The `m4c-pin` injected at t+5s repaints session 0's whole
4714 # stripe long after both snapshots have landed, and that repaint carries
4715 # m4a-pin with it. So `m4b-pin` then `m4a-pin` then `m4c-pin` is ordered
4716 # under BOTH interleavings: whichever snapshot lost the race, the marker
4717 # it was carrying is still ahead of the cursor in the injected repaint.
4718 "$MUXD" run --sock "$SOCK30" --shell /bin/sh > "$OUT.wc.d" 2>&1 &
4719 D27PID=$!
4720 wait_sock "$SOCK30" "$OUT.wc.d" "wall-chord daemon never bound"
4721
4722 ( sleep 5; "$MUXA" send 'printf "m4c-%s\n" pin\n' \
4723 --sock "$SOCK30" --session 0 > "$OUT.wcinj" 2>&1 ) &
4724 WCINJPID=$!
4725 set +e
4726 timeout 60 "$PTYCLIENT" --cols 100 --rows 30 --out "$OUT.wc" --err "$OUT.wc.err" \
4727 -- "$MUX" --sock "$SOCK30" > "$OUT.wc.log" 2>&1 <<'EOF'
4728 expect \x1b[?1049h 15000
4729 settle 400 15000
4730 send printf 'm4a-%s\\n' pin\n
4731 expect m4a-pin 15000
4732 settle 400 15000
4733 send \x1cc
4734 expect \x1b[?1049h 15000
4735 settle 400 15000
4736 send printf 'm4b-%s\\n' pin\n
4737 expect m4b-pin 15000
4738 settle 400 15000
4739 send \x1cw
4740 expect m4b-pin 20000
4741 expect m4a-pin 20000
4742 expect m4c-pin 20000
4743 settle 500 15000
4744 send q
4745 expect \x1b[?1049h 15000
4746 expect m4b-pin 15000
4747 settle 400 15000
4748 send \x1c\x1c
4749 waitexit 10000
4750 EOF
4751 RC=$?
4752 set -e
4753 wait "$WCINJPID" 2>/dev/null || true
4754 [ "$RC" -eq 0 ] || {
4755 echo "e2e FAIL: wall chord: ptyclient leg exited $RC (did \\x1cw wall, did q return?):"
4756 cat "$OUT.wc.log"; cat "$OUT.wcinj"; exit 1; }
4757 # The two stripes, asserted on bytes only a wall can produce: a label bar
4758 # per tile, naming the spelling the CHORD built out of the target this
4759 # client was dialling. Nothing an attached session paints looks like this,
4760 # so unlike a bare marker grep these cannot be satisfied by the paints from
4761 # before the wall.
4762 grep -q -- "--sock $SOCK30#0 \[up\]" "$OUT.wc" || {
4763 echo "e2e FAIL: wall chord: no stripe for session 0"; exit 1; }
4764 grep -q -- "--sock $SOCK30#1 \[up\]" "$OUT.wc" || {
4765 echo "e2e FAIL: wall chord: no stripe for session 1"; exit 1; }
4766 # The detour left both sessions running: a wall is something the CLIENT
4767 # does, so neither shell may have been taken down by it, and each still
4768 # holds its own marker.
4769 for s in 0:m4a 1:m4b; do
4770 _sess="${s%%:*}"; _mark="${s##*:}-pin"
4771 timeout 20 "$MUXA" capture --sock "$SOCK30" --session "$_sess" > "$OUT.wccap" 2>&1
4772 grep -q "$_mark" "$OUT.wccap" || {
4773 echo "e2e FAIL: wall chord: session $_sess does not hold $_mark:"
4774 cat "$OUT.wccap"; exit 1; }
4775 done
4776 # The wall's tiles attach at 0x0 and must never claim a grid — scenario
4777 # 33's passivity check, made here because these attaches were spawned by
4778 # the client rather than typed by a user.
4779 timeout 20 "$MUXA" status --sock "$SOCK30" --session 1 > "$OUT.wcst" 2>&1
4780 grep -q '"cols":100' "$OUT.wcst" || {
4781 echo "e2e FAIL: wall chord: the wall's 0x0 attach moved session 1's grid:"
4782 cat "$OUT.wcst"; exit 1; }
4783 assert_stopped "$SOCK30" "$D27PID" "wall chord" "$OUT.wcstop"
4784 D27PID=""
4785 ok "Ctrl-\\ w: the wall shows both sessions, and q gives the session back"
4786
4675 # The long-lived daemon has served every scenario that wanted it; stop it 4787 # The long-lived daemon has served every scenario that wanted it; stop it
4676 # NOW so its allocator verdict is written while the suite is still running 4788 # NOW so its allocator verdict is written while the suite is still running
4677 # and can say so. SIGTERM runs the clean-shutdown path, so the defer chain 4789 # and can say so. SIGTERM runs the clean-shutdown path, so the defer chain
@@ -4687,7 +4799,7 @@ DPID=""
4687 4799
4688 # The pins. Literals, not variables set from counting something else — 4800 # The pins. Literals, not variables set from counting something else —
4689 # "assert the literal, never the constant the code under test reads" 4801 # "assert the literal, never the constant the code under test reads"
4690 # (decisions.md, M10). 36 scenario checkpoints; 35 convergence points. 4802 # (decisions.md, M10). 37 scenario checkpoints; 35 convergence points.
4691 # Anyone adding a scenario updates these by hand, on purpose. 4803 # Anyone adding a scenario updates these by hand, on purpose.
4692 # 4804 #
4693 # M18 added three checkpoints and no convergence points: its wall block 4805 # M18 added three checkpoints and no convergence points: its wall block
@@ -4718,9 +4830,12 @@ DPID=""
4718 # sessions' grids against each other — one holding a marker, one refusing 4830 # sessions' grids against each other — one holding a marker, one refusing
4719 # to — and assert_converged speaks about one session at a time. The 36th is 4831 # to — and assert_converged speaks about one session at a time. The 36th is
4720 # the session ring, and no convergence point for that same reason a second 4832 # the session ring, and no convergence point for that same reason a second
4721 # time: it compares THREE sessions' grids against each other. 4833 # time: it compares THREE sessions' grids against each other. The 37th is
4722 [ "$OK_COUNT" = "36" ] || { 4834 # Ctrl-\ w, and no convergence point for the CLI wall's reason: what it
4723 echo "e2e FAIL: $OK_COUNT scenario checkpoints ran, the pin says 36 —" 4835 # asserts on is a ptyclient capture holding two sessions' bytes at once,
4836 # which no dump of one grid can witness.
4837 [ "$OK_COUNT" = "37" ] || {
4838 echo "e2e FAIL: $OK_COUNT scenario checkpoints ran, the pin says 37 —"
4724 echo " a scenario was added (update the pin) or silently lost" 4839 echo " a scenario was added (update the pin) or silently lost"
4725 exit 1 4840 exit 1
4726 } 4841 }
@@ -4728,4 +4843,4 @@ DPID=""
4728 echo "e2e FAIL: $CONV_COUNT convergence points ran, the pin says 35" 4843 echo "e2e FAIL: $CONV_COUNT convergence points ran, the pin says 35"
4729 exit 1 4844 exit 1
4730 } 4845 }
4731 echo "e2e OK (36 scenarios, 35 convergence points)" 4846 echo "e2e OK (37 scenarios, 35 convergence points)"