a73x

test/e2e_07_wallcli.sh

Ref:   Size: 23.7 KiB   History

# shellcheck shell=sh
# e2e_07_wallcli.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 CLI wall: its own daemon, so its sessions can't be confused with any
# other block's, and a state home of its own, because a wall is now the
# HOSTS FILE and the shared one is every daemon this suite has attached to.
SOCK26="${TMPDIR:-/tmp}/muxd-e2e-cliwall-$$.sock"
defer_sock "$SOCK26"
CWSTATE="${TMPDIR:-/tmp}/mux-e2e-cliwall-state-$$"
defer_rm "$CWSTATE"
# The prefix chord (Ctrl-\ as a command key): its own daemon, so a chord
# that fails to detach cannot strand another block's session.
SOCK27="${TMPDIR:-/tmp}/muxd-e2e-prefix-$$.sock"
defer_sock "$SOCK27"
PFXSTATE="${TMPDIR:-/tmp}/mux-e2e-prefix-state-$$"
defer_rm "$PFXSTATE"
# Ctrl-\ c (create a session and switch to it): its own daemon, because the
# assertion is about which sessions EXIST — another block's session on the
# same daemon would change the name the chord picks.
SOCK28="${TMPDIR:-/tmp}/muxd-e2e-newsess-$$.sock"
defer_sock "$SOCK28"
NSWSTATE="${TMPDIR:-/tmp}/mux-e2e-newsess-state-$$"
defer_rm "$NSWSTATE"
# Ctrl-\ n / Ctrl-\ p (step around the ring): its own daemon for the same
# reason as SOCK28, and more sharply — the ring IS the list of sessions on
# one daemon, so a stray session from another block would change where a
# step lands.
SOCK29="${TMPDIR:-/tmp}/muxd-e2e-ring-$$.sock"
defer_sock "$SOCK29"
RINGSTATE="${TMPDIR:-/tmp}/mux-e2e-ring-state-$$"
defer_rm "$RINGSTATE"
# M5 (the self-attach refusal): its own daemon because the assertion reads
# the SOCKET PATH out of a session shell's environment and compares it to
# the one this block started — a shared daemon would make the comparison a
# tautology about whichever block bound first.
SOCK31="${TMPDIR:-/tmp}/muxd-e2e-selfattach-$$.sock"
defer_sock "$SOCK31"

# ---- the CLI wall: the hub's multiattach, in one terminal --------------
# One daemon, two named sessions, one `mux` on a real pty showing both AT
# ONCE — the browser wall's claim, made by the CLI. Three facts, each with
# its own witness: both sessions' markers painted (the snapshot path), a
# line injected into b WHILE the wall is attached painted too (the delta
# path — the wall is a live replica, not a capture), and `\x1cd` ended the
# process with exit 0 (the wall left, not died). Markers are
# shell-EXPANDED (`%s`+pin), the M18 trick: a hit is the shell's work,
# never an echo of anything typed here.
#
# The one expect anchors on the injected delta, deliberately: tile pumps
# paint their first stripes in whichever order their snapshots land — a
# thread race, and ptyclient's expect cursor cannot bet on it. The
# injected delta is the one byte sequence guaranteed late (t+5s into a
# replica that must already hold b's snapshot); the snapshot markers are
# asserted order-free by the greps on the capture below.
#
# Nothing on the command line names the second tile: `a` is named because
# the wall is entered zoomed on it, and `b` comes from the LAYOUT — the
# file is the wall now, so a pane is a leaf somebody wrote down. `z` is
# the control: a live session on the same daemon that no leaf names, and
# it must be nowhere on this terminal. Without it the leg could not tell
# "the layout put b here" from "every session the daemon has lands here",
# which is exactly the model that was replaced.
start_daemon "$SOCK26" "$OUT.cwall.d" "CLI wall daemon never bound" --shell /bin/sh
D23PID=$DPID

