a73x

ee04b68c

test: legible stop failures — the regrade's predicted catch must print, not abort

a73x   2026-08-10 19:31

Commit message
test: legible stop failures — the regrade's predicted catch must print, not abort

`muxd stop` failing is exactly what the stop_req resurrection produces,
and both teardowns swallowed it: a bare `"$MUXD" stop` under `set -e`
exits the suite with no line of its own, and the trap then rm's the one
file that held the reason. Measured against a socket that accepts and
never unlinks, which is the shape a broken stop_req leaves behind:

  before — sh rc=1, no `e2e FAIL` line anywhere, `$OUT.stop` deleted;
  after  — `e2e FAIL: stop exited 1, want 0` followed by
           `muxd stop: <sock> still answering after 2s (...log: ...)`.

Both stops are now `set +e` / `RC_STOP=$?` / `set -e` with a labelled
failure, the pty leg's stderr captured rather than sent to /dev/null so
it pins `^muxd: stopped` too. The proxy arc's marker FAIL was byte-for-
byte identical to the M10 start block's at :1150 across two different
milestones' scenarios; it now names its own site and dumps the client
capture as well as the stderr, matching that precedent's two-file dump.
The up-line comment stops claiming the pid capture is early enough to
be safe and states the gap instead, in the register :1150 uses, crediting
the trap's stop-by-socket-path as what actually covers it — so nobody
deletes that as redundant.

Reviewer suggestions: S1 taken — the two pid-death polls are now
`wait_pid_gone PID LABEL` beside `wait_gone`, whose comment says why a
pid poll is not the socket poll it sits next to (stop's verdict is the
unlink; the process trails it). S3 taken — evidence dumps on the
warm-daemon and both socket-left-behind failures. S5 taken — the pty
leg pins the stopped line. S2 declined as subsumed: the `_i` counters
moved into the helper, where the underscore prefix now means what the
file uses it to mean, and no top-level `_i` remains in this code.

Diagnostics only: no assertion was weakened, added, or removed, and the
counts are unchanged at 15 scenarios / 28 convergence points. Verified
`make build && make e2e` green on that line, and because S1 changed loop
structure, `SOAK_N=3 make soak` → 3/3 green. Afterwards `ps` shows no
muxd, mux, or ptyclient, and /tmp holds no muxd-e2e-*/mux-e2e-* files.

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

