a73x

test/e2e_05_session.sh

Ref:   Size: 22.9 KiB   History

# shellcheck shell=sh
# e2e_05_session.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.
# M18. Two daemons — the CLI multi-session block's and the wall's — and one
# more hub on the next 5000-spaced port. Only two sockets for four-and-some
# sessions, which is the whole point of the milestone: sessions are named on
# one socket, not spread across one socket each.
SOCK21="${TMPDIR:-/tmp}/muxd-e2e-m18-$$.sock"
defer_sock "$SOCK21"
SOCK22="${TMPDIR:-/tmp}/muxd-e2e-m18web-$$.sock"
defer_sock "$SOCK22"
WPORT3=$(( 51000 + ($$ % 4000) ))
# ...and a third, the only M18 daemon holding a UDP port: it serves the
# session socket AND a QUIC listener at once, which is what lets one block
# ask whether a session name means the same session on either transport.
# Next 5000-spaced band after WPORT3.
SOCK23="${TMPDIR:-/tmp}/muxd-e2e-m18quic-$$.sock"
defer_sock "$SOCK23"
QPORT5=$(( 56000 + ($$ % 4000) ))
# A key of its own rather than the M8 block's $QKEY: that one is rm_swept
# the moment the M8 scenarios finish (see the sweep after the key-source
# block), which is hundreds of lines before this one runs. Reaching for a
# deleted file would make this block's daemon fail to bind for a reason
# that has nothing to do with sessions.
M18KEY="${TMPDIR:-/tmp}/mux-e2e-m18key-$$"
defer_rm "$M18KEY"

# --- M18: two sessions on one daemon are two shells ---------------------
# The milestone, observed from outside: ONE daemon, one socket path, one
# key — and two sessions that share nothing. A connection IS a session
# (decision 6), so the two clients below differ in exactly one flag, and
# every instrument here is asked to PROVE the separation rather than
# assumed to have it: for each session both a positive grep (its own
# marker is here) and a negative one (the other's is not), because "a
# holds a's marker" is satisfied just as well by a single shared shell.
#
# Ordered ahead of the M-web block on the legible-catch rule. The wall
# scenarios below now attach BY NAME, and a regression in name resolution
# would hang one of them with nothing printed — fifteen seconds into a
# scenario whose label says "WebSocket". This block reaches the same
# resolver with a CLI in front of it, and fails there first.
start_daemon "$SOCK21" "$OUT.m18.d" "M18 multi-session daemon never bound" --shell /bin/sh
D18PID=$DPID

# Two clients, two names, one socket. Each plants a marker its own shell
# has to EXPAND — the typed line reads `printf "m18a-%s\n" pin` and only
# the output reads `m18a-pin`, so a grep that hits is the shell's work and
# not an echo of our keystrokes (the tiny-attach block's trick).
#
# Both run in the foreground and DETACH (\034\034, the Ctrl-\ chord) rather than staying
# up: the session has to outlive its client for every assertion below to
# mean anything. A session ends when its shell exits, not when its last
# client leaves (decision 8) — so these greps run against two sessions
# with nobody attached, which is also why none of their answers can be
# explained by a live client holding something open.
pipe_mux "$OUT.m18a" "$OUT.m18a.err" timeout 40 "$MUX" --sock "$SOCK21" --session a
pipe_send 'printf "m18a-%%s\\n" pin\n'
await_out "$OUT.m18a" "m18a-pin" "m18a-pin never reached the client"
pipe_detach
wait_grid "$SOCK21" "m18a-pin" "M18: session a's marker" a

pipe_mux "$OUT.m18b" "$OUT.m18b.err" timeout 40 "$MUX" --sock "$SOCK21" --session b
pipe_send 'printf "m18b-%%s\\n" pin\n'
await_out "$OUT.m18b" "m18b-pin" "m18b-pin never reached the client"
pipe_detach
wait_grid "$SOCK21" "m18b-pin" "M18: session b's marker" b

