a73x

8d60e36e

test: e2e zooms the second stripe and types into that session

a73x   2026-08-19 17:17

Commit message
test: e2e zooms the second stripe and types into that session

The leg presses `2`, `Enter`, types a shell-expanded marker, and detaches
with `\x1cd`. Four witnesses: b's bar carries the selection marker (a's
is marked from the start, so only b's proves a move), b's grid holds the
marker and a's does not, b's grid is this pty's 100 columns while a's is
still 80 (the zoom claimed the grid, the tiles never did), and the marker
paints a SECOND time after the detach — from a session that has gone
quiet, so only the stale-generation repaint can have put it there.

Both alternate-screen expects are handover transitions rather than the
wall's own entry, which is what keeps the leg honest across a terminal
that is torn down and rebuilt twice.

test/e2e.sh
Old New
@@ -279,6 +279,13 @@ D27PID=""
279 SOCK31="${TMPDIR:-/tmp}/muxd-e2e-selfattach-$$.sock" 279 SOCK31="${TMPDIR:-/tmp}/muxd-e2e-selfattach-$$.sock"
280 D28PID="" 280 D28PID=""
281 281
282 # The wall's zoom: its own daemon because the leg asserts on a session's
283 # GRID SIZE — the zoomed client claims it, the tiles must not — and a
284 # session another block created at another size would make that number
285 # say nothing about this one.
286 SOCK32="${TMPDIR:-/tmp}/muxd-e2e-wallzoom-$$.sock"
287 D29PID=""
288
282 # One counter out of a MUX_PREDICT_STATS line. The client prints exactly one 289 # One counter out of a MUX_PREDICT_STATS line. The client prints exactly one
283 # such line on exit; every field is a key=value pair, so a rename or reorder 290 # such line on exit; every field is a key=value pair, so a rename or reorder
284 # in the client shows up here as an empty read rather than a wrong number. 291 # in the client shows up here as an empty read rather than a wrong number.
@@ -804,6 +811,7 @@ cleanup() {
804 [ -n "$D26PID" ] && kill "$D26PID" 2>/dev/null || true 811 [ -n "$D26PID" ] && kill "$D26PID" 2>/dev/null || true
805 [ -n "$D27PID" ] && kill "$D27PID" 2>/dev/null || true 812 [ -n "$D27PID" ] && kill "$D27PID" 2>/dev/null || true
806 [ -n "$D28PID" ] && kill "$D28PID" 2>/dev/null || true 813 [ -n "$D28PID" ] && kill "$D28PID" 2>/dev/null || true
814 [ -n "$D29PID" ] && kill "$D29PID" 2>/dev/null || true
807 # The stops still precede the socket rm below, like SOCK14-17 above: 815 # The stops still precede the socket rm below, like SOCK14-17 above:
808 # unlinking a socket first would leave a live daemon nothing could reach 816 # unlinking a socket first would leave a live daemon nothing could reach
809 # by path. 817 # by path.
@@ -817,6 +825,7 @@ cleanup() {
817 [ -S "$SOCK29" ] && "$MUXD" stop --sock "$SOCK29" 2>/dev/null || true 825 [ -S "$SOCK29" ] && "$MUXD" stop --sock "$SOCK29" 2>/dev/null || true
818 [ -S "$SOCK30" ] && "$MUXD" stop --sock "$SOCK30" 2>/dev/null || true 826 [ -S "$SOCK30" ] && "$MUXD" stop --sock "$SOCK30" 2>/dev/null || true
819 [ -S "$SOCK31" ] && "$MUXD" stop --sock "$SOCK31" 2>/dev/null || true 827 [ -S "$SOCK31" ] && "$MUXD" stop --sock "$SOCK31" 2>/dev/null || true
828 [ -S "$SOCK32" ] && "$MUXD" stop --sock "$SOCK32" 2>/dev/null || true
820 829
821 # ---- the leak sweep (hygiene kit, 6a) ---- 830 # ---- the leak sweep (hygiene kit, 6a) ----
822 # Here rather than at the bottom of the file, which `set -e` reaches only 831 # Here rather than at the bottom of the file, which `set -e` reaches only
@@ -829,7 +838,7 @@ cleanup() {
829 "$GPID" "$APID" "$PAPID" "$HAPID" "$HDPID" \ 838 "$GPID" "$APID" "$PAPID" "$HAPID" "$HDPID" \
830 "$D14PID" "$D15PID" "$D16PID" "$D17PID" "$D18PID" "$D19PID" \ 839 "$D14PID" "$D15PID" "$D16PID" "$D17PID" "$D18PID" "$D19PID" \
831 "$D20PID" "$D21PID" "$D22PID" "$D23PID" "$D24PID" "$D25PID" \ 840 "$D20PID" "$D21PID" "$D22PID" "$D23PID" "$D24PID" "$D25PID" \
832 "$D26PID" "$D27PID" "$D28PID" 841 "$D26PID" "$D27PID" "$D28PID" "$D29PID"
833 _leak=0 842 _leak=0
834 leak_sweep "$_rc" || _leak=1 843 leak_sweep "$_rc" || _leak=1
835 844
@@ -4886,6 +4895,124 @@ assert_stopped "$SOCK31" "$D28PID" "self attach" "$OUT.sastop"
4886 D28PID="" 4895 D28PID=""
4887 ok "a session shell carries MUX_SOCK/MUX_SESSION, and mux refuses to attach to itself" 4896 ok "a session shell carries MUX_SOCK/MUX_SESSION, and mux refuses to attach to itself"
4888 4897
4898 # ---- the wall zooms: Enter types into the selected session --------------
4899 #
4900 # Scenario 33 proved the wall WATCHES two sessions. This proves the wall
4901 # can be typed through without ever forwarding a keystroke itself: `2`
4902 # selects the second stripe, `Enter` spawns a real client on this
4903 # terminal, and what is typed there is the shell's, not the wall's.
4904 #
4905 # Four claims, four witnesses:
4906 #
4907 # * the selection moved — b's label bar carries the `> ` marker, which
4908 # it cannot have before the `2` (a's is marked from the start, so only
4909 # b's bar is evidence of a move).
4910 # * the zoom typed into the SELECTED session — b's grid holds wzoom-pin
4911 # and a's does not. A shell-EXPANDED marker (the M18 trick), so a hit
4912 # is the shell's work and never an echo of what this script sent.
4913 # * the zoom was a REAL attach, not a tile that started forwarding —
4914 # b's grid is 100 cols wide, this pty's size, claimed the legitimate
4915 # way. a's is still 80: the wall's own tiles attach 0x0 and claimed
4916 # nothing, scenario 33's passivity check made again with a full-size
4917 # client in the same process tree.
4918 # * the wall came BACK — session A's marker paints a SECOND time after
4919 # the detach, asserted both in order (the expect) and by COUNT (twice
4920 # in the capture). Session a is the tile the zoom never touched:
4921 # nothing was typed at it, its grid was never resized, so no frame
4922 # arrives to trigger a paint and only the stale-generation repaint
4923 # the zoom leaves behind can put those bytes on the screen again.
4924 # Asserted on A rather than on the zoomed session for exactly that
4925 # reason — measured, not assumed: with the generation bump removed,
4926 # b's stripe repaints anyway (the client that detached moved b's
4927 # world), and a leg anchored on b passes with the lever gone.
4928 #
4929 # The wall is entered on ONE expect, not one per tile, for scenario 33's
4930 # reason learned here the hard way: tile threads race, `expect` is a
4931 # stream cursor, and the SECOND of two markers is buried for good whenever
4932 # it painted first — a leg that then hangs on a wall that painted
4933 # perfectly. One anchor cannot be buried, and the settle after it is what
4934 # says both stripes have finished. Only ONE stripe is expected after the
4935 # detach for the same reason.
4936 #
4937 # Both `\x1b[?1049h` expects are alternate-screen TRANSITIONS, not the
4938 # wall's own entry: the first is the zoomed client's (the wall left the
4939 # alternate screen to hand over a sane tty), the second is the wall
4940 # re-entering after `\x1cd`. Anchoring on them rather than on a settle is
4941 # what keeps the leg honest while a terminal is torn down and rebuilt
4942 # twice.
4943 "$MUXD" run --sock "$SOCK32" --shell /bin/sh > "$OUT.wz.d" 2>&1 &
4944 D29PID=$!
4945 wait_sock "$SOCK32" "$OUT.wz.d" "wall-zoom daemon never bound"
4946
4947 # Two sessions, each with its own marker, created exactly as scenario 33
4948 # creates them: no tty on either client, so both grids are the default 80
4949 # wide — the number the passivity check reads back later.
4950 { printf 'printf "wza-%%s\\n" pin\n'; sleep 2; printf '\034\034'; } | \
4951 timeout 40 "$MUX" --sock "$SOCK32" --session a > "$OUT.wza" 2> "$OUT.wza.err"
4952 wait_grid "$SOCK32" "wza-pin" "wall zoom: session a's marker" a
4953 { printf 'printf "wzb-%%s\\n" pin\n'; sleep 2; printf '\034\034'; } | \
4954 timeout 40 "$MUX" --sock "$SOCK32" --session b > "$OUT.wzb" 2> "$OUT.wzb.err"
4955 wait_grid "$SOCK32" "wzb-pin" "wall zoom: session b's marker" b
4956
4957 set +e
4958 timeout 90 "$PTYCLIENT" --cols 100 --rows 30 --out "$OUT.wzcap" --err "$OUT.wzcap.err" -- \
4959 "$MUX" wall "--sock $SOCK32#a" "--sock $SOCK32#b" > "$OUT.wzpc" 2>&1 <<'EOF'
4960 expect wzb-pin 20000
4961 settle 700 20000
4962 send 2
4963 settle 500 15000
4964 send \r
4965 expect \x1b[?1049h 15000
4966 settle 700 20000
4967 send printf 'wzoom-%s\\n' pin\n
4968 expect wzoom-pin 15000
4969 settle 400 15000
4970 send \x1cd
4971 expect \x1b[?1049h 15000
4972 expect wza-pin 20000
4973 settle 500 15000
4974 send q
4975 waitexit 10000
4976 EOF
4977 RC=$?
4978 set -e
4979 [ "$RC" -eq 0 ] || {
4980 echo "e2e FAIL: wall zoom: ptyclient leg exited $RC (did \\r zoom, did \\x1cd come back?):"
4981 cat "$OUT.wzpc"; exit 1; }
4982 # The move itself, on bytes only a selected bar can carry.
4983 grep -q -- "> --sock $SOCK32#b" "$OUT.wzcap" || {
4984 echo "e2e FAIL: wall zoom: session b's bar never showed the selection marker"; exit 1; }
4985 # The repaint counted rather than merely ordered: a's marker is on this
4986 # terminal twice, once for the stripe's own paint and once for the one the
4987 # zoom's exit demanded. `grep -o`, not `grep -c`: a stripe paint is escape
4988 # sequences with no newlines in them, so a line count reads 1 either way.
4989 _wza_paints=$(grep -o "wza-pin" "$OUT.wzcap" | wc -l)
4990 [ "$_wza_paints" -ge 2 ] || {
4991 echo "e2e FAIL: wall zoom: session a's stripe painted $_wza_paints time(s), so the wall never came back:"
4992 cat "$OUT.wzpc"; exit 1; }
4993 # What the user typed landed in the session the digit picked, and only
4994 # there. The negative is the whole point of the digit.
4995 timeout 20 "$MUXA" capture --sock "$SOCK32" --session b > "$OUT.wzcapb" 2>&1
4996 grep -q "wzoom-pin" "$OUT.wzcapb" || {
4997 echo "e2e FAIL: wall zoom: session b never got what the zoom typed:"
4998 cat "$OUT.wzcapb"; exit 1; }
4999 timeout 20 "$MUXA" capture --sock "$SOCK32" --session a > "$OUT.wzcapa" 2>&1
5000 grep -q "wzoom-pin" "$OUT.wzcapa" && {
5001 echo "e2e FAIL: wall zoom: the zoom typed into session a, which was not selected:"
5002 cat "$OUT.wzcapa"; exit 1; }
5003 # The size split: the zoom claimed the grid, the tiles never did.
5004 timeout 20 "$MUXA" status --sock "$SOCK32" --session b > "$OUT.wzstb" 2>&1
5005 grep -q '"cols":100' "$OUT.wzstb" || {
5006 echo "e2e FAIL: wall zoom: the zoomed client did not claim session b's grid:"
5007 cat "$OUT.wzstb"; exit 1; }
5008 timeout 20 "$MUXA" status --sock "$SOCK32" --session a > "$OUT.wzsta" 2>&1
5009 grep -q '"cols":80' "$OUT.wzsta" || {
5010 echo "e2e FAIL: wall zoom: a 0x0 wall attach moved session a's grid:"
5011 cat "$OUT.wzsta"; exit 1; }
5012 assert_stopped "$SOCK32" "$D29PID" "wall zoom" "$OUT.wzstop"
5013 D29PID=""
5014 ok "the wall zooms: Enter types into the selected session, Ctrl-\\ d comes back"
5015
4889 # The long-lived daemon has served every scenario that wanted it; stop it 5016 # The long-lived daemon has served every scenario that wanted it; stop it
4890 # NOW so its allocator verdict is written while the suite is still running 5017 # NOW so its allocator verdict is written while the suite is still running
4891 # and can say so. SIGTERM runs the clean-shutdown path, so the defer chain 5018 # and can say so. SIGTERM runs the clean-shutdown path, so the defer chain
@@ -4901,7 +5028,7 @@ DPID=""
4901 5028
4902 # The pins. Literals, not variables set from counting something else — 5029 # The pins. Literals, not variables set from counting something else —
4903 # "assert the literal, never the constant the code under test reads" 5030 # "assert the literal, never the constant the code under test reads"
4904 # (decisions.md, M10). 38 scenario checkpoints; 35 convergence points. 5031 # (decisions.md, M10). 39 scenario checkpoints; 35 convergence points.
4905 # Anyone adding a scenario updates these by hand, on purpose. 5032 # Anyone adding a scenario updates these by hand, on purpose.
4906 # 5033 #
4907 # M18 added three checkpoints and no convergence points: its wall block 5034 # M18 added three checkpoints and no convergence points: its wall block
@@ -4938,9 +5065,14 @@ DPID=""
4938 # which no dump of one grid can witness. The 38th is the self-attach 5065 # which no dump of one grid can witness. The 38th is the self-attach
4939 # refusal, and no convergence point because it has no client to converge: 5066 # refusal, and no convergence point because it has no client to converge:
4940 # the leg exists to prove no client was ever created, and its evidence is a 5067 # the leg exists to prove no client was ever created, and its evidence is a
4941 # status field and a shell's exit code, neither of which is a grid. 5068 # status field and a shell's exit code, neither of which is a grid. The
4942 [ "$OK_COUNT" = "38" ] || { 5069 # 39th is the wall's zoom, and no convergence point for the CLI wall's
4943 echo "e2e FAIL: $OK_COUNT scenario checkpoints ran, the pin says 38 —" 5070 # reason a third time: its subject is a ptyclient capture — one terminal
5071 # torn down and rebuilt around a child client — and the two grid checks it
5072 # does make are `muxa status` sizes, which assert_converged does not speak
5073 # about at all.
5074 [ "$OK_COUNT" = "39" ] || {
5075 echo "e2e FAIL: $OK_COUNT scenario checkpoints ran, the pin says 39 —"
4944 echo " a scenario was added (update the pin) or silently lost" 5076 echo " a scenario was added (update the pin) or silently lost"
4945 exit 1 5077 exit 1
4946 } 5078 }
@@ -4948,4 +5080,4 @@ DPID=""
4948 echo "e2e FAIL: $CONV_COUNT convergence points ran, the pin says 35" 5080 echo "e2e FAIL: $CONV_COUNT convergence points ran, the pin says 35"
4949 exit 1 5081 exit 1
4950 } 5082 }
4951 echo "e2e OK (38 scenarios, 35 convergence points)" 5083 echo "e2e OK (39 scenarios, 35 convergence points)"