test/e2e.sh
Old New
@@ -147,6 +147,22 @@ wait_gone() {
147 done 147 done
148 } 148 }
149 149
150 # wait_pid_gone PID LABEL — poll until a tracked pid is gone (2s, the same
151 # budget `muxd stop` gives itself). Its own helper rather than wait_gone's
152 # socket probe, because the two answer different questions: `muxd: stopped`
153 # is printed on the first probe that gets a REFUSAL, which is the socket
154 # being unlinked, and the process can still be a fraction behind that. Only
155 # a pid can say the daemon itself ended, which is the assertion the M13
156 # teardowns owe — never the stop command's own claim.
157 wait_pid_gone() {
158 _i=0
159 while kill -0 "$1" 2>/dev/null; do
160 _i=$((_i + 1)); [ "$_i" -lt 40 ] || {
161 echo "e2e FAIL: $2: stop reported stopped but pid $1 still runs"; exit 1; }
162 sleep 0.05
163 done
164 }
165
150 # The transport child for a given socket: a process named muxd running the 166 # The transport child for a given socket: a process named muxd running the
151 # `proxy` subcommand on that path. NEVER `pkill -f proxy` — the client's own 167 # `proxy` subcommand on that path. NEVER `pkill -f proxy` — the client's own
152 # argv contains the --via command string, so a pattern kill takes out the 168 # argv contains the --via command string, so a pattern kill takes out the
@@ -2030,13 +2046,18 @@ ok "reconnect while scrolled: view restored, prediction resumed"
2030 SHELL=/bin/sh timeout 30 "$MUX" --via "$MUXD proxy --sock $SOCK14" \ 2046 SHELL=/bin/sh timeout 30 "$MUX" --via "$MUXD proxy --sock $SOCK14" \
2031 > "$OUT.as" 2> "$OUT.as.err" || true 2047 > "$OUT.as" 2> "$OUT.as.err" || true
2032 "$MUXD" dump --sock "$SOCK14" | grep -q "auto-start" || { 2048 "$MUXD" dump --sock "$SOCK14" | grep -q "auto-start" || {
2033 echo "e2e FAIL: auto-started daemon lost the marker" 2049 echo "e2e FAIL: proxy auto-start: attached daemon lost the marker"
2034 cat "$OUT.as.err"; exit 1; } 2050 cat "$OUT.as" "$OUT.as.err" 2>/dev/null; exit 1; }
2035 grep -q '^muxd proxy: starting' "$OUT.as.err" || { 2051 grep -q '^muxd proxy: starting' "$OUT.as.err" || {
2036 echo "e2e FAIL: cold attach printed no starting line"; cat "$OUT.as.err"; exit 1; } 2052 echo "e2e FAIL: cold attach printed no starting line"; cat "$OUT.as.err"; exit 1; }
2037 # The pid is the whole point of the up-line: it is the only handle the 2053 # The pid is the whole point of the up-line: it is the only handle the
2038 # suite has on a daemon that is nobody's child, so it is read before 2054 # suite has on a daemon that is nobody's child. Known gap, accepted: the
2039 # anything else here can fail and leave one unreachable. 2055 # two assertions above exit before this capture runs, so a failure in
2056 # either leaves a daemon with no pid tracked — and hoisting the capture
2057 # would not close it, since the window from the spawn to this sed cannot
2058 # be made zero. Unlike the M10 start block's version of this gap (:1150)
2059 # it is covered anyway: the trap also stops SOCK14/SOCK15 by PATH, which
2060 # needs no pid.
2040 APID=$(sed -n 's/.* pid=\([0-9]*\).*/\1/p' "$OUT.as.err" | head -1) 2061 APID=$(sed -n 's/.* pid=\([0-9]*\).*/\1/p' "$OUT.as.err" | head -1)
2041 [ -n "$APID" ] || { echo "e2e FAIL: proxy up-line carries no pid"; cat "$OUT.as.err"; exit 1; } 2062 [ -n "$APID" ] || { echo "e2e FAIL: proxy up-line carries no pid"; cat "$OUT.as.err"; exit 1; }
2042 kill -0 "$APID" || { echo "e2e FAIL: auto-started daemon not alive"; exit 1; } 2063 kill -0 "$APID" || { echo "e2e FAIL: auto-started daemon not alive"; exit 1; }
@@ -2054,25 +2075,30 @@ grep -q 'starting' "$OUT.as2.err" && {
2054 # The same daemon, asserted by pid rather than by the marker alone: a 2075 # The same daemon, asserted by pid rather than by the marker alone: a
2055 # second daemon on the path would have started a fresh shell, but a 2076 # second daemon on the path would have started a fresh shell, but a
2056 # reader should not have to reason that out from a missing string. 2077 # reader should not have to reason that out from a missing string.
2057 kill -0 "$APID" || { echo "e2e FAIL: warm attach replaced the daemon"; exit 1; } 2078 kill -0 "$APID" || {
2079 echo "e2e FAIL: warm attach replaced the daemon (pid $APID is gone)"
2080 cat "$OUT.as2.err"; exit 1; }
2058 assert_converged "$OUT.as2" "$SOCK14" "auto-start warm attach" 2081 assert_converged "$OUT.as2" "$SOCK14" "auto-start warm attach"
2059 2082
2060 # The verb under test is the teardown: stopped line, exit 0, socket gone, 2083 # The verb under test is the teardown: exit 0, stopped line, socket gone,
2061 # and the daemon OBSERVED dead by pid — never the command's claim alone. 2084 # and the daemon OBSERVED dead by pid — never the command's claim alone.
2085 #
2086 # The exit code is checked through `set +e` rather than left to `set -e`,
2087 # and that is the difference between a caught regression and a mystery: a
2088 # stop that fails exits 1, which would abort the suite with no line of its
2089 # own, and the trap would then rm the one file saying why. The regrade
2090 # resurrects exactly that failure, so it has to PRINT.
2091 set +e
2062 "$MUXD" stop --sock "$SOCK14" 2> "$OUT.stop" 2092 "$MUXD" stop --sock "$SOCK14" 2> "$OUT.stop"
2093 RC_STOP=$?
2094 set -e
2095 [ "$RC_STOP" = "0" ] || {
2096 echo "e2e FAIL: stop exited $RC_STOP, want 0"; cat "$OUT.stop"; exit 1; }
2063 grep -q '^muxd: stopped' "$OUT.stop" || { 2097 grep -q '^muxd: stopped' "$OUT.stop" || {
2064 echo "e2e FAIL: stop did not report stopped"; cat "$OUT.stop"; exit 1; } 2098 echo "e2e FAIL: stop did not report stopped"; cat "$OUT.stop"; exit 1; }
2065 [ ! -S "$SOCK14" ] || { echo "e2e FAIL: stop left the socket behind"; exit 1; } 2099 [ ! -S "$SOCK14" ] || {
2066 # `muxd: stopped` is printed on the first probe that gets a refusal, which 2100 echo "e2e FAIL: stop left the socket behind"; ls -l "$SOCK14"; exit 1; }
2067 # is the socket being unlinked — the process itself can still be a 2101 wait_pid_gone "$APID" "proxy auto-start"
2068 # fraction behind it. Hence the poll: 2s, the same budget the stop verb
2069 # gives itself, and only then a failure.
2070 _i=0
2071 while kill -0 "$APID" 2>/dev/null; do
2072 _i=$((_i + 1)); [ "$_i" -lt 40 ] || {
2073 echo "e2e FAIL: stop reported stopped but pid $APID still runs"; exit 1; }
2074 sleep 0.05
2075 done
2076 APID="" 2102 APID=""
2077 2103
2078 # Idempotence control: stop with nothing there is exit 0 and says so. 2104 # Idempotence control: stop with nothing there is exit 0 and says so.
@@ -2132,16 +2158,23 @@ grep -q '^mux: starting' "$OUT.pa.err" || {
2132 PAPID=$(sed -n 's/.* pid=\([0-9]*\).*/\1/p' "$OUT.pa.err" | head -1) 2158 PAPID=$(sed -n 's/.* pid=\([0-9]*\).*/\1/p' "$OUT.pa.err" | head -1)
2133 [ -n "$PAPID" ] || { echo "e2e FAIL: mux up-line carries no pid"; cat "$OUT.pa.err"; exit 1; } 2159 [ -n "$PAPID" ] || { echo "e2e FAIL: mux up-line carries no pid"; cat "$OUT.pa.err"; exit 1; }
2134 assert_converged "$OUT.pa" "$SOCK15" "local mux auto-start" 2160 assert_converged "$OUT.pa" "$SOCK15" "local mux auto-start"
2135 "$MUXD" stop --sock "$SOCK15" 2>/dev/null 2161 # Same teardown, same reasons — and the stderr is captured rather than
2136 [ ! -S "$SOCK15" ] || { echo "e2e FAIL: stop left the pty leg's socket"; exit 1; } 2162 # discarded, so this leg pins the stopped line too. $OUT.stop is reused
2137 _i=0 2163 # deliberately: the proxy arc removed it above, and it is in the trap's
2138 while kill -0 "$PAPID" 2>/dev/null; do 2164 # rm list either way.
2139 _i=$((_i + 1)); [ "$_i" -lt 40 ] || { 2165 set +e
2140 echo "e2e FAIL: stop reported stopped but pid $PAPID still runs"; exit 1; } 2166 "$MUXD" stop --sock "$SOCK15" 2> "$OUT.stop"
2141 sleep 0.05 2167 RC_STOP=$?
2142 done 2168 set -e
2169 [ "$RC_STOP" = "0" ] || {
2170 echo "e2e FAIL: pty leg stop exited $RC_STOP, want 0"; cat "$OUT.stop"; exit 1; }
2171 grep -q '^muxd: stopped' "$OUT.stop" || {
2172 echo "e2e FAIL: pty leg stop did not report stopped"; cat "$OUT.stop"; exit 1; }
2173 [ ! -S "$SOCK15" ] || {
2174 echo "e2e FAIL: stop left the pty leg's socket"; ls -l "$SOCK15"; exit 1; }
2175 wait_pid_gone "$PAPID" "pty leg"
2143 PAPID="" 2176 PAPID=""
2144 rm -f "$OUT.pa" "$OUT.pa.err" "$OUT.pa.log" 2177 rm -f "$OUT.pa" "$OUT.pa.err" "$OUT.pa.log" "$OUT.stop"
2145 ok "local mux auto-start under the pty fixture" 2178 ok "local mux auto-start under the pty fixture"
2146 2179
2147 # The pins. Literals, not variables set from counting something else — 2180 # The pins. Literals, not variables set from counting something else —