# Each session's grid holds its own marker and NOT the other's.
if dump_session "$SOCK21" a | grep -q "m18b-pin"; then
    echo "e2e FAIL: M18: b's marker is on a's grid — one shell, not two:"
    dump_session "$SOCK21" a; exit 1
fi
if dump_session "$SOCK21" b | grep -q "m18a-pin"; then
    echo "e2e FAIL: M18: a's marker is on b's grid — one shell, not two:"
    dump_session "$SOCK21" b; exit 1
fi

# The default session was created at daemon init and nobody ever typed
# into it. It is the third session, and its emptiness is the assertion
# that `--session` ROUTES rather than decorating one shared grid.
if dump_session "$SOCK21" | grep -q -e "m18a-pin" -e "m18b-pin"; then
    echo "e2e FAIL: M18: a named session's marker reached the default grid:"
    dump_session "$SOCK21"; exit 1
fi

# stats names all three, and names them one segment each. `sessions=3` on
# its own would survive a daemon that counted right and routed wrong, so
# the per-session segments are grepped by name too.
"$MUX" d stats --sock "$SOCK21" > "$OUT.m18stats" 2>&1
grep -q 'sessions=3' "$OUT.m18stats" || {
    echo "e2e FAIL: M18: stats does not see three sessions:"
    cat "$OUT.m18stats"; exit 1; }
for _s in a b; do
    grep -q "session $_s clients=" "$OUT.m18stats" || {
        echo "e2e FAIL: M18: stats has no segment for session $_s:"
        cat "$OUT.m18stats"; exit 1; }
done

# An unknown name is ANSWERED, in words, on the dump's own channel — not a
# hang and not a fabricated "connection lost" (decision 9). The exit code
# is 0 because the daemon replied; the text is the whole point.
dump_session "$SOCK21" zz > "$OUT.m18zz" 2>&1
grep -q 'mux d: no such session: zz' "$OUT.m18zz" || {
    echo "e2e FAIL: M18: an unknown session did not say so; dump answered:"
    cat "$OUT.m18zz"; exit 1; }

# mux a addresses a session by name on the verbs that never attach at all.
# status_req and debug_dump grew the same name tail attach did (decision
# 14), and that is what lets an agent ask about one session of several
# without claiming a grid — mux a attaches at 0x0 or not at all, so without
# the tail it could only ever have answered for the default session.
set +e
timeout 20 "$MUX" a status --sock "$SOCK21" --session b > "$OUT.m18mst" 2>&1
RC=$?
set -e
rc0 "M18: mux a status --session b exited $RC:" "$OUT.m18mst"
# Not the exit code alone: a silently empty object would pass that. `cols`
# is a field only a decoded StatusReply can put there (agent.sh's shape).
grep -q '"cols"' "$OUT.m18mst" || {
    echo "e2e FAIL: M18: mux a status --session b answered without a status reply:"
    cat "$OUT.m18mst"; exit 1; }

set +e
timeout 20 "$MUX" a capture --sock "$SOCK21" --session a > "$OUT.m18mcap" 2>&1
RC=$?
set -e
rc0 "M18: mux a capture --session a exited $RC:" "$OUT.m18mcap"
grep -q 'm18a-pin' "$OUT.m18mcap" || {
    echo "e2e FAIL: M18: mux a capture --session a did not return a's grid:"
    cat "$OUT.m18mcap"; exit 1; }
if grep -q 'm18b-pin' "$OUT.m18mcap"; then
    echo "e2e FAIL: M18: mux a capture --session a returned b's grid too:"
    cat "$OUT.m18mcap"; exit 1
fi

