a73x

test/e2e_12_panes.sh

Ref:   Size: 64.4 KiB   History

# shellcheck shell=sh
# e2e_12_panes.sh — sourced by test/e2e.sh after e2e_lib.sh. Scenarios run in
# the order they stand in; see the lib's header for what this file may
# assume and what it must register.
# The scrollback-rect leg: a focused tile's history page must own only its
# sub-rect, so its daemon gets a socket of its own for the grid-size
# reason every wall leg has.
SOCK53="${TMPDIR:-/tmp}/muxd-e2e-sbrect-$$.sock"
defer_sock "$SOCK53"
SB2STATE="${TMPDIR:-/tmp}/mux-e2e-sbrect-state-$$"
defer_rm "$SB2STATE"
SOCK54="${TMPDIR:-/tmp}/muxd-e2e-fullscreen-$$.sock"
defer_sock "$SOCK54"
FSSTATE="${TMPDIR:-/tmp}/mux-e2e-fullscreen-state-$$"
defer_rm "$FSSTATE"
SOCK55="${TMPDIR:-/tmp}/muxd-e2e-resize-$$.sock"
defer_sock "$SOCK55"
RSZSTATE="${TMPDIR:-/tmp}/mux-e2e-resize-state-$$"
defer_rm "$RSZSTATE"
SOCK56="${TMPDIR:-/tmp}/muxd-e2e-spanclear-$$.sock"
defer_sock "$SOCK56"
SPCSTATE="${TMPDIR:-/tmp}/mux-e2e-spanclear-state-$$"
defer_rm "$SPCSTATE"
SOCK57="${TMPDIR:-/tmp}/muxd-e2e-hjkl-$$.sock"
defer_sock "$SOCK57"
HJKSTATE="${TMPDIR:-/tmp}/mux-e2e-hjkl-state-$$"
defer_rm "$HJKSTATE"
SOCK58="${TMPDIR:-/tmp}/muxd-e2e-splitbirth-$$.sock"
defer_sock "$SOCK58"
SPLSTATE="${TMPDIR:-/tmp}/mux-e2e-splitbirth-state-$$"
defer_rm "$SPLSTATE"
SOCK59="${TMPDIR:-/tmp}/muxd-e2e-lprrestore-$$.sock"
defer_sock "$SOCK59"
SOCK60="${TMPDIR:-/tmp}/muxd-e2e-lpheal-$$.sock"
defer_sock "$SOCK60"
SOCK61="${TMPDIR:-/tmp}/muxd-e2e-lpdegrade-$$.sock"
defer_sock "$SOCK61"
SOCK62="${TMPDIR:-/tmp}/muxd-e2e-gonepanes-$$.sock"
defer_sock "$SOCK62"

# --- the cursor rests in the focused tile, whoever painted last ----------
# The cursor-ownership leg: a daemon of its own, for the grid-size reason
# every wall leg has — the assertion is about where the cursor RESTS, and a
# session another block created would change the tile it rests in.
SOCK52="${TMPDIR:-/tmp}/muxd-e2e-cursor-$$.sock"
defer_sock "$SOCK52"
CUSTATE="${TMPDIR:-/tmp}/mux-e2e-cursor-state-$$"
defer_rm "$CUSTATE"
#
# Every painter ends by showing the cursor where its own stripe sits, so
# without an owner the visible cursor lands on whichever pump painted last:
# focus on tile 1, a prompt redraw in tile 2, and the eye is told 2 is live
# while the keys go elsewhere. The focused tile's paint now records its
# screen cursor in Shared, and an unfocused paint's last act is to put the
# cursor back there. This leg focuses tile 3 of a three-tile wall and
# asserts the terminal's final cursor position is in tile 3's stripe —
# deterministic, because every paint now ends at the focused tile's cursor.
#
# Three sessions on a daemon of its own — the daemon's own `0` and two
# named ones — each marked by its shell before the wall attaches (the
# shell-expanded marker trick: a hit is the shell's work, never an echo of
# anything typed here), and all three written into a layout, because the
# file is the wall and a session no leaf names gets no stripe. The
# ptyclient wall focuses tile 3, settles, and detaches; the capture's last
# cursor-position escape is the witness.
start_daemon "$SOCK52" "$OUT.cu.d" "cursor-ownership daemon never bound" --shell /bin/sh
D52PID=$DPID

pipe_mux "$OUT.cua" "$OUT.cua.err" env XDG_STATE_HOME="$CUSTATE" timeout 40 "$MUX" --sock "$SOCK52"
pipe_send 'printf "ma-%%s\\n" pin\n'
await_out "$OUT.cua" "ma-pin" "cursor: session a's marker never reached the client"
pipe_detach
wait_grid "$SOCK52" "ma-pin" "cursor: session a's marker"
pipe_mux "$OUT.cub" "$OUT.cub.err" env XDG_STATE_HOME="$CUSTATE" timeout 40 "$MUX" --sock "$SOCK52" --session b
pipe_send 'printf "mb-%%s\\n" pin\n'
await_out "$OUT.cub" "mb-pin" "cursor: session b's marker never reached the client"
pipe_detach
wait_grid "$SOCK52" "mb-pin" "cursor: session b's marker" b
pipe_mux "$OUT.cuc" "$OUT.cuc.err" env XDG_STATE_HOME="$CUSTATE" timeout 40 "$MUX" --sock "$SOCK52" --session c
pipe_send 'printf "mc-%%s\\n" pin\n'
await_out "$OUT.cuc" "mc-pin" "cursor: session c's marker never reached the client"
pipe_detach
wait_grid "$SOCK52" "mc-pin" "cursor: session c's marker" c

set +e
seed_layout "$CUSTATE" stacked "--sock $SOCK52#0" "--sock $SOCK52#b" "--sock $SOCK52#c"
XDG_STATE_HOME="$CUSTATE" timeout 40 "$PTYCLIENT" --cols 40 --rows 30 --out "$OUT.cucap" --err "$OUT.cucap.err" -- \
    "$MUX" --sock "$SOCK52" > "$OUT.cupc" 2>&1 <<'EOF'
