a73x

f88b6984

test: the zoom claims the grid without attaching, and skips for free

a73x   2026-08-19 22:51

Commit message
test: the zoom claims the grid without attaching, and skips for free

The wall-zoom scenario evolves in place rather than growing a twin: the same
legs, with the assertion that inverted. "The zoom was a REAL attach, not a
tile that started forwarding" was the child-spawn design's whole point; the
promoted tile proves the opposite, so the leg now asserts that the daemon
never saw a second client on the session while the zoom held it, and drops
the `\x1b[?1049h` anchors — an in-place zoom never leaves the alternate
screen, so an alternate-screen transition is no longer the honest thing to
wait on. The size half is unchanged and still load-bearing: b at 100 cols is
the promote, a at 80 is the tile nobody zoomed.

The witness is `muxd stats`'s per-session `clients=` gauge, sampled for the
whole life of the wall, asserted on the PEAK. A gauge read once could miss a
window; a peak over seconds of zoom cannot. `assert_never_two_clients` also
insists it saw the session AT ALL, so a watcher that sampled nothing cannot
pass a flat-count claim by having witnessed nothing.

Two scenarios join it. The skip leg zooms A, `Ctrl-\ n` to B, `Ctrl-\ l`
back to A, marking each session as it stands there — the markers prove the
zoom moved, the flat count proves it moved without dialling. The mute leg is
the new invariant's teeth: keys typed at an UNZOOMED wall reach no session,
anchored by a positive marker the same session gets one zoom later, so a leg
that silently did not run fails loudly instead of passing a negative grep.

Both markers were measured, not assumed. `zs-b` matched inside `zs-back`, so
the skip leg's negatives are numbered words that share no prefix. And the mute
leg's `wallnope` could never have appeared in a grid at all — `n` and `p` are
selection keys, so what a leak really delivers is `wallne`; `wallmute` is
eight letters the unzoomed wall swallows whole, re-checked under the mutation
that is supposed to kill this leg before being believed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