# Lifetime: a session ends when ITS shell exits, and the daemon outlives
# every one of those deaths (decision 8). This client attaches to a session
# that ALREADY EXISTS — attach-or-create's join arm, the other half of the
# two creations above — and types the exit that ends it.
#
# `exit 7`, not `exit`: a PIPED mux is a wall of one and hands the shell's
# own status back, because scripts read it. Every other pipe_waitexit in
# this suite wants the default 0, which a client that invented its own code
# would satisfy just as well; seven is a number only the shell can be the
# source of. (128 is pinned too, on a SIGKILLed shell, in the hosts group —
# this is the ordinary path.)
pipe_mux "$OUT.m18ax" "$OUT.m18ax.err" timeout 40 "$MUX" --sock "$SOCK21" --session a
pipe_send 'exit 7\n'
pipe_waitexit "M18: a piped mux carries the shell's own exit code" 7

wait_sessions "$SOCK21" 2 "M18: session a's shell exited"

# The daemon is still serving, and b is untouched by its neighbour's
# death: the death freed ONE session, which at N=1 would have been the
# daemon's own exit.
if dump_session "$SOCK21" a | grep -q "m18a-pin"; then
    echo "e2e FAIL: M18: session a still answers after its shell exited:"
    dump_session "$SOCK21" a; exit 1
fi
dump_session "$SOCK21" b | grep -q "m18b-pin" || {
    echo "e2e FAIL: M18: session b lost its grid when session a died:"
    dump_session "$SOCK21" b; exit 1; }

# ...and it outlives the LAST death too. `x` ends a session, never a box: b
# and the default are exited in turn, and what is left is a daemon with
# nothing on it — the state the host picker calls `no sessions`, and the
# reason an emptied host is still there to be picked.
pipe_mux "$OUT.m18bx" "$OUT.m18bx.err" timeout 40 "$MUX" --sock "$SOCK21" --session b
pipe_send 'exit\n'
pipe_waitexit "M18: the client of session b"
pipe_mux "$OUT.m18dx" "$OUT.m18dx.err" timeout 40 "$MUX" --sock "$SOCK21"
pipe_send 'exit\n'
pipe_waitexit "M18: the client of the default session"
wait_sessions "$SOCK21" 0 "M18: every one of the three shells has exited"

# The daemon is asked of the OS, not of itself: `stats` answering above
# already needed a live daemon, but only a pid can say the PROCESS is still
# here. The two seconds are not a wait for anything and are not derived from
# anything — the retired arm had no grace, it handed the code straight from
# `reap` to `run` in the same pass. They are a settle window, chosen, so
# that a daemon leaving a BEAT after its table empties — through `deinit`,
# or through some later arm nobody has written yet — is caught here rather
# than by the next scenario finding a dead socket.
sleep 2
kill -0 "$D18PID" 2>/dev/null || {
    echo "e2e FAIL: M18: the daemon left with its last session — only mux d stop ends one"
    cat "$OUT.m18.d"; exit 1; }

# And it can be born into again: a bare `mux --sock` names no session, so it
# asks for the default, and an EMPTY daemon creates it rather than refusing.
# This is `mux HOST` reaching a box someone emptied yesterday.
pipe_mux "$OUT.m18rb" "$OUT.m18rb.err" timeout 40 "$MUX" --sock "$SOCK21"
pipe_send 'printf "m18rb-%%s\\n" pin\n'
await_out "$OUT.m18rb" "m18rb-pin" "m18rb-pin never reached the reborn default session"
pipe_detach
wait_sessions "$SOCK21" 1 "M18: the emptied daemon was born into again"
wait_grid "$SOCK21" "m18rb-pin" "M18: the reborn default session's marker"

assert_stopped "$SOCK21" "$D18PID" "M18 multi-session" "$OUT.m18stop"
D18PID=""
ok "two sessions on one daemon are two shells; one dies without the other"
ok "a daemon outlives its last session and is born into again"

# --- M18 on the wall: one daemon, two sessions, two tiles ---------------
# What the milestone was FOR (decision 2): the wall showing the same host
# twice. ONE host line names the daemon and its two sessions are two
# tiles, so the hub dials it twice and the two connections are two
# sessions — the browser's spelling of everything the block above proved
# with a CLI.
start_daemon "$SOCK22" "$OUT.m18w.d" "M18 wall daemon never bound" --shell /bin/sh
D19PID=$DPID