expect mc-pin 20000
settle 500 15000
send \x1c3
settle 500 15000
send \x1cd
waitexit 10000
EOF
RC=$?
set -e
rc0 "cursor-ownership: ptyclient leg exited $RC:" "$OUT.cupc" "$OUT.cucap.err"
# WHICH rows tile 3's stripe owns is read off the capture rather than
# counted. The layout's leaves are seated in file order, so the third digit
# IS the third stripe down — but that is `seedLayout`'s claim, not this
# one's, and a leg that hard-codes rows 21-30 fails for whichever reason
# comes first. Tile 3's own label bar's CUP row is the top of its stripe,
# and the nine rows under that are its content (30 rows over three tiles,
# one of each ten on the bar).
_bar3=$(grep -ao $'\x1b\\[[0-9][0-9]*;1H\x1b\\[7m 3> ' "$OUT.cucap" | tail -1)
_bar_row=${_bar3#??}
_bar_row=${_bar_row%%;*}
[ -n "$_bar_row" ] || {
    echo "e2e FAIL: cursor-ownership: tile 3's focused label bar never painted,"
    echo "          so there is no stripe to hold the cursor to:"
    cat "$OUT.cucap"; exit 1; }
# The focused tile's paint records its cursor; an unfocused paint's last act
# is a bare CUP putting it back there, with no show of its own. So the
# terminal's final cursor position — the last CUP in the capture, show or
# none — rests in tile 3's stripe whichever pump painted last. The last
# CUP+show would be the last painter's OWN cursor (racy); the last CUP of
# any kind is the focused tile's, and that is what the fix owns.
_cup_re=$'\x1b\\[[0-9][0-9]*;[0-9][0-9]*H'
_last_cup=$(grep -ao "$_cup_re" "$OUT.cucap" | tail -1)
# _last_cup is ESC[<row>;<col>H; peel the ESC[ prefix and the ;colH suffix.
_cur_row=${_last_cup#??}
_cur_row=${_cur_row%%;*}
[ -n "$_cur_row" ] && [ "$_cur_row" -gt "$_bar_row" ] && \
    [ "$_cur_row" -le $((_bar_row + 9)) ] || {
    echo "e2e FAIL: cursor-ownership: the last CUP was '$_last_cup' (row ${_cur_row:-none}),"
    echo "          want tile 3's stripe (rows $((_bar_row + 1))-$((_bar_row + 9))):"
    cat "$OUT.cucap"; exit 1; }
assert_stopped "$SOCK52" "$D52PID" "cursor-ownership" "$OUT.custop"
D52PID=""
ok "the cursor rests in the focused tile, whichever pump painted last"

# ---- a focused tile's scrollback page owns only its sub-rect ----------
#
# The one-tile wheel leg above has no label bar, so its geometry is the
# plain client's: the one paint at row 0 that DOES own the whole screen.
# That exemption is what let a scrollback page at a row_off through that
# opened with `\x1b[H\x1b[2J` — clearing the whole terminal and painting
# history at row 1, while the exit path repaints only the focused tile's
# own rect, so every neighbour stayed blank. This leg is the twin that
# catches it: two tiles over 30 rows, the focused one scrolls back on the
# wheel, and the neighbour's pin must still be the last content painted in
# its rows, with no whole-screen clear following it.
#
# Two sessions on one daemon. The focused tile (a, the first spelling)
# fills a scrollback the wheel can page into; the neighbour (b) holds a
# pin and is never touched again. Both shells are /bin/sh and the content
# is typed, the focus-skip leg's pattern — the neighbour's pin is the
# session's own output, not an echo of the command that made it.
start_daemon "$SOCK53" "$OUT.sb2.d" "scrollback-rect daemon never bound" --shell /bin/sh
D54PID=$DPID

# Session a: 100 lines of history, then cat. The wall attaches only once
# seq has finished, so the page the wheel fetches is content this tile was
# never sent live — the wheel leg's reason, verbatim.
pipe_mux "$OUT.sb2a" "$OUT.sb2a.err" env XDG_STATE_HOME="$SB2STATE" timeout 40 "$MUX" --sock "$SOCK53"
pipe_send 'seq 1 100 | sed '\''s/^/wln/'\''; exec cat\n'
await_out "$OUT.sb2a" "wln100" "scrollback-rect: session a never finished seq"
pipe_detach
wait_grid "$SOCK53" "wln100" "scrollback-rect: session a's tail"

# Session b: a pin the capture can grep for, then cat. `printf "sb2%s\n"
# nbr` so the pin is the output, not the echoed command — the focus-skip
# leg's prefix trick.
pipe_mux "$OUT.sb2b" "$OUT.sb2b.err" env XDG_STATE_HOME="$SB2STATE" timeout 40 "$MUX" --sock "$SOCK53" --session b
pipe_send 'printf "sb2%%s\\n" nbr; exec cat\n'
await_out "$OUT.sb2b" "sb2nbr" "scrollback-rect: session b's pin never reached the client"
pipe_detach
wait_grid "$SOCK53" "sb2nbr" "scrollback-rect: session b's pin" b

set +e
seed_layout "$SB2STATE" stacked "--sock $SOCK53#0" "--sock $SOCK53#b"
XDG_STATE_HOME="$SB2STATE" timeout 90 "$PTYCLIENT" --cols 40 --rows 30 --out "$OUT.sb2cap" --err "$OUT.sb2cap.err" -- \
    "$MUX" --sock "$SOCK53" > "$OUT.sb2pc" 2>&1 <<'EOF'
expect sb2nbr 20000
settle 700 20000
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
expect \x1b[7m[scroll] 15000
settle 500 15000
send \x1cd
waitexit 10000
EOF
RC=$?
set -e
rc0 "scrollback-rect: ptyclient leg exited $RC (did the focused wheel scroll?):" "$OUT.sb2pc"
# The neighbour's pin is still the last content painted in its rows: the
# capture holds it, and nothing after it cleared the whole screen. A
# scrollback page that owned the whole terminal (the bug) would open with
# `\x1b[H\x1b[2J` right here, wiping the neighbour's rows; the fix clears
# per row inside the focused tile's sub-rect only.
grep -qaF "sb2nbr" "$OUT.sb2cap" || {
    echo "e2e FAIL: scrollback-rect: the neighbour's pin left the capture:"
    cat "$OUT.sb2pc"; exit 1; }
_sb2last=$(grep -aboF -- "sb2nbr" "$OUT.sb2cap" | tail -1 | cut -d: -f1)
tail -c +$((_sb2last + 1)) "$OUT.sb2cap" | grep -aF -- "$(printf '\x1b[2J')" && {
    echo "e2e FAIL: scrollback-rect: a whole-screen clear followed the neighbour's paint:"
    cat "$OUT.sb2pc"; exit 1; }
assert_stopped "$SOCK53" "$D54PID" "scrollback-rect" "$OUT.sb2stop"
D54PID=""
ok "a focused tile's scrollback page owns only its sub-rect, and the neighbour's pin stays"

# ---- fullscreen: f toggles, focus follows, restore -------------------
#
# `f` is a layout op, not a mode: the focused tile takes the whole
# terminal, every other gets 0x0. A 0x0 rect claims nothing, so hidden
# panes stay attached and paint nothing. Focus moves while fullscreened
# re-flatten, so Ctrl-\ 2 gives tile 2 the full rect. `f` again restores
# the real layout. This leg proves all three with `mux a capture` — the
# marker each session received names which tile owned the terminal.
#
# No convergence point: `mux a capture` reads the daemon's grid, not the
# terminal's, and the daemon's grid is the side channel this asserts on.
start_daemon "$SOCK54" "$OUT.fs.d" "fullscreen daemon never bound" --shell /bin/sh
D55PID=$DPID

pipe_mux "$OUT.fsa" "$OUT.fsa.err" env XDG_STATE_HOME="$FSSTATE" timeout 40 "$MUX" --sock "$SOCK54"
pipe_send 'printf "fs-%%s\\n" init-a\n'
await_out "$OUT.fsa" "fs-init-a" "fullscreen: session a's marker never reached the client"
pipe_detach
wait_grid "$SOCK54" "fs-init-a" "fullscreen: session a's marker"
pipe_mux "$OUT.fsb" "$OUT.fsb.err" env XDG_STATE_HOME="$FSSTATE" timeout 40 "$MUX" --sock "$SOCK54" --session b
pipe_send 'printf "fs-%%s\\n" init-b\n'
await_out "$OUT.fsb" "fs-init-b" "fullscreen: session b's marker never reached the client"
pipe_detach
wait_grid "$SOCK54" "fs-init-b" "fullscreen: session b's marker" b

set +e
# wide: 80x24 trips the aspect rule (80 >= 48 → beside)
seed_layout "$FSSTATE" beside "--sock $SOCK54#0" "--sock $SOCK54#b"
XDG_STATE_HOME="$FSSTATE" timeout 90 "$PTYCLIENT" --cols 80 --rows 24 --out "$OUT.fscap" --err "$OUT.fscap.err" -- \
    "$MUX" --sock "$SOCK54" > "$OUT.fspc" 2>&1 <<'EOF'
expect fs-init-b 20000
settle 700 20000
send \x1cf
settle 700 20000
send printf 'fs-%s\n' one\n
expect fs-one 10000
settle 400 15000
send \x1c2
settle 700 20000
send printf 'fs-%s\n' two\n
expect fs-two 10000
settle 400 15000
send \x1cf
settle 700 20000
send \x1cd
waitexit 10000
EOF
RC=$?
set -e
rc0 "fullscreen: ptyclient leg exited $RC:" "$OUT.fspc" "$OUT.fscap.err"
# fs-one landed in session a (the tile that was fullscreened first);
# fs-two landed in session b (the tile Ctrl-\ 2 moved the full rect to).
timeout 20 "$MUX" a capture --sock "$SOCK54" > "$OUT.fsfa" 2>&1
timeout 20 "$MUX" a capture --sock "$SOCK54" --session b > "$OUT.fsfb" 2>&1
grep -q "fs-one" "$OUT.fsfa" || {
    echo "e2e FAIL: fullscreen: session a never got fs-one:"
    cat "$OUT.fsfa"; exit 1; }
grep -q "fs-one" "$OUT.fsfb" && {
    echo "e2e FAIL: fullscreen: fs-one reached session b, which was hidden:"
    cat "$OUT.fsfb"; exit 1; }
grep -q "fs-two" "$OUT.fsfb" || {
    echo "e2e FAIL: fullscreen: Ctrl-\\ 2 did not move the full rect to session b:"
    cat "$OUT.fsfb"; exit 1; }
grep -q "fs-two" "$OUT.fsfa" && {
    echo "e2e FAIL: fullscreen: fs-two reached session a, which was hidden after the focus move:"
    cat "$OUT.fsfa"; exit 1; }
# What the human saw after Ctrl-\ 2: the `>` moved and the digits did not.
# A bar's digit is the chord that reaches that tile for its whole life, not
# a count of who holds the focus — the render oracle is the only witness,
# since the daemon's grid has no bars in it. The label is cut to the stripe
# at 80 columns beside, so the marker and the head of the spelling are what
# survive; the digit is what tells the two stripes apart.
"$RENDER" --cols 80 --rows 24 < "$OUT.fscap" > "$OUT.fsgrid" || {
    echo "e2e FAIL: fullscreen: render oracle failed"
    cat "$OUT.fsgrid"; exit 1; }
grep -q -- " 2> --sock" "$OUT.fsgrid" || {
    echo "e2e FAIL: fullscreen: Ctrl-\\ 2 did not leave the \`>\` on tile 2's digit:"
    cat "$OUT.fsgrid"; exit 1; }
grep -q -- " 1  --sock" "$OUT.fsgrid" || {
    echo "e2e FAIL: fullscreen: tile 1 lost its digit or kept the \`>\`:"
    cat "$OUT.fsgrid"; exit 1; }
assert_stopped "$SOCK54" "$D55PID" "fullscreen" "$OUT.fsstop"
D55PID=""
ok "fullscreen gives the focused tile the terminal; focus follows; f restores"

# ---- Ctrl-\ r trades cells between panes -----------------------------
#
# Two beside panes at 80x24 (aspect rule → beside). Ctrl-\ r enters
# resize mode; lll grows the focused pane's width by 3 cells, moving the
# rail 3 columns right. The rail is a reverse-video space painted at a
# CUP column; grep the column before and after. Esc exits resize mode,
# and typed prose lands in the focused session (marker echo).
#
# No convergence point: the rail column is a terminal-capture assertion,
# not a daemon-grid comparison.
start_daemon "$SOCK55" "$OUT.rsz.d" "resize daemon never bound" --shell /bin/sh
D56PID=$DPID

pipe_mux "$OUT.rsza" "$OUT.rsza.err" env XDG_STATE_HOME="$RSZSTATE" timeout 40 "$MUX" --sock "$SOCK55"
pipe_send 'printf "rsz-%%s\\n" init-a\n'
await_out "$OUT.rsza" "rsz-init-a" "resize: session a's marker never reached the client"
pipe_detach
wait_grid "$SOCK55" "rsz-init-a" "resize: session a's marker"
pipe_mux "$OUT.rszb" "$OUT.rszb.err" env XDG_STATE_HOME="$RSZSTATE" timeout 40 "$MUX" --sock "$SOCK55" --session b
pipe_send 'printf "rsz-%%s\\n" init-b\n'
await_out "$OUT.rszb" "rsz-init-b" "resize: session b's marker never reached the client"
pipe_detach
wait_grid "$SOCK55" "rsz-init-b" "resize: session b's marker" b

set +e
# 80x24: aspect rule makes beside; two panes split 39/40 with the rail
# at column 40 (1-indexed). Three l presses grow the left pane by 3,
# moving the rail to column 43.
seed_layout "$RSZSTATE" beside "--sock $SOCK55#0" "--sock $SOCK55#b"
XDG_STATE_HOME="$RSZSTATE" timeout 90 "$PTYCLIENT" --cols 80 --rows 24 --out "$OUT.rszcap" --err "$OUT.rszcap.err" -- \
    "$MUX" --sock "$SOCK55" > "$OUT.rszpc" 2>&1 <<'EOF'
expect rsz-init-b 20000
settle 700 20000
send \x1cr
settle 300 10000
send l
settle 300 10000
send l
settle 300 10000
send l
settle 500 15000
send \x1b
settle 300 10000
send printf 'rsz-%s\n' grown\n
expect rsz-grown 10000
settle 400 15000
send \x1cd
waitexit 10000
EOF
RC=$?
set -e
rc0 "resize: ptyclient leg exited $RC:" "$OUT.rszpc" "$OUT.rszcap.err"
# The rail paints as ESC[row;colH followed by ESC[7m (reverse video).
# Extract the column from every CUP that precedes a reverse-video
# space — the rail's column — and verify it moved right by 3.
# Before resize: rail at column 40 (1-indexed). After lll: column 43.
# All rail columns; the last one is the post-resize position.
_last_rail=$(rail_cols "$OUT.rszcap" | tail -1)
[ -n "$_last_rail" ] || {
    echo "e2e FAIL: resize: no rail glyph found in the capture:"
    cat "$OUT.rszcap"; exit 1; }
# 39 cols + rail at 40 initially; 42 cols + rail at 43 after lll.
[ "$_last_rail" -ge 43 ] || {
    echo "e2e FAIL: resize: rail column is $_last_rail, want >=43 (40 + 3 grows):"
    cat "$OUT.rszcap"; exit 1; }
# Esc ended resize mode and typed prose reached the focused session.
timeout 20 "$MUX" a capture --sock "$SOCK55" > "$OUT.rszsta" 2>&1
timeout 20 "$MUX" a capture --sock "$SOCK55" --session b > "$OUT.rszstb" 2>&1
if ! grep -q "rsz-grown" "$OUT.rszsta" && ! grep -q "rsz-grown" "$OUT.rszstb"; then
    echo "e2e FAIL: resize: rsz-grown reached neither session after Esc:"
    cat "$OUT.rszsta" "$OUT.rszstb"; exit 1
fi
assert_stopped "$SOCK55" "$D56PID" "resize" "$OUT.rszstop"
D56PID=""
ok "Ctrl-\\ r trades cells between panes; Esc returns to prose"

# ---- span-clear survival: the engine oracle -------------------------
#
# Two beside panes at 80x24 (aspect rule: cols >= 2*rows → beside). A
# marker in the RIGHT pane's session, then a flood in the LEFT pane's
# session (seq 1 200). The regression this catches forever is a whole-line
# clear (\x1b[2K) blanking the neighbor: a painter that emits too much
# still passes a byte grep, so the assertion replays the wall client's
# FULL captured stdout through the render fixture (an engine, not a grep)
# and checks the right pane's marker text survives on the replayed grid.
start_daemon "$SOCK56" "$OUT.spc.d" "span-clear daemon never bound" --shell /bin/sh
D57PID=$DPID

pipe_mux "$OUT.spca" "$OUT.spca.err" env XDG_STATE_HOME="$SPCSTATE" timeout 40 "$MUX" --sock "$SOCK56"
pipe_send 'printf "spc-%%s\\n" left-flood\n'
await_out "$OUT.spca" "spc-left-flood" "span-clear: left session's marker never reached the client"
pipe_detach
wait_grid "$SOCK56" "spc-left-flood" "span-clear: left session's marker"
pipe_mux "$OUT.spcb" "$OUT.spcb.err" env XDG_STATE_HOME="$SPCSTATE" timeout 40 "$MUX" --sock "$SOCK56" --session b
pipe_send 'printf "spc-%%s\\n" right-survivor\n'
await_out "$OUT.spcb" "spc-right-survivor" "span-clear: right session's marker never reached the client"
pipe_detach
wait_grid "$SOCK56" "spc-right-survivor" "span-clear: right session's marker" b

set +e
# 80x24: aspect rule → beside. The wall client is a tty session: its
# stdout is the terminal's full byte stream, captured by ptyclient.
seed_layout "$SPCSTATE" beside "--sock $SOCK56#0" "--sock $SOCK56#b"
XDG_STATE_HOME="$SPCSTATE" timeout 90 "$PTYCLIENT" --cols 80 --rows 24 --out "$OUT.spccap" --err "$OUT.spccap.err" -- \
    "$MUX" --sock "$SOCK56" > "$OUT.spcpc" 2>&1 <<'EOF'
expect spc-right-survivor 20000
settle 700 20000
send \x1ch
settle 500 15000
send seq 1 200\n
expect 200 20000
settle 700 20000
send \x1cd
waitexit 10000
EOF
RC=$?
set -e
rc0 "span-clear: ptyclient leg exited $RC:" "$OUT.spcpc" "$OUT.spccap.err"
# The oracle: replay the captured stream through the render fixture at the
# same dimensions, producing the final grid text. The right pane's marker
# must still be on that grid — a span-clear regression blanks it.
"$RENDER" --cols 80 --rows 24 < "$OUT.spccap" > "$OUT.spcgrid" || {
    echo "e2e FAIL: span-clear: render fixture failed on the capture:"
    cat "$OUT.spcgrid"; exit 1; }
grep -q "spc-right-survivor" "$OUT.spcgrid" || {
    echo "e2e FAIL: span-clear: right pane's marker lost from the replayed grid:"
    echo "          a span-clear regression blanked the neighbor:"
    cat "$OUT.spcgrid"; exit 1; }
assert_stopped "$SOCK56" "$D57PID" "span-clear" "$OUT.spcstop"
D57PID=""
ok "a span-clear flood in the left pane cannot blank the right pane's grid"

# ---- hjkl focus walk -------------------------------------------------
#
# One tile, split right (births a second session), split below (births a
# third). Then walk focus with hjkl and type a distinct marker after each
# move; mux a capture names which session received it. Covers at least a
# left move, a right move, and one vertical move.
#
# No convergence point: the assertion is per-session mux a capture, not a
# daemon-grid diff.
start_daemon "$SOCK57" "$OUT.hjk.d" "hjkl daemon never bound" --shell /bin/sh
D58PID=$DPID

pipe_mux "$OUT.hjkcap" "$OUT.hjkcap.err" env XDG_STATE_HOME="$HJKSTATE" timeout 40 "$MUX" --sock "$SOCK57"
pipe_send 'printf "hjk-%%s\\n" origin\n'
await_out "$OUT.hjkcap" "hjk-origin" "hjkl: origin marker never reached the client"
pipe_detach
wait_grid "$SOCK57" "hjk-origin" "hjkl: origin marker"

set +e
# 80x24: aspect rule → beside for the first split; the second split goes
# below, producing a 1 | (2 over 3) tree.
no_saved_tree "$HJKSTATE"
XDG_STATE_HOME="$HJKSTATE" timeout 90 "$PTYCLIENT" --cols 80 --rows 24 --out "$OUT.hjkcap" --err "$OUT.hjkcap.err" -- \
    "$MUX" --sock "$SOCK57" > "$OUT.hjkpc" 2>&1 <<'EOF'
expect hjk-origin 20000
settle 700 20000
send \x1c|
settle 1000 20000
send \x1c-
settle 1000 20000
send \x1ch
settle 500 15000
send printf 'hjk-%s\n' went-left\n
expect hjk-went-left 10000
settle 500 15000
send \x1cl
settle 500 15000
send printf 'hjk-%s\n' went-right\n
expect hjk-went-right 10000
settle 500 15000
send \x1cj
settle 500 15000
send printf 'hjk-%s\n' went-down\n
expect hjk-went-down 10000
settle 500 15000
send \x1cd
waitexit 10000
EOF
RC=$?
set -e
rc0 "hjkl: ptyclient leg exited $RC:" "$OUT.hjkpc" "$OUT.hjkcap.err"
# Three sessions exist now: a (the origin), and two birthed by splits.
# The daemon assigns numeric names to the new sessions; capture all three.
timeout 20 "$MUX" a capture --sock "$SOCK57" > "$OUT.hjkfa" 2>&1
timeout 20 "$MUX" a capture --sock "$SOCK57" --session 1 > "$OUT.hjkfb" 2>&1
timeout 20 "$MUX" a capture --sock "$SOCK57" --session 2 > "$OUT.hjkfc" 2>&1
# went-left: typed after \x1ch from the right pane → lands in session a.
grep -q "hjk-went-left" "$OUT.hjkfa" || {
    echo "e2e FAIL: hjkl: hjk-went-left not in session a (left focus move):"
    cat "$OUT.hjkfa"; exit 1; }
# went-right: typed after \x1cl from session a → lands in session 1.
grep -q "hjk-went-right" "$OUT.hjkfb" || {
    echo "e2e FAIL: hjkl: hjk-went-right not in session 1 (right focus move):"
    cat "$OUT.hjkfb"; exit 1; }
# went-down: typed after \x1cj from session 1 → lands in session 2.
grep -q "hjk-went-down" "$OUT.hjkfc" || {
    echo "e2e FAIL: hjkl: hjk-went-down not in session 2 (down focus move):"
    cat "$OUT.hjkfc"; exit 1; }
assert_stopped "$SOCK57" "$D58PID" "hjkl" "$OUT.hjkstop"
D58PID=""
ok "hjkl walks focus across panes in both axes"

# ---- split births a session beside -----------------------------------
#
# From one pane, Ctrl-\ | splits right and births a new session. The
# daemon's session count rises to two, and the new pane's label bar sits
# at a column to the right of the rail (the rail paints as ESC[r;cH
# ESC[7m — the same idiom the resize leg uses).
start_daemon "$SOCK58" "$OUT.spl.d" "split-birth daemon never bound" --shell /bin/sh
D59PID=$DPID

pipe_mux "$OUT.splcap" "$OUT.splcap.err" env XDG_STATE_HOME="$SPLSTATE" timeout 40 "$MUX" --sock "$SOCK58"
pipe_send 'printf "spl-%%s\\n" origin\n'
await_out "$OUT.splcap" "spl-origin" "split-birth: origin marker never reached the client"
pipe_detach
wait_grid "$SOCK58" "spl-origin" "split-birth: origin marker"

set +e
# 80x24: aspect rule → beside. One tile starts, then Ctrl-\ | births a
# second session to the right.
no_saved_tree "$SPLSTATE"
XDG_STATE_HOME="$SPLSTATE" timeout 90 "$PTYCLIENT" --cols 80 --rows 24 --out "$OUT.splcap" --err "$OUT.splcap.err" -- \
    "$MUX" --sock "$SOCK58" > "$OUT.splpc" 2>&1 <<'EOF'
expect spl-origin 20000
settle 700 20000
send \x1c|
settle 1500 20000
send \x1cd
waitexit 10000
EOF
RC=$?
set -e
rc0 "split-birth: ptyclient leg exited $RC:" "$OUT.splpc" "$OUT.splcap.err"
# The daemon held its own default session and nothing else; the split
# birthed one more, so two live sessions proves the chord created one.
wait_sessions "$SOCK58" 2 "split-birth: split did not birth a new session"
# The rail paints as ESC[row;colH ESC[7m (reverse video). The new pane's
# label bar is at a column greater than the rail's column — it sits to
# the right of the separator, which is the whole point of beside.
_rail_col=$(rail_cols "$OUT.splcap" | head -1)
[ -n "$_rail_col" ] || {
    echo "e2e FAIL: split-birth: no rail glyph found in the capture:"
    cat "$OUT.splcap"; exit 1; }
# The rail sits at the boundary; the new pane's label bar is beyond it.
# A label bar is a CUP followed by text, not reverse-video. Find a CUP
# whose column exceeds the rail's — that is the right pane's bar start.
_bar_col=$(grep -ao $'\x1b\\[[0-9]*;[0-9]*H' "$OUT.splcap" | \
    sed 's/\x1b\[[0-9]*;\([0-9]*\)H/\1/' | sort -n | \
    awk -v r="$_rail_col" '$1 > r { print; exit }')
[ -n "$_bar_col" ] || {
    echo "e2e FAIL: split-birth: no label bar found right of the rail (col $_rail_col):"
    cat "$OUT.splcap"; exit 1; }
assert_stopped "$SOCK58" "$D59PID" "split-birth" "$OUT.splstop"
D59PID=""
ok "Ctrl-\\ | births a session beside; its label bar sits right of the rail"

# ---- layout sidecar: restore round-trip ------------------------------
#
# A no-argv `mux` opens the layout, and the resize writes the tree back to
# it. Reattaching with the same state home loads it verbatim
# — the resize survived the round trip. The rail's CUP column in the second
# capture is >= 43 (three right-resizes from a 40|40 split), and a marker
# typed into the restored wall lands in the focused session.
LPSTATE="${TMPDIR:-/tmp}/mux-e2e-lprrestore-state-$$"
defer_rm "$LPSTATE"
LPHOSTS="$LPSTATE/mux/hosts"
LPLAYOUT="$LPSTATE/mux/layout"
start_daemon "$SOCK59" "$OUT.lpr.d" "layout-restore daemon never bound" --shell /bin/sh
D60PID=$DPID

pipe_mux "$OUT.lpra" "$OUT.lpra.err" env XDG_STATE_HOME="$LPSTATE" timeout 40 "$MUX" --sock "$SOCK59"
pipe_send 'printf "lpr-%%s\\n" origin-0\n'
await_out "$OUT.lpra" "lpr-origin-0" "layout-restore: the default session's marker never reached the client"
pipe_detach
wait_grid "$SOCK59" "lpr-origin-0" "layout-restore: the default session's marker"
pipe_mux "$OUT.lprb" "$OUT.lprb.err" env XDG_STATE_HOME="$LPSTATE" timeout 40 "$MUX" --sock "$SOCK59" --session b
pipe_send 'printf "lpr-%%s\\n" origin-b\n'
await_out "$OUT.lprb" "lpr-origin-b" "layout-restore: session b marker never reached the client"
pipe_detach
wait_grid "$SOCK59" "lpr-origin-b" "layout-restore: session b marker" b

# Write the hosts file by hand: ONE line, the daemon — and the layout that
# names its two sessions, because the file is the wall and a pane is a leaf
# somebody wrote down. What this leg then measures is the RESIZE: run 1
# moves the boundary the seed cut, and run 2 has to open on the moved one.
mkdir -p "$LPSTATE/mux"
printf -- '--sock %s\n' "$SOCK59" > "$LPHOSTS"
seed_layout "$LPSTATE" beside "--sock $SOCK59#0" "--sock $SOCK59#b"

set +e
# First ptyclient: hydrate, focus pane b, move the boundary three cells,
# detach. The RESIZE is what saves the file — every change to the tree
# does, and the exit does not — so the focus move comes first, and the
# record the resize writes is the focus it finds. Shrinking the right
# pane's width grows its left neighbour, so the boundary still ends up
# three cells right of the seed's 40|40 and pane 0 still weighs 43.
XDG_STATE_HOME="$LPSTATE" timeout 90 "$PTYCLIENT" --cols 80 --rows 24 \
    --out "$OUT.lprcap1" --err "$OUT.lprcap1.err" -- \
    "$MUX" > "$OUT.lprpc1" 2>&1 <<'EOF'
expect lpr-origin-0 20000
settle 700 20000
send \x1cl
settle 500 5000
send \x1cr
settle 300 5000
send h
settle 300 5000
send h
settle 300 5000
send h
settle 500 5000
send \x1b
settle 500 5000
send \x1cd
waitexit 10000
EOF
RC=$?
set -e
rc0 "layout-restore: first ptyclient exited $RC:" "$OUT.lprpc1" "$OUT.lprcap1.err"

# The sidecar exists and its header is the format the spec mandates.
[ -f "$LPLAYOUT" ] || {
    echo "e2e FAIL: layout-restore: sidecar not saved at $LPLAYOUT"
    ls -la "$LPSTATE/mux/" 2>&1; exit 1; }
_head1=$(head -1 "$LPLAYOUT")
_head2=$(sed -n '2p' "$LPLAYOUT")
[ "$_head1" = "mux-layout 1" ] || {
    echo "e2e FAIL: layout-restore: sidecar header is '$_head1', want 'mux-layout 1'"
    cat "$LPLAYOUT"; exit 1; }
# A 2-pane beside at 80 cols: the root is `beside 0`.
case "$_head2" in
    "beside 0") ;;
    *) echo "e2e FAIL: layout-restore: sidecar root is '$_head2', want 'beside 0'"
       cat "$LPLAYOUT"; exit 1 ;;
esac
# The focus record: the focus was on pane b (leaf 1) when the resize saved.
grep -q '^focus 1$' "$LPLAYOUT" || {
    echo "e2e FAIL: layout-restore: sidecar carries no focus record"
    cat "$LPLAYOUT"; exit 1; }

set +e
# Second ptyclient: reattach. The sidecar restores the resized tree —
# the rail sits at column >= 43. Type a marker, then detach.
XDG_STATE_HOME="$LPSTATE" timeout 90 "$PTYCLIENT" --cols 80 --rows 24 \
    --out "$OUT.lprcap2" --err "$OUT.lprcap2.err" -- \
    "$MUX" > "$OUT.lprpc2" 2>&1 <<'EOF'
expect lpr-origin-0 20000
settle 1000 20000
send printf 'lpr-rt-%s\n' marker\n
expect lpr-rt-marker 10000
settle 500 15000
send \x1cd
waitexit 10000
EOF
RC=$?
set -e
rc0 "layout-restore: second ptyclient exited $RC:" "$OUT.lprpc2" "$OUT.lprcap2.err"
# The rail's CUP column in the second capture must be >= 43 — three
# right-resizes moved the boundary from 41 to 44.
_max_rail=$(rail_cols "$OUT.lprcap2" | tail -1)
[ -n "$_max_rail" ] || {
    echo "e2e FAIL: layout-restore: no rail glyph in the second capture:"
    cat "$OUT.lprcap2"; exit 1; }
[ "$_max_rail" -ge 43 ] || {
    echo "e2e FAIL: layout-restore: rail column $_max_rail < 43 (resize did not survive)"
    cat "$OUT.lprcap2"; exit 1; }
# The seed's whole point: the restored wall paints its saved cut FIRST
# and never moves. Run 2 does no resizing, so a second rail column in
# its capture is a re-cut the user watched (the old restore painted the
# default cut at ~40, then healed to 43+ two seconds in). The glyph is
# matched whole — CUP, reverse video, ONE space, reset — because a label
# bar starts with the same CUP+SGR at its pane's own left column, and
# `rail_cols`' looser prefix reads the right pane's bar as a rail.
_rail_count=$(grep -aoE $'\x1b\[[0-9]+;[0-9]+H\x1b\[7m \x1b\[0m' "$OUT.lprcap2" |
    sed -E 's/.*;([0-9]+)H.*/\1/' | sort -n | uniq | wc -l)
[ "$_rail_count" -eq 1 ] || {
    echo "e2e FAIL: layout-restore: $_rail_count rail columns in run 2 — the restored wall moved after it painted:"
    rail_cols "$OUT.lprcap2"; cat "$OUT.lprpc2"; exit 1; }
# The marker typed into the restored wall landed in session b — the
# focus was saved on pane b and restore put it back there, not tile 0.
timeout 20 "$MUX" a capture --sock "$SOCK59" --session b > "$OUT.lprfb" 2>&1
grep -q "lpr-rt-marker" "$OUT.lprfb" || {
    echo "e2e FAIL: layout-restore: marker not in session b (focus did not survive):"
    cat "$OUT.lprfb"; exit 1; }
timeout 20 "$MUX" a capture --sock "$SOCK59" > "$OUT.lprfa" 2>&1
! grep -q "lpr-rt-marker" "$OUT.lprfa" || {
    echo "e2e FAIL: layout-restore: marker leaked into the default session (focus was tile 0, not b):"
    cat "$OUT.lprfa"; exit 1; }
assert_stopped "$SOCK59" "$D60PID" "layout-restore" "$OUT.lprstop"
D60PID=""
rm -rf "$LPSTATE"
ok "a resized layout survives a detach/reattach round trip via the sidecar"

# ---- a poll GRADES the panes and adds none ---------------------------
#
# The drift a wall of daemons can actually suffer, and it is no longer a
# file being edited: a 2-pane layout is saved, and then one of those
# sessions ENDS while another is born on the same daemon. What the next
# poll may do about that is the whole claim, and it is now three things
# and no fourth:
#
#   * the survivor keeps its pane, its session and its weight.
#   * the pane whose session the list no longer names STANDS, wearing
#     `[session ended]` — the host answered and did not name it, which is
#     an offer to start it again and not a rect to take away.
#   * the newcomer joins NOTHING. It is a live session on a listed daemon
#     with no leaf naming it, so it is on no wall: not a tile, not a
#     stripe, and not a line in the file the detach writes.
#
# The third is the one this leg exists for. Until 2026-09-02 a poll's list
# was where tiles came from, so a session born anywhere — by another
# terminal, by a script, by `mux a` — appeared on every wall over that
# daemon and re-cut it. The file is the wall now; the poll only grades what
# the file already names.
LPHSTATE="${TMPDIR:-/tmp}/mux-e2e-lpheal-state-$$"
defer_rm "$LPHSTATE"
LPHHOSTS="$LPHSTATE/mux/hosts"
LPHLAYOUT="$LPHSTATE/mux/layout"
start_daemon "$SOCK60" "$OUT.lph.d" "layout-heal daemon never bound" --shell /bin/sh
D61PID=$DPID

pipe_mux "$OUT.lpha" "$OUT.lpha.err" env XDG_STATE_HOME="$LPHSTATE" timeout 40 "$MUX" --sock "$SOCK60"
pipe_send 'printf "lph-%%s\\n" survivor-0\n'
await_out "$OUT.lpha" "lph-survivor-0" "layout-heal: the default session's marker never reached the client"
pipe_detach
wait_grid "$SOCK60" "lph-survivor-0" "layout-heal: the default session's marker"
pipe_mux "$OUT.lphb" "$OUT.lphb.err" env XDG_STATE_HOME="$LPHSTATE" timeout 40 "$MUX" --sock "$SOCK60" --session b
pipe_send 'printf "lph-%%s\\n" doomed-b\n'
await_out "$OUT.lphb" "lph-doomed-b" "layout-heal: session b marker never reached the client"
pipe_detach
wait_grid "$SOCK60" "lph-doomed-b" "layout-heal: session b marker" b

# One line, the daemon, and a two-leaf layout over it: the wall run 1
# resizes and saves is authored here, not discovered by a poll.
mkdir -p "$LPHSTATE/mux"
printf -- '--sock %s\n' "$SOCK60" > "$LPHHOSTS"
seed_layout "$LPHSTATE" beside "--sock $SOCK60#0" "--sock $SOCK60#b"

set +e
XDG_STATE_HOME="$LPHSTATE" timeout 90 "$PTYCLIENT" --cols 80 --rows 24 \
    --out "$OUT.lphcap1" --err "$OUT.lphcap1.err" -- \
    "$MUX" > "$OUT.lphpc1" 2>&1 <<'EOF'
expect lph-survivor-0 20000
settle 700 20000
send \x1cr
settle 300 5000
send l
settle 300 5000
send l
settle 300 5000
send l
settle 500 5000
send \x1b
settle 500 5000
send \x1cd
waitexit 10000
EOF
RC=$?
set -e
rc0 "layout-heal: first ptyclient exited $RC:" "$OUT.lphpc1" "$OUT.lphcap1.err"
[ -f "$LPHLAYOUT" ] || {
    echo "e2e FAIL: layout-heal: sidecar not saved after first detach"
    ls -la "$LPHSTATE/mux/" 2>&1; exit 1; }

# Drift, and the daemon is the one that does it: session b ENDS and
# session c is born. The layout still describes [0, b] and it is still the
# wall — `0` binds, b's pane is graded gone, and c is a session on this
# daemon that this wall has never heard of. Nothing was edited; this is the
# only kind of drift a list of daemons can suffer.
pipe_mux "$OUT.lphb2" "$OUT.lphb2.err" env XDG_STATE_HOME="$LPHSTATE" timeout 40 "$MUX" --sock "$SOCK60" --session b
pipe_send 'exit 0\n'
pipe_waitexit "layout-heal: session b's shell"
pipe_mux "$OUT.lphc" "$OUT.lphc.err" env XDG_STATE_HOME="$LPHSTATE" timeout 40 "$MUX" --sock "$SOCK60" --session c
pipe_send 'printf "lph-%%s\\n" newcomer-c\n'
await_out "$OUT.lphc" "lph-newcomer-c" "layout-heal: session c marker never reached the client"
pipe_detach
wait_grid "$SOCK60" "lph-newcomer-c" "layout-heal: session c marker" c
wait_sessions "$SOCK60" 2 "layout-heal: the daemon should hold 0 and c"

set +e
XDG_STATE_HOME="$LPHSTATE" timeout 90 "$PTYCLIENT" --cols 80 --rows 24 \
    --out "$OUT.lphcap2" --err "$OUT.lphcap2.err" -- \
    "$MUX" > "$OUT.lphpc2" 2>&1 <<'EOF'
expect lph-survivor-0 20000
settle 2500 30000
send \x1c1
settle 500 15000
send printf 'lph-heal-%s\n' marker\n
expect lph-heal-marker 10000
settle 500 15000
send \x1cd
waitexit 10000
EOF
RC=$?
set -e
rc0 "layout-heal: second ptyclient exited $RC:" "$OUT.lphpc2" "$OUT.lphcap2.err"
# The wall must not have refused: the ptyclient exited 0 (checked above),
# and stderr mentions no layout error.
grep -qi "layout" "$OUT.lphcap2.err" && {
    echo "e2e FAIL: layout-heal: stderr mentions layout (heal was not silent):"
    cat "$OUT.lphcap2.err"; exit 1; }
# Run 1 is the anchor: it really was cut beside and `lll` really moved the
# rail, so run 2's tree below is a comparison and not an empty capture. A
# BESIDE cut puts a rail in a column of its own; a stacked cut has one only
# at column 1, which is a label bar and what `rail_cols`' callers filter
# out.
_lph_r1=$(rail_cols "$OUT.lphcap1" | awk '$1 > 1' | tail -1)
[ -n "$_lph_r1" ] && [ "$_lph_r1" -ge 43 ] || {
    echo "e2e FAIL: layout-heal: run 1's rail ended at column ${_lph_r1:-none}, want >= 43"
    echo "          — the resize this leg saves a tree for did not happen"
    cat "$OUT.lphpc1"; exit 1; }
# The money assertion is that run 2's cut came from the FILE and not from
# the aspect rule, and run 2's own SCREEN is where that reads: the default
# cut of two panes at 80x24 is `beside` with equal weights and a rail at
# column 40, while the tree run 1 saved puts it where run 1's `lll` left
# it. Same terminal, same tree, same flatten — so a rail as far right as
# run 1's is the seed having run AND won, and a rail at 40 would be the
# aspect rule having cut this wall instead.
_lph_r2=$(rail_cols "$OUT.lphcap2" | awk '$1 > 1' | tail -1)
[ -n "$_lph_r2" ] && [ "$_lph_r2" -ge 43 ] || {
    echo "e2e FAIL: layout-heal: run 2's rail ended at column ${_lph_r2:-none}, want >= 43"
    echo "          — the aspect rule cut this wall, so the saved tree never reached it"
    cat "$OUT.lphpc2"; exit 1; }
# ...and the FILE is still run 1's bytes. Run 2 changed no pane and no
# weight, and the exit-time save is gone (a second terminal showing a
# stale tree used to write it back over the first's edits), so a run that
# changes nothing writes nothing. Measured 2026-09-02:
#
#     saved by run 1:   beside 0 / leaf 43 #0 / leaf 36 #b / focus 0
#
# Two leaves, and the poll that ran under run 2 for two and a half seconds
# added neither the newcomer nor a collapse: the ended session's leaf keeps
# its own 36 where it stood.
_lph_root=$(sed -n '2p' "$LPHLAYOUT")
[ "$_lph_root" = "beside 0" ] || {
    echo "e2e FAIL: layout-heal: the layout's root is '$_lph_root', want 'beside 0'"
    echo "          — something rewrote the tree run 1 saved"
    cat "$LPHLAYOUT"; exit 1; }
_lph_leaves=$(sed -n 's/^ *leaf [0-9][0-9]* .*#\(.*\)$/\1/p' "$LPHLAYOUT" | tr '\n' ' ')
[ "$_lph_leaves" = "0 b " ] || {
    echo "e2e FAIL: layout-heal: the layout holds leaves '$_lph_leaves', want '0 b ':"
    cat "$LPHLAYOUT"; exit 1; }
grep -q -- "^ leaf 43 .*#0\$" "$LPHLAYOUT" || {
    echo "e2e FAIL: layout-heal: the survivor's weight did not survive the poll:"
    cat "$LPHLAYOUT"; exit 1; }
# The newcomer joined nothing. Two and a half seconds of quiet is several
# poll answers, and every one of them saw session c on this daemon's list;
# a wall that took its panes from that list would have cut a third stripe
# and painted c's own marker into it. `wc -l` on the matches and not
# `grep -c`, the hosts group's reason: a pty capture is escape bytes with
# almost no newlines in it.
[ "$(grep -aoF "$SOCK60#c" "$OUT.lphcap2" | wc -l)" -eq 0 ] || {
    echo "e2e FAIL: layout-heal: a pane for session c is on the wall and no leaf"
    echo "          names it — the poll is birthing tiles again:"
    cat "$OUT.lphpc2"; exit 1; }
[ "$(grep -aoF "lph-newcomer-c" "$OUT.lphcap2" | wc -l)" -eq 0 ] || {
    echo "e2e FAIL: layout-heal: session c's own output painted on a wall whose"
    echo "          layout never named it:"
    cat "$OUT.lphpc2"; exit 1; }
# ...and the leaf the list disowned says on screen why it is still there.
# The state word survives a bar's truncation where the socket path does
# not, so this is the needle that reads at any pane width.
grep -qa '\[session ended\]' "$OUT.lphcap2" || {
    echo "e2e FAIL: layout-heal: the ended session's pane wore no [session ended]:"
    cat "$OUT.lphpc2"; exit 1; }
timeout 20 "$MUX" a capture --sock "$SOCK60" > "$OUT.lphfa" 2>&1
grep -q "lph-heal-marker" "$OUT.lphfa" || {
    echo "e2e FAIL: layout-heal: the survivor's marker is not in the session its leaf kept:"
    cat "$OUT.lphfa"; exit 1; }
assert_stopped "$SOCK60" "$D61PID" "layout-heal" "$OUT.lphstop"
D61PID=""
rm -rf "$LPHSTATE"
ok "a poll changes nothing on the wall: a newcomer stays off it, the survivor keeps its pane, the ended one's pane stands gone"

# ---- layout file: a refused file degrades loudly and is NOT rewritten -
#
# A corrupted layout (`bogus 9`) is not a fatal error: the seed refuses it,
# the default tree is built, and the wall comes up. The assertions are that
# a marker typed into the wall reaches the focused session (the wall
# works), that the refusal NAMES the line it gave up on so there is
# something to fix, and that the file still holds those bytes when the wall
# leaves.
#
# Loud and not silent, which is the change: the layout used to be derived
# convenience — a stale cut was a convenience to forget — and it is the
# WALL now. A file the user authored and mux threw away without a word is
# panes gone with no undo and no sentence to search for, so the refusal is
# printed once, before the alternate screen, with the offending line in it.
#
# And NOT rewritten, which is the second change: this run has an entry pane
# and used to save its one-leaf tree over the refused file on start-up, so
# the line the user was told to fix was gone by the time they looked for
# it, and with it every other pane the file named. A run that refuses the
# file stops writing it for good.
#
# `mux --sock S` and not a bare `mux`, because a refused file leaves the
# wall with no panes at all: the entry attach is what gives this leg a
# stripe to type into, and session b — live on the same daemon, named by
# nothing — is what makes "the entry pane ALONE" a claim rather than a
# count of one.
LPDSTATE="${TMPDIR:-/tmp}/mux-e2e-lpdegrade-state-$$"
defer_rm "$LPDSTATE"
LPDHOSTS="$LPDSTATE/mux/hosts"
LPDLAYOUT="$LPDSTATE/mux/layout"
start_daemon "$SOCK61" "$OUT.lpd.d" "layout-degrade daemon never bound" --shell /bin/sh
D62PID=$DPID

pipe_mux "$OUT.lpda" "$OUT.lpda.err" env XDG_STATE_HOME="$LPDSTATE" timeout 40 "$MUX" --sock "$SOCK61"
pipe_send 'printf "lpd-%%s\\n" origin\n'
await_out "$OUT.lpda" "lpd-origin" "layout-degrade: the default session's marker never reached the client"
pipe_detach
wait_grid "$SOCK61" "lpd-origin" "layout-degrade: the default session's marker"

pipe_mux "$OUT.lpdb" "$OUT.lpdb.err" env XDG_STATE_HOME="$LPDSTATE" timeout 40 "$MUX" --sock "$SOCK61" --session b
pipe_send 'printf "lpd-%%s\\n" bee\n'
await_out "$OUT.lpdb" "lpd-bee" "layout-degrade: session b's marker never reached the client"
pipe_detach
wait_grid "$SOCK61" "lpd-bee" "layout-degrade: session b's marker" b

mkdir -p "$LPDSTATE/mux"
printf -- '--sock %s\n' "$SOCK61" > "$LPDHOSTS"
# A garbage layout: wrong magic, wrong version. `parseReporting` gives up
# on the first line and hands it back, `seedSidecar` prints it and returns
# null, and the wall starts as if the file were not there.
printf 'bogus 9\n' > "$LPDLAYOUT"

set +e
XDG_STATE_HOME="$LPDSTATE" timeout 90 "$PTYCLIENT" --cols 80 --rows 24 \
    --out "$OUT.lpdcap" --err "$OUT.lpdcap.err" -- \
    "$MUX" --sock "$SOCK61" > "$OUT.lpdpc" 2>&1 <<'EOF'
expect lpd-origin 20000
settle 2000 25000
send printf 'lpd-degrade-%s\n' marker\n
expect lpd-degrade-marker 10000
settle 500 15000
send \x1cd
waitexit 10000
EOF
RC=$?
set -e
rc0 "layout-degrade: ptyclient exited $RC:" "$OUT.lpdpc" "$OUT.lpdcap.err"
# The wall came up: the marker reached the focused session.
timeout 20 "$MUX" a capture --sock "$SOCK61" > "$OUT.lpdfa" 2>&1
grep -q "lpd-degrade-marker" "$OUT.lpdfa" || {
    echo "e2e FAIL: layout-degrade: marker not in session 0 (wall did not come up):"
    cat "$OUT.lpdfa"; exit 1; }
# The refusal was SAID, and it named the file and the line. Read off the
# client's own stderr file, which is where a sentence printed before the
# alternate screen lands — on a real terminal that is the shell prompt the
# user typed `mux` at.
grep -qF "mux: layout ignored (" "$OUT.lpdcap.err" || {
    echo "e2e FAIL: layout-degrade: a refused layout said nothing; stderr holds:"
    cat "$OUT.lpdcap.err"; exit 1; }
grep -qF "bogus 9" "$OUT.lpdcap.err" || {
    echo "e2e FAIL: layout-degrade: the refusal never printed the line it gave up on:"
    cat "$OUT.lpdcap.err"; exit 1; }
# The entry pane ALONE. Session b is live on this daemon and no leaf names
# it, so a wall of one is the whole screen — no second stripe, and no rail
# to cut it with. `rail_cols` reads a bar at column 1 as a rail, so the
# filter is the gone-panes leg's: a column of its own past the first.
[ "$(grep -aoF "lpd-bee" "$OUT.lpdcap" | wc -l)" -eq 0 ] || {
    echo "e2e FAIL: layout-degrade: session b painted on a wall that refused its"
    echo "          file and named nothing else:"
    cat "$OUT.lpdpc"; exit 1; }
# One PANE bar and no other. Counted off the label bars themselves rather
# than off `rail_cols`, which sees every reverse-video run and so also sees
# the notice this wall now carries — a right-aligned banner at column 19.
# A tile bar is ` N> spelling`; two panes are two of them, wherever the cut
# put their rows.
_lpd_bars=$(grep -ao $'\x1b\\[[0-9][0-9]*;[0-9][0-9]*H\x1b\\[7m [0-9][0-9]*>' "$OUT.lpdcap" |
    sed 's/.*\x1b\[\([0-9]*;[0-9]*\)H.*/\1/' | sort -u | wc -l)
[ "$_lpd_bars" -eq 1 ] || {
    echo "e2e FAIL: layout-degrade: $_lpd_bars pane bars — the refused wall"
    echo "          came up with more than the entry pane:"
    cat "$OUT.lpdpc"; exit 1; }
# The refusal is on the WALL too, not only on the stderr the alternate
# screen covered up: a user who ran `mux` from a prompt they have since
# scrolled past has this line to search for.
grep -aqF '[layout not saved: the layout file was refused - fix bogus 9]' "$OUT.lpdcap" || {
    echo "e2e FAIL: layout-degrade: the wall never said the file was refused:"
    cat "$OUT.lpdpc"; exit 1; }
# ...and the file is UNTOUCHED. The entry pane's start-up save and the
# detach's both ran on a wall that had one leaf; either would have replaced
# the bytes the user has to fix with a wall of one.
printf 'bogus 9\n' > "$OUT.lpdwant"
cmp -s "$LPDLAYOUT" "$OUT.lpdwant" || {
    echo "e2e FAIL: layout-degrade: the refused file was rewritten; it now holds:"
    cat "$LPDLAYOUT"; exit 1; }
ok "a corrupted layout is reported with its line, the wall starts as if it were missing, and the file is left alone"

# ---- layout file: a GOOD file refused for one leaf keeps all of them ---
#
# The refusal that costs most is not garbage: it is a well-formed wall with
# one leaf naming a host the hosts file no longer lists — what
# `mux hosts rm` used to leave behind, and what a hand-edited hosts line
# leaves behind still. The whole file is refused, because seating part of a
# wall silently is how a user loses one, and the run must therefore leave
# every leaf where it is: three leaves in, three leaves out, byte for byte.
#
# Three leaves and two hosts, not one of each: a file that survived because
# nothing in it was seatable would prove nothing, so two of these leaves
# name sessions that are live on the listed daemon right now.
LPKSTATE="${TMPDIR:-/tmp}/mux-e2e-lpkeep-state-$$"
defer_rm "$LPKSTATE"
LPKLAYOUT="$LPKSTATE/mux/layout"
LPKGHOST="${TMPDIR:-/tmp}/mux-e2e-lpkeep-absent-$$.sock"
mkdir -p "$LPKSTATE/mux"
printf -- '--sock %s\n' "$SOCK61" > "$LPKSTATE/mux/hosts"
cat > "$LPKLAYOUT" <<EOF
mux-layout 1
beside 0
 leaf 40 --sock $SOCK61#0
 stacked 30
  leaf 50 --sock $SOCK61#b
  leaf 50 --sock $LPKGHOST#z
focus 0
EOF
cp "$LPKLAYOUT" "$OUT.lpkwant"

set +e
XDG_STATE_HOME="$LPKSTATE" timeout 90 "$PTYCLIENT" --cols 80 --rows 24 \
    --out "$OUT.lpkcap" --err "$OUT.lpkcap.err" -- \
    "$MUX" --sock "$SOCK61" > "$OUT.lpkpc" 2>&1 <<'EOF'
expect lpd-origin 20000
settle 2000 25000
send printf 'lpd-keep-%s\n' marker\n
expect lpd-keep-marker 10000
settle 500 15000
send \x1cd
waitexit 10000
EOF
RC=$?
set -e
rc0 "layout-keep: ptyclient exited $RC:" "$OUT.lpkpc" "$OUT.lpkcap.err"
# The refusal named the leaf it could not seat, not the file's first line:
# every line here parses, and the one that is wrong is wrong about the
# hosts file.
grep -qF "mux: layout ignored (" "$OUT.lpkcap.err" || {
    echo "e2e FAIL: layout-keep: a refused layout said nothing; stderr holds:"
    cat "$OUT.lpkcap.err"; exit 1; }
grep -qF "$LPKGHOST#z" "$OUT.lpkcap.err" || {
    echo "e2e FAIL: layout-keep: the refusal never named the unlisted host's leaf:"
    cat "$OUT.lpkcap.err"; exit 1; }
# The wall came up on the entry pane alone, which is what makes the file
# below a file this run could have overwritten and did not.
grep -aqF "lpd-keep-marker" "$OUT.lpkcap" || {
    echo "e2e FAIL: layout-keep: the marker never painted; the wall did not come up:"
    cat "$OUT.lpkpc"; exit 1; }
cmp -s "$LPKLAYOUT" "$OUT.lpkwant" || {
    echo "e2e FAIL: layout-keep: the refused file changed. Want:"
    cat "$OUT.lpkwant"; echo "got:"; cat "$LPKLAYOUT"; exit 1; }
rm -rf "$LPKSTATE"
ok "a layout refused for one unlisted host keeps all three of its leaves byte for byte"

assert_stopped "$SOCK61" "$D62PID" "layout-degrade" "$OUT.lpdstop"
D62PID=""
rm -rf "$LPDSTATE"

# ---- gone panes: a rebooted daemon keeps the cut ----------------------
#
# The spec's scenario, end to end: three sessions, a saved three-pane cut,
# and a daemon that comes back EMPTY. The wall paints the saved cut and
# never moves it — panes b and c dress `gone` where they stood instead of
# collapsing out, which is what the pre-gone-panes build did on the first
# poll answer. Enter in pane b creates session b anew in the same rect; a
# later run `x`es pane c, and only THAT collapses it.
#
# Three sessions and not two: the fates have to differ per pane (one binds
# live, one revives, one is dismissed), and a two-pane fixture could not
# tell "the cut stood" from "the cut happened to be the default".
GPSTATE="${TMPDIR:-/tmp}/mux-e2e-gonepanes-state-$$"
defer_rm "$GPSTATE"
GPHOSTS="$GPSTATE/mux/hosts"
GPLAYOUT="$GPSTATE/mux/layout"
start_daemon "$SOCK62" "$OUT.gp.d" "gone-panes daemon never bound" --shell /bin/sh
D63PID=$DPID

pipe_mux "$OUT.gpa" "$OUT.gpa.err" env XDG_STATE_HOME="$GPSTATE" timeout 40 "$MUX" --sock "$SOCK62"
pipe_send 'printf "gp-%%s\\n" zero\n'
await_out "$OUT.gpa" "gp-zero" "gone-panes: the default session's marker never reached the client"
pipe_detach
wait_grid "$SOCK62" "gp-zero" "gone-panes: the default session's marker"
pipe_mux "$OUT.gpb" "$OUT.gpb.err" env XDG_STATE_HOME="$GPSTATE" timeout 40 "$MUX" --sock "$SOCK62" --session b
pipe_send 'printf "gp-%%s\\n" bee\n'
await_out "$OUT.gpb" "gp-bee" "gone-panes: session b's marker never reached the client"
pipe_detach
wait_grid "$SOCK62" "gp-bee" "gone-panes: session b's marker" b
pipe_mux "$OUT.gpc" "$OUT.gpc.err" env XDG_STATE_HOME="$GPSTATE" timeout 40 "$MUX" --sock "$SOCK62" --session c
pipe_send 'printf "gp-%%s\\n" sea\n'
await_out "$OUT.gpc" "gp-sea" "gone-panes: session c's marker never reached the client"
pipe_detach
wait_grid "$SOCK62" "gp-sea" "gone-panes: session c's marker" c

# One line, the daemon, and the three-leaf layout that IS this wall. Run 1
# does not discover these panes; it paints the file and saves it back, and
# the cut it saves is what every later run is measured against.
mkdir -p "$GPSTATE/mux"
printf -- '--sock %s\n' "$SOCK62" > "$GPHOSTS"
seed_layout "$GPSTATE" beside "--sock $SOCK62#0" "--sock $SOCK62#b" "--sock $SOCK62#c"

set +e
# Run 1: hydrate the three-pane wall on a tty and detach. The detach saves
# the sidecar, which is the cut every later run has to be measured against.
XDG_STATE_HOME="$GPSTATE" timeout 90 "$PTYCLIENT" --cols 80 --rows 24 \
    --out "$OUT.gpcap1" --err "$OUT.gpcap1.err" -- \
    "$MUX" > "$OUT.gppc1" 2>&1 <<'EOF'
expect gp-zero 20000
settle 1200 25000
send \x1cd
waitexit 10000
EOF
RC=$?
set -e
rc0 "gone-panes: run 1's ptyclient exited $RC:" "$OUT.gppc1" "$OUT.gpcap1.err"
[ -f "$GPLAYOUT" ] || {
    echo "e2e FAIL: gone-panes: sidecar not saved at $GPLAYOUT"
    ls -la "$GPSTATE/mux/" 2>&1; exit 1; }
# Three leaves, and the ORDER matters below: a seeded pane takes the tile
# index of its leaf, so `Ctrl-\ 2` is leaf 1 and `Ctrl-\ 3` is leaf 2. Read
# back off the file run 1 SAVED rather than assumed from the file it was
# seeded with, so a save that reordered the tree retargets nothing silently.
_gp_leaves=$(sed -n 's/^ *leaf [0-9][0-9]* .*#\(.*\)$/\1/p' "$GPLAYOUT" | tr '\n' ' ')
[ "$_gp_leaves" = "0 b c " ] || {
    echo "e2e FAIL: gone-panes: sidecar leaves are '$_gp_leaves', want '0 b c ':"
    cat "$GPLAYOUT"; exit 1; }
# The rails of the saved cut: a three-way `beside` at 80 columns has two.
# The glyph is matched WHOLE — CUP, reverse video, ONE space, reset — for
# the layout-restore leg's reason: a label bar opens with the same CUP+SGR
# at its pane's own left column, and a looser prefix counts bars as rails.
_gp_railre=$'\x1b\[[0-9]+;[0-9]+H\x1b\[7m \x1b\[0m'
_gp_rails1=$(grep -aoE "$_gp_railre" "$OUT.gpcap1" |
    sed -E 's/.*;([0-9]+)H.*/\1/' | sort -n | uniq | tr '\n' ' ')
[ "$(printf '%s' "$_gp_rails1" | wc -w)" -eq 2 ] || {
    echo "e2e FAIL: gone-panes: run 1 painted rails at '$_gp_rails1', want two columns"
    echo "          — the three-pane cut this leg measures never happened"
    cat "$OUT.gppc1"; exit 1; }

# The reboot: the same socket, a fresh daemon, none of our sessions on it.
# A fresh daemon is born holding `0`, so pane 0 has a live session to bind
# and panes b and c have none — reachable host, missing sessions, which is
# exactly the `gone` case and not the `unreachable` one.
assert_stopped "$SOCK62" "$D63PID" "gone-panes" "$OUT.gpstop1"
start_daemon "$SOCK62" "$OUT.gp.d2" "gone-panes daemon 2 never bound" --shell /bin/sh
D63PID=$DPID

set +e
# Run 2: the saved cut must paint and STAY. No chord, no resize, nothing
# but time — two and a half seconds of quiet is several poll answers, and
# the first of them is what used to re-cut this wall. The marker at the end
# proves the one pane that DID find its session is bound to a live shell.
XDG_STATE_HOME="$GPSTATE" timeout 90 "$PTYCLIENT" --cols 80 --rows 24 \
    --out "$OUT.gpcap2" --err "$OUT.gpcap2.err" -- \
    "$MUX" > "$OUT.gppc2" 2>&1 <<'EOF'
settle 2500 30000
send printf 'gp-%s\n' watch\n
expect gp-watch 20000
settle 700 15000
send \x1cd
waitexit 10000
EOF
RC=$?
set -e
rc0 "gone-panes: run 2's ptyclient exited $RC:" "$OUT.gppc2" "$OUT.gpcap2.err"
# The word reached the grid. A bar is byte-truncated from the LABEL end and
# the state survives that cut (`labelText`), so `[session ended]` is on screen for
# any pane width this leg could produce.
grep -qa '\[session ended\]' "$OUT.gpcap2" || {
    echo "e2e FAIL: gone-panes: no pane wore [session ended] after the reboot:"
    cat "$OUT.gppc2"; exit 1; }
# The rails did not move. A capture is a stream and holds every rail the
# run ever painted, so this catches a re-cut that MOVES a boundary; the
# sidecar below is what catches one that removes a pane (measured against
# the pre-gone-panes build 2026-09-01: it collapsed to a single leaf while
# leaving both of the original rails in this capture).
_gp_rails2=$(grep -aoE "$_gp_railre" "$OUT.gpcap2" |
    sed -E 's/.*;([0-9]+)H.*/\1/' | sort -n | uniq | tr '\n' ' ')
[ "$_gp_rails1" = "$_gp_rails2" ] || {
    echo "e2e FAIL: gone-panes: rails moved across the reboot (run 1: $_gp_rails1 run 2: $_gp_rails2)"
    cat "$OUT.gppc2"; exit 1; }
# ...and the structural half, which is the one the pre-gone-panes build
# fails: run 2 detached from a wall of THREE panes, still named 0, b and c.
# A build that collapses a disowned pane saves a sidecar of one leaf here.
_gp_leaves2=$(sed -n 's/^ *leaf [0-9][0-9]* .*#\(.*\)$/\1/p' "$GPLAYOUT" | tr '\n' ' ')
[ "$_gp_leaves2" = "0 b c " ] || {
    echo "e2e FAIL: gone-panes: after the reboot the saved cut is '$_gp_leaves2', want '0 b c ':"
    echo "          — the panes whose sessions the daemon forgot were collapsed out"
    cat "$GPLAYOUT"; exit 1; }

set +e
# Run 3: the two verbs a gone pane has. Enter in pane b (leaf 1) creates
# session b anew in the same rect; `x` in pane c (leaf 2) drops that tile
# on the KEYPRESS — one press, because a pane with no pump reads no ask and
# `endKey` answers `.drop` rather than arming the two-step.
XDG_STATE_HOME="$GPSTATE" timeout 90 "$PTYCLIENT" --cols 80 --rows 24 \
    --out "$OUT.gpcap3" --err "$OUT.gpcap3.err" -- \
    "$MUX" > "$OUT.gppc3" 2>&1 <<'EOF'
settle 2500 30000
send \x1c2
settle 700 15000
send \r
settle 2000 25000
send printf 'gp-%s\n' reborn\n
expect gp-reborn 20000
settle 700 15000
send \x1c3
settle 700 15000
send \x1cx
settle 1500 20000
send \x1cd
waitexit 10000
EOF
RC=$?
set -e
rc0 "gone-panes: run 3's ptyclient exited $RC:" "$OUT.gppc3" "$OUT.gpcap3.err"
# The revived session is REAL and carries the name its pane remembered:
# the daemon's own grid for `b` holds the marker, and the entry session
# does not — a revive that created `0` again, or created nothing and typed
# into the focused survivor, fails one of these two.
wait_sessions "$SOCK62" 2 "gone-panes: the daemon should hold 0 and the revived b"
timeout 20 "$MUX" a capture --sock "$SOCK62" --session b > "$OUT.gprb" 2>&1
grep -q "gp-reborn" "$OUT.gprb" || {
    echo "e2e FAIL: gone-panes: the reborn marker is not in session b:"
    cat "$OUT.gprb"; exit 1; }
timeout 20 "$MUX" a capture --sock "$SOCK62" > "$OUT.gpra" 2>&1
! grep -q "gp-reborn" "$OUT.gpra" || {
    echo "e2e FAIL: gone-panes: the reborn marker landed in session 0 — Enter did not"
    echo "          revive the pane it was pressed in:"
    cat "$OUT.gpra"; exit 1; }
# `x` dismissed pane c and nothing else: two leaves, and c is not one of
# them. The sidecar is written on DETACH, so this reads run 3's save.
_gp_leaves3=$(sed -n 's/^ *leaf [0-9][0-9]* .*#\(.*\)$/\1/p' "$GPLAYOUT" | tr '\n' ' ')
[ "$_gp_leaves3" = "0 b " ] || {
    echo "e2e FAIL: gone-panes: after x the saved cut is '$_gp_leaves3', want '0 b ':"
    cat "$GPLAYOUT"; exit 1; }
assert_stopped "$SOCK62" "$D63PID" "gone-panes" "$OUT.gpstop2"
D63PID=""
rm -rf "$GPSTATE"
ok "a rebooted daemon's panes wear gone and the cut never moves; Enter revives, x dismisses"

# --- Ctrl-\ X ends the focused pane's session, x still only removes it ---
# The chord's whole point is that it asks over the PANE'S OWN connection,
# which the picker's `x` cannot do (its row may have no pane here). Two
# consequences this leg grades, and they are why it is not a copy of the
# picker's end leg in 09_hosts:
#
#   * the daemon excludes the asking client from the "others hold it"
#     count, so a session only this wall's pane holds ends on ONE press
#     where the picker's `x` on the same session needs two.
#   * the refusal banner names `X`, the key that asked. A banner that said
#     `x` would send the user to the key that removes the pane and leaves
#     the shell running — the exact session they meant to end, now on no
#     wall of theirs.
#
# Three panes on a daemon of its own, and two shells whose pids they printed
# themselves before any wall existed: `kill -0` on those pids is the oracle
# for every claim here, because a wall that says nothing and a daemon that
# reaped nothing look identical from the grid.
SOCK63="${TMPDIR:-/tmp}/muxd-e2e-endchord-$$.sock"
defer_sock "$SOCK63"
ECSTATE="${TMPDIR:-/tmp}/mux-e2e-endchord-state-$$"
defer_rm "$ECSTATE"
start_daemon "$SOCK63" "$OUT.ec.d" "end-chord daemon never bound" --shell /bin/sh
D64PID=$DPID

# Sessions b and c, each made by a throwaway attach whose shell prints its
# own pid. The shell's ECHO of the command carries the prefix with no
# digits behind it, so the pattern demands at least one (the picker leg's
# reason, and the same spelling).
pipe_mux "$OUT.ecb" "$OUT.ecb.err" env XDG_STATE_HOME="$ECSTATE" timeout 60 \
    "$MUX" --sock "$SOCK63" --session b
pipe_send 'printf "ecb-%%s\\n" $$\n'
await_out "$OUT.ecb" "ecb-[0-9]" "end-chord: session b's shell never printed its pid"
pipe_detach "end-chord: session b's maker"
ECB=$(grep -o 'ecb-[0-9][0-9]*' "$OUT.ecb" | head -1 | cut -d- -f2)
[ -n "$ECB" ] || {
    echo "e2e FAIL: end-chord: never read session b's shell pid:"; cat "$OUT.ecb"; exit 1; }

pipe_mux "$OUT.ecc" "$OUT.ecc.err" env XDG_STATE_HOME="$ECSTATE" timeout 60 \
    "$MUX" --sock "$SOCK63" --session c
pipe_send 'printf "ecc-%%s\\n" $$\n'
await_out "$OUT.ecc" "ecc-[0-9]" "end-chord: session c's shell never printed its pid"
pipe_detach "end-chord: session c's maker"
ECC=$(grep -o 'ecc-[0-9][0-9]*' "$OUT.ecc" | head -1 | cut -d- -f2)
[ -n "$ECC" ] || {
    echo "e2e FAIL: end-chord: never read session c's shell pid:"; cat "$OUT.ecc"; exit 1; }

for _ec in "$ECB" "$ECC"; do
    kill -0 "$_ec" 2>/dev/null || {
        echo "e2e FAIL: end-chord: shell $_ec is not running before the leg"; exit 1; }
done
wait_sessions "$SOCK63" 3 "end-chord: the daemon should hold 0, b and c"

mkdir -p "$ECSTATE/mux"
printf -- '--sock %s\n' "$SOCK63" > "$ECSTATE/mux/hosts"
# Three leaves, so the pane that goes has neighbours to be told apart from
# the wall going, and so ending the second of them never leaves an empty
# wall — an empty wall opens the picker, which would eat the detach chord.
seed_layout "$ECSTATE" stacked \
    "--sock $SOCK63#0" "--sock $SOCK63#b" "--sock $SOCK63#c"

# The other holder of session b, attached before the wall exists and silent
# throughout: what makes the first press a refusal rather than an end.
pipe_mux "$OUT.echold" "$OUT.echold.err" env XDG_STATE_HOME="$ECSTATE" timeout 120 \
    "$MUX" --sock "$SOCK63" --session b
pipe_send 'printf "echold-%%s\\n" pin\n'
await_out "$OUT.echold" "echold-pin" "end-chord: the other holder never got session b's grid"

set +e
# Run 1: ONE press on a session someone else holds. The banner is the
# daemon's count in this client's words, and the shell must outlive it —
# a first press that ended anything is the regression this run exists for.
XDG_STATE_HOME="$ECSTATE" timeout 90 "$PTYCLIENT" --cols 80 --rows 30 \
    --out "$OUT.eccap1" --err "$OUT.eccap1.err" -- \
    "$MUX" > "$OUT.ecpc1" 2>&1 <<'EOF'
settle 2500 30000
send \x1c2
settle 700 20000
send \x1cX
expect 1 other attached - X again to end 20000
settle 700 20000
send \x1cd
waitexit 15000
EOF
RC=$?
set -e
rc0 "end-chord: run 1's ptyclient exited $RC:" "$OUT.ecpc1" "$OUT.eccap1.err"
# The OS, not the wall's own word: the refusal refused.
kill -0 "$ECB" 2>/dev/null || {
    echo "e2e FAIL: end-chord: one X ended session b's shell $ECB, which another"
    echo "          client was holding — the daemon's first refusal did nothing:"
    cat "$OUT.ecpc1"; exit 1; }
wait_sessions "$SOCK63" 3 "end-chord: a refused end must leave every session up"

set +e
# Run 2, both halves of the contract in one wall:
#
#   * pane 2 (session b, held by the pipe client too): X, the refusal, then
#     a second X inside the 3 s window forces the end.
#   * pane 3 (session c, held by nobody but this pane): ONE X ends it,
#     because the daemon does not count the client that is asking.
#
# The second X follows the `expect` and not a timer, so it lands the moment
# the refusal is on the screen — well inside `wallview.end_arm_ms`. The arm
# is per-tile and per-process, so run 1's is not carried in: run 2 asks and
# is refused again before it forces.
XDG_STATE_HOME="$ECSTATE" timeout 90 "$PTYCLIENT" --cols 80 --rows 30 \
    --out "$OUT.eccap2" --err "$OUT.eccap2.err" -- \
    "$MUX" > "$OUT.ecpc2" 2>&1 <<'EOF'
settle 2500 30000
send \x1c2
settle 700 20000
send \x1cX
expect 1 other attached - X again to end 20000
send \x1cX
settle 2500 25000
send \x1c3
settle 700 20000
send \x1cX
settle 2500 25000
send \x1cd
waitexit 15000
EOF
RC=$?
set -e
rc0 "end-chord: run 2's ptyclient exited $RC:" "$OUT.ecpc2" "$OUT.eccap2.err"
# The OS on both shells. wait_pid_gone allows two seconds, which is four
# times `Pty.term_grace_ms` — past the grace the daemon SIGKILLs, so a
# shell still here is a session that was never ended.
wait_pid_gone "$ECB" "end-chord: session b's shell after the second X"
wait_pid_gone "$ECC" "end-chord: session c's shell after its single X"
# The other holder was hung up on with the session's status in hand. 128 is
# what a shell killed by a signal reports (pty.zig checkExited), and it is
# the one number that says the DAEMON ended the session rather than the
# client noticing a closed socket.
pipe_waitexit "end-chord: the other holder of session b" 128
# The daemon agrees, and kept the one session nobody asked about.
wait_sessions "$SOCK63" 1 "end-chord: only 0 should be left"
# The screen, judged on the GRID: a bar is in the stream either way, and
# only the render says what was still there when the wall came down.
"$RENDER" --cols 80 --rows 30 < "$OUT.eccap2" > "$OUT.ecgrid" || {
    echo "e2e FAIL: end-chord: render oracle failed"; cat "$OUT.ecgrid"; exit 1; }
for _ec in "$SOCK63#b" "$SOCK63#c"; do
    grep -q -- "--sock $_ec" "$OUT.ecgrid" && {
        echo "e2e FAIL: end-chord: '$_ec' is still a pane on the final wall:"
        cat "$OUT.ecgrid"; exit 1; }
done
grep -q -- "--sock $SOCK63#0" "$OUT.ecgrid" || {
    echo "e2e FAIL: end-chord: the pane nobody ended lost its place on the wall:"
    cat "$OUT.ecgrid"; exit 1; }
assert_stopped "$SOCK63" "$D64PID" "end-chord" "$OUT.ecstop"
D64PID=""
rm -rf "$ECSTATE"
ok "Ctrl-\\ X ends the focused pane's session: refused while another client holds it, forced by the second press, and one press when the pane is the only holder"