pipe_mux "$OUT.cwa" "$OUT.cwa.err" env XDG_STATE_HOME="$CWSTATE" timeout 40 "$MUX" --sock "$SOCK26" --session a
pipe_send 'printf "cwa-%%s\\n" pin\n'
await_out "$OUT.cwa" "cwa-pin" "cwa-pin never reached the client"
pipe_detach
wait_grid "$SOCK26" "cwa-pin" "CLI wall: session a's marker" a
pipe_mux "$OUT.cwb" "$OUT.cwb.err" env XDG_STATE_HOME="$CWSTATE" timeout 40 "$MUX" --sock "$SOCK26" --session b
pipe_send 'printf "cwb-%%s\\n" pin\n'
await_out "$OUT.cwb" "cwb-pin" "cwb-pin never reached the client"
pipe_detach
wait_grid "$SOCK26" "cwb-pin" "CLI wall: session b's marker" b
pipe_mux "$OUT.cwz" "$OUT.cwz.err" env XDG_STATE_HOME="$CWSTATE" timeout 40 "$MUX" --sock "$SOCK26" --session z
pipe_send 'printf "cwz-%%s\\n" pin\n'
await_out "$OUT.cwz" "cwz-pin" "cwz-pin never reached the client"
pipe_detach
wait_grid "$SOCK26" "cwz-pin" "CLI wall: session z's marker" z

( sleep 5; "$MUX" a send 'printf "cwlive-%s\n" pin\n' \
    --sock "$SOCK26" --session b > "$OUT.cwinj" 2>&1 ) &
CWINJPID=$!
defer_kill "$CWINJPID"
set +e
seed_layout "$CWSTATE" stacked "--sock $SOCK26#a" "--sock $SOCK26#b"
XDG_STATE_HOME="$CWSTATE" timeout 40 "$PTYCLIENT" --cols 40 --rows 30 \
    --out "$OUT.cwcap" --err "$OUT.cwcap.err" -- \
    "$MUX" --sock "$SOCK26" --session a > "$OUT.cwpc" 2>&1 <<'EOF'
expect cwlive-pin 20000
send \x1cd
waitexit 10000
EOF
RC=$?
set -e
wait "$CWINJPID" 2>/dev/null || true
rc0 "CLI wall: ptyclient leg exited $RC:" "$OUT.cwpc" "$OUT.cwinj"
# The multiattach fact itself: both sessions' bytes in ONE process's
# capture. Redundant with the expects above only until someone edits the
# script block; a grep each keeps the claim standing on its own.
grep -q "cwa-pin" "$OUT.cwcap" || {
    echo "e2e FAIL: CLI wall: a's marker never painted"; exit 1; }
grep -q "cwb-pin" "$OUT.cwcap" || {
    echo "e2e FAIL: CLI wall: b's marker never painted"; exit 1; }
grep -q "cwlive-pin" "$OUT.cwcap" || {
    echo "e2e FAIL: CLI wall: the live delta never painted"; exit 1; }
# ...and the session no leaf names is nowhere on this terminal. It was
# live on the same daemon throughout — `wait_grid` above says the daemon
# really holds it — so its absence here is the layout deciding what the
# wall shows and not the daemon having nothing to offer.
grep -q "cwz-pin" "$OUT.cwcap" && {
    echo "e2e FAIL: CLI wall: session z is on the wall and no leaf names it —"
    echo "          the wall is taking its panes from the daemon's list again:"
    cat "$OUT.cwpc"; exit 1; }
# Every tile claims its rect: a and b were created 80-wide by pipe, and
# this 40-wide wall resized both on attach. There is no 0x0 passivity to
# assert — the wall writes the sessions it shows.
assert_stopped "$SOCK26" "$D23PID" "CLI wall" "$OUT.cwstop"
D23PID=""
ok "mux --sock: the entry pane and the layout's other panes, and a live delta, on one terminal"