# The sessions FIRST: a tile is a live session on a listed daemon, so
# there is nothing for the hub to show until these exist. Their content is
# planted here too and left behind — the tiles are passive 0x0 wall tiles
# and can never type anything themselves.
pipe_mux "$OUT.m18wa" "$OUT.m18wa.err" timeout 40 "$MUX" --sock "$SOCK22" --session a
pipe_send 'printf "wall-a-%%s\\n" pin\n'
await_out "$OUT.m18wa" "wall-a-pin" "M18 wall: session a's marker never reached the client"
pipe_detach
wait_grid "$SOCK22" "wall-a-pin" "M18 wall: session a's marker" a
pipe_mux "$OUT.m18wb" "$OUT.m18wb.err" timeout 40 "$MUX" --sock "$SOCK22" --session b
pipe_send 'printf "wall-b-%%s\\n" pin\n'
await_out "$OUT.m18wb" "wall-b-pin" "M18 wall: session b's marker never reached the client"
pipe_detach
wait_grid "$SOCK22" "wall-b-pin" "M18 wall: session b's marker" b

# Its own state home: `mux web --sock S` RECORDS S in the hosts file it
# then serves, and every other leg's daemons are in the shared one.
hostroom m18wall
# The hub serves the LAYOUT's panes, so the three tiles are three leaves.
# The claim is unchanged — one label, three sessions beside it — and what
# writes them down is no longer a poll.
seed_layout "$HOSTROOM" stacked "--sock $SOCK22#0" "--sock $SOCK22#a" "--sock $SOCK22#b"
XDG_STATE_HOME="$HOSTROOM" "$MUX" web --sock "$SOCK22" --port "$WPORT3" > "$OUT.m18wh" 2>&1 &
W3PID=$!
defer_kill "$W3PID"
M18ORIG="http://127.0.0.1:$WPORT3"
# Three tiles, one per leaf. `tile 2:` is the last of them, so waiting on
# it is waiting for the whole wall.
wait_for "$OUT.m18wh" "tile 2:" 15 || {
    echo "e2e FAIL: M18 wall hub never announced its tiles"; cat "$OUT.m18wh"; exit 1; }

# ONE label, three sessions beside it, in the file's own leaf order —
# which is the milestone's whole claim, read off the page's own answer.
M18WANT="[{\"label\":\"--sock $SOCK22\",\"session\":\"0\"},"
M18WANT="$M18WANT{\"label\":\"--sock $SOCK22\",\"session\":\"a\"},"
M18WANT="$M18WANT{\"label\":\"--sock $SOCK22\",\"session\":\"b\"}]"
[ "$(tiles_shape "$M18ORIG")" = "$M18WANT" ] || {
    echo "e2e FAIL: M18 wall: one host's sessions are not its tiles"
    echo "  got:  $(tiles_shape "$M18ORIG")"
    echo "  want: $M18WANT"; exit 1; }

# The stand-in spells the name the way mux.js does — bytes appended after
# the fixed 20 — and attaches at 0x0, the passivity contract: both
# sessions already exist, so these are joins, and a slot that claimed no
# size can never move the grid.
for _sn in a b; do
    case "$_sn" in
        a) _mine=wall-a-pin; _theirs=wall-b-pin ;;
        *) _mine=wall-b-pin; _theirs=wall-a-pin ;;
    esac
    _t=$(tile_id_of "$M18ORIG" "--sock $SOCK22" "$_sn")
    [ -n "$_t" ] || {
        echo "e2e FAIL: M18 wall: session $_sn has no tile; the wall holds:"
        curl -s "$M18ORIG/tiles"; exit 1; }
    set +e
    timeout 40 "$WSCLIENT" --port "$WPORT3" --tile "$_t" \
        --out "$OUT.m18ws$_sn" --err "$OUT.m18ws$_sn.err" <<EOF
