a73x

55be7cab

test: the wall claims its rows, and a neighbour survives a scroll

a73x   2026-08-24 11:32

Commit message
test: the wall claims its rows, and a neighbour survives a scroll

The focus-skip leg now pins "rows":14 beside its cols assertion, and a
new two-tile scrollback leg asserts the unfocused neighbour's content
survives a focused tile's history page (no whole-screen clear after its
pin); scenario pin bumped.

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

test/e2e.sh
Old New
@@ -366,6 +366,12 @@ SOCK42="${TMPDIR:-/tmp}/muxd-e2e-wallappmouse-$$.sock"
366 D39PID="" 366 D39PID=""
367 ZMOUSESH="${TMPDIR:-/tmp}/mux-e2e-wallappmouse-$$.sh" 367 ZMOUSESH="${TMPDIR:-/tmp}/mux-e2e-wallappmouse-$$.sh"
368 368
369 # The scrollback-rect leg: a focused tile's history page must own only its
370 # sub-rect, so its daemon gets a socket of its own for the grid-size
371 # reason every wall leg has.
372 SOCK53="${TMPDIR:-/tmp}/muxd-e2e-sbrect-$$.sock"
373 D54PID=""
374
369 # The wall as attach HISTORY (phase 2). A daemon AND a state home of its 375 # The wall as attach HISTORY (phase 2). A daemon AND a state home of its
370 # own, for the dynamic-wall leg's reason turned up one notch: what these 376 # own, for the dynamic-wall leg's reason turned up one notch: what these
371 # two blocks read back is the wall FILE, and every `mux` in this suite now 377 # two blocks read back is the wall FILE, and every `mux` in this suite now
@@ -1307,6 +1313,7 @@ cleanup() {
1307 [ -S "$SOCK49" ] && "$MUXD" stop --sock "$SOCK49" 2>/dev/null || true 1313 [ -S "$SOCK49" ] && "$MUXD" stop --sock "$SOCK49" 2>/dev/null || true
1308 [ -S "$SOCK50" ] && "$MUXD" stop --sock "$SOCK50" 2>/dev/null || true 1314 [ -S "$SOCK50" ] && "$MUXD" stop --sock "$SOCK50" 2>/dev/null || true
1309 [ -S "$SOCK52" ] && "$MUXD" stop --sock "$SOCK52" 2>/dev/null || true 1315 [ -S "$SOCK52" ] && "$MUXD" stop --sock "$SOCK52" 2>/dev/null || true
1316 [ -S "$SOCK53" ] && "$MUXD" stop --sock "$SOCK53" 2>/dev/null || true
1310 1317
1311 # ---- the leak sweep (hygiene kit, 6a) ---- 1318 # ---- the leak sweep (hygiene kit, 6a) ----
1312 # Here rather than at the bottom of the file, which `set -e` reaches only 1319 # Here rather than at the bottom of the file, which `set -e` reaches only
@@ -1321,7 +1328,7 @@ cleanup() {
1321 "$D20PID" "$D21PID" "$D22PID" "$D23PID" "$D24PID" "$D25PID" \ 1328 "$D20PID" "$D21PID" "$D22PID" "$D23PID" "$D24PID" "$D25PID" \
1322 "$D26PID" "$D27PID" "$D28PID" "$D29PID" "$D30PID" "$D31PID" \ 1329 "$D26PID" "$D27PID" "$D28PID" "$D29PID" "$D30PID" "$D31PID" \
1323 "$D32PID" "$D33PID" "$D34PID" "$D35PID" "$D36PID" "$D37PID" \ 1330 "$D32PID" "$D33PID" "$D34PID" "$D35PID" "$D36PID" "$D37PID" \
1324 "$D38PID" "$D39PID" "$D40PID" "$D41PID" "$D42PID" "$D43PID" 1331 "$D38PID" "$D39PID" "$D40PID" "$D41PID" "$D42PID" "$D43PID" "$D54PID"
1325 _leak=0 1332 _leak=0
1326 leak_sweep "$_rc" || _leak=1 1333 leak_sweep "$_rc" || _leak=1
1327 1334
@@ -1388,7 +1395,9 @@ cleanup() {
1388 "$OUT.whlstop" "$OUT.msestop" 1395 "$OUT.whlstop" "$OUT.msestop"
1389 rm -f "$OUT.pgr" "$OUT.pgr.err" "$OUT.pgr.log" "$OUT.pgr.d" "$LESSSH" \ 1396 rm -f "$OUT.pgr" "$OUT.pgr.err" "$OUT.pgr.log" "$OUT.pgr.d" "$LESSSH" \
1390 "$LESSDATA" "$OUT.pgrcap" "$OUT.pgrstop" "$SOCK35" \ 1397 "$LESSDATA" "$OUT.pgrcap" "$OUT.pgrstop" "$SOCK35" \
1391 "$OUT.pipe" "$OUT.pipe.d" "$OUT.pipecap" "$OUT.pipestop" "$SOCK36" 1398 "$OUT.pipe" "$OUT.pipe.d" "$OUT.pipecap" "$OUT.pipestop" "$SOCK36" \
1399 "$OUT.sb2.d" "$OUT.sb2a" "$OUT.sb2a.err" "$OUT.sb2b" "$OUT.sb2b.err" \
1400 "$OUT.sb2cap" "$OUT.sb2cap.err" "$OUT.sb2pc" "$OUT.sb2stop" "$SOCK53"
1392 # ...and the non-tty capture that leg's session feeds. 1401 # ...and the non-tty capture that leg's session feeds.
1393 rm -f "$OUT.nogate" 1402 rm -f "$OUT.nogate"
1394 # ...and its other half: the paste capture and the file nvim wrote, which 1403 # ...and its other half: the paste capture and the file nvim wrote, which
@@ -5704,13 +5713,18 @@ grep -q "zs-two" "$OUT.zsfa" && {
5704 assert_attach_delta "$ZSATT_BEFORE" "$ZSATT_AFTER" 2 "focus skip" 5713 assert_attach_delta "$ZSATT_BEFORE" "$ZSATT_AFTER" 2 "focus skip"
5705 assert_never_two_clients "$OUT.zswatch" a "focus skip" 5714 assert_never_two_clients "$OUT.zswatch" a "focus skip"
5706 assert_never_two_clients "$OUT.zswatch" b "focus skip" 5715 assert_never_two_clients "$OUT.zswatch" b "focus skip"
5707 # Both tiles claimed their rect: 92 cols each (this leg's own width), the 5716 # Both tiles claimed their rect: 92 cols each (this leg's own width) and 14
5708 # size the wall attached at. A focus move resizes nothing. 5717 # rows each (30 terminal rows cut into two stripes of 15, minus one label
5718 # bar each — layoutStripes' arithmetic, held to the size the wall attached
5719 # at). A focus move resizes nothing.
5709 for _s in a b; do 5720 for _s in a b; do
5710 timeout 20 "$MUXA" status --sock "$SOCK37" --session "$_s" > "$OUT.zsst$_s" 2>&1 5721 timeout 20 "$MUXA" status --sock "$SOCK37" --session "$_s" > "$OUT.zsst$_s" 2>&1
5711 grep -q '"cols":92' "$OUT.zsst$_s" || { 5722 grep -q '"cols":92' "$OUT.zsst$_s" || {
5712 echo "e2e FAIL: focus skip: session $_s is not at the width its tile claimed:" 5723 echo "e2e FAIL: focus skip: session $_s is not at the width its tile claimed:"
5713 cat "$OUT.zsst$_s"; exit 1; } 5724 cat "$OUT.zsst$_s"; exit 1; }
5725 grep -q '"rows":14' "$OUT.zsst$_s" || {
5726 echo "e2e FAIL: focus skip: session $_s is not at the height its tile claimed:"
5727 cat "$OUT.zsst$_s"; exit 1; }
5714 done 5728 done
5715 assert_stopped "$SOCK37" "$D34PID" "focus skip" "$OUT.zsstop" 5729 assert_stopped "$SOCK37" "$D34PID" "focus skip" "$OUT.zsstop"
5716 D34PID="" 5730 D34PID=""
@@ -7343,6 +7357,77 @@ assert_stopped "$SOCK52" "$D52PID" "cursor-ownership" "$OUT.custop"
7343 D52PID="" 7357 D52PID=""
7344 ok "the cursor rests in the focused tile, whichever pump painted last" 7358 ok "the cursor rests in the focused tile, whichever pump painted last"
7345 7359
7360 # ---- a focused tile's scrollback page owns only its sub-rect ----------
7361 #
7362 # The one-tile wheel leg above has no label bar, so its geometry is the
7363 # plain client's: the one paint at row 0 that DOES own the whole screen.
7364 # That exemption is what let a scrollback page at a row_off through that
7365 # opened with `\x1b[H\x1b[2J` — clearing the whole terminal and painting
7366 # history at row 1, while the exit path repaints only the focused tile's
7367 # own rect, so every neighbour stayed blank. This leg is the twin that
7368 # catches it: two tiles over 30 rows, the focused one scrolls back on the
7369 # wheel, and the neighbour's pin must still be the last content painted in
7370 # its rows, with no whole-screen clear following it.
7371 #
7372 # Two sessions on one daemon. The focused tile (a, the first spelling)
7373 # fills a scrollback the wheel can page into; the neighbour (b) holds a
7374 # pin and is never touched again. Both shells are /bin/sh and the content
7375 # is typed, the focus-skip leg's pattern — the neighbour's pin is the
7376 # session's own output, not an echo of the command that made it.
7377 "$MUXD" run --sock "$SOCK53" --shell /bin/sh > "$OUT.sb2.d" 2>&1 &
7378 D54PID=$!
7379 wait_sock "$SOCK53" "$OUT.sb2.d" "scrollback-rect daemon never bound"
7380
7381 # Session a: 100 lines of history, then cat. The wall attaches only once
7382 # seq has finished, so the page the wheel fetches is content this tile was
7383 # never sent live — the wheel leg's reason, verbatim.
7384 pipe_mux "$OUT.sb2a" "$OUT.sb2a.err" timeout 40 "$MUX" --sock "$SOCK53" --session a
7385 pipe_send 'seq 1 100 | sed '\''s/^/wln/'\''; exec cat\n'
7386 await_out "$OUT.sb2a" "wln100" "scrollback-rect: session a never finished seq"
7387 pipe_detach
7388 wait_grid "$SOCK53" "wln100" "scrollback-rect: session a's tail" a
7389
7390 # Session b: a pin the capture can grep for, then cat. `printf "sb2%s\n"
7391 # nbr` so the pin is the output, not the echoed command — the focus-skip
7392 # leg's prefix trick.
7393 pipe_mux "$OUT.sb2b" "$OUT.sb2b.err" timeout 40 "$MUX" --sock "$SOCK53" --session b
7394 pipe_send 'printf "sb2%%s\\n" nbr; exec cat\n'
7395 await_out "$OUT.sb2b" "sb2nbr" "scrollback-rect: session b's pin never reached the client"
7396 pipe_detach
7397 wait_grid "$SOCK53" "sb2nbr" "scrollback-rect: session b's pin" b
7398
7399 set +e
7400 timeout 90 "$PTYCLIENT" --cols 92 --rows 30 --out "$OUT.sb2cap" --err "$OUT.sb2cap.err" -- \
7401 "$MUX" wall "--sock $SOCK53#a" "--sock $SOCK53#b" > "$OUT.sb2pc" 2>&1 <<'EOF'
7402 expect sb2nbr 20000
7403 settle 700 20000
7404 send \x1b[<64;10;5M\x1b[<64;10;5M\x1b[<64;10;5M\x1b[<64;10;5M\x1b[<64;10;5M\x1b[<64;10;5M\x1b[<64;10;5M\x1b[<64;10;5M
7405 expect \x1b[7m[scroll] 15000
7406 settle 500 15000
7407 send \x1cd
7408 waitexit 10000
7409 EOF
7410 RC=$?
7411 set -e
7412 [ "$RC" -eq 0 ] || {
7413 echo "e2e FAIL: scrollback-rect: ptyclient leg exited $RC (did the focused wheel scroll?):"
7414 cat "$OUT.sb2pc"; exit 1; }
7415 # The neighbour's pin is still the last content painted in its rows: the
7416 # capture holds it, and nothing after it cleared the whole screen. A
7417 # scrollback page that owned the whole terminal (the bug) would open with
7418 # `\x1b[H\x1b[2J` right here, wiping the neighbour's rows; the fix clears
7419 # per row inside the focused tile's sub-rect only.
7420 grep -qaF "sb2nbr" "$OUT.sb2cap" || {
7421 echo "e2e FAIL: scrollback-rect: the neighbour's pin left the capture:"
7422 cat "$OUT.sb2pc"; exit 1; }
7423 _sb2last=$(grep -aboF -- "sb2nbr" "$OUT.sb2cap" | tail -1 | cut -d: -f1)
7424 tail -c +$((_sb2last + 1)) "$OUT.sb2cap" | grep -aF -- "$(printf '\x1b[2J')" && {
7425 echo "e2e FAIL: scrollback-rect: a whole-screen clear followed the neighbour's paint:"
7426 cat "$OUT.sb2pc"; exit 1; }
7427 assert_stopped "$SOCK53" "$D54PID" "scrollback-rect" "$OUT.sb2stop"
7428 D54PID=""
7429 ok "a focused tile's scrollback page owns only its sub-rect, and the neighbour's pin stays"
7430
7346 # The long-lived daemon has served every scenario that wanted it; stop it 7431 # The long-lived daemon has served every scenario that wanted it; stop it
7347 # NOW so its allocator verdict is written while the suite is still running 7432 # NOW so its allocator verdict is written while the suite is still running
7348 # and can say so. SIGTERM runs the clean-shutdown path, so the defer chain 7433 # and can say so. SIGTERM runs the clean-shutdown path, so the defer chain
@@ -7488,8 +7573,8 @@ DPID=""
7488 # it is the one leg that holds an ssh-agent under SIGSTOP, and a trap that 7573 # it is the one leg that holds an ssh-agent under SIGSTOP, and a trap that
7489 # has to CONT before it kills is cheaper to reason about with nothing 7574 # has to CONT before it kills is cheaper to reason about with nothing
7490 # after it. 7575 # after it.
7491 [ "$OK_COUNT" = "62" ] || { 7576 [ "$OK_COUNT" = "63" ] || {
7492 echo "e2e FAIL: $OK_COUNT scenario checkpoints ran, the pin says 62 —" 7577 echo "e2e FAIL: $OK_COUNT scenario checkpoints ran, the pin says 63 —"
7493 echo " a scenario was added (update the pin) or silently lost" 7578 echo " a scenario was added (update the pin) or silently lost"
7494 exit 1 7579 exit 1
7495 } 7580 }
@@ -7497,4 +7582,4 @@ DPID=""
7497 echo "e2e FAIL: $CONV_COUNT convergence points ran, the pin says 37" 7582 echo "e2e FAIL: $CONV_COUNT convergence points ran, the pin says 37"
7498 exit 1 7583 exit 1
7499 } 7584 }
7500 echo "e2e OK (61 scenarios, 37 convergence points)" 7585 echo "e2e OK (62 scenarios, 37 convergence points)"