# ---- M1: Ctrl-\ is a prefix, not an action ------------------------------
#
# Two claims in one client, because they are the same claim seen from both
# sides: the chord layer eats exactly the two bytes of a chord and nothing
# else.
#
#   (a) an unknown command key is swallowed WITH its prefix. `\x1cz` is sent
#       at an empty prompt (`z` is bound to nothing; `x` would forget the
#       tile), then a marker command. If either byte had reached the pty the
#       shell would have read `zprintf ...` and the marker would never
#       print — so the marker is the assertion, and the explicit grep for
#       `zprintf` names what went wrong when it does.
#   (b) `\x1cd` detaches. The client exits 0 and the session it left behind
#       still answers, which is what separates a detach from a kill.
#
# A real pty (ptyclient) rather than a pipeline: the shell only echoes what
# it was really given when there is a terminal to echo to, and the echo is
# what carries the stray `z` if the layer leaks one.
start_daemon "$SOCK27" "$OUT.pfx.d" "prefix daemon never bound" --shell /bin/sh
D24PID=$DPID

set +e
no_saved_tree "$PFXSTATE"
XDG_STATE_HOME="$PFXSTATE" timeout 40 "$PTYCLIENT" --cols 80 --rows 24 \
    --out "$OUT.pfx" --err "$OUT.pfx.err" \
    -- "$MUX" --sock "$SOCK27" > "$OUT.pfx.log" 2>&1 <<'EOF'