attach 0 0 $_sn
expectstate up 10000
expectgrid $_mine 15000
settle 500 10000
dumpexit
EOF
    RC=$?
    set -e
    [ "$RC" -eq 0 ] || {
        echo "e2e FAIL: M18 wall: tile $_t (session $_sn) wsclient exited $RC"
        cat -v "$OUT.m18ws$_sn.err" 2>/dev/null; cat "$OUT.m18wh"; exit 1; }

    # The replica converges on ITS session's grid...
    assert_ws_converged "$OUT.m18ws$_sn" "$SOCK22" "M18 wall: tile $_t" "$_sn"
    # ...and the other session's content never crossed into it. The
    # convergence check above is a diff against one grid and would pass on
    # a hub that fanned every session's frames to every tile only if that
    # grid happened to match; this grep is the direct question.
    if grep -q "$_theirs" "$OUT.m18ws$_sn"; then
        echo "e2e FAIL: M18 wall: tile $_t (session $_sn) received the other session's content:"
        cat "$OUT.m18ws$_sn"; exit 1
    fi
done

softkill "$W3PID" || true
wait_pid_gone "$W3PID" "M18 wall: hub killed by tracked pid"
W3PID=""
assert_stopped "$SOCK22" "$D19PID" "M18 wall" "$OUT.m18wstop"
D19PID=""
ok "the wall shows one daemon's sessions as its tiles: three tiles, one socket, no crosstalk"

# --- M18 over QUIC: two dials are two sessions, and a name means one -----
# --- session whichever transport carried the attach ----------------------
# Decision 6 — a connection IS a session on EVERY transport — and QUIC is
# where that claim is least obvious. A QUIC client is promoted to a client
# slot when its HANDSHAKE completes, before its attach has arrived, so its
# slot starts session-less and the name on the attach is the only thing
# that ever binds it. Two dials to one port therefore have to come out as
# two shells, and the block above proves nothing about this one: it went
# through a unix socket, where the slot and the attach arrive together.
#
# The second half is the operator's question rather than the protocol's.
# The session table belongs to the DAEMON, not to the listener that
# accepted a connection — so `a` must name the same shell whether it was
# reached over QUIC or over the socket. Nothing else in this suite asks a
# daemon anything with two transports open at once.
head -c 32 /dev/urandom > "$M18KEY"
chmod 600 "$M18KEY"
# The port is an OBSERVATION, not a derivation (decisions.md, hygiene kit).
# This band sits INSIDE /proc/sys/net/ipv4/ip_local_port_range, and the
# scenarios above open plenty of outbound QUIC sockets — one of them can
# already hold the candidate by the time this daemon asks for it. Soak
# caught exactly that, once in ten runs: `mux d: a daemon is already
# listening on udp 127.0.0.1:57943`, from a client socket, not a daemon.
#
# So step on the daemon's own refusal until one binds, the way agent.sh's
# start_quic does. Anything OTHER than that refusal is a real failure and
# is reported with the daemon's own words; only the taken-port case walks.
M18_TRIES=0
while : ; do
    "$MUX" d start --sock "$SOCK23" --quic "127.0.0.1:$QPORT5" --key "$M18KEY" \
        --quic-idle-ms 15000 --shell /bin/sh > "$OUT.m18q.d" 2>&1 &
    D20PID=$!
    defer_kill "$D20PID"
    _i=0
    # Not wait_sock: this wait also watches the daemon's own pid, because a
    # daemon that refused the udp port EXITS and the retry below wants to
    # read its words now rather than after the full budget. The budget is
    # scaled for wait_sock's reason all the same.
    while [ ! -S "$SOCK23" ] && kill -0 "$D20PID" 2>/dev/null && [ "$_i" -lt $(( 60 * TIME_SCALE )) ]; do
        sleep 0.1; _i=$((_i + 1))
    done
    [ -S "$SOCK23" ] && break
    grep -q 'already listening on udp' "$OUT.m18q.d" || {
        echo "e2e FAIL: M18 quic daemon never bound its session socket"
        cat "$OUT.m18q.d"; exit 1; }
    wait "$D20PID" 2>/dev/null || true
    D20PID=""
    M18_TRIES=$((M18_TRIES + 1))
    [ "$M18_TRIES" -lt 8 ] || {
        echo "e2e FAIL: M18 quic: 8 candidate udp ports were taken, last $QPORT5"
        exit 1; }
    # A step that is not a multiple of any other band's spacing, so a walk
    # from here cannot march into the ports another scenario reserved.
    QPORT5=$((QPORT5 + 137))
