a73x

test/e2e_06_web.sh

Ref:   Size: 34.3 KiB   History

# shellcheck shell=sh
# e2e_06_web.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 pipe_mux client in flight, and its FIFO. Both are registered by
# pipe_mux itself; its `timeout` stays the primary guarantee and the trap
# is the backstop for a scenario that exits between pipe_mux and
# pipe_detach, leaving a client the shell would otherwise orphan.
# M-web. Three daemons (the passivity pin's, the hub-basic one, and the
# tear scenario's two incarnations share SOCK20), two hub processes, and
# a port band of its own following the 5000-spacing convention — INSIDE
# the ephemeral range, which is fine for ports we bind (only provably-DEAD
# ports must stay below 32768; see HDEADPORT above).
SOCK18="${TMPDIR:-/tmp}/muxd-e2e-web-a-$$.sock"
defer_sock "$SOCK18"
SOCK19="${TMPDIR:-/tmp}/muxd-e2e-web-b-$$.sock"
defer_sock "$SOCK19"
SOCK20="${TMPDIR:-/tmp}/muxd-e2e-web-c-$$.sock"
defer_sock "$SOCK20"
WPORT=$(( 41000 + ($$ % 4000) ))
WPORT2=$(( 46000 + ($$ % 4000) ))
# The hosts-wall leg: TWO daemons — a wall of one host is blind to host
# order and to "only its own list may drop a tile" — one hub, and a STATE
# HOME of its own on the 61000 band, the last 5000-spaced one that fits
# under 65535 (the QUIC block's 56000 being the one before). The
# state home is the load-bearing part: this leg reads the HOSTS file back
# as an artifact, and $XDG_STATE_HOME above is shared with every other
# scenario — every `mux` attach records its daemon there, so a file read
# out of it would hold some other block's sockets. Never the developer's
# ~/.local/state either; that is why every hub here is spawned with the
# override in front of it.
SOCK25="${TMPDIR:-/tmp}/muxd-e2e-hostwall-a-$$.sock"
defer_sock "$SOCK25"
SOCK76="${TMPDIR:-/tmp}/muxd-e2e-hostwall-b-$$.sock"
defer_sock "$SOCK76"
WPORT4=$(( 61000 + ($$ % 4000) ))
DWSTATE="${TMPDIR:-/tmp}/mux-e2e-hostwall-state-$$"
defer_rm "$DWSTATE"

# --- a 1x1 attach is refused the grid and can never claim it.
# The degenerate size a client can genuinely arrive with — a terminal one
# column wide — driven here on a real 1x1 pty. Nothing spells passivity
# this way (that is 0x0, and it is refused by contract); this leg is the
# daemon staying unclaimable on the size threshold ALONE. Pinned
# DAEMON-SIDE and hub-free on purpose: the mechanism is applySize's cols<2
# refusal plus claimGrid's 0x0-slot refusal (server.zig), and it must hold
# for any client that sends a size nobody can live at. The probe for "the grid did not move" is textual: a marker
# string typed at 80 wide can only appear CONTIGUOUS in `mux d dump` if
# the grid is still 80 wide — a 1-column grid puts every glyph on its
# own row, so grep itself is the geometry assertion.
start_daemon "$SOCK18" "$OUT.weba.d" "tiny attach daemon never bound" --shell /bin/sh
D14PID=$DPID

# The 80x24 client: types the first marker, holds the session open while
# the 1x1 attacher comes and goes, types the second marker (the geometry
# probe) once the attacher is provably on, detaches.
pipe_mux "$OUT.weba" "$OUT.weba.err" timeout 40 "$MUX" --sock "$SOCK18"
pipe_send 'printf "wall-%%s\\n" pin\n'
wait_grid "$SOCK18" "wall-pin" "tiny attach: the first marker"

# The 1x1 attacher, on a real 1x1 pty. Its attach is answered with a
# unicast snapshot (the alt-screen enter proves the first frame came),
# its keystroke is the claim attempt, and the 'z' it waits for is the
# second marker echoing — proof it was STILL ATTACHED while the marker
# landed, so the grid it could have wrecked was live the whole time. The
# marker is typed only after the fixture reports its claim sent (`done 2`),
# so the ordering is witnessed rather than paced.
hostroom web1x1
XDG_STATE_HOME="$HOSTROOM" timeout 40 "$PTYCLIENT" --cols 1 --rows 1 --out "$OUT.web1x1" --err "$OUT.web1x1.err" \
    -- "$MUX" --sock "$SOCK18" > "$OUT.web1x1.log" 2>&1 <<'EOF' &