expect \x1b[?1049h 15000
settle 400 15000
send \x1cz
settle 400 15000
send printf 'm1-%s\\n' pin\n
expect m1-pin 15000
settle 400 15000
send \x1cd
waitexit 10000
EOF
RC=$?
set -e
rc0 "prefix: ptyclient leg exited $RC (did \\x1cd detach?):" "$OUT.pfx.log"
grep -q "m1-pin" "$OUT.pfx" || {
    echo "e2e FAIL: prefix: the marker never printed"; exit 1; }
grep -q "zprintf" "$OUT.pfx" && {
    echo "e2e FAIL: prefix: the unknown command key reached the pty"
    cat "$OUT.pfx"; exit 1; }
# Detached, not exited: the session outlives the client that left it.
"$MUX" a status --sock "$SOCK27" > "$OUT.pfxst" 2>&1 || {
    echo "e2e FAIL: prefix: the session did not survive the detach:"
    cat "$OUT.pfxst"; exit 1; }
assert_stopped "$SOCK27" "$D24PID" "prefix" "$OUT.pfxstop"
D24PID=""
ok "Ctrl-\\ prefix: an unknown chord is swallowed, Ctrl-\\ d detaches"

# ---- M2: Ctrl-\ c creates a session and gives it a tile -------------------
#
# The claim is a MOVE, which takes two witnesses: the client ends up
# somewhere new, and the place it left is still there and unchanged.
#
#   * one pty client attaches to the default session, runs a marker, types
#     `\x1cc`, and runs a second marker. Both markers reach a shell — the
#     second one proves the client is attached to a session that works, not
#     merely alive after the chord.
#   * `mux a status --session 1` proves the daemon really created the name
#     the client picked (lowest free integer beside the default "0").
#   * the default session's capture still holds the FIRST marker and not the
#     second. Without that pair the same log would be produced by a chord
#     that did nothing at all.
#
# The arrival signal is the new tile's own LABEL BAR. `\x1cc` used to tear
# the terminal down and build it back up — a fresh `?1049h` per switch —
# because each switch was a new client process; then it became a zoom, and
# the signal was `setFocus`'s `\x1b[2J`. Now the chord ADDS A TILE beside
# the focus and leaves both on screen, so neither happens: what is new on
# the terminal is a second stripe whose marker carries the focus (`2> `,
# `paintLabel`). A one-tile wall draws no bar at all, so that byte sequence
# cannot be anything but the tile the chord just made.
#
# Then the refusal, which is the branch with the interesting answer: a
# birth the WALL has no room for. The daemon is not full here and could not
# usefully be — `max_tiles` and `max_sessions` are both 32, so a wall of one
# host runs out of stripes before its daemon runs out of slots, and the
# terminal runs out of rows long before either. Eight sessions on a 24-row
# terminal is a saturated wall exactly (three rows a stripe: two for the
# daemon's floor, one for the bar), and a ninth has nowhere to go.
start_daemon "$SOCK28" "$OUT.nsw.d" "new-session daemon never bound" --shell /bin/sh
D25PID=$DPID

set +e
no_saved_tree "$NSWSTATE"
XDG_STATE_HOME="$NSWSTATE" timeout 40 "$PTYCLIENT" --cols 40 --rows 24 \
    --out "$OUT.nsw" --err "$OUT.nsw.err" \
    -- "$MUX" --sock "$SOCK28" > "$OUT.nsw.log" 2>&1 <<'EOF'
expect \x1b[?1049h 15000
settle 400 15000
send printf 'm2a-%s\\n' pin\n
expect m2a-pin 15000
settle 400 15000
send \x1cc
expect 2> --sock 15000
settle 600 15000
send printf 'm2b-%s\\n' pin\n
expect m2b-pin 15000
settle 400 15000
send \x1c\x1c
waitexit 10000
EOF
RC=$?
set -e
rc0 "new session: ptyclient leg exited $RC (did \\x1cc make a tile?):" "$OUT.nsw.log"
"$MUX" a status --sock "$SOCK28" --session 1 > "$OUT.nswst" 2>&1 || {
    echo "e2e FAIL: new session: the chord did not create session 1:"
    cat "$OUT.nswst"; exit 1; }
# The session the client LEFT: still live, still holding what it was shown
# before the chord, and innocent of everything typed after it.
"$MUX" a capture --sock "$SOCK28" > "$OUT.nswcap" 2>&1
grep -q "m2a-pin" "$OUT.nswcap" || {
    echo "e2e FAIL: new session: the default session lost its own marker:"
    cat "$OUT.nswcap"; exit 1; }
grep -q "m2b-pin" "$OUT.nswcap" && {
    echo "e2e FAIL: new session: the second marker ran in the OLD session"
    cat "$OUT.nswcap"; exit 1; }
"$MUX" a capture --sock "$SOCK28" --session 1 > "$OUT.nswcap1" 2>&1
grep -q "m2b-pin" "$OUT.nswcap1" || {
    echo "e2e FAIL: new session: the chord's own session never got the marker:"
    cat "$OUT.nswcap1"; exit 1; }

# Six more, and then a layout naming all eight, so the wall the next
# client opens is eight tiles on 24 rows — saturated, with nothing left
# over. The sessions are made first and the file written second because
# the seed refuses a leaf whose host is not listed, not one whose session
# is not live; making them anyway keeps the fixture a wall of eight real
# panes rather than eight `[session ended]` rects, which have no pump to
# refuse a ninth on top of.
#
# Authored, not polled: the file is the wall, so eight tiles is eight
# leaves. Exactly saturated on purpose — a wall with a pane it cannot cut
# says so on the notice line, and that notice would overwrite the one this
# leg is waiting to read.
fill_sessions "$SOCK28" "$OUT.nswfill" fill 2 7
wait_sessions "$SOCK28" 8 "new session: the wall's terminal should be exactly full"

set +e
seed_layout "$NSWSTATE" stacked \
    "--sock $SOCK28#0" "--sock $SOCK28#1" \
    "--sock $SOCK28#fill2" "--sock $SOCK28#fill3" "--sock $SOCK28#fill4" \
    "--sock $SOCK28#fill5" "--sock $SOCK28#fill6" "--sock $SOCK28#fill7"
XDG_STATE_HOME="$NSWSTATE" timeout 60 "$PTYCLIENT" --cols 40 --rows 24 \
    --out "$OUT.nsw2" --err "$OUT.nsw2.err" \
    -- "$MUX" --sock "$SOCK28" > "$OUT.nsw2.log" 2>&1 <<'EOF'
expect m2a-pin 25000
settle 2500 25000
send \x1cc
settle 1500 15000
send printf 'm2d-%s\\n' pin\n
expect m2d-pin 15000
settle 400 15000
# ...and then a focus move, which is what makes the refusal SPEAKABLE: the
# notice is left in `shared` for whichever Core claims the terminal next,
# so a refusal that moves no focus (the two sends above) paints nothing.
# `n` claims a neighbour, that claim takes the notice, and the banner is
# the sentence. `expect`, so a wall that went silent fails here rather than
# in a grep after the run.
send \x1cn
expect no room on the wall 15000
send \x1c\x1c
waitexit 10000
EOF
RC=$?
set -e
rc0 "new session: the saturated-wall leg exited $RC:" "$OUT.nsw2.log"
# The refusal is NOT fatal, and the three claims below are what "recovered"
# means — none of which the log shows on its own.
#
#   * the user was told. The sentence is read off the STREAM and not off a
#     rendered grid: a banner is a transient overwrite that the next repaint
#     takes back, so the final screen is not where it lives. Nothing else in
#     this suite spells this string, so a match is the client having said it.
#
#   * the focus never left the tile the chord was typed in: the marker run
#     AFTER the refusal is in the default session's grid, beside the one
#     from before it. A capture holding only m2a-pin would equally describe
#     a client that died there.
grep -qF -- "[no room on the wall for another tile]" "$OUT.nsw2" || {
    echo "e2e FAIL: new session: the refused chord said nothing — the notice"
    echo "          never reached the terminal on the next claim:"
    cat "$OUT.nsw2.log"; exit 1; }
"$MUX" a capture --sock "$SOCK28" > "$OUT.nswcapd" 2>&1
grep -q "m2d-pin" "$OUT.nswcapd" || {
    echo "e2e FAIL: new session: the refused chord did not come back to the focused session:"
    cat "$OUT.nswcapd"; exit 1; }
#   * and the name it was reaching for was never created. Without this the
#     leg would pass on a wall that refused the tile and made the session
#     anyway — a session nobody can see is the worse of the two failures.
"$MUX" a status --sock "$SOCK28" --session 2 > "$OUT.nswst2" 2>&1 && {
    echo "e2e FAIL: new session: a refused tile left a session behind:"
    cat "$OUT.nswst2"; exit 1; }
assert_stopped "$SOCK28" "$D25PID" "new session" "$OUT.nswstop"
D25PID=""
ok "Ctrl-\\ c: a new session is created and given a tile, the old one intact; a full wall refuses and the client stays where it was"

# ---- M3: Ctrl-\ n / Ctrl-\ p step around the session ring ----------------
#
# THREE sessions, not two, and that is the whole point: with two, next and
# previous land on the same place and the leg would pass with the two
# directions swapped. Three makes a wrong direction land on a different
# marker.
#
# Each session is given its own marker before the stepping starts, so the
# ring's answer is read off the SCREEN rather than off a name: after a
# step, the marker that arrives says which grid the daemon just sent.
#
#   * `\x1cn` from the last slot must wrap to the first — m3a-pin, the
#     default session's marker. A step in the other direction would bring
#     m3b-pin, and the expect would time out on it.
#   * `\x1cp` from the first slot must wrap back to the last — m3c-pin.
#
# The markers arrive a second time because arriving somewhere repaints that
# session's grid; ptyclient's expect cursor consumes matches, so the m3a-pin
# the leg waited for at the top cannot satisfy the one it waits for after
# the wrap.
#
# PHASE 3C changed what a step COSTS, not where it lands. `n` used to detach,
# re-dial and re-attach, and the leg waited for the new client's `?1049h`.
# Now the ring moves the ZOOM: sessions 0, 1 and 2 all have tiles (the `c`
# chords made them), their replicas are hot, and the step is a local repaint
# at zero round trips. The alternate screen is entered once and never left,
# so the arrival signal is `setFocus`'s screen clear followed by the marker
# the repaint carries — and the marker is what this leg was always really
# reading the answer off.
#
# The captures afterwards pin which marker lives where — three sessions
# that each kept their own line, so the stepping moved the CLIENT and left
# the sessions where they were.
start_daemon "$SOCK29" "$OUT.ring.d" "session-ring daemon never bound" --shell /bin/sh
D26PID=$DPID

set +e
# tall: Ctrl-\ c adds tiles at 80x24 (80 >= 48)
no_saved_tree "$RINGSTATE"
XDG_STATE_HOME="$RINGSTATE" timeout 60 "$PTYCLIENT" --cols 40 --rows 24 \
    --out "$OUT.ring" --err "$OUT.ring.err" \
    -- "$MUX" --sock "$SOCK29" > "$OUT.ring.log" 2>&1 <<'EOF'
expect \x1b[?1049h 15000
settle 400 15000
send printf 'm3a-%s\\n' pin\n
expect m3a-pin 15000
settle 400 15000
send \x1cc
expect \x1b[2J 15000
settle 400 15000
send printf 'm3b-%s\\n' pin\n
expect m3b-pin 15000
settle 400 15000
send \x1cc
expect \x1b[2J 15000
settle 400 15000
send printf 'm3c-%s\\n' pin\n
expect m3c-pin 15000
settle 400 15000
send \x1cn
expect m3a-pin 15000
settle 400 15000
send \x1cp
expect m3c-pin 15000
settle 400 15000
send \x1c\x1c
waitexit 10000
EOF
RC=$?
set -e
rc0 "session ring: ptyclient leg exited $RC (did \\x1cn wrap?):" "$OUT.ring.log"
for s in 0:m3a 1:m3b 2:m3c; do
    _sess="${s%%:*}"; _mark="${s##*:}-pin"
    timeout 20 "$MUX" a capture --sock "$SOCK29" --session "$_sess" > "$OUT.ringcap" 2>&1
    grep -q "$_mark" "$OUT.ringcap" || {
        echo "e2e FAIL: session ring: session $_sess does not hold $_mark:"
        cat "$OUT.ringcap"; exit 1; }
    # Its neighbours' markers are not in it: the client moved between
    # sessions, it did not drag one session's shell along behind it.
    grep -v "$_mark" "$OUT.ringcap" | grep -qE 'm3[abc]-pin' && {
        echo "e2e FAIL: session ring: session $_sess holds another session's marker:"
        cat "$OUT.ringcap"; exit 1; }
done
assert_stopped "$SOCK29" "$D26PID" "session ring" "$OUT.ringstop"
D26PID=""
ok "Ctrl-\\ n / Ctrl-\\ p: the ring steps both ways and wraps at both ends"

# ---- M5: a session shell knows where it is, and mux refuses the self-loop --
#
# The incident: `mux --sock S` typed in a shell of session 0 on S attached
# that session to itself. Paint became a delta became a repaint, the inner
# client took the alternate screen, and it ate every keystroke — and since
# Ctrl-\ became a prefix the OUTER keyboard cannot steer an inner client
# back out. There is no escape chord to offer, so the loop is refused.
#
# Three claims, in the order that makes each one's failure legible:
#
#  1. The daemon's identity reached a REAL shell. The SHELL does the
#     comparing — `[ "$MUX_SOCK" = ... ]` — so a pass means the child saw
#     the path this block bound, not that the script can rebuild a string.
#  2. The refusal fires on the self-pair, and the loop never starts. The
#     money assertion is `"alt_screen":false` AFTER the injection: before
#     this milestone that read true, because an inner client really had
#     taken the screen. A message grep alone would still pass if the client
#     printed the line and attached anyway.
#  3. a wall spelling is a DAEMON, so the session this shell is inside
#     cannot be named on the command line at all: `mux hosts add` refuses
#     the `#` by name.
#
# Every marker is assembled by printf from pieces and never typed whole, so
# the grid's echo of the command line can never satisfy the grep. `; echo`
# rather than a `\n` inside printf's format: mux a's send turns every `\n`
# into a real newline, so a format string carrying one would put a shell
# continuation prompt in the middle of the assertion.
#
# No ptyclient here on purpose — a real client on a real pty is what the
# refusal PREVENTS, so the leg that injects it into the session shell is the
# leg that tests it. This block therefore needs no expect timing at all.
#
# 200 columns, and no client ever attaches to narrow it: every assertion
# here greps a MESSAGE out of a grid dump, and a grid wraps. At 80 the wall
# refusal — which names the spelling, so its length follows $TMPDIR — split
# mid-sentence and a correct refusal read as a missing one. Width is the fix
# that does not make the needle shorter than the claim.
MUXABS=$(cd "$(dirname "$MUX")" && pwd)/$(basename "$MUX")
start_daemon "$SOCK31" "$OUT.sa.d" "self-attach daemon never bound" --shell /bin/sh --cols 200
D28PID=$DPID

timeout 20 "$MUX" a send \
    "[ \"\$MUX_SOCK\" = \"$SOCK31\" ] && printf 'ENV%s-%s' OK \"\$MUX_SESSION\"; echo\n" \
    --sock "$SOCK31" --session 0 > "$OUT.saenv" 2>&1
sleep 1
timeout 20 "$MUX" a capture --sock "$SOCK31" --session 0 > "$OUT.sacap1" 2>&1
# ENVOK-0 is both halves at once: the socket matched, and the name the shell
# was told is the RESOLVED default, not the empty spelling the wire uses.
grep -q "ENVOK-0" "$OUT.sacap1" || {
    echo "e2e FAIL: self-attach: the session shell was not told MUX_SOCK=$SOCK31 and MUX_SESSION=0:"
    cat "$OUT.saenv"; cat "$OUT.sacap1"; exit 1; }

timeout 20 "$MUX" a send \
    "$MUXABS --sock $SOCK31; printf 'SELF%s-%s' rc \$?; echo\n" \
    --sock "$SOCK31" --session 0 > "$OUT.saself" 2>&1
sleep 2
timeout 20 "$MUX" a capture --sock "$SOCK31" --session 0 > "$OUT.sacap2" 2>&1
grep -q "unset MUX_SESSION to override" "$OUT.sacap2" || {
    echo "e2e FAIL: self-attach: mux did not refuse the session it was running in:"
    cat "$OUT.saself"; cat "$OUT.sacap2"; exit 1; }
# Exit 2, the usage-error code every other refusal in this binary uses, and
# proof the process ENDED rather than sitting there attached.
grep -q "SELFrc-2" "$OUT.sacap2" || {
    echo "e2e FAIL: self-attach: the refusal did not exit 2 (or did not exit):"
    cat "$OUT.sacap2"; exit 1; }
timeout 20 "$MUX" a status --sock "$SOCK31" --session 0 > "$OUT.sast" 2>&1
grep -q '"alt_screen":false' "$OUT.sast" || {
    echo "e2e FAIL: self-attach: an inner client took the alternate screen — the loop ran:"
    cat "$OUT.sast"; exit 1; }

# The wall's own half of the rule, and it is a SILENCE rather than a
# refusal: `mux hosts add` names a DAEMON, so the session this shell is
# inside cannot be spelled at all — the wall simply never births a tile for
# it (`planHostDiff`'s `self_name`). A `#` spelling is refused by the
# grammar before any of that, which is scenario 09's leg.
timeout 20 "$MUX" a send \
    "$MUXABS hosts add '--sock $SOCK31#0'; printf 'WALL%s-%s' rc \$?; echo\n" \
    --sock "$SOCK31" --session 0 > "$OUT.sawall" 2>&1
sleep 2
timeout 20 "$MUX" a capture --sock "$SOCK31" --session 0 > "$OUT.sacap3" 2>&1
grep -q -- "names a session after" "$OUT.sacap3" || {
    echo "e2e FAIL: self-attach: 'mux hosts add' took a session spelling:"
    cat "$OUT.sawall"; cat "$OUT.sacap3"; exit 1; }
grep -q "WALLrc-2" "$OUT.sacap3" || {
    echo "e2e FAIL: self-attach: the hosts refusal did not exit 2 (or did not exit):"
    cat "$OUT.sacap3"; exit 1; }
assert_stopped "$SOCK31" "$D28PID" "self attach" "$OUT.sastop"
D28PID=""
ok "a session shell carries MUX_SOCK/MUX_SESSION, and mux refuses to attach to itself"