done

pipe_mux "$OUT.m18qa" "$OUT.m18qa.err" timeout 40 "$MUX" "quic://127.0.0.1:$QPORT5" --key "$M18KEY" --quic-idle-ms 15000 --session a
pipe_send 'printf "q18a-%%s\\n" pin\n'
await_out "$OUT.m18qa" "q18a-pin" "q18a-pin never reached the client"
pipe_detach "M18 quic: the --session a dial"
wait_grid "$SOCK23" "q18a-pin" "M18 quic: session a's marker" a

pipe_mux "$OUT.m18qb" "$OUT.m18qb.err" timeout 40 "$MUX" "quic://127.0.0.1:$QPORT5" --key "$M18KEY" --quic-idle-ms 15000 --session b
pipe_send 'printf "q18b-%%s\\n" pin\n'
await_out "$OUT.m18qb" "q18b-pin" "q18b-pin never reached the client"
pipe_detach "M18 quic: the --session b dial"
wait_grid "$SOCK23" "q18b-pin" "M18 quic: session b's marker" b

if dump_session "$SOCK23" a | grep -q "q18b-pin"; then
    echo "e2e FAIL: M18 quic: b's marker is on a's grid — one session, not two:"
    dump_session "$SOCK23" a; exit 1
fi
if dump_session "$SOCK23" b | grep -q "q18a-pin"; then
    echo "e2e FAIL: M18 quic: a's marker is on b's grid — one session, not two:"
    dump_session "$SOCK23" b; exit 1
fi
"$MUX" d stats --sock "$SOCK23" | grep -q 'sessions=3' || {
    echo "e2e FAIL: M18 quic: two dials did not make two sessions beside the default:"
    "$MUX" d stats --sock "$SOCK23"; exit 1; }

# The cross-transport join. Same name, different door: this must land in
# the shell the QUIC client left behind, not spawn a second one.
pipe_mux "$OUT.m18qx" "$OUT.m18qx.err" timeout 40 "$MUX" --sock "$SOCK23" --session a
pipe_send 'printf "x18a-%%s\\n" pin\n'
await_out "$OUT.m18qx" "x18a-pin" "x18a-pin never reached the client"
pipe_detach "M18 quic: the socket client of session a"
wait_grid "$SOCK23" "x18a-pin" "M18 quic: the socket client's marker in a" a

# Both markers on one grid is the whole assertion: the QUIC client's
# output and the socket client's, in the session they both named.
dump_session "$SOCK23" a | grep -q "q18a-pin" || {
    echo "e2e FAIL: M18 quic: joining 'a' over the socket did not find the QUIC dial's shell:"
    dump_session "$SOCK23" a; exit 1; }
# ...and it JOINED rather than created: a fourth session here would mean
# the name resolved per-transport, which is the bug this pair exists for.
"$MUX" d stats --sock "$SOCK23" | grep -q 'sessions=3' || {
    echo "e2e FAIL: M18 quic: a socket join of an existing name made a new session:"
    "$MUX" d stats --sock "$SOCK23"; exit 1; }
