d6523df1
test: e2e walls two sessions from inside one, and comes back
a73x 2026-08-19 17:17
Commit message
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)" |