414d282a
test: e2e — auto-start arcs (proxy + pty), muxd stop teardown, pins bumped
a73x 2026-08-10 19:05
Commit message
test/e2e.sh
| Old | New | ||
|---|---|---|---|
| @@ -75,6 +75,16 @@ TP1SH="${TMPDIR:-/tmp}/mux-e2e-tp1-$$.sh" | |||
| 75 | # tp1 runs its fixture in the BACKGROUND — the tear has to happen while the | 75 | # tp1 runs its fixture in the BACKGROUND — the tear has to happen while the |
| 76 | # script is mid-flight — so its pid is tracked and killed like a daemon's. | 76 | # script is mid-flight — so its pid is tracked and killed like a daemon's. |
| 77 | TP1PID="" | 77 | TP1PID="" |
| 78 | # M13 auto-start + stop. Two paths: the proxy arc's daemon is spawned BY | ||
| 79 | # the proxy, the pty leg's by local mux under the M12 fixture. Both are | ||
| 80 | # torn down by `muxd stop` — the verb under test is also the cleanup, and | ||
| 81 | # the trap only backstops it. Neither daemon is a child of this shell, so | ||
| 82 | # each pid is read off the up-line its own spawner printed, which is the | ||
| 83 | # only handle that can never name a bystander. | ||
| 84 | SOCK14="${TMPDIR:-/tmp}/muxd-e2e-astart-$$.sock" | ||
| 85 | SOCK15="${TMPDIR:-/tmp}/muxd-e2e-aspty-$$.sock" | ||
| 86 | APID="" | ||
| 87 | PAPID="" | ||
| 78 | 88 | ||
| 79 | # One counter out of a MUX_PREDICT_STATS line. The client prints exactly one | 89 | # One counter out of a MUX_PREDICT_STATS line. The client prints exactly one |
| 80 | # such line on exit; every field is a key=value pair, so a rename or reorder | 90 | # such line on exit; every field is a key=value pair, so a rename or reorder |
| @@ -254,6 +264,16 @@ cleanup() { | |||
| 254 | [ -n "$SPID" ] && kill "$SPID" 2>/dev/null || true | 264 | [ -n "$SPID" ] && kill "$SPID" 2>/dev/null || true |
| 255 | [ -n "$TPID" ] && kill "$TPID" 2>/dev/null || true | 265 | [ -n "$TPID" ] && kill "$TPID" 2>/dev/null || true |
| 256 | [ -n "$GPID" ] && kill "$GPID" 2>/dev/null || true | 266 | [ -n "$GPID" ] && kill "$GPID" 2>/dev/null || true |
| 267 | # The M13 auto-started daemons, by the pid their spawner's up-line | ||
| 268 | # reported. Cleared by the scenarios themselves once `muxd stop` has | ||
| 269 | # been OBSERVED to end them, so a pid here means a failing run. | ||
| 270 | [ -n "$APID" ] && kill "$APID" 2>/dev/null || true | ||
| 271 | [ -n "$PAPID" ] && kill "$PAPID" 2>/dev/null || true | ||
| 272 | # ...and the same two by socket, because a spawn that failed between | ||
| 273 | # the fork and the up-line leaves a daemon this trap holds no pid for. | ||
| 274 | # `muxd stop` on a path nobody serves is a no-op that exits 0. | ||
| 275 | [ -n "${MUXD:-}" ] && "$MUXD" stop --sock "$SOCK14" >/dev/null 2>&1 || true | ||
| 276 | [ -n "${MUXD:-}" ] && "$MUXD" stop --sock "$SOCK15" >/dev/null 2>&1 || true | ||
| 257 | rm -f "$SOCK8" "$SOCK8T" "$SOCK11" "$OUT.start" "$OUT.start2" "$OUT.s8" \ | 277 | rm -f "$SOCK8" "$SOCK8T" "$SOCK11" "$OUT.start" "$OUT.start2" "$OUT.s8" \ |
| 258 | "$OUT.ra" "$OUT.rb" "$OUT.goal" "$OUT.g9" "$OUT.dead" \ | 278 | "$OUT.ra" "$OUT.rb" "$OUT.goal" "$OUT.g9" "$OUT.dead" \ |
| 259 | "$SOCK9" "$SOCK10" "$OUT.env1" "$OUT.env2" \ | 279 | "$SOCK9" "$SOCK10" "$OUT.env1" "$OUT.env2" \ |
| @@ -287,6 +307,11 @@ cleanup() { | |||
| 287 | "$OUT.tp1.doc" "$OUT.tp1.doc.render" "$OUT.tp1.doc.dump" \ | 307 | "$OUT.tp1.doc" "$OUT.tp1.doc.render" "$OUT.tp1.doc.dump" \ |
| 288 | "$OUT.tp1.doc.render.n" "$OUT.tp1.doc.dump.n" "$OUT.tp1.doc.diff" \ | 308 | "$OUT.tp1.doc.render.n" "$OUT.tp1.doc.dump.n" "$OUT.tp1.doc.diff" \ |
| 289 | "$OUT.tp1.doc.rvt" "$OUT.tp1.doc.dvt" | 309 | "$OUT.tp1.doc.rvt" "$OUT.tp1.doc.dvt" |
| 310 | # M13 auto-start + stop. | ||
| 311 | rm -f "$SOCK14" "$SOCK15" \ | ||
| 312 | "$OUT.as" "$OUT.as.err" "$OUT.as2" "$OUT.as2.err" \ | ||
| 313 | "$OUT.stop" "$OUT.stop2" \ | ||
| 314 | "$OUT.pa" "$OUT.pa.err" "$OUT.pa.log" | ||
| 290 | # The convergence files a FAILING assert_converged leaves behind | 315 | # The convergence files a FAILING assert_converged leaves behind |
| 291 | # (.render/.dump/.rvt/.dvt/.diff for that capture) are deliberately not | 316 | # (.render/.dump/.rvt/.dvt/.diff for that capture) are deliberately not |
| 292 | # chased here: on a failing run they are the evidence. | 317 | # chased here: on a failing run they are the evidence. |
| @@ -1991,17 +2016,145 @@ D13PID="" | |||
| 1991 | rm -f "$OUT.tp1" "$OUT.tp1.err" "$OUT.tp1.log" "$OUT.tp1.d" "$TP1SH" | 2016 | rm -f "$OUT.tp1" "$OUT.tp1.err" "$OUT.tp1.log" "$OUT.tp1.d" "$TP1SH" |
| 1992 | ok "reconnect while scrolled: view restored, prediction resumed" | 2017 | ok "reconnect while scrolled: view restored, prediction resumed" |
| 1993 | 2018 | ||
| 2019 | # --- M13: attach auto-start (proxy) + muxd stop ------------------------ | ||
| 2020 | # | ||
| 2021 | # Nothing is serving SOCK14: the attach itself must produce the daemon. | ||
| 2022 | # SHELL pinned because the auto-started daemon gets no --shell flag and | ||
| 2023 | # resolves $SHELL — the suite must not inherit the developer's. | ||
| 2024 | # | ||
| 2025 | # `|| true` for the same reason the M6 --via scenario has it: without it | ||
| 2026 | # `set -e` aborts the run on the client's exit status before the labelled | ||
| 2027 | # diagnostics below get to print the stderr that says why. `timeout` is | ||
| 2028 | # what keeps that from being a licence to hang. | ||
| 2029 | { printf 'printf "auto-%%s\\n" start\n'; sleep 2; printf '\034'; } | \ | ||
| 2030 | SHELL=/bin/sh timeout 30 "$MUX" --via "$MUXD proxy --sock $SOCK14" \ | ||
| 2031 | > "$OUT.as" 2> "$OUT.as.err" || true | ||
| 2032 | "$MUXD" dump --sock "$SOCK14" | grep -q "auto-start" || { | ||
| 2033 | echo "e2e FAIL: auto-started daemon lost the marker" | ||
| 2034 | cat "$OUT.as.err"; exit 1; } | ||
| 2035 | grep -q '^muxd proxy: starting' "$OUT.as.err" || { | ||
| 2036 | 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 | ||
| 2038 | # suite has on a daemon that is nobody's child, so it is read before | ||
| 2039 | # anything else here can fail and leave one unreachable. | ||
| 2040 | 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; } | ||
| 2042 | kill -0 "$APID" || { echo "e2e FAIL: auto-started daemon not alive"; exit 1; } | ||
| 2043 | assert_converged "$OUT.as" "$SOCK14" "auto-start via proxy" | ||
| 2044 | |||
| 2045 | # Silence is the fast path: a warm attach must print NO spawn progress. | ||
| 2046 | { printf 'printf "auto-%%s\\n" again\n'; sleep 2; printf '\034'; } | \ | ||
| 2047 | SHELL=/bin/sh timeout 30 "$MUX" --via "$MUXD proxy --sock $SOCK14" \ | ||
| 2048 | > "$OUT.as2" 2> "$OUT.as2.err" || true | ||
| 2049 | grep -q 'starting' "$OUT.as2.err" && { | ||
| 2050 | echo "e2e FAIL: warm attach printed spawn progress" | ||
| 2051 | cat "$OUT.as2.err"; exit 1; } | ||
| 2052 | "$MUXD" dump --sock "$SOCK14" | grep -q "auto-again" || { | ||
| 2053 | echo "e2e FAIL: warm attach did not reach the same daemon"; exit 1; } | ||
| 2054 | # 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 | ||
| 2056 | # 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; } | ||
| 2058 | assert_converged "$OUT.as2" "$SOCK14" "auto-start warm attach" | ||
| 2059 | |||
| 2060 | # The verb under test is the teardown: stopped line, exit 0, socket gone, | ||
| 2061 | # and the daemon OBSERVED dead by pid — never the command's claim alone. | ||
| 2062 | "$MUXD" stop --sock "$SOCK14" 2> "$OUT.stop" | ||
| 2063 | grep -q '^muxd: stopped' "$OUT.stop" || { | ||
| 2064 | 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; } | ||
| 2066 | # `muxd: stopped` is printed on the first probe that gets a refusal, which | ||
| 2067 | # is the socket being unlinked — the process itself can still be a | ||
| 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="" | ||
| 2077 | |||
| 2078 | # Idempotence control: stop with nothing there is exit 0 and says so. | ||
| 2079 | set +e | ||
| 2080 | "$MUXD" stop --sock "$SOCK14" 2> "$OUT.stop2" | ||
| 2081 | RC_STOP=$? | ||
| 2082 | set -e | ||
| 2083 | [ "$RC_STOP" = "0" ] || { | ||
| 2084 | echo "e2e FAIL: stop-when-nothing exited $RC_STOP, want 0"; cat "$OUT.stop2"; exit 1; } | ||
| 2085 | grep -q "nothing listening on $SOCK14" "$OUT.stop2" || { | ||
| 2086 | echo "e2e FAIL: stop-when-nothing said the wrong thing"; cat "$OUT.stop2"; exit 1; } | ||
| 2087 | rm -f "$OUT.as" "$OUT.as.err" "$OUT.as2" "$OUT.as2.err" "$OUT.stop" "$OUT.stop2" | ||
| 2088 | ok "attach auto-start via proxy; muxd stop tears it down" | ||
| 2089 | |||
| 2090 | # --- M13: local mux auto-start, under the pty fixture ------------------ | ||
| 2091 | # | ||
| 2092 | # A local attach is tty-gated territory, so it runs on the M12 fixture. | ||
| 2093 | # PATH is prefixed with the build dir: findInPath must resolve exactly | ||
| 2094 | # the muxd under test, and the suite proves the resolution by the daemon | ||
| 2095 | # existing afterwards. | ||
| 2096 | # | ||
| 2097 | # The script keeps the two M12 rules that every pty leg is written to: | ||
| 2098 | # it opens on the alt-screen enter rather than on a `send`. The client | ||
| 2099 | # takes the tty raw with TCSAFLUSH (client.zig), which DISCARDS input | ||
| 2100 | # queued before it got there — and here that window is a whole cold | ||
| 2101 | # spawn wide, so typing into it is typing into /dev/null; | ||
| 2102 | # the marker text never appears in the line that produces it, and the | ||
| 2103 | # detach waits on `settle` rather than on the marker. A shell writes its | ||
| 2104 | # output and its next prompt as two separate writes, so a script that | ||
| 2105 | # detaches on the output stops one frame short of the grid the daemon | ||
| 2106 | # holds, and the convergence check below would blame the product for it. | ||
| 2107 | set +e | ||
| 2108 | SHELL=/bin/sh PATH="$(dirname "$MUXD"):$PATH" timeout 40 \ | ||
| 2109 | "$PTYCLIENT" --cols 80 --rows 24 --out "$OUT.pa" --err "$OUT.pa.err" \ | ||
| 2110 | -- "$MUX" --sock "$SOCK15" > "$OUT.pa.log" 2>&1 <<'EOF' | ||
| 2111 | expect \x1b[?1049h 15000 | ||
| 2112 | send printf 'pty-%s\\n' auto\n | ||
| 2113 | expect pty-auto 15000 | ||
| 2114 | settle 500 15000 | ||
| 2115 | send \x1c | ||
| 2116 | waitexit 10000 | ||
| 2117 | EOF | ||
| 2118 | RC=$? | ||
| 2119 | set -e | ||
| 2120 | [ "$RC" -eq 0 ] || { | ||
| 2121 | echo "e2e FAIL: local auto-start ptyclient exited $RC:" | ||
| 2122 | cat "$OUT.pa.log"; cat -v "$OUT.pa.err" 2>/dev/null; exit 1; } | ||
| 2123 | "$MUXD" dump --sock "$SOCK15" | grep -q "pty-auto" || { | ||
| 2124 | echo "e2e FAIL: local auto-start lost the marker" | ||
| 2125 | cat "$OUT.pa.log" "$OUT.pa.err" 2>/dev/null; exit 1; } | ||
| 2126 | # The fixture hands the client a FILE for stderr, so the progress lines | ||
| 2127 | # are non-tty ones and land here rather than on the pty — which is also | ||
| 2128 | # why the capture the convergence check reads is undisturbed by them. | ||
| 2129 | grep -q '^mux: starting' "$OUT.pa.err" || { | ||
| 2130 | echo "e2e FAIL: local auto-start printed no mux-prefixed starting line" | ||
| 2131 | cat "$OUT.pa.err"; exit 1; } | ||
| 2132 | 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; } | ||
| 2134 | assert_converged "$OUT.pa" "$SOCK15" "local mux auto-start" | ||
| 2135 | "$MUXD" stop --sock "$SOCK15" 2>/dev/null | ||
| 2136 | [ ! -S "$SOCK15" ] || { echo "e2e FAIL: stop left the pty leg's socket"; exit 1; } | ||
| 2137 | _i=0 | ||
| 2138 | while kill -0 "$PAPID" 2>/dev/null; do | ||
| 2139 | _i=$((_i + 1)); [ "$_i" -lt 40 ] || { | ||
| 2140 | echo "e2e FAIL: stop reported stopped but pid $PAPID still runs"; exit 1; } | ||
| 2141 | sleep 0.05 | ||
| 2142 | done | ||
| 2143 | PAPID="" | ||
| 2144 | rm -f "$OUT.pa" "$OUT.pa.err" "$OUT.pa.log" | ||
| 2145 | ok "local mux auto-start under the pty fixture" | ||
| 2146 | |||
| 1994 | # The pins. Literals, not variables set from counting something else — | 2147 | # The pins. Literals, not variables set from counting something else — |
| 1995 | # "assert the literal, never the constant the code under test reads" | 2148 | # "assert the literal, never the constant the code under test reads" |
| 1996 | # (decisions.md, M10). 13 scenario checkpoints; 25 convergence points. | 2149 | # (decisions.md, M10). 15 scenario checkpoints; 28 convergence points. |
| 1997 | # Anyone adding a scenario updates these by hand, on purpose. | 2150 | # Anyone adding a scenario updates these by hand, on purpose. |
| 1998 | [ "$OK_COUNT" = "13" ] || { | 2151 | [ "$OK_COUNT" = "15" ] || { |
| 1999 | echo "e2e FAIL: $OK_COUNT scenario checkpoints ran, the pin says 13 —" | 2152 | echo "e2e FAIL: $OK_COUNT scenario checkpoints ran, the pin says 15 —" |
| 2000 | echo " a scenario was added (update the pin) or silently lost" | 2153 | echo " a scenario was added (update the pin) or silently lost" |
| 2001 | exit 1 | 2154 | exit 1 |
| 2002 | } | 2155 | } |
| 2003 | [ "$CONV_COUNT" = "25" ] || { | 2156 | [ "$CONV_COUNT" = "28" ] || { |
| 2004 | echo "e2e FAIL: $CONV_COUNT convergence points ran, the pin says 25" | 2157 | echo "e2e FAIL: $CONV_COUNT convergence points ran, the pin says 28" |
| 2005 | exit 1 | 2158 | exit 1 |
| 2006 | } | 2159 | } |
| 2007 | echo "e2e OK (13 scenarios, 25 convergence points)" | 2160 | echo "e2e OK (15 scenarios, 28 convergence points)" |