if dump_session "$SOCK23" b | grep -q "x18a-pin"; then
    echo "e2e FAIL: M18 quic: the socket client's marker leaked into b:"
    dump_session "$SOCK23" b; exit 1
fi

ok "quic: two dials are two sessions, and one name is one session on either transport"

# --- the admin verbs by QUIC: the path-less daemon's door (145807a2) ------
#
# The incident's end state: the daemon's socket file is gone and (in the
# real one) a successor holds the path, so the re-bind is refused and
# `--sock` reaches the wrong daemon or none. This daemon has a QUIC arm,
# and the verbs the daemon serves on a client slot — stats, dump, stop —
# now take `--quic HOST:PORT --key` as `mux a` always has. Graded with
# the file really deleted: the daemon takes it back within a second (the
# boot group pins that), so every ask below is made in the window where
# only QUIC can reach it, or after, and either way through UDP alone.
rm "$SOCK23"
"$MUX" d stats --quic "127.0.0.1:$QPORT5" --key "$M18KEY" > "$OUT.m18qstats" 2>&1 || {
    echo "e2e FAIL: mux d stats --quic failed"; cat "$OUT.m18qstats"; exit 1; }
grep -q 'sessions=3' "$OUT.m18qstats" || {
    echo "e2e FAIL: stats over quic did not count the three sessions"; cat "$OUT.m18qstats"; exit 1; }
"$MUX" d dump --quic "127.0.0.1:$QPORT5" --key "$M18KEY" --session a > "$OUT.m18qdump" 2>&1 || {
    echo "e2e FAIL: mux d dump --quic failed"; cat "$OUT.m18qdump"; exit 1; }
grep -q 'q18a-pin' "$OUT.m18qdump" || {
    echo "e2e FAIL: dump over quic did not carry session a's marker"; cat "$OUT.m18qdump"; exit 1; }
# Both doors at once is a refusal at parse, rc 2, and no frame is sent.
set +e
"$MUX" d stats --sock "$SOCK23" --quic "127.0.0.1:$QPORT5" > /dev/null 2> "$OUT.m18qboth"
_rc=$?
set -e
[ "$_rc" = "2" ] || { echo "e2e FAIL: --sock with --quic exited $_rc, want 2"; cat "$OUT.m18qboth"; exit 1; }
grep -q 'name two daemons' "$OUT.m18qboth" || {
    echo "e2e FAIL: --sock with --quic did not say why"; cat "$OUT.m18qboth"; exit 1; }
# The stop, and the OS says it took: the pid goes, and whatever socket file
# the re-bind put back goes with it (deinit's unlink), so no leftover
# names a daemon that is not there.
set +e
"$MUX" d stop --quic "127.0.0.1:$QPORT5" --key "$M18KEY" 2> "$OUT.m18qstop"
_rc=$?
set -e
[ "$_rc" = "0" ] || { echo "e2e FAIL: mux d stop --quic exited $_rc, want 0"; cat "$OUT.m18qstop"; exit 1; }
grep -q '^mux d: stopped' "$OUT.m18qstop" || {
    echo "e2e FAIL: stop over quic did not report stopped"; cat "$OUT.m18qstop"; exit 1; }
wait_pid_gone "$D20PID" "stop over quic reported stopped"
[ ! -S "$SOCK23" ] || { echo "e2e FAIL: stop over quic left $SOCK23"; ls -l "$SOCK23"; exit 1; }
# And a stopped port is a refusal, not a silent no-op: a script must not
# read a firewall or a wrong key as "already stopped".
set +e
"$MUX" d stop --quic "127.0.0.1:$QPORT5" --key "$M18KEY" 2> "$OUT.m18qstop2"
_rc=$?
set -e
[ "$_rc" = "1" ] || { echo "e2e FAIL: stop on a dead quic port exited $_rc, want 1"; cat "$OUT.m18qstop2"; exit 1; }
D20PID=""
ok "quic: stats, dump and stop reach a daemon whose socket file is gone (145807a2)"