a73x

789fd74e

test: the wall's column era gets its legs

a73x   2026-08-24 18:06

Commit message
test: the wall's column era gets its legs

Three e2e legs at the end of the suite:

1. Span-clear survival — the engine oracle. Two beside panes at 80x24;
   flood the left pane's session with seq 1 200, then replay the wall
   client's full captured stdout through the render fixture (an engine,
   not a byte grep) and assert the right pane's marker survives on the
   replayed grid. A painter that emits too much passes byte greps, so
   only the replayed grid is the oracle — this leg catches a \x1b[2K
   full-line-clear regression forever.

2. hjkl focus walk. Split right then below (three panes), walk focus
   with h/l/j, type a distinct marker after each move, and assert via
   muxa capture which session received it.

3. Split births a session beside. From one pane, Ctrl-\ | → muxd stats
   names the new session; the new pane's label bar sits at a column
   right of the rail (same CUP+reverse-video idiom as the resize leg).

The final summary line now derives from the counters themselves —
`echo "e2e OK ($OK_COUNT scenarios, $CONV_COUNT convergence points)"` —
so it can never rot behind a hand-written number again. The pin checks
stay hand-written constants; they are the gate.

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

test/e2e.sh
Old New
@@ -372,9 +372,15 @@ ZMOUSESH="${TMPDIR:-/tmp}/mux-e2e-wallappmouse-$$.sh"
372 SOCK53="${TMPDIR:-/tmp}/muxd-e2e-sbrect-$$.sock" 372 SOCK53="${TMPDIR:-/tmp}/muxd-e2e-sbrect-$$.sock"
373 SOCK54="${TMPDIR:-/tmp}/muxd-e2e-fullscreen-$$.sock" 373 SOCK54="${TMPDIR:-/tmp}/muxd-e2e-fullscreen-$$.sock"
374 SOCK55="${TMPDIR:-/tmp}/muxd-e2e-resize-$$.sock" 374 SOCK55="${TMPDIR:-/tmp}/muxd-e2e-resize-$$.sock"
375 SOCK56="${TMPDIR:-/tmp}/muxd-e2e-spanclear-$$.sock"
376 SOCK57="${TMPDIR:-/tmp}/muxd-e2e-hjkl-$$.sock"
377 SOCK58="${TMPDIR:-/tmp}/muxd-e2e-splitbirth-$$.sock"
375 D54PID="" 378 D54PID=""
376 D55PID="" 379 D55PID=""
377 D56PID="" 380 D56PID=""
381 D57PID=""
382 D58PID=""
383 D59PID=""
378 384
379 # The wall as attach HISTORY (phase 2). A daemon AND a state home of its 385 # The wall as attach HISTORY (phase 2). A daemon AND a state home of its
380 # own, for the dynamic-wall leg's reason turned up one notch: what these 386 # own, for the dynamic-wall leg's reason turned up one notch: what these
@@ -1275,6 +1281,9 @@ cleanup() {
1275 [ -n "${D51PID:-}" ] && kill "$D51PID" 2>/dev/null || true 1281 [ -n "${D51PID:-}" ] && kill "$D51PID" 2>/dev/null || true
1276 [ -n "${D52PID:-}" ] && kill "$D52PID" 2>/dev/null || true 1282 [ -n "${D52PID:-}" ] && kill "$D52PID" 2>/dev/null || true
1277 [ -n "${D55PID:-}" ] && kill "$D55PID" 2>/dev/null || true 1283 [ -n "${D55PID:-}" ] && kill "$D55PID" 2>/dev/null || true
1284 [ -n "${D57PID:-}" ] && kill "$D57PID" 2>/dev/null || true
1285 [ -n "${D58PID:-}" ] && kill "$D58PID" 2>/dev/null || true
1286 [ -n "${D59PID:-}" ] && kill "$D59PID" 2>/dev/null || true
1278 # The ssh-agents the forwarding legs start. Not mux processes and so not 1287 # The ssh-agents the forwarding legs start. Not mux processes and so not
1279 # the leak sweep's business, but they are daemons this file forked: left 1288 # the leak sweep's business, but they are daemons this file forked: left
1280 # alive they outlive the suite holding a private key, which is the one 1289 # alive they outlive the suite holding a private key, which is the one
@@ -1321,6 +1330,9 @@ cleanup() {
1321 [ -S "$SOCK53" ] && "$MUXD" stop --sock "$SOCK53" 2>/dev/null || true 1330 [ -S "$SOCK53" ] && "$MUXD" stop --sock "$SOCK53" 2>/dev/null || true
1322 [ -S "$SOCK54" ] && "$MUXD" stop --sock "$SOCK54" 2>/dev/null || true 1331 [ -S "$SOCK54" ] && "$MUXD" stop --sock "$SOCK54" 2>/dev/null || true
1323 [ -S "$SOCK55" ] && "$MUXD" stop --sock "$SOCK55" 2>/dev/null || true 1332 [ -S "$SOCK55" ] && "$MUXD" stop --sock "$SOCK55" 2>/dev/null || true
1333 [ -S "$SOCK56" ] && "$MUXD" stop --sock "$SOCK56" 2>/dev/null || true
1334 [ -S "$SOCK57" ] && "$MUXD" stop --sock "$SOCK57" 2>/dev/null || true
1335 [ -S "$SOCK58" ] && "$MUXD" stop --sock "$SOCK58" 2>/dev/null || true
1324 1336
1325 # ---- the leak sweep (hygiene kit, 6a) ---- 1337 # ---- the leak sweep (hygiene kit, 6a) ----
1326 # Here rather than at the bottom of the file, which `set -e` reaches only 1338 # Here rather than at the bottom of the file, which `set -e` reaches only
@@ -1336,7 +1348,7 @@ cleanup() {
1336 "$D26PID" "$D27PID" "$D28PID" "$D29PID" "$D30PID" "$D31PID" \ 1348 "$D26PID" "$D27PID" "$D28PID" "$D29PID" "$D30PID" "$D31PID" \
1337 "$D32PID" "$D33PID" "$D34PID" "$D35PID" "$D36PID" "$D37PID" \ 1349 "$D32PID" "$D33PID" "$D34PID" "$D35PID" "$D36PID" "$D37PID" \
1338 "$D38PID" "$D39PID" "$D40PID" "$D41PID" "$D42PID" "$D43PID" "$D54PID" \ 1350 "$D38PID" "$D39PID" "$D40PID" "$D41PID" "$D42PID" "$D43PID" "$D54PID" \
1339 "$D55PID" "$D56PID" 1351 "$D55PID" "$D56PID" "$D57PID" "$D58PID" "$D59PID"
1340 _leak=0 1352 _leak=0
1341 leak_sweep "$_rc" || _leak=1 1353 leak_sweep "$_rc" || _leak=1
1342 1354
@@ -1411,7 +1423,14 @@ cleanup() {
1411 "$OUT.fsstop" "$SOCK54" \ 1423 "$OUT.fsstop" "$SOCK54" \
1412 "$OUT.rsz.d" "$OUT.rszcap" "$OUT.rszcap.err" "$OUT.rszpc" \ 1424 "$OUT.rsz.d" "$OUT.rszcap" "$OUT.rszcap.err" "$OUT.rszpc" \
1413 "$OUT.rsza" "$OUT.rsza.err" "$OUT.rszb" "$OUT.rszb.err" \ 1425 "$OUT.rsza" "$OUT.rsza.err" "$OUT.rszb" "$OUT.rszb.err" \
1414 "$OUT.rszsta" "$OUT.rszstb" "$OUT.rszstop" "$SOCK55" 1426 "$OUT.rszsta" "$OUT.rszstb" "$OUT.rszstop" "$SOCK55" \
1427 "$OUT.spc.d" "$OUT.spca" "$OUT.spca.err" "$OUT.spcb" "$OUT.spcb.err" \
1428 "$OUT.spccap" "$OUT.spccap.err" "$OUT.spcpc" "$OUT.spcgrid" \
1429 "$OUT.spcstop" "$SOCK56" \
1430 "$OUT.hjk.d" "$OUT.hjkcap" "$OUT.hjkcap.err" "$OUT.hjkpc" \
1431 "$OUT.hjkfa" "$OUT.hjkfb" "$OUT.hjkfc" "$OUT.hjkstop" "$SOCK57" \
1432 "$OUT.spl.d" "$OUT.splcap" "$OUT.splcap.err" "$OUT.splpc" \
1433 "$OUT.splsta" "$OUT.splstop" "$SOCK58"
1415 # ...and the non-tty capture that leg's session feeds. 1434 # ...and the non-tty capture that leg's session feeds.
1416 rm -f "$OUT.nogate" 1435 rm -f "$OUT.nogate"
1417 # ...and its other half: the paste capture and the file nvim wrote, which 1436 # ...and its other half: the paste capture and the file nvim wrote, which
@@ -7755,8 +7774,198 @@ assert_stopped "$SOCK55" "$D56PID" "resize" "$OUT.rszstop"
7755 D56PID="" 7774 D56PID=""
7756 ok "Ctrl-\\ r trades cells between panes; Esc returns to prose" 7775 ok "Ctrl-\\ r trades cells between panes; Esc returns to prose"
7757 7776
7758 [ "$OK_COUNT" = "65" ] || { 7777 # ---- span-clear survival: the engine oracle -------------------------
7759 echo "e2e FAIL: $OK_COUNT scenario checkpoints ran, the pin says 65 —" 7778 #
7779 # Two beside panes at 80x24 (aspect rule: cols >= 2*rows → beside). A
7780 # marker in the RIGHT pane's session, then a flood in the LEFT pane's
7781 # session (seq 1 200). The regression this catches forever is a whole-line
7782 # clear (\x1b[2K) blanking the neighbor: a painter that emits too much
7783 # still passes a byte grep, so the assertion replays the wall client's
7784 # FULL captured stdout through the render fixture (an engine, not a grep)
7785 # and checks the right pane's marker text survives on the replayed grid.
7786 "$MUXD" run --sock "$SOCK56" --shell /bin/sh > "$OUT.spc.d" 2>&1 &
7787 D57PID=$!
7788 wait_sock "$SOCK56" "$OUT.spc.d" "span-clear daemon never bound"
7789
7790 pipe_mux "$OUT.spca" "$OUT.spca.err" timeout 40 "$MUX" --sock "$SOCK56" --session a
7791 pipe_send 'printf "spc-%%s\\n" left-flood\n'
7792 await_out "$OUT.spca" "spc-left-flood" "span-clear: left session's marker never reached the client"
7793 pipe_detach
7794 wait_grid "$SOCK56" "spc-left-flood" "span-clear: left session's marker" a
7795 pipe_mux "$OUT.spcb" "$OUT.spcb.err" timeout 40 "$MUX" --sock "$SOCK56" --session b
7796 pipe_send 'printf "spc-%%s\\n" right-survivor\n'
7797 await_out "$OUT.spcb" "spc-right-survivor" "span-clear: right session's marker never reached the client"
7798 pipe_detach
7799 wait_grid "$SOCK56" "spc-right-survivor" "span-clear: right session's marker" b
7800
7801 set +e
7802 # 80x24: aspect rule → beside. The wall client is a tty session: its
7803 # stdout is the terminal's full byte stream, captured by ptyclient.
7804 timeout 90 "$PTYCLIENT" --cols 80 --rows 24 --out "$OUT.spccap" --err "$OUT.spccap.err" -- \
7805 "$MUX" wall "--sock $SOCK56#a" "--sock $SOCK56#b" > "$OUT.spcpc" 2>&1 <<'EOF'
7806 expect spc-right-survivor 20000
7807 settle 700 20000
7808 send \x1ch
7809 settle 500 15000
7810 send seq 1 200\n
7811 expect 200 20000
7812 settle 700 20000
7813 send \x1cd
7814 waitexit 10000
7815 EOF
7816 RC=$?
7817 set -e
7818 [ "$RC" -eq 0 ] || {
7819 echo "e2e FAIL: span-clear: ptyclient leg exited $RC:"
7820 cat "$OUT.spcpc" "$OUT.spccap.err"; exit 1; }
7821 # The oracle: replay the captured stream through the render fixture at the
7822 # same dimensions, producing the final grid text. The right pane's marker
7823 # must still be on that grid — a span-clear regression blanks it.
7824 "$RENDER" --cols 80 --rows 24 < "$OUT.spccap" > "$OUT.spcgrid" || {
7825 echo "e2e FAIL: span-clear: render fixture failed on the capture:"
7826 cat "$OUT.spcgrid"; exit 1; }
7827 grep -q "spc-right-survivor" "$OUT.spcgrid" || {
7828 echo "e2e FAIL: span-clear: right pane's marker lost from the replayed grid:"
7829 echo " a span-clear regression blanked the neighbor:"
7830 cat "$OUT.spcgrid"; exit 1; }
7831 assert_stopped "$SOCK56" "$D57PID" "span-clear" "$OUT.spcstop"
7832 D57PID=""
7833 ok "a span-clear flood in the left pane cannot blank the right pane's grid"
7834
7835 # ---- hjkl focus walk -------------------------------------------------
7836 #
7837 # One tile, split right (births a second session), split below (births a
7838 # third). Then walk focus with hjkl and type a distinct marker after each
7839 # move; muxa capture names which session received it. Covers at least a
7840 # left move, a right move, and one vertical move.
7841 #
7842 # No convergence point: the assertion is per-session muxa capture, not a
7843 # daemon-grid diff.
7844 "$MUXD" run --sock "$SOCK57" --shell /bin/sh > "$OUT.hjk.d" 2>&1 &
7845 D58PID=$!
7846 wait_sock "$SOCK57" "$OUT.hjk.d" "hjkl daemon never bound"
7847
7848 pipe_mux "$OUT.hjkcap" "$OUT.hjkcap.err" timeout 40 "$MUX" --sock "$SOCK57" --session a
7849 pipe_send 'printf "hjk-%%s\\n" origin\n'
7850 await_out "$OUT.hjkcap" "hjk-origin" "hjkl: origin marker never reached the client"
7851 pipe_detach
7852 wait_grid "$SOCK57" "hjk-origin" "hjkl: origin marker" a
7853
7854 set +e
7855 # 80x24: aspect rule → beside for the first split; the second split goes
7856 # below, producing a 1 | (2 over 3) tree.
7857 timeout 90 "$PTYCLIENT" --cols 80 --rows 24 --out "$OUT.hjkcap" --err "$OUT.hjkcap.err" -- \
7858 "$MUX" wall "--sock $SOCK57#a" > "$OUT.hjkpc" 2>&1 <<'EOF'
7859 expect hjk-origin 20000
7860 settle 700 20000
7861 send \x1c|
7862 settle 1000 20000
7863 send \x1c-
7864 settle 1000 20000
7865 send \x1ch
7866 settle 500 15000
7867 send printf 'hjk-%s\n' went-left\n
7868 expect hjk-went-left 10000
7869 settle 500 15000
7870 send \x1cl
7871 settle 500 15000
7872 send printf 'hjk-%s\n' went-right\n
7873 expect hjk-went-right 10000
7874 settle 500 15000
7875 send \x1cj
7876 settle 500 15000
7877 send printf 'hjk-%s\n' went-down\n
7878 expect hjk-went-down 10000
7879 settle 500 15000
7880 send \x1cd
7881 waitexit 10000
7882 EOF
7883 RC=$?
7884 set -e
7885 [ "$RC" -eq 0 ] || {
7886 echo "e2e FAIL: hjkl: ptyclient leg exited $RC:"
7887 cat "$OUT.hjkpc" "$OUT.hjkcap.err"; exit 1; }
7888 # Three sessions exist now: a (the origin), and two birthed by splits.
7889 # The daemon assigns numeric names to the new sessions; capture all three.
7890 timeout 20 "$MUXA" capture --sock "$SOCK57" --session a > "$OUT.hjkfa" 2>&1
7891 timeout 20 "$MUXA" capture --sock "$SOCK57" --session 1 > "$OUT.hjkfb" 2>&1
7892 timeout 20 "$MUXA" capture --sock "$SOCK57" --session 2 > "$OUT.hjkfc" 2>&1
7893 # went-left: typed after \x1ch from the right pane → lands in session a.
7894 grep -q "hjk-went-left" "$OUT.hjkfa" || {
7895 echo "e2e FAIL: hjkl: hjk-went-left not in session a (left focus move):"
7896 cat "$OUT.hjkfa"; exit 1; }
7897 # went-right: typed after \x1cl from session a → lands in session 1.
7898 grep -q "hjk-went-right" "$OUT.hjkfb" || {
7899 echo "e2e FAIL: hjkl: hjk-went-right not in session 1 (right focus move):"
7900 cat "$OUT.hjkfb"; exit 1; }
7901 # went-down: typed after \x1cj from session 1 → lands in session 2.
7902 grep -q "hjk-went-down" "$OUT.hjkfc" || {
7903 echo "e2e FAIL: hjkl: hjk-went-down not in session 2 (down focus move):"
7904 cat "$OUT.hjkfc"; exit 1; }
7905 assert_stopped "$SOCK57" "$D58PID" "hjkl" "$OUT.hjkstop"
7906 D58PID=""
7907 ok "hjkl walks focus across panes in both axes"
7908
7909 # ---- split births a session beside -----------------------------------
7910 #
7911 # From one pane, Ctrl-\ | splits right and births a new session. The
7912 # daemon's session count rises to two, and the new pane's label bar sits
7913 # at a column to the right of the rail (the rail paints as ESC[r;cH
7914 # ESC[7m — the same idiom the resize leg uses).
7915 "$MUXD" run --sock "$SOCK58" --shell /bin/sh > "$OUT.spl.d" 2>&1 &
7916 D59PID=$!
7917 wait_sock "$SOCK58" "$OUT.spl.d" "split-birth daemon never bound"
7918
7919 pipe_mux "$OUT.splcap" "$OUT.splcap.err" timeout 40 "$MUX" --sock "$SOCK58" --session a
7920 pipe_send 'printf "spl-%%s\\n" origin\n'
7921 await_out "$OUT.splcap" "spl-origin" "split-birth: origin marker never reached the client"
7922 pipe_detach
7923 wait_grid "$SOCK58" "spl-origin" "split-birth: origin marker" a
7924
7925 set +e
7926 # 80x24: aspect rule → beside. One tile starts, then Ctrl-\ | births a
7927 # second session to the right.
7928 timeout 90 "$PTYCLIENT" --cols 80 --rows 24 --out "$OUT.splcap" --err "$OUT.splcap.err" -- \
7929 "$MUX" wall "--sock $SOCK58#a" > "$OUT.splpc" 2>&1 <<'EOF'
7930 expect spl-origin 20000
7931 settle 700 20000
7932 send \x1c|
7933 settle 1500 20000
7934 send \x1cd
7935 waitexit 10000
7936 EOF
7937 RC=$?
7938 set -e
7939 [ "$RC" -eq 0 ] || {
7940 echo "e2e FAIL: split-birth: ptyclient leg exited $RC:"
7941 cat "$OUT.splpc" "$OUT.splcap.err"; exit 1; }
7942 # The daemon held sessions 0 (default) and a; the split birthed one
7943 # more, so three live sessions proves the chord created a new session.
7944 wait_sessions "$SOCK58" 3 "split-birth: split did not birth a new session"
7945 # The rail paints as ESC[row;colH ESC[7m (reverse video). The new pane's
7946 # label bar is at a column greater than the rail's column — it sits to
7947 # the right of the separator, which is the whole point of beside.
7948 _rail_re=$'\x1b\\[[0-9][0-9]*;[0-9][0-9]*H\x1b\\[7m'
7949 _rail_col=$(grep -ao "$_rail_re" "$OUT.splcap" | \
7950 sed 's/.*\x1b\[[0-9]*;\([0-9]*\)H\x1b\[7m/\1/' | sort -n | head -1)
7951 [ -n "$_rail_col" ] || {
7952 echo "e2e FAIL: split-birth: no rail glyph found in the capture:"
7953 cat "$OUT.splcap"; exit 1; }
7954 # The rail sits at the boundary; the new pane's label bar is beyond it.
7955 # A label bar is a CUP followed by text, not reverse-video. Find a CUP
7956 # whose column exceeds the rail's — that is the right pane's bar start.
7957 _bar_col=$(grep -ao $'\x1b\\[[0-9]*;[0-9]*H' "$OUT.splcap" | \
7958 sed 's/\x1b\[[0-9]*;\([0-9]*\)H/\1/' | sort -n | \
7959 awk -v r="$_rail_col" '$1 > r { print; exit }')
7960 [ -n "$_bar_col" ] || {
7961 echo "e2e FAIL: split-birth: no label bar found right of the rail (col $_rail_col):"
7962 cat "$OUT.splcap"; exit 1; }
7963 assert_stopped "$SOCK58" "$D59PID" "split-birth" "$OUT.splstop"
7964 D59PID=""
7965 ok "Ctrl-\\ | births a session beside; its label bar sits right of the rail"
7966
7967 [ "$OK_COUNT" = "68" ] || {
7968 echo "e2e FAIL: $OK_COUNT scenario checkpoints ran, the pin says 68 —"
7760 echo " a scenario was added (update the pin) or silently lost" 7969 echo " a scenario was added (update the pin) or silently lost"
7761 exit 1 7970 exit 1
7762 } 7971 }
@@ -7764,4 +7973,4 @@ ok "Ctrl-\\ r trades cells between panes; Esc returns to prose"
7764 echo "e2e FAIL: $CONV_COUNT convergence points ran, the pin says 37" 7973 echo "e2e FAIL: $CONV_COUNT convergence points ran, the pin says 37"
7765 exit 1 7974 exit 1
7766 } 7975 }
7767 echo "e2e OK (64 scenarios, 37 convergence points)" 7976 echo "e2e OK ($OK_COUNT scenarios, $CONV_COUNT convergence points)"