expect \x1b[?1049h 15000
send \x20
expect z 20000
settle 300 10000
send \x1c\x1c
waitexit 10000
EOF
W1X1PID=$!
defer_kill "$W1X1PID"
wait_for "$OUT.web1x1.log" "ptyclient: done 2" 20 || {
    echo "e2e FAIL: tiny attach: the 1x1 client never got as far as its claim"
    cat "$OUT.web1x1.log"; cat -v "$OUT.web1x1.err" 2>/dev/null; exit 1; }
pipe_send 'printf "zz-%%s\\n" web\n'
set +e
wait "$W1X1PID"
RC=$?
set -e
[ "$RC" -eq 0 ] || {
    echo "e2e FAIL: tiny attach: 1x1 client exited $RC"
    cat "$OUT.web1x1.log"; cat -v "$OUT.web1x1.err" 2>/dev/null; exit 1; }

# The geometry assertion: both markers contiguous AFTER the 1x1 client
# attached, typed, and left. A grid moved to 1 wide cannot hold either.
"$MUX" d dump --sock "$SOCK18" | grep -q "wall-pin" || {
    echo "e2e FAIL: tiny attach: first marker lost after 1x1 attach:"
    "$MUX" d dump --sock "$SOCK18"; exit 1; }
"$MUX" d dump --sock "$SOCK18" | grep -q "zz-web" || {
    echo "e2e FAIL: tiny attach: the 1x1 attacher moved the grid (marker not contiguous):"
    "$MUX" d dump --sock "$SOCK18"; exit 1; }

await_out "$OUT.weba" "zz-web" "tiny attach: the second marker never reached the 80x24 client"
pipe_detach "tiny attach: 80x24 client"
assert_converged "$OUT.weba" "$SOCK18" "tiny attach: the 80x24 client never glitched"

assert_stopped "$SOCK18" "$D14PID" "tiny attach" "$OUT.webstop"
D14PID=""
ok "a 1x1 attach is refused the grid and can never claim it"

# --- M-web (b): the hub pumps a real session; wrong Origin refused.
start_daemon "$SOCK19" "$OUT.webb.d" "web hub daemon never bound" --shell /bin/sh
D15PID=$DPID
# A state home of its own: `mux web --sock S` RECORDS S in the hosts file
# it then serves, so a shared home would give this hub every other leg's
# daemons as tiles too.
hostroom webb
# ...and a layout of its own: the hub serves the LAYOUT's panes, so a hosts
# file alone is a hub with nothing on it. One leaf, the daemon's own
# session 0, which is the tile the stand-in below dials.
seed_layout "$HOSTROOM" stacked "--sock $SOCK19#0"
XDG_STATE_HOME="$HOSTROOM" "$MUX" web --sock "$SOCK19" --port "$WPORT" > "$OUT.webh" 2>&1 &
W1PID=$!
defer_kill "$W1PID"
# `serving` is the door; a TILE is a leaf of the layout, announced as the
# hub seats it. The stand-in dials `/ws/0`, so wait for the id to exist.
wait_for "$OUT.webh" "tile 0:" 10 || {
    echo "e2e FAIL: hub never announced a tile"; cat "$OUT.webh"; exit 1; }

# The typing client stays attached until after the browser leg below, so
# the hub pumps a session that has a live CLI client on it too.
pipe_mux "$OUT.webb" "$OUT.webb.err" timeout 40 "$MUX" --sock "$SOCK19"
pipe_send 'printf "web-%%s\\n" b1\n'
wait_grid "$SOCK19" "web-b1" "web hub: the typing client's marker"

# The browser stand-in: a passive 0x0 wall tile through the hub. Its
# final grid is the daemon's replica as REPLAYED THROUGH the WebSocket
# leg, dumped in mux d dump's own format.
set +e
timeout 40 "$WSCLIENT" --port "$WPORT" --tile 0 --out "$OUT.webws" --err "$OUT.webws.err" <<'EOF'
attach 0 0
expectstate up 10000
expectgrid web-b1 15000
settle 500 10000
dumpexit
EOF
RC=$?
set -e
[ "$RC" -eq 0 ] || {
    echo "e2e FAIL: web hub: wsclient exited $RC"
    cat -v "$OUT.webws.err" 2>/dev/null; cat "$OUT.webh"; exit 1; }

assert_ws_converged "$OUT.webws" "$SOCK19" "web hub"

# Wrong Origin: refused at HTTP, before any upgrade — the wsclient sees
# a non-101 and exits 4 — and the daemon never sees a client for it.
CLIENTS_BEFORE=$("$MUX" d stats --sock "$SOCK19" | sed -n 's/.*clients=\([0-9]*\).*/\1/p')
set +e
timeout 20 "$WSCLIENT" --port "$WPORT" --tile 0 --origin http://evil.example \
    --out "$OUT.webevil" --err "$OUT.webevil.err" < /dev/null
RC=$?
set -e
[ "$RC" -eq 4 ] || {
    echo "e2e FAIL: web hub: evil-origin wsclient exited $RC, want 4"
    cat -v "$OUT.webevil.err" 2>/dev/null; exit 1; }
grep -q "upgrade refused.*403" "$OUT.webevil.err" || {
    echo "e2e FAIL: web hub: refusal was not the 403 it must be:"
    cat -v "$OUT.webevil.err"; exit 1; }
CLIENTS_AFTER=$("$MUX" d stats --sock "$SOCK19" | sed -n 's/.*clients=\([0-9]*\).*/\1/p')
[ "$CLIENTS_BEFORE" = "$CLIENTS_AFTER" ] || {
    echo "e2e FAIL: web hub: evil origin reached the daemon (clients $CLIENTS_BEFORE -> $CLIENTS_AFTER)"; exit 1; }

pipe_detach "web hub: typing client"
assert_converged "$OUT.webb" "$SOCK19" "web hub: the typing client"

softkill "$W1PID" || true
wait_pid_gone "$W1PID" "web hub: killed by tracked pid"
W1PID=""
assert_stopped "$SOCK19" "$D15PID" "web hub" "$OUT.webstop2"
D15PID=""
ok "hub pumps a real session; wrong origin refused"

# --- M-web (c): the hub narrates the tear; the replica re-attaches
# across an epoch. mux d stop kills the daemon under a live tile; the hub
# must say `reconnecting`, redial the restarted daemon (same socket, new
# epoch), say `up`, and the stand-in's re-attach — quoting coordinates
# the NEW daemon has never issued — must be answered with a snapshot
# that converges on the new session's content.
start_daemon "$SOCK20" "$OUT.webc.d" "web tear daemon never bound" --shell /bin/sh
D16PID=$DPID
hostroom webc
seed_layout "$HOSTROOM" stacked "--sock $SOCK20#0"
XDG_STATE_HOME="$HOSTROOM" "$MUX" web --sock "$SOCK20" --port "$WPORT2" > "$OUT.webh2" 2>&1 &
W2PID=$!
defer_kill "$W2PID"
wait_for "$OUT.webh2" "tile 0:" 10 || {
    echo "e2e FAIL: tear hub never announced a tile"; cat "$OUT.webh2"; exit 1; }

pipe_mux "$OUT.webc" "$OUT.webc.err" timeout 30 "$MUX" --sock "$SOCK20"
pipe_send 'printf "web-%%s\\n" c1\n'
await_out "$OUT.webc" "web-c1" "web-c1 never reached the client"
pipe_detach
wait_grid "$SOCK20" "web-c1" "web tear: the first session's marker"

timeout 90 "$WSCLIENT" --port "$WPORT2" --tile 0 --out "$OUT.webws2" --err "$OUT.webws2.err" <<'EOF' &
attach 0 0
expectstate up 10000
expectgrid web-c1 15000
expectstate reconnecting 25000
expectstate up 25000
attach 0 0
expectgrid web-c2 30000
settle 500 10000
dumpexit
EOF
WCLIPID=$!
defer_kill "$WCLIPID"
# Let the stand-in reach its first expectgrid before the tear; the marker
# it waits for is already on the grid, so one settle-length is plenty.
sleep 1

assert_stopped "$SOCK20" "$D16PID" "web tear: the first daemon, under a live tile" "$OUT.webstop3"
D16PID=""

start_daemon "$SOCK20" "$OUT.webc2.d" "web tear restart never bound" --shell /bin/sh
D17PID=$DPID

pipe_mux "$OUT.webc2" "$OUT.webc2.err" timeout 30 "$MUX" --sock "$SOCK20"
pipe_send 'printf "web-%%s\\n" c2\n'
await_out "$OUT.webc2" "web-c2" "web-c2 never reached the client"
pipe_detach

set +e
wait "$WCLIPID"
RC=$?
set -e
WCLIPID=""
[ "$RC" -eq 0 ] || {
    echo "e2e FAIL: web tear: wsclient exited $RC"
    cat -v "$OUT.webws2.err" 2>/dev/null; cat "$OUT.webh2"; exit 1; }

assert_ws_converged "$OUT.webws2" "$SOCK20" "web tear: after the epoch crossing"

softkill "$W2PID" || true
wait_pid_gone "$W2PID" "web tear: hub killed by tracked pid"
W2PID=""
# Its own capture, not a second write to $OUT.webstop3: that file still
# holds the FIRST stop's stderr, and overwriting it before anything reads
# it is how the evidence for a failure up there disappears.
assert_stopped "$SOCK20" "$D17PID" "web tear: the restarted daemon" "$OUT.webstop4"
D17PID=""
ok "hub narrates the tear; the replica re-attaches across an epoch"

# --- M-wall: the hub's wall is the LAYOUT. `/tiles` lists its leaves in
# the file's own order, a session born anywhere else is not on it, and `+`
# writes the pane it births back into the file. Nothing here is authored
# in the browser beyond that one verb: the routes that used to add, remove
# and reorder answer 405. Every mux web here runs with a state home of its
# own (see $DWSTATE), so the file this leg reads back is the file this leg
# wrote — and never the developer's real one.
#
# TWO daemons and an off-origin session on the first, and the layout puts
# them in an order NO other rule would produce — B's session before A's
# two, and A's `b` before A's `0`. A wall in host order then daemon order
# (which is what the hub used to build) reads `A#0 A#b B#0` here, so the
# order assertion below is a claim about the FILE and not a restatement of
# how the fixture happens to be built.
start_daemon "$SOCK25" "$OUT.dwa.d" "host wall daemon A never bound" --shell /bin/sh
D22PID=$DPID
start_daemon "$SOCK76" "$OUT.dwb.d" "host wall daemon B never bound" --shell /bin/sh
D23PID=$DPID

# A's second session, made the way a user makes one. It has to outlive its
# client: a session ends when its shell exits, not when a client leaves.
pipe_mux "$OUT.dwmk" "$OUT.dwmk.err" timeout 40 "$MUX" --sock "$SOCK25" --session b
pipe_send 'printf "host-%%s\\n" mk\n'
wait_grid "$SOCK25" "host-mk" "host wall: session b exists" b
pipe_detach "host wall: the session-b maker"

# An EMPTY file is an empty wall, not a refusal — and specifically not the
# local socket: nothing asked for a daemon, and a read never starts one.
# Run before the argv hub so the file this leg then builds is built from
# nothing, and the one stderr line is asserted where it is the only line.
mkdir -p "$DWSTATE/mux"
: > "$DWSTATE/mux/hosts"
XDG_STATE_HOME="$DWSTATE" "$MUX" web --port "$WPORT4" > "$OUT.dwempty" 2>&1 &
W5PID=$!
defer_kill "$W5PID"
wait_for "$OUT.dwempty" "serving" 10 || {
    echo "e2e FAIL: host wall: the empty-file hub never served"; cat "$OUT.dwempty"; exit 1; }
grep -q "no hosts" "$OUT.dwempty" || {
    echo "e2e FAIL: host wall: an empty wall said nothing about being empty:"
    cat "$OUT.dwempty"; exit 1; }
[ "$(curl -s "http://127.0.0.1:$WPORT4/tiles")" = "[]" ] || {
    echo "e2e FAIL: host wall: the empty-file hub served tiles:"
    curl -s "http://127.0.0.1:$WPORT4/tiles"; exit 1; }
# ...and it added nothing. A hub that fell back to the local socket would
# have written a line here, and every later assertion in this leg would be
# reading a file with a daemon in it that nobody asked for.
[ ! -s "$DWSTATE/mux/hosts" ] || {
    echo "e2e FAIL: host wall: the empty-file hub wrote a host line:"
    cat "$DWSTATE/mux/hosts"; exit 1; }
softkill "$W5PID" || true
wait_pid_gone "$W5PID" "host wall: empty-file hub killed by tracked pid"
W5PID=""

# The wall, authored: three leaves over the two daemons, in an order
# neither host order nor list order would give.
seed_layout "$DWSTATE" beside \
    "--sock $SOCK76#0" "--sock $SOCK25#b" "--sock $SOCK25#0"

# Argv is RECORDED, not a view of its own: `mux web "--sock A" "--sock B"`
# is `mux hosts add` twice and then a hub on the file.
XDG_STATE_HOME="$DWSTATE" "$MUX" web "--sock $SOCK25" "--sock $SOCK76" --port "$WPORT4" > "$OUT.dwh" 2>&1 &
W4PID=$!
defer_kill "$W4PID"
DWORIG="http://127.0.0.1:$WPORT4"
# `serving` first: the hub records its argv and reads the file back before
# it binds, so the door being open is what says the write has happened.
wait_for "$OUT.dwh" "serving" 10 || {
    echo "e2e FAIL: host wall: hub never reported serving"; cat "$OUT.dwh"; exit 1; }
grep -qxF -- "--sock $SOCK25" "$DWSTATE/mux/hosts" 2>/dev/null || {
    echo "e2e FAIL: host wall: argv did not reach the hosts file; it holds:"
    cat "$DWSTATE/mux/hosts" 2>/dev/null; exit 1; }
grep -qxF -- "--sock $SOCK76" "$DWSTATE/mux/hosts" || {
    echo "e2e FAIL: host wall: the second host did not reach the file; it holds:"
    cat "$DWSTATE/mux/hosts"; exit 1; }
# The retired file is retired: a hub that still wrote one would keep two
# answers to "what is on the wall" alive, which is the whole point of D2.
[ ! -e "$DWSTATE/mux/wall" ] || {
    echo "e2e FAIL: host wall: the hub wrote a wall file; it holds:"
    cat "$DWSTATE/mux/wall"; exit 1; }

# Three tiles, one per leaf, announced as the hub seats them. Ids are
# BIRTH order and a `+` mints more of them, so the ids are not this leg's
# to predict — the wall's ORDER is the FILE's, and `/ws/<id>` is read out
# of the answer rather than assumed.
for _t in 0 1 2; do
    wait_for "$OUT.dwh" "tile $_t:" 15 || {
        echo "e2e FAIL: host wall: tile $_t never appeared; the hub said:"
        cat "$OUT.dwh"; exit 1; }
done
# One string, not three greps: which tile sits where is the claim, and
# separate greps would pass on a wall holding them in any order at all.
dw_shape() { tiles_shape "$DWORIG"; }
dw_id_of() { tile_id_of "$DWORIG" "$1" "$2"; }
DWWANT="[{\"label\":\"--sock $SOCK76\",\"session\":\"0\"},"
DWWANT="$DWWANT{\"label\":\"--sock $SOCK25\",\"session\":\"b\"},"
DWWANT="$DWWANT{\"label\":\"--sock $SOCK25\",\"session\":\"0\"}]"
[ "$(dw_shape)" = "$DWWANT" ] || {
    echo "e2e FAIL: host wall: /tiles is not the layout file's leaf order"
    echo "  got:  $(dw_shape)"
    echo "  want: $DWWANT"; exit 1; }
# The ids this leg then names. Read from the answer, so a hub that numbered
# differently is still asserted against — and a missing id fails here.
DWID_A0=$(dw_id_of "--sock $SOCK25" 0)
DWID_AB=$(dw_id_of "--sock $SOCK25" b)
DWID_B0=$(dw_id_of "--sock $SOCK76" 0)
[ -n "$DWID_A0" ] && [ -n "$DWID_AB" ] && [ -n "$DWID_B0" ] || {
    echo "e2e FAIL: host wall: could not read the three tile ids out of:"
    curl -s "$DWORIG/tiles"; exit 1; }
# Every pane says how its poller graded it, which is the field the page
# reads to decide what to paint. Three live sessions on two answering
# daemons, so nothing here is `gone`.
curl -s "$DWORIG/tiles" | grep -q '"state":"gone"' && {
    echo "e2e FAIL: host wall: a pane on a live session was graded gone:"
    curl -s "$DWORIG/tiles"; exit 1; }

# A session born ANYWHERE ELSE is not on this wall. It is a live session on
# a listed daemon that the hub is polling once a second, and the only
# reason it gets no tile is that no leaf names it — which is the whole of
# the model. Three seconds is several poll answers, and it is SCALED like
# every other budget in this suite (`TIME_SCALE`): under test/coverage.sh a
# traced hub is half again slower, and a wait that did not scale would make
# "no tile appeared" a claim about how little the hub got done rather than
# about what it decided.
hostroom dwoff
pipe_mux "$OUT.dwoff" "$OUT.dwoff.err" env XDG_STATE_HOME="$HOSTROOM" timeout 40 \
    "$MUX" --sock "$SOCK25" --session offwall
pipe_send 'printf "dwoff-%%s\\n" mark\n'
await_out "$OUT.dwoff" "dwoff-mark" "host wall: the off-wall session never came up"
pipe_detach "host wall: the off-wall session's maker"
wait_grid "$SOCK25" "dwoff-mark" "host wall: the off-wall session's marker" offwall
sleep "$(( 3 * TIME_SCALE ))"
[ "$(dw_shape)" = "$DWWANT" ] || {
    echo "e2e FAIL: host wall: a session born off the wall became a tile; /tiles holds:"
    curl -s "$DWORIG/tiles"; exit 1; }
# ...and it is not in the file either: nothing but a `+` writes a pane.
grep -qF -- "--sock $SOCK25#offwall" "$DWSTATE/mux/layout" && {
    echo "e2e FAIL: host wall: a session born off the wall was written into the layout:"
    cat "$DWSTATE/mux/layout"; exit 1; }

# A tile is a REAL session: a marker typed through the CLI door on A#b is
# read back through the WebSocket door of the tile that names it — A#b's
# id, not A#0's, which is the same daemon and a different session. The stand-in
# attaches at 0x0 and the CLI client has already detached, so the passivity
# contract is in play in passing.
pipe_mux "$OUT.dwcli" "$OUT.dwcli.err" timeout 40 "$MUX" --sock "$SOCK25" --session b
pipe_send 'printf "dyn-%%s\\n" w1\n'
await_out "$OUT.dwcli" "dyn-w1" "dyn-w1 never reached the client"
pipe_detach "host wall: CLI client"
wait_grid "$SOCK25" "dyn-w1" "host wall: CLI marker" b
set +e
timeout 40 "$WSCLIENT" --port "$WPORT4" --tile "$DWID_AB" --out "$OUT.dwws" --err "$OUT.dwws.err" <<'EOF'
attach 0 0 b
expectstate up 10000
expectgrid dyn-w1 15000
dumpexit
EOF
RC=$?
set -e
[ "$RC" -eq 0 ] || {
    echo "e2e FAIL: host wall: wsclient exited $RC"
    cat -v "$OUT.dwws.err" 2>/dev/null; cat "$OUT.dwh"; exit 1; }

# No Origin: refused before anything happens. A text/plain POST is a CSRF
# "simple request" any page can fire at localhost, so the gate is what keeps
# this page's power this page's — and the wall must be untouched after it.
RC=$(curl -s -o /dev/null -w '%{http_code}' -X POST "$DWORIG/tiles/$DWID_A0")
[ "$RC" = "403" ] || { echo "e2e FAIL: host wall: originless POST got $RC, want 403"; exit 1; }
# The wall unchanged AND the hub still answering: a bodyless POST is what
# `curl -X POST` sends, and std's keep-alive body discard asserts on one —
# which aborted the whole hub, every tile with it, until every answer on
# this route started closing its connection.
[ "$(dw_shape)" = "$DWWANT" ] || {
    echo "e2e FAIL: host wall: the refused POST still changed the wall (or killed the hub):"
    curl -s "$DWORIG/tiles"; exit 1; }

# Route precision: /tiles is an EXACT match, not a prefix — `/tilesgarbage`
# must fall through to the asset router's plain 404, never into the block
# that spawns.
RC=$(curl -s -o /dev/null -w '%{http_code}' -H "Origin: $DWORIG" -X POST "$DWORIG/tilesgarbage")
[ "$RC" = "404" ] || { echo "e2e FAIL: host wall: /tilesgarbage POST got $RC, want 404"; exit 1; }

# The three verbs the browser used to author the wall with. They are gone
# with the wall file, and a 405 is how a page built against the old hub
# learns that rather than silently appearing to work.
for _m in POST PUT; do
    RC=$(curl -s -o /dev/null -w '%{http_code}' -H "Origin: $DWORIG" -X "$_m" --data "--sock $SOCK25" "$DWORIG/tiles")
    [ "$RC" = "405" ] || { echo "e2e FAIL: host wall: $_m /tiles got $RC, want 405"; exit 1; }
done
RC=$(curl -s -o /dev/null -w '%{http_code}' -H "Origin: $DWORIG" -X DELETE "$DWORIG/tiles/$DWID_A0")
[ "$RC" = "405" ] || { echo "e2e FAIL: host wall: DELETE /tiles/<id> got $RC, want 405"; exit 1; }
[ "$(dw_shape)" = "$DWWANT" ] || {
    echo "e2e FAIL: host wall: a refused verb still changed the wall:"
    curl -s "$DWORIG/tiles"; exit 1; }

# `+`: a new session on THAT tile's daemon, named by the daemon —
# `nextFreeName` off A's own list, and the pane WRITTEN INTO THE FILE
# beside the one it was born from. Two roads would be two walls, so there
# is one: the birth appends the leaf and the hub seats a tile for it.
SESS_BEFORE=$("$MUX" d stats --sock "$SOCK25" | sed -n 's/.*sessions=\([0-9]*\).*/\1/p')
[ -n "$SESS_BEFORE" ] || {
    echo "e2e FAIL: host wall: no sessions= in stats (the field moved?); it says:"
    "$MUX" d stats --sock "$SOCK25"; exit 1; }
# A's list is `0`, `b` and the `offwall` session made above, so the lowest
# free integer beside them is `1`.
R=$(curl -s -H "Origin: $DWORIG" -X POST "$DWORIG/tiles/$DWID_A0")
[ "$R" = '{"session":"1"}' ] || {
    echo "e2e FAIL: host wall: spawn on A's tile returned $R, want session 1"; exit 1; }
wait_until 60 "host wall: the born session never became a tile; the wall holds:" \
    '[ -n "$(dw_id_of "--sock $SOCK25" 1)" ]' 'curl -s "$DWORIG/tiles"'
# Asked of the DAEMON, not of the hub: a hub reporting on a birth it made
# cannot catch itself being wrong about whether a shell exists.
SESS_AFTER=$("$MUX" d stats --sock "$SOCK25" | sed -n 's/.*sessions=\([0-9]*\).*/\1/p')
[ "$SESS_AFTER" = "$(( SESS_BEFORE + 1 ))" ] || {
    echo "e2e FAIL: host wall: spawn did not make a session ($SESS_BEFORE -> $SESS_AFTER)"; exit 1; }
# An id nothing names is a 404, not a birth on some other host.
RC=$(curl -s -o /dev/null -w '%{http_code}' -H "Origin: $DWORIG" -X POST "$DWORIG/tiles/999")
[ "$RC" = "404" ] || { echo "e2e FAIL: host wall: spawn on an unknown id got $RC, want 404"; exit 1; }

# The id the born tile is wearing now, so "never reused" can be asserted
# against a number rather than against a hope.
DWID_A1=$(dw_id_of "--sock $SOCK25" 1)
[ -n "$DWID_A1" ] || { echo "e2e FAIL: host wall: the born tile has no id"; exit 1; }

# A second `+`, on the OTHER daemon's pane. Its session is called `1` too —
# B's own list is just `0` — and it must be a different TILE: ids are the
# hub's, not each host's, and a browser holds `/ws/<id>` across every
# change it did not make, so an id handed out twice would silently
# re-point that socket at a different shell on a different machine.
#
# The other daemon and not the same anchor twice, because the same anchor
# twice inside one poll interval is a DUPLICATE now and answers 409
# (aed8e90a) — both presses read one `sessions_reply` and ask for one
# name. That refusal has a pin of its own further down, in the shape that
# does not race a poll; what this pair needs is two births that really
# happen, which is what two daemons give it.
R=$(curl -s -H "Origin: $DWORIG" -X POST "$DWORIG/tiles/$DWID_B0")
[ "$R" = '{"session":"1"}' ] || {
    echo "e2e FAIL: host wall: spawn on B's tile returned $R, want session 1"; exit 1; }
wait_until 60 "host wall: B's birth never became a tile; the wall holds:" \
    '[ -n "$(dw_id_of "--sock $SOCK76" 1)" ]' 'curl -s "$DWORIG/tiles"'
DWID_B1=$(dw_id_of "--sock $SOCK76" 1)
[ "$DWID_B1" != "$DWID_A1" ] || {
    echo "e2e FAIL: host wall: two daemons' session 1 came back on ONE id $DWID_A1"
    curl -s "$DWORIG/tiles"; exit 1; }

# The FILE is the wall, so both panes have to be in it — each immediately
# after the leaf it was born beside, which is where `Tree.insert` puts a
# sibling and where a terminal wall opening this file will paint it. B's
# lands SECOND, in the middle of the file, which is what says the anchor
# and not the end is where a birth goes.
DWLEAVES=$(sed -n 's/^ *leaf [0-9][0-9]* //p' "$DWSTATE/mux/layout" | tr '\n' ' ')
DWLWANT="--sock $SOCK76#0 --sock $SOCK76#1 --sock $SOCK25#b --sock $SOCK25#0 --sock $SOCK25#1 "
[ "$DWLEAVES" = "$DWLWANT" ] || {
    echo "e2e FAIL: host wall: a born pane is not beside the one it was born from"
    echo "  got:  $DWLEAVES"
    echo "  want: $DWLWANT"
    cat "$DWSTATE/mux/layout"; exit 1; }
# ...and `/tiles` says the same, in the same order: one wall, read twice.
DWWANT="[{\"label\":\"--sock $SOCK76\",\"session\":\"0\"},"
DWWANT="$DWWANT{\"label\":\"--sock $SOCK76\",\"session\":\"1\"},"
DWWANT="$DWWANT{\"label\":\"--sock $SOCK25\",\"session\":\"b\"},"
DWWANT="$DWWANT{\"label\":\"--sock $SOCK25\",\"session\":\"0\"},"
DWWANT="$DWWANT{\"label\":\"--sock $SOCK25\",\"session\":\"1\"}]"
[ "$(dw_shape)" = "$DWWANT" ] || {
    echo "e2e FAIL: host wall: /tiles and the layout disagree about the born panes"
    echo "  got:  $(dw_shape)"
    echo "  want: $DWWANT"; exit 1; }

# A session that ENDS dresses its pane `gone` and does not take it: the
# pane is a rect the user wrote down and an offer to start it again, not a
# session. The shell is gone per the OS rather than per the hub — the pid
# is read off the GRID, the shell's own `$$`, because a daemon reporting on
# its own children cannot catch itself being wrong about whether one is
# still there.
timeout 20 "$MUX" a send 'printf "bornpid-%s\n" $$\n' --sock "$SOCK25" --session 1 > /dev/null 2>&1
wait_until 80 "host wall: the born session never said its own pid; it holds:" \
    'timeout 20 "$MUX" a capture --sock "$SOCK25" --session 1 > "$OUT.dwpid" 2>&1; grep -q "bornpid-[0-9]" "$OUT.dwpid"' \
    'cat "$OUT.dwpid"'
DWSHELL=$(sed -n 's/.*bornpid-\([0-9][0-9]*\).*/\1/p' "$OUT.dwpid" | head -1)
[ -n "$DWSHELL" ] || {
    echo "e2e FAIL: host wall: the born session never said its own pid; it holds:"
    cat "$OUT.dwpid"; exit 1; }
timeout 20 "$MUX" a send 'exit\n' --sock "$SOCK25" --session 1 > /dev/null 2>&1 || true
wait_until 80 "host wall: the ended session's pane never wore gone; the wall holds:" \
    'curl -s "$DWORIG/tiles" | tr "{" "\n" | grep -q "\"session\":\"1\",\"state\":\"gone\""' \
    'curl -s "$DWORIG/tiles"'
# Asked of the OS, not of the daemon — through the oracle helpers, so the
# spelling of "is this pid alive" belongs to e2e_lib.sh and this line asks
# the question rather than naming /proc.
! pid_alive "$DWSHELL" || {
    echo "e2e FAIL: host wall: the shell pid $DWSHELL outlived its session per the OS"
    pid_args "$DWSHELL"; echo; exit 1; }
# The pane STAYS, on its own id and in its own place: five leaves, and the
# ended one still among them. A hub that vanished a graded pane would take
# a rect the user authored on the strength of one poll answer.
[ "$(dw_id_of "--sock $SOCK25" 1)" = "$DWID_A1" ] || {
    echo "e2e FAIL: host wall: the ended session's pane left or changed id;"
    echo "          the wall holds:"; curl -s "$DWORIG/tiles"; exit 1; }

# ...and a `+` that would land on a pane the file ALREADY names is refused
# rather than served. This is the deterministic shape of it: session 1 has
# ended, so `nextFreeName` hands `1` straight back, while the pane for `1`
# is still a leaf — press `+` and the hub would write `--sock A#1` twice.
# `wall_layout.seedLayout` refuses a repeated leaf by refusing the WHOLE
# file, so before aed8e90a one press here cost the next terminal `mux` on
# this machine its entire wall. 409 with `duplicate`, and nothing born:
# the page refetches onto the tile that is already there.
DWDUPRC=$(curl -s -o "$OUT.dwdup" -w '%{http_code}' -H "Origin: $DWORIG" -X POST "$DWORIG/tiles/$DWID_A0")
[ "$DWDUPRC" = "409" ] || {
    echo "e2e FAIL: host wall: a + onto a pane the file already names got $DWDUPRC, want 409"
    cat "$OUT.dwdup"; curl -s "$DWORIG/tiles"; exit 1; }
grep -q "duplicate" "$OUT.dwdup" || {
    echo "e2e FAIL: host wall: the refused + did not say why:"; cat "$OUT.dwdup"; exit 1; }
# Nothing born and nothing written: the daemon's count and the file are
# both what they were, which is what separates a refusal from a 502 after
# a session had already been made.
[ "$(dw_shape)" = "$DWWANT" ] || {
    echo "e2e FAIL: host wall: the refused + still changed the wall; /tiles holds:"
    curl -s "$DWORIG/tiles"; exit 1; }
[ "$(sed -n 's/^ *leaf [0-9][0-9]* //p' "$DWSTATE/mux/layout" | tr '\n' ' ')" = "$DWLWANT" ] || {
    echo "e2e FAIL: host wall: the refused + still wrote a leaf:"
    cat "$DWSTATE/mux/layout"; exit 1; }

# An id the wall does not have is answered in HTTP: a 404 the page can
# read, rather than an upgrade followed by a silent close it can only
# guess at. A page holding one is not hypothetical — a hub restarted on a
# shorter file hands out fewer ids than the last one did. The stand-in
# sees a non-101 and exits 4, the wrong-Origin shape.
set +e
timeout 20 "$WSCLIENT" --port "$WPORT4" --tile 999 \
    --out "$OUT.dwdead" --err "$OUT.dwdead.err" < /dev/null
RC=$?
set -e
[ "$RC" -eq 4 ] || {
    echo "e2e FAIL: host wall: wsclient on an unknown tile exited $RC, want 4"
    cat -v "$OUT.dwdead.err" 2>/dev/null; exit 1; }
grep -q "upgrade refused.*404" "$OUT.dwdead.err" || {
    echo "e2e FAIL: host wall: an unknown id was not refused with a 404:"
    cat -v "$OUT.dwdead.err"; exit 1; }

# A daemon that goes AWAY is not a session that ended: an unreachable poll
# is no evidence either way, so B's tile rides it out and comes back when B
# does — same tile, same id. A wall that vanished tiles on a failed poll
# would tear itself down over one dropped packet, and the browser would
# lose a socket to every network hiccup.
DWSHAPE_OUT=$(dw_shape)
assert_stopped "$SOCK76" "$D23PID" "host wall: daemon B" "$OUT.dwstopb"
D23PID=""
sleep 3
[ "$(dw_shape)" = "$DWSHAPE_OUT" ] || {
    echo "e2e FAIL: host wall: a stopped daemon's tiles vanished; the wall holds:"
    curl -s "$DWORIG/tiles"
    echo "  was: $DWSHAPE_OUT"; exit 1; }
start_daemon "$SOCK76" "$OUT.dwb2.d" "host wall daemon B restart never bound" --shell /bin/sh
D23PID=$DPID
sleep 3
[ "$(dw_id_of "--sock $SOCK76" 0)" = "$DWID_B0" ] || {
    echo "e2e FAIL: host wall: B's tile did not heal onto its own id $DWID_B0:"
    curl -s "$DWORIG/tiles"; exit 1; }

# A port a live hub owns is refused, in `mux d`'s words. Asked of a REAL
# second process while the first is serving, because the bug was one the
# kernel allowed: SO_REUSEPORT let both bind, `ss` showed two LISTEN rows,
# and a browser reached whichever the kernel handed the connection to.
set +e
# Under timeout, like the QUIC edition in e2e_01_boot.sh and for the same
# reason: a second hub that WINS the port serves until killed, so the
# regression has to show up as 124 rather than as a hung suite.
XDG_STATE_HOME="$DWSTATE" timeout 10 "$MUX" web --port "$WPORT4" > "$OUT.dwtaken" 2>&1
RC=$?
set -e
[ "$RC" -eq 1 ] || {
    echo "e2e FAIL: host wall: a second hub on $WPORT4 exited $RC (want 1; 124 means it bound and served)"
    cat "$OUT.dwtaken"; exit 1; }
grep -q "already running" "$OUT.dwtaken" || {
    echo "e2e FAIL: host wall: the taken-port refusal did not say already running:"
    cat "$OUT.dwtaken"; exit 1; }
grep -q "serving" "$OUT.dwtaken" && {
    echo "e2e FAIL: host wall: the second hub announced itself on a taken port:"
    cat "$OUT.dwtaken"; exit 1; }
# Asked of the OS, not of either hub, through the oracle's tcp_listeners
# (the Linux arm reads /proc/net/tcp, the Darwin arm asks lsof): exactly one
# socket LISTENs there.
LISTENERS=$(tcp_listeners "$WPORT4")
[ "$LISTENERS" -eq 1 ] || {
    echo "e2e FAIL: host wall: $LISTENERS listeners on $WPORT4, want 1"; exit 1; }
# ...and the survivor is the first hub, still serving the wall it built:
# the refusal happens in the newcomer, and the running hub never hears it.
[ -n "$(curl -s "$DWORIG/tiles")" ] || {
    echo "e2e FAIL: host wall: the refused hub disturbed the live one"; exit 1; }

softkill "$W4PID" || true
wait_pid_gone "$W4PID" "host wall: hub killed by tracked pid"
W4PID=""

# `#SESSION` is refused at both mouths, in the same words, and neither
# refusal binds a port: a hub that started and then complained would be a
# daemon list with a session pinned into it.
set +e
XDG_STATE_HOME="$DWSTATE" "$MUX" web "--sock $SOCK25#b" --port "$WPORT4" > "$OUT.dwrefuse" 2>&1
RC=$?
set -e
[ "$RC" -eq 2 ] || {
    echo "e2e FAIL: host wall: mux web with a #SESSION exited $RC, want 2"
    cat "$OUT.dwrefuse"; exit 1; }
grep -q "#" "$OUT.dwrefuse" && grep -qi "session" "$OUT.dwrefuse" || {
    echo "e2e FAIL: host wall: the refusal never named the '#'; it said:"
    cat "$OUT.dwrefuse"; exit 1; }
grep -q "serving" "$OUT.dwrefuse" && {
    echo "e2e FAIL: host wall: a refused argv still bound a port:"
    cat "$OUT.dwrefuse"; exit 1; }

# The same refusal from the FILE, and the offending line printed back: the
# fix is in the file, so the message has to name what to fix.
printf 'box#old\n' >> "$DWSTATE/mux/hosts"
set +e
XDG_STATE_HOME="$DWSTATE" "$MUX" web --port "$WPORT4" > "$OUT.dwrefuse2" 2>&1
RC=$?
set -e
[ "$RC" -eq 2 ] || {
    echo "e2e FAIL: host wall: a '#' line in the file exited $RC, want 2"
    cat "$OUT.dwrefuse2"; exit 1; }
grep -q "box#old" "$OUT.dwrefuse2" || {
    echo "e2e FAIL: host wall: the refusal never printed the bad line; it said:"
    cat "$OUT.dwrefuse2"; exit 1; }
grep -q "serving" "$OUT.dwrefuse2" && {
    echo "e2e FAIL: host wall: a refused file still bound a port:"
    cat "$OUT.dwrefuse2"; exit 1; }

assert_stopped "$SOCK76" "$D23PID" "host wall: daemon B" "$OUT.dwstopb2"
D23PID=""
assert_stopped "$SOCK25" "$D22PID" "host wall: daemon A" "$OUT.dwstop"
D22PID=""
ok "the hub's wall is the layout: /tiles lists its leaves in order, a session born elsewhere is not listed, and + writes the new pane into the file"