test/e2e.sh
Old New
@@ -318,6 +318,20 @@ LESSDATA="${TMPDIR:-/tmp}/mux-e2e-pager-$$.txt"
318 SOCK36="${TMPDIR:-/tmp}/muxd-e2e-pipestdin-$$.sock" 318 SOCK36="${TMPDIR:-/tmp}/muxd-e2e-pipestdin-$$.sock"
319 D33PID="" 319 D33PID=""
320 320
321 # The zoom-as-lens pair, both on daemons of their own for SOCK32's reason:
322 # each asserts on grid sizes and on how many clients a session ever had at
323 # once, and a session another block created — or another block's client
324 # sitting on this daemon — would make both numbers say nothing.
325 SOCK37="${TMPDIR:-/tmp}/muxd-e2e-zoomskip-$$.sock"
326 D34PID=""
327 SOCK38="${TMPDIR:-/tmp}/muxd-e2e-zoommute-$$.sock"
328 D35PID=""
329 # The dead tile. Its own daemon because the leg needs a session name that
330 # does NOT exist — a 0x0 attach joins but never creates, so the daemon
331 # refuses it — and any other block's daemon might have one by that name.
332 SOCK39="${TMPDIR:-/tmp}/muxd-e2e-zoomdead-$$.sock"
333 D36PID=""
334
321 # One counter out of a MUX_PREDICT_STATS line. The client prints exactly one 335 # One counter out of a MUX_PREDICT_STATS line. The client prints exactly one
322 # such line on exit; every field is a key=value pair, so a rename or reorder 336 # such line on exit; every field is a key=value pair, so a rename or reorder
323 # in the client shows up here as an empty read rather than a wrong number. 337 # in the client shows up here as an empty read rather than a wrong number.
@@ -450,6 +464,90 @@ wait_sessions() {
450 exit 1 464 exit 1
451 } 465 }
452 466
467 # --- the wall's zoom: did the daemon see a second attach? --------------
468 #
469 # The in-place zoom's whole claim is that it PROMOTES the tile's existing
470 # connection — no new attach, no dial. Proving a negative needs a
471 # daemon-side witness, and there are two here doing different jobs.
472 #
473 # THE ASSERTION is `attaches=`, a cumulative counter of every attach this
474 # daemon accepted (server.zig Stats). Read once before the wall starts and
475 # once after it exits, the delta is exactly how many times anything attached
476 # across the whole leg — which for a wall of N tiles must be N, one per tile
477 # at startup, and never N+1 however far the zoom moved. It is read from
478 # `muxd stats`, which is plain human text and connects as an OBSERVER: the
479 # reading itself never attaches, so it cannot pollute what it measures.
480 #
481 # THE GAUGE is `session NAME clients=`, sampled every 200ms into a file and
482 # asserted on its peak. It is kept because it localises a failure — it says
483 # WHICH session grew a second watcher and roughly when — but it is the
484 # weaker of the two and must never be the only one: a connection that closes
485 # as another opens never exceeds 1, and one that lives less than a sample
486 # interval is invisible to it. The counter cannot miss either.
487 #
488 # Nothing else may attach while a leg is being measured (`muxa capture` and
489 # `wait_grid` are clients too), so both readings bracket the ptyclient leg
490 # and nothing more.
491 #
492 # attaches_now SOCK — the daemon's cumulative accepted-attach count.
493 attaches_now() {
494 timeout 5 "$MUXD" stats --sock "$1" 2>/dev/null |
495 sed -n 's/.*[^_]attaches=\([0-9]*\).*/\1/p'
496 }
497
498 # assert_attach_delta BEFORE AFTER WANT LABEL — how many attaches happened.
499 # Empty readings fail loudly rather than arithmetically: `$(())` on an empty
500 # string is 0, and 0-0=0 would pass this check having measured nothing at
501 # all — the vacuous green this whole block exists to refuse.
502 assert_attach_delta() {
503 [ -n "$1" ] && [ -n "$2" ] || {
504 echo "e2e FAIL: $4: stats gave no attaches= reading (before='$1' after='$2')"
505 echo " — the flat-attach claim would be vacuous"
506 exit 1; }
507 [ "$(($2 - $1))" = "$3" ] || {
508 echo "e2e FAIL: $4: the daemon accepted $(($2 - $1)) attaches (want $3)"
509 echo " — the wall's tiles attach once each and the zoom never does"
510 exit 1; }
511 }
512
513 # watch_clients SOCK FILE — start sampling in the background.
514 WATCH_PID=""
515 watch_clients() {
516 touch "$2.on"
517 ( while [ -e "$2.on" ]; do
518 timeout 5 "$MUXD" stats --sock "$1" 2>/dev/null
519 sleep 0.2
520 done ) > "$2" &
521 WATCH_PID=$!
522 }
523
524 # unwatch_clients FILE — stop sampling.
525 unwatch_clients() {
526 rm -f "$1.on"
527 wait "$WATCH_PID" 2>/dev/null || true
528 WATCH_PID=""
529 }
530
531 # assert_never_two_clients FILE NAME LABEL — the peak `clients=` this
532 # session ever showed is 1. Also asserts it was ever 1, which is the anchor:
533 # a watcher that sampled an empty file, or a wall whose tiles never attached,
534 # would otherwise pass this leg by having witnessed nothing at all.
535 #
536 # Localisation, not proof — `assert_attach_delta` is the proof. This says
537 # which session grew a second watcher; the counter says whether anything
538 # attached at all.
539 assert_never_two_clients() {
540 _peak=$(grep -o "session $2 clients=[0-9]*" "$1" | sed 's/.*=//' | sort -n | tail -1)
541 [ -n "$_peak" ] || {
542 echo "e2e FAIL: $3: no stats sample ever named session $2 — the watch"
543 echo " saw nothing, so its flat-count claim is vacuous"
544 exit 1; }
545 [ "$_peak" = "1" ] || {
546 echo "e2e FAIL: $3: session $2 was watched by $_peak clients at once"
547 echo " (want 1) — the zoom attached instead of promoting"
548 exit 1; }
549 }
550
453 # wait_pid_gone PID LABEL — poll until a tracked pid is gone (2s, the same 551 # wait_pid_gone PID LABEL — poll until a tracked pid is gone (2s, the same
454 # budget `muxd stop` gives itself). Its own helper rather than wait_gone's 552 # budget `muxd stop` gives itself). Its own helper rather than wait_gone's
455 # socket probe, because the two answer different questions: `muxd: stopped` 553 # socket probe, because the two answer different questions: `muxd: stopped`
@@ -848,6 +946,9 @@ cleanup() {
848 [ -n "$D31PID" ] && kill "$D31PID" 2>/dev/null || true 946 [ -n "$D31PID" ] && kill "$D31PID" 2>/dev/null || true
849 [ -n "$D32PID" ] && kill "$D32PID" 2>/dev/null || true 947 [ -n "$D32PID" ] && kill "$D32PID" 2>/dev/null || true
850 [ -n "$D33PID" ] && kill "$D33PID" 2>/dev/null || true 948 [ -n "$D33PID" ] && kill "$D33PID" 2>/dev/null || true
949 [ -n "$D34PID" ] && kill "$D34PID" 2>/dev/null || true
950 [ -n "$D35PID" ] && kill "$D35PID" 2>/dev/null || true
951 [ -n "$D36PID" ] && kill "$D36PID" 2>/dev/null || true
851 # The stops still precede the socket rm below, like SOCK14-17 above: 952 # The stops still precede the socket rm below, like SOCK14-17 above:
852 # unlinking a socket first would leave a live daemon nothing could reach 953 # unlinking a socket first would leave a live daemon nothing could reach
853 # by path. 954 # by path.
@@ -866,6 +967,9 @@ cleanup() {
866 [ -S "$SOCK34" ] && "$MUXD" stop --sock "$SOCK34" 2>/dev/null || true 967 [ -S "$SOCK34" ] && "$MUXD" stop --sock "$SOCK34" 2>/dev/null || true
867 [ -S "$SOCK35" ] && "$MUXD" stop --sock "$SOCK35" 2>/dev/null || true 968 [ -S "$SOCK35" ] && "$MUXD" stop --sock "$SOCK35" 2>/dev/null || true
868 [ -S "$SOCK36" ] && "$MUXD" stop --sock "$SOCK36" 2>/dev/null || true 969 [ -S "$SOCK36" ] && "$MUXD" stop --sock "$SOCK36" 2>/dev/null || true
970 [ -S "$SOCK37" ] && "$MUXD" stop --sock "$SOCK37" 2>/dev/null || true
971 [ -S "$SOCK38" ] && "$MUXD" stop --sock "$SOCK38" 2>/dev/null || true
972 [ -S "$SOCK39" ] && "$MUXD" stop --sock "$SOCK39" 2>/dev/null || true
869 973
870 # ---- the leak sweep (hygiene kit, 6a) ---- 974 # ---- the leak sweep (hygiene kit, 6a) ----
871 # Here rather than at the bottom of the file, which `set -e` reaches only 975 # Here rather than at the bottom of the file, which `set -e` reaches only
@@ -879,7 +983,7 @@ cleanup() {
879 "$D14PID" "$D15PID" "$D16PID" "$D17PID" "$D18PID" "$D19PID" \ 983 "$D14PID" "$D15PID" "$D16PID" "$D17PID" "$D18PID" "$D19PID" \
880 "$D20PID" "$D21PID" "$D22PID" "$D23PID" "$D24PID" "$D25PID" \ 984 "$D20PID" "$D21PID" "$D22PID" "$D23PID" "$D24PID" "$D25PID" \
881 "$D26PID" "$D27PID" "$D28PID" "$D29PID" "$D30PID" "$D31PID" \ 985 "$D26PID" "$D27PID" "$D28PID" "$D29PID" "$D30PID" "$D31PID" \
882 "$D32PID" "$D33PID" 986 "$D32PID" "$D33PID" "$D34PID" "$D35PID" "$D36PID"
883 _leak=0 987 _leak=0
884 leak_sweep "$_rc" || _leak=1 988 leak_sweep "$_rc" || _leak=1
885 989
@@ -4950,14 +5054,15 @@ assert_stopped "$SOCK31" "$D28PID" "self attach" "$OUT.sastop"
4950 D28PID="" 5054 D28PID=""
4951 ok "a session shell carries MUX_SOCK/MUX_SESSION, and mux refuses to attach to itself" 5055 ok "a session shell carries MUX_SOCK/MUX_SESSION, and mux refuses to attach to itself"
4952 5056
4953 # ---- the wall zooms: Enter types into the selected session -------------- 5057 # ---- the wall zooms IN PLACE: Enter promotes the selected tile ----------
4954 # 5058 #
4955 # Scenario 33 proved the wall WATCHES two sessions. This proves the wall 5059 # Scenario 33 proved the wall WATCHES two sessions. This proves the wall can
4956 # can be typed through without ever forwarding a keystroke itself: `2` 5060 # be typed through by PROMOTING one of the connections it already holds:
4957 # selects the second stripe, `Enter` spawns a real client on this 5061 # `2` selects the second stripe, `Enter` resizes that tile's attach from 0x0
4958 # terminal, and what is typed there is the shell's, not the wall's. 5062 # to this pty's size and starts forwarding keystrokes down it, `Ctrl-\ d`
5063 # gives the terminal back to the wall.
4959 # 5064 #
4960 # Four claims, four witnesses: 5065 # Five claims, five witnesses:
4961 # 5066 #
4962 # * the selection moved — b's label bar carries the `> ` marker, which 5067 # * the selection moved — b's label bar carries the `> ` marker, which
4963 # it cannot have before the `2` (a's is marked from the start, so only 5068 # it cannot have before the `2` (a's is marked from the start, so only
@@ -4965,21 +5070,29 @@ ok "a session shell carries MUX_SOCK/MUX_SESSION, and mux refuses to attach to i
4965 # * the zoom typed into the SELECTED session — b's grid holds wzoom-pin 5070 # * the zoom typed into the SELECTED session — b's grid holds wzoom-pin
4966 # and a's does not. A shell-EXPANDED marker (the M18 trick), so a hit 5071 # and a's does not. A shell-EXPANDED marker (the M18 trick), so a hit
4967 # is the shell's work and never an echo of what this script sent. 5072 # is the shell's work and never an echo of what this script sent.
4968 # * the zoom was a REAL attach, not a tile that started forwarding — 5073 # * the zoom CLAIMED the grid — b's grid is 100 cols wide, this pty's
4969 # b's grid is 100 cols wide, this pty's size, claimed the legitimate 5074 # size. That is the promote: a tile that attached 0x0 asked for the
4970 # way. a's is still 80: the wall's own tiles attach 0x0 and claimed 5075 # terminal's size before its first keystroke, so the client that types
4971 # nothing, scenario 33's passivity check made again with a full-size 5076 # is a full-size one and latest-wins has nothing to complain about.
4972 # client in the same process tree. 5077 # * ...and it claimed it WITHOUT a second attach. This is the assertion
5078 # that inverted when the child-spawn zoom was deleted: the old design
5079 # proved the zoom was a real attach (a client of its own, in the same
5080 # process tree), and the new one proves the opposite — the daemon's
5081 # cumulative `attaches=` counter moves by exactly 2 across the wall's
5082 # whole life, one per tile at startup, because the connection that
5083 # typed is the connection the tile already had. Session a's grid stays
5084 # 80 for the passivity half of the same claim: a tile nobody zoomed
5085 # claims nothing, still.
4973 # * the wall came BACK — session A's marker paints a SECOND time after 5086 # * the wall came BACK — session A's marker paints a SECOND time after
4974 # the detach, asserted both in order (the expect) and by COUNT (twice 5087 # the unzoom, asserted both in order (the expect) and by COUNT (twice
4975 # in the capture). Session a is the tile the zoom never touched: 5088 # in the capture). Session a is the tile the zoom never touched:
4976 # nothing was typed at it, its grid was never resized, so no frame 5089 # nothing was typed at it, its grid was never resized, so no frame
4977 # arrives to trigger a paint and only the stale-generation repaint 5090 # arrives to trigger a paint and only the stale-generation repaint
4978 # the zoom leaves behind can put those bytes on the screen again. 5091 # the demote leaves behind can put those bytes on the screen again.
4979 # Asserted on A rather than on the zoomed session for exactly that 5092 # Asserted on A rather than on the zoomed session for exactly that
4980 # reason — measured, not assumed: with the generation bump removed, 5093 # reason — measured, not assumed: with the generation bump removed,
4981 # b's stripe repaints anyway (the client that detached moved b's 5094 # b's stripe repaints anyway (the session the zoom typed at moves on
4982 # world), and a leg anchored on b passes with the lever gone. 5095 # its own), and a leg anchored on b passes with the lever gone.
4983 # 5096 #
4984 # The wall is entered on ONE expect, not one per tile, for scenario 33's 5097 # The wall is entered on ONE expect, not one per tile, for scenario 33's
4985 # reason learned here the hard way: tile threads race, `expect` is a 5098 # reason learned here the hard way: tile threads race, `expect` is a
@@ -4987,14 +5100,13 @@ ok "a session shell carries MUX_SOCK/MUX_SESSION, and mux refuses to attach to i
4987 # it painted first — a leg that then hangs on a wall that painted 5100 # it painted first — a leg that then hangs on a wall that painted
4988 # perfectly. One anchor cannot be buried, and the settle after it is what 5101 # perfectly. One anchor cannot be buried, and the settle after it is what
4989 # says both stripes have finished. Only ONE stripe is expected after the 5102 # says both stripes have finished. Only ONE stripe is expected after the
4990 # detach for the same reason. 5103 # unzoom for the same reason.
4991 # 5104 #
4992 # Both `\x1b[?1049h` expects are alternate-screen TRANSITIONS, not the 5105 # There is no `\x1b[?1049h` anchor any more, and its absence is the point:
4993 # wall's own entry: the first is the zoomed client's (the wall left the 5106 # the child-spawn zoom tore this terminal down and rebuilt it twice, so an
4994 # alternate screen to hand over a sane tty), the second is the wall 5107 # alternate-screen TRANSITION was the honest thing to wait on. An in-place
4995 # re-entering after `\x1cd`. Anchoring on them rather than on a settle is 5108 # zoom never leaves the alternate screen at all — it repaints inside it —
4996 # what keeps the leg honest while a terminal is torn down and rebuilt 5109 # so the anchors are the session's own bytes arriving full-screen.
4997 # twice.
4998 "$MUXD" run --sock "$SOCK32" --shell /bin/sh > "$OUT.wz.d" 2>&1 & 5110 "$MUXD" run --sock "$SOCK32" --shell /bin/sh > "$OUT.wz.d" 2>&1 &
4999 D29PID=$! 5111 D29PID=$!
5000 wait_sock "$SOCK32" "$OUT.wz.d" "wall-zoom daemon never bound" 5112 wait_sock "$SOCK32" "$OUT.wz.d" "wall-zoom daemon never bound"
@@ -5009,6 +5121,13 @@ wait_grid "$SOCK32" "wza-pin" "wall zoom: session a's marker" a
5009 timeout 40 "$MUX" --sock "$SOCK32" --session b > "$OUT.wzb" 2> "$OUT.wzb.err" 5121 timeout 40 "$MUX" --sock "$SOCK32" --session b > "$OUT.wzb" 2> "$OUT.wzb.err"
5010 wait_grid "$SOCK32" "wzb-pin" "wall zoom: session b's marker" b 5122 wait_grid "$SOCK32" "wzb-pin" "wall zoom: session b's marker" b
5011 5123
5124 # Brackets the wall and nothing else: every `muxa` and `wait_grid` call in
5125 # this scenario is itself a client, and one inside the watch would be
5126 # counted as the second attach this leg exists to refute.
5127 # Read AFTER the two setup clients have come and gone, so the delta below
5128 # belongs to the wall alone.
5129 WZATT_BEFORE=$(attaches_now "$SOCK32")
5130 watch_clients "$SOCK32" "$OUT.wzwatch"
5012 set +e 5131 set +e
5013 timeout 90 "$PTYCLIENT" --cols 100 --rows 30 --out "$OUT.wzcap" --err "$OUT.wzcap.err" -- \ 5132 timeout 90 "$PTYCLIENT" --cols 100 --rows 30 --out "$OUT.wzcap" --err "$OUT.wzcap.err" -- \
5014 "$MUX" wall "--sock $SOCK32#a" "--sock $SOCK32#b" > "$OUT.wzpc" 2>&1 <<'EOF' 5133 "$MUX" wall "--sock $SOCK32#a" "--sock $SOCK32#b" > "$OUT.wzpc" 2>&1 <<'EOF'
@@ -5017,13 +5136,11 @@ settle 700 20000
5017 send 2 5136 send 2
5018 settle 500 15000 5137 settle 500 15000
5019 send \r 5138 send \r
5020 expect \x1b[?1049h 15000
5021 settle 700 20000 5139 settle 700 20000
5022 send printf 'wzoom-%s\\n' pin\n 5140 send printf 'wzoom-%s\\n' pin\n
5023 expect wzoom-pin 15000 5141 expect wzoom-pin 15000
5024 settle 400 15000 5142 settle 400 15000
5025 send \x1cd 5143 send \x1cd
5026 expect \x1b[?1049h 15000
5027 expect wza-pin 20000 5144 expect wza-pin 20000
5028 settle 500 15000 5145 settle 500 15000
5029 send q 5146 send q
@@ -5031,6 +5148,9 @@ waitexit 10000
5031 EOF 5148 EOF
5032 RC=$? 5149 RC=$?
5033 set -e 5150 set -e
5151 unwatch_clients "$OUT.wzwatch"
5152 # Read BEFORE the muxa captures below, every one of which is an attach.
5153 WZATT_AFTER=$(attaches_now "$SOCK32")
5034 [ "$RC" -eq 0 ] || { 5154 [ "$RC" -eq 0 ] || {
5035 echo "e2e FAIL: wall zoom: ptyclient leg exited $RC (did \\r zoom, did \\x1cd come back?):" 5155 echo "e2e FAIL: wall zoom: ptyclient leg exited $RC (did \\r zoom, did \\x1cd come back?):"
5036 cat "$OUT.wzpc"; exit 1; } 5156 cat "$OUT.wzpc"; exit 1; }
@@ -5039,12 +5159,22 @@ grep -q -- "> --sock $SOCK32#b" "$OUT.wzcap" || {
5039 echo "e2e FAIL: wall zoom: session b's bar never showed the selection marker"; exit 1; } 5159 echo "e2e FAIL: wall zoom: session b's bar never showed the selection marker"; exit 1; }
5040 # The repaint counted rather than merely ordered: a's marker is on this 5160 # The repaint counted rather than merely ordered: a's marker is on this
5041 # terminal twice, once for the stripe's own paint and once for the one the 5161 # terminal twice, once for the stripe's own paint and once for the one the
5042 # zoom's exit demanded. `grep -o`, not `grep -c`: a stripe paint is escape 5162 # unzoom demanded. `grep -o`, not `grep -c`: a stripe paint is escape
5043 # sequences with no newlines in them, so a line count reads 1 either way. 5163 # sequences with no newlines in them, so a line count reads 1 either way.
5044 _wza_paints=$(grep -o "wza-pin" "$OUT.wzcap" | wc -l) 5164 _wza_paints=$(grep -o "wza-pin" "$OUT.wzcap" | wc -l)
5045 [ "$_wza_paints" -ge 2 ] || { 5165 [ "$_wza_paints" -ge 2 ] || {
5046 echo "e2e FAIL: wall zoom: session a's stripe painted $_wza_paints time(s), so the wall never came back:" 5166 echo "e2e FAIL: wall zoom: session a's stripe painted $_wza_paints time(s), so the wall never came back:"
5047 cat "$OUT.wzpc"; exit 1; } 5167 cat "$OUT.wzpc"; exit 1; }
5168 # No second attach, and this is the assertion that inverted: what used to
5169 # prove a real child client now proves there was none. TWO is the whole
5170 # budget — one per tile, at wall startup — and the zoom, the typing and the
5171 # unzoom add nothing to it. A number, not a gauge, so a client that opened
5172 # and closed between two samples, or overlapped another for a millisecond,
5173 # is counted anyway.
5174 assert_attach_delta "$WZATT_BEFORE" "$WZATT_AFTER" 2 "wall zoom"
5175 # ...and which session, if it ever were two.
5176 assert_never_two_clients "$OUT.wzwatch" a "wall zoom"
5177 assert_never_two_clients "$OUT.wzwatch" b "wall zoom"
5048 # What the user typed landed in the session the digit picked, and only 5178 # What the user typed landed in the session the digit picked, and only
5049 # there. The negative is the whole point of the digit. 5179 # there. The negative is the whole point of the digit.
5050 timeout 20 "$MUXA" capture --sock "$SOCK32" --session b > "$OUT.wzcapb" 2>&1 5180 timeout 20 "$MUXA" capture --sock "$SOCK32" --session b > "$OUT.wzcapb" 2>&1
@@ -5055,18 +5185,19 @@ timeout 20 "$MUXA" capture --sock "$SOCK32" --session a > "$OUT.wzcapa" 2>&1
5055 grep -q "wzoom-pin" "$OUT.wzcapa" && { 5185 grep -q "wzoom-pin" "$OUT.wzcapa" && {
5056 echo "e2e FAIL: wall zoom: the zoom typed into session a, which was not selected:" 5186 echo "e2e FAIL: wall zoom: the zoom typed into session a, which was not selected:"
5057 cat "$OUT.wzcapa"; exit 1; } 5187 cat "$OUT.wzcapa"; exit 1; }
5058 # The size split: the zoom claimed the grid, the tiles never did. 5188 # The size split: the promoted tile claimed the grid, the unzoomed one
5189 # never did.
5059 timeout 20 "$MUXA" status --sock "$SOCK32" --session b > "$OUT.wzstb" 2>&1 5190 timeout 20 "$MUXA" status --sock "$SOCK32" --session b > "$OUT.wzstb" 2>&1
5060 grep -q '"cols":100' "$OUT.wzstb" || { 5191 grep -q '"cols":100' "$OUT.wzstb" || {
5061 echo "e2e FAIL: wall zoom: the zoomed client did not claim session b's grid:" 5192 echo "e2e FAIL: wall zoom: the promoted tile did not claim session b's grid:"
5062 cat "$OUT.wzstb"; exit 1; } 5193 cat "$OUT.wzstb"; exit 1; }
5063 timeout 20 "$MUXA" status --sock "$SOCK32" --session a > "$OUT.wzsta" 2>&1 5194 timeout 20 "$MUXA" status --sock "$SOCK32" --session a > "$OUT.wzsta" 2>&1
5064 grep -q '"cols":80' "$OUT.wzsta" || { 5195 grep -q '"cols":80' "$OUT.wzsta" || {
5065 echo "e2e FAIL: wall zoom: a 0x0 wall attach moved session a's grid:" 5196 echo "e2e FAIL: wall zoom: a tile nobody zoomed moved session a's grid:"
5066 cat "$OUT.wzsta"; exit 1; } 5197 cat "$OUT.wzsta"; exit 1; }
5067 assert_stopped "$SOCK32" "$D29PID" "wall zoom" "$OUT.wzstop" 5198 assert_stopped "$SOCK32" "$D29PID" "wall zoom" "$OUT.wzstop"
5068 D29PID="" 5199 D29PID=""
5069 ok "the wall zooms: Enter types into the selected session, Ctrl-\\ d comes back" 5200 ok "the wall zooms in place: Enter promotes the tile, Ctrl-\\ d demotes it"
5070 5201
5071 # ---- the wheel scrolls back, and an app that asks gets it instead ------- 5202 # ---- the wheel scrolls back, and an app that asks gets it instead -------
5072 # 5203 #
@@ -5319,6 +5450,290 @@ assert_stopped "$SOCK36" "$D33PID" "pipe stdin" "$OUT.pipestop"
5319 D33PID="" 5450 D33PID=""
5320 ok "a client with no terminal of its own forwards SGR-shaped bytes untouched" 5451 ok "a client with no terminal of its own forwards SGR-shaped bytes untouched"
5321 5452
5453 # ---- the zoom SKIPS between tiles, and the daemon never notices ---------
5454 #
5455 # The headline of "zoom is a lens": every tile's replica is hot whether or
5456 # not it is the zoom, so moving the zoom between two tiles is a local
5457 # repaint. No re-dial, no attach, no snapshot round trip — which is a claim
5458 # about what did NOT happen, and needs a daemon-side witness rather than a
5459 # stopwatch.
5460 #
5461 # One ptyclient leg, four sessions' worth of typing on two tiles:
5462 #
5463 # Enter zoom tile 1 (session a) — promote
5464 # type mark a with zs-one, shell-expanded
5465 # Ctrl-\ n move the zoom to tile 2 (session b)
5466 # type mark b with zs-two
5467 # Ctrl-\ l skip back to the last tile zoomed, which is a
5468 # type mark a again with zs-three
5469 # Ctrl-\ w unzoom (the spec's spelling; scenario 39 does `d`)
5470 #
5471 # Three claims:
5472 #
5473 # * every marker landed in the session the zoom was standing in, and in
5474 # no other. zs-one and zs-three in a, zs-two in b, and each absent from
5475 # the other session — the negatives are what prove the zoom MOVED
5476 # rather than the wall having quietly kept typing at one connection.
5477 # * the daemon accepted exactly TWO attaches for the whole run — one per
5478 # tile, at wall startup. If `Ctrl-\ n` re-dialled, or zoomed by
5479 # spawning, it would be three or more, and the counter says so however
5480 # briefly the extra connection lived or however neatly it closed the
5481 # old one first. This is the assertion that inverts the child-spawn
5482 # era's "the zoom was a REAL attach".
5483 # * both sessions end up at this pty's size, because both were promoted
5484 # and a demote deliberately gives nothing back — `applySize` refuses
5485 # sub-minimum resizes, so there is no 0x0 to hand back, and a size
5486 # nobody types at claims nothing under latest-wins.
5487 "$MUXD" run --sock "$SOCK37" --shell /bin/sh > "$OUT.zs.d" 2>&1 &
5488 D34PID=$!
5489 wait_sock "$SOCK37" "$OUT.zs.d" "zoom-skip daemon never bound"
5490
5491 # Both sessions exist before the wall does, with a marker each so the wall
5492 # has something to paint and this leg has an anchor to enter on.
5493 { printf 'printf "zsa-%%s\\n" pin\n'; sleep 2; printf '\034\034'; } | \
5494 timeout 40 "$MUX" --sock "$SOCK37" --session a > "$OUT.zsa" 2> "$OUT.zsa.err"
5495 wait_grid "$SOCK37" "zsa-pin" "zoom skip: session a's marker" a
5496 { printf 'printf "zsb-%%s\\n" pin\n'; sleep 2; printf '\034\034'; } | \
5497 timeout 40 "$MUX" --sock "$SOCK37" --session b > "$OUT.zsb" 2> "$OUT.zsb.err"
5498 wait_grid "$SOCK37" "zsb-pin" "zoom skip: session b's marker" b
5499
5500 ZSATT_BEFORE=$(attaches_now "$SOCK37")
5501 watch_clients "$SOCK37" "$OUT.zswatch"
5502 set +e
5503 timeout 90 "$PTYCLIENT" --cols 92 --rows 30 --out "$OUT.zscap" --err "$OUT.zscap.err" -- \
5504 "$MUX" wall "--sock $SOCK37#a" "--sock $SOCK37#b" > "$OUT.zspc" 2>&1 <<'EOF'
5505 expect zsb-pin 20000
5506 settle 700 20000
5507 send \r
5508 settle 700 20000
5509 send printf 'zs-%s\\n' one\n
5510 expect zs-one 15000
5511 settle 400 15000
5512 send \x1cn
5513 settle 700 20000
5514 send printf 'zs-%s\\n' two\n
5515 expect zs-two 15000
5516 settle 400 15000
5517 send \x1cl
5518 settle 700 20000
5519 send printf 'zs-%s\\n' three\n
5520 expect zs-three 15000
5521 settle 400 15000
5522 send \x1cw
5523 expect zsb-pin 20000
5524 settle 500 15000
5525 send q
5526 waitexit 10000
5527 EOF
5528 RC=$?
5529 set -e
5530 unwatch_clients "$OUT.zswatch"
5531 ZSATT_AFTER=$(attaches_now "$SOCK37")
5532 [ "$RC" -eq 0 ] || {
5533 echo "e2e FAIL: zoom skip: ptyclient leg exited $RC (did \\x1cn and \\x1cl move the zoom?):"
5534 cat "$OUT.zspc"; exit 1; }
5535 # Where each marker landed. `muxa capture` reads the session's own grid, so
5536 # none of this can be an echo of what this script typed at a terminal.
5537 timeout 20 "$MUXA" capture --sock "$SOCK37" --session a > "$OUT.zsfa" 2>&1
5538 timeout 20 "$MUXA" capture --sock "$SOCK37" --session b > "$OUT.zsfb" 2>&1
5539 #
5540 # The markers are numbered rather than named after their sessions, and
5541 # deliberately so: `zs-b` and `zs-back` were the first spelling, and `zs-b`
5542 # matched inside `zs-back` — a negative grep that could never fail, in the
5543 # leg whose whole content is negative greps. Numbered words share no
5544 # prefix, which is the property this needs.
5545 for _m in zs-one zs-three; do
5546 grep -q "$_m" "$OUT.zsfa" || {
5547 echo "e2e FAIL: zoom skip: session a never got $_m:"
5548 cat "$OUT.zsfa"; exit 1; }
5549 grep -q "$_m" "$OUT.zsfb" && {
5550 echo "e2e FAIL: zoom skip: $_m reached session b, which the zoom had left:"
5551 cat "$OUT.zsfb"; exit 1; }
5552 done
5553 grep -q "zs-two" "$OUT.zsfb" || {
5554 echo "e2e FAIL: zoom skip: Ctrl-\\ n did not put the zoom on session b:"
5555 cat "$OUT.zsfb"; exit 1; }
5556 grep -q "zs-two" "$OUT.zsfa" && {
5557 echo "e2e FAIL: zoom skip: zs-two reached session a, so the zoom never moved:"
5558 cat "$OUT.zsfa"; exit 1; }
5559 # The whole point, said by the daemon: two tiles, two attaches, and never a
5560 # third across a zoom, a skip to the other tile, a skip back and an unzoom.
5561 # This is the leg the counter matters most in — the two moves it makes are
5562 # exactly the ones a gauge could not tell from a re-dial, since a connection
5563 # that closes as another opens never shows two at once.
5564 assert_attach_delta "$ZSATT_BEFORE" "$ZSATT_AFTER" 2 "zoom skip"
5565 assert_never_two_clients "$OUT.zswatch" a "zoom skip"
5566 assert_never_two_clients "$OUT.zswatch" b "zoom skip"
5567 # Both promoted, both still promoted after the demote. 92 is this leg's own
5568 # width so the number cannot be another scenario's leftover.
5569 for _s in a b; do
5570 timeout 20 "$MUXA" status --sock "$SOCK37" --session "$_s" > "$OUT.zsst$_s" 2>&1
5571 grep -q '"cols":92' "$OUT.zsst$_s" || {
5572 echo "e2e FAIL: zoom skip: session $_s did not keep the size its promote claimed:"
5573 cat "$OUT.zsst$_s"; exit 1; }
5574 done
5575 assert_stopped "$SOCK37" "$D34PID" "zoom skip" "$OUT.zsstop"
5576 D34PID=""
5577 ok "the zoom skips between tiles with Ctrl-\\ n / Ctrl-\\ l, and the daemon sees no new attach"
5578
5579 # ---- an unzoomed wall forwards nothing ---------------------------------
5580 #
5581 # The teeth of the new invariant. "An unzoomed tile claims nothing" is not
5582 # kept by the tile reading 0x0 — a demoted slot deliberately keeps its
5583 # promoted size — it is kept by the wall never forwarding a keystroke while
5584 # it is zoomed out. So the assertion is a marker that must NOT appear.
5585 #
5586 # A negative grep passes just as well when the leg never ran, and this suite
5587 # has been bitten by that before, so the same leg carries its own anchor:
5588 # the SAME session, the SAME terminal, one zoom later, gets a marker that
5589 # MUST appear. Both halves read one `muxa capture`. If the wall never came
5590 # up, or the ptyclient script fell over early, the positive fails loudly
5591 # and the negative's silence is never mistaken for evidence.
5592 #
5593 # Every letter of `wallmute` is one the unzoomed wall SWALLOWS — not one is
5594 # a selection key. That is the whole reason the word is spelled this way,
5595 # and it was measured rather than assumed: the first spelling was
5596 # `wallnope`, whose `n` and `p` the wall consumes as selection keys, so what
5597 # a leak would actually have delivered is `wallne` — and a grep for
5598 # `wallnope` could not have matched even with the wall forwarding
5599 # everything. A negative that cannot fail is not a test.
5600 #
5601 # None of these bytes may reach a shell, where ECHO would put them on the
5602 # grid even without a newline. No newline is sent, for a second reason: at
5603 # the wall, `\n` is Enter.
5604 "$MUXD" run --sock "$SOCK38" --shell /bin/sh > "$OUT.zm.d" 2>&1 &
5605 D35PID=$!
5606 wait_sock "$SOCK38" "$OUT.zm.d" "zoom-mute daemon never bound"
5607 { printf 'printf "zma-%%s\\n" pin\n'; sleep 2; printf '\034\034'; } | \
5608 timeout 40 "$MUX" --sock "$SOCK38" --session a > "$OUT.zma" 2> "$OUT.zma.err"
5609 wait_grid "$SOCK38" "zma-pin" "zoom mute: session a's marker" a
5610 { printf 'printf "zmb-%%s\\n" pin\n'; sleep 2; printf '\034\034'; } | \
5611 timeout 40 "$MUX" --sock "$SOCK38" --session b > "$OUT.zmb" 2> "$OUT.zmb.err"
5612 wait_grid "$SOCK38" "zmb-pin" "zoom mute: session b's marker" b
5613
5614 set +e
5615 timeout 90 "$PTYCLIENT" --cols 96 --rows 30 --out "$OUT.zmcap" --err "$OUT.zmcap.err" -- \
5616 "$MUX" wall "--sock $SOCK38#a" "--sock $SOCK38#b" > "$OUT.zmpc" 2>&1 <<'EOF'
5617 expect zmb-pin 20000
5618 settle 700 20000
5619 send wallmute
5620 settle 700 20000
5621 send 1
5622 settle 500 15000
5623 send \r
5624 settle 700 20000
5625 send printf 'zmzoom-%s\\n' ok\n
5626 expect zmzoom-ok 15000
5627 settle 400 15000
5628 send \x1cw
5629 expect zma-pin 20000
5630 settle 500 15000
5631 send q
5632 waitexit 10000
5633 EOF
5634 RC=$?
5635 set -e
5636 [ "$RC" -eq 0 ] || {
5637 echo "e2e FAIL: zoom mute: ptyclient leg exited $RC:"
5638 cat "$OUT.zmpc"; exit 1; }
5639 timeout 20 "$MUXA" capture --sock "$SOCK38" --session a > "$OUT.zmfa" 2>&1
5640 timeout 20 "$MUXA" capture --sock "$SOCK38" --session b > "$OUT.zmfb" 2>&1
5641 # The anchor first, so a leg that did not run cannot pass the negative by
5642 # having done nothing at all.
5643 grep -q "zmzoom-ok" "$OUT.zmfa" || {
5644 echo "e2e FAIL: zoom mute: the zoom never typed into session a, so the"
5645 echo " silent half below proves nothing:"
5646 cat "$OUT.zmfa"; cat "$OUT.zmpc"; exit 1; }
5647 for _s in a b; do
5648 grep -q "wallmute" "$OUT.zmf$_s" && {
5649 echo "e2e FAIL: zoom mute: the UNZOOMED wall forwarded keystrokes to session $_s:"
5650 cat "$OUT.zmf$_s"; exit 1; }
5651 done
5652 # ...and the tile nobody zoomed is untouched in the other sense too: still
5653 # 80 wide, the size it was created at with no tty in sight.
5654 timeout 20 "$MUXA" status --sock "$SOCK38" --session b > "$OUT.zmstb" 2>&1
5655 grep -q '"cols":80' "$OUT.zmstb" || {
5656 echo "e2e FAIL: zoom mute: an unzoomed tile moved session b's grid:"
5657 cat "$OUT.zmstb"; exit 1; }
5658 assert_stopped "$SOCK38" "$D35PID" "zoom mute" "$OUT.zmstop"
5659 D35PID=""
5660 ok "an unzoomed wall forwards nothing; the zoom one keystroke later does"
5661
5662 # ---- a tile whose pump has died still says something ---------------------
5663 #
5664 # Only pumps answer `repaint_gen`, so a tile whose pump has ENDED has
5665 # nobody to redraw it. That was survivable while the screen was cleared
5666 # once per zoom; phase 1 clears it on every `n`/`p`/`l` as well, and two
5667 # symptoms fell out of the same hole — observed in a live capture, not
5668 # inferred:
5669 #
5670 # * zooming a dead tile painted an entirely blank terminal. No cursor,
5671 # no label, no hint: indistinguishable from a hung multiplexer.
5672 # * after any zoom, a dead tile's stripe and bar never came back, so the
5673 # wall silently lost a row for the rest of its life.
5674 #
5675 # Both are the keyboard's to fix, because it is the only thread left. The
5676 # tile here is dead by the most ordinary route there is: a spelling naming
5677 # a session that does not exist. A wall tile attaches 0x0, a 0x0 attach
5678 # joins but never creates, so the daemon refuses it and that pump ends —
5679 # `[refused]`, the state the wall already had a word for.
5680 #
5681 # Deliberately NOT asserted: that a `[reconnecting]` tile gets the same
5682 # treatment. It must not — it still has a thread that will repaint its hot
5683 # replica within a poll timeout, and the keyboard drawing over that would
5684 # replace something true with something stale.
5685 "$MUXD" run --sock "$SOCK39" --shell /bin/sh > "$OUT.zd.d" 2>&1 &
5686 D36PID=$!
5687 wait_sock "$SOCK39" "$OUT.zd.d" "zoom-dead daemon never bound"
5688 { printf 'printf "zdlive-%%s\\n" pin\n'; sleep 2; printf '\034\034'; } | \
5689 timeout 40 "$MUX" --sock "$SOCK39" --session a > "$OUT.zda" 2> "$OUT.zda.err"
5690 wait_grid "$SOCK39" "zdlive-pin" "zoom dead: the live session's marker" a
5691
5692 set +e
5693 timeout 90 "$PTYCLIENT" --cols 90 --rows 24 --out "$OUT.zdcap" --err "$OUT.zdcap.err" -- \
5694 "$MUX" wall "--sock $SOCK39#a" "--sock $SOCK39#ghost" > "$OUT.zdpc" 2>&1 <<'EOF'
5695 expect zdlive-pin 20000
5696 settle 800 20000
5697 send 2
5698 settle 500 15000
5699 send \r
5700 settle 800 20000
5701 send \x1cw
5702 settle 800 20000
5703 send q
5704 waitexit 10000
5705 EOF
5706 RC=$?
5707 set -e
5708 [ "$RC" -eq 0 ] || {
5709 echo "e2e FAIL: zoom dead: ptyclient leg exited $RC:"
5710 cat "$OUT.zdpc"; exit 1; }
5711 # The dead tile narrated its own refusal at all, which is the state word
5712 # the bar can only have if the pump reached the refusal path.
5713 grep -q -- "--sock $SOCK39#ghost \[refused\]" "$OUT.zdcap" || {
5714 echo "e2e FAIL: zoom dead: the dead tile's bar never said [refused]"; exit 1; }
5715 # Zooming it put a line on the screen instead of nothing. The way out is
5716 # part of the assertion: a screen that says only "refused" and offers no
5717 # chord is the same dead end with better manners.
5718 grep -q "Ctrl-\\\\ w for the wall" "$OUT.zdcap" || {
5719 echo "e2e FAIL: zoom dead: zooming a dead tile painted a blank terminal"
5720 echo " (no label, no state, no way out)"; exit 1; }
5721 # ...and its bar came BACK after the unzoom. Counted, not merely present:
5722 # one paint is the wall's first draw, which happens before any zoom cleared
5723 # the screen and so proves nothing about coming back.
5724 _zd_bars=$(grep -o -- "--sock $SOCK39#ghost \[refused\]" "$OUT.zdcap" | wc -l)
5725 [ "$_zd_bars" -ge 2 ] || {
5726 echo "e2e FAIL: zoom dead: the dead tile's bar painted $_zd_bars time(s), so it"
5727 echo " never came back after the zoom cleared the screen:"
5728 cat "$OUT.zdpc"; exit 1; }
5729 # The live tile is unharmed by any of it — the wall is still a wall.
5730 grep -q -- "--sock $SOCK39#a \[up\]" "$OUT.zdcap" || {
5731 echo "e2e FAIL: zoom dead: the live tile's bar never said [up]"; exit 1; }
5732 assert_stopped "$SOCK39" "$D36PID" "zoom dead" "$OUT.zdstop"
5733 D36PID=""
5734 ok "a tile whose pump has died narrates its zoom and gets its stripe back"
5735
5736
5322 # The long-lived daemon has served every scenario that wanted it; stop it 5737 # The long-lived daemon has served every scenario that wanted it; stop it
5323 # NOW so its allocator verdict is written while the suite is still running 5738 # NOW so its allocator verdict is written while the suite is still running
5324 # and can say so. SIGTERM runs the clean-shutdown path, so the defer chain 5739 # and can say so. SIGTERM runs the clean-shutdown path, so the defer chain
@@ -5390,9 +5805,20 @@ DPID=""
5390 # subject is a full-screen application's redraws is one this suite has 5805 # subject is a full-screen application's redraws is one this suite has
5391 # never claimed to reproduce byte for byte. The 43rd is the client with no 5806 # never claimed to reproduce byte for byte. The 43rd is the client with no
5392 # terminal of its own, and no convergence point because it HAS no terminal: 5807 # terminal of its own, and no convergence point because it HAS no terminal:
5393 # there is no capture to converge, which is the entire point of the leg. 5808 # there is no capture to converge, which is the entire point of the leg. The
5394 [ "$OK_COUNT" = "43" ] || { 5809 # 44th is the zoom skipping between tiles, and no convergence point for the
5395 echo "e2e FAIL: $OK_COUNT scenario checkpoints ran, the pin says 43 —" 5810 # CLI wall's reason a fourth time: its subject is one terminal's capture
5811 # holding two sessions' bytes, plus a COUNT the daemon kept while nobody was
5812 # looking — neither of which is a grid. The 45th is its negative twin, the
5813 # unzoomed wall that forwards nothing, and no convergence point because what
5814 # it asserts on is the ABSENCE of bytes from a grid, which converging two
5815 # grids that agree about that absence would say nothing about. The 46th is
5816 # the tile whose pump died, and no convergence point because its subject is
5817 # a session that DOES NOT EXIST: what the leg reads is what a terminal was
5818 # told about a refused attach, and there is no grid on either side to
5819 # converge.
5820 [ "$OK_COUNT" = "46" ] || {
5821 echo "e2e FAIL: $OK_COUNT scenario checkpoints ran, the pin says 46 —"
5396 echo " a scenario was added (update the pin) or silently lost" 5822 echo " a scenario was added (update the pin) or silently lost"
5397 exit 1 5823 exit 1
5398 } 5824 }
@@ -5400,4 +5826,4 @@ DPID=""
5400 echo "e2e FAIL: $CONV_COUNT convergence points ran, the pin says 35" 5826 echo "e2e FAIL: $CONV_COUNT convergence points ran, the pin says 35"
5401 exit 1 5827 exit 1
5402 } 5828 }
5403 echo "e2e OK (43 scenarios, 35 convergence points)" 5829 echo "e2e OK (46 scenarios, 35 convergence points)"