a73x

0e9ae602

refactor(test): one owner for the agent suite's daemon bring-up

a73x   2026-08-13 21:17

Commit message
refactor(test): one owner for the agent suite's daemon bring-up

Three scenarios spelled the same four lines: start_daemon, check for a
pid, wait_ready, and a `why` string for each half. start_ready folds them
into one call.

The pid variable is set the instant the pid is known, BEFORE the readiness
wait and whatever that wait decides — a daemon that came up and then never
answered is still a daemon this run started, and a version that assigned
only on success would leave it running past cleanup. That ordering is why
the helper takes the variable's name rather than returning the pid on
stdout: a command substitution would have to choose between carrying the
pid and carrying the failure reason.

The TUI and QUIC-tear scenarios keep their own bring-up: they want a
failure to become a stashed message rather than an immediate return.

Suite passes 9/9, run twice.

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

test/agent.sh
Old New
@@ -222,6 +222,32 @@ wait_ready() {
222 return 1 222 return 1
223 } 223 }
224 224
225 # start_ready PIDVAR LOG SOCK ARGS... — bring a daemon up and wait for it to
226 # answer, storing its pid in the named variable and calling `why` with the
227 # whole story if either half fails. The bring-up is identical in every scenario
228 # that needs a plain daemon, and a scenario that got only PART of it right — a
229 # pid but no readiness wait — would fail later, somewhere else, as a flake.
230 #
231 # PIDVAR is set the INSTANT the pid is known, before the readiness wait and
232 # whatever that wait decides. That ordering is the load-bearing part: a daemon
233 # that came up and then never answered is still a daemon this run started, and
234 # a version that only assigned on success would leave it running past cleanup.
235 #
236 # The two scenarios that do NOT use this (the TUI and the QUIC tear) want a
237 # daemon that failed to come up to be a stashed message rather than an
238 # immediate return, and folding that in would cost more than it saves.
239 start_ready() {
240 _var="$1"
241 _log="$2"
242 _sock="$3"
243 shift 3
244 eval "$_var=\$(start_daemon \"\$_log\" \"\$@\")"
245 eval "_pid=\$$_var"
246 [ -n "$_pid" ] || why "daemon never printed an up-line [$(cat "$_log")]" || return 1
247 wait_ready "$_sock" || why "daemon never answered on $_sock" || return 1
248 return 0
249 }
250
225 now_ms() { python3 -c 'import time; print(int(time.time() * 1000))'; } 251 now_ms() { python3 -c 'import time; print(int(time.time() * 1000))'; }
226 252
227 echo "agent: ports ${PORT_TEAR}/${PORT_RELAY}/${PORT_QUIET}/${PORT_SINK}, tmp $TMP" 253 echo "agent: ports ${PORT_TEAR}/${PORT_RELAY}/${PORT_QUIET}/${PORT_SINK}, tmp $TMP"
@@ -232,9 +258,7 @@ echo "agent: ports ${PORT_TEAR}/${PORT_RELAY}/${PORT_QUIET}/${PORT_SINK}, tmp $T
232 # in `exit_code` the command's rather than a guess. 258 # in `exit_code` the command's rather than a guess.
233 scen_marks() { 259 scen_marks() {
234 [ -x /bin/bash ] || { WHY="no /bin/bash to inject marks into"; return 2; } 260 [ -x /bin/bash ] || { WHY="no /bin/bash to inject marks into"; return 2; }
235 D_MARKS=$(start_daemon "$TMP/marks.log" --sock "$SOCK_MARKS" --shell /bin/bash) 261 start_ready D_MARKS "$TMP/marks.log" "$SOCK_MARKS" --sock "$SOCK_MARKS" --shell /bin/bash || return 1
236 [ -n "$D_MARKS" ] || why "daemon never printed an up-line [$(cat "$TMP/marks.log")]" || return 1
237 wait_ready "$SOCK_MARKS" || why "daemon never answered on $SOCK_MARKS" || return 1
238 262
239 timeout 20 "$MUXA" run --sock "$SOCK_MARKS" --timeout 8000 'true' >"$TMP/m1" 2>&1 263 timeout 20 "$MUXA" run --sock "$SOCK_MARKS" --timeout 8000 'true' >"$TMP/m1" 2>&1
240 _rc=$? 264 _rc=$?
@@ -399,9 +423,7 @@ run_scenario "ephemeral TUI: alt_screen seen, quit driven, death reported as JSO
399 # question a markless session can answer two different ways, and "I don't 423 # question a markless session can answer two different ways, and "I don't
400 # know" is not one of them. 424 # know" is not one of them.
401 scen_settle() { 425 scen_settle() {
402 D_SETTLE=$(start_daemon "$TMP/settle.log" --sock "$SOCK_SETTLE" --shell /bin/sh) 426 start_ready D_SETTLE "$TMP/settle.log" "$SOCK_SETTLE" --sock "$SOCK_SETTLE" --shell /bin/sh || return 1
403 [ -n "$D_SETTLE" ] || why "daemon never printed an up-line [$(cat "$TMP/settle.log")]" || return 1
404 wait_ready "$SOCK_SETTLE" || why "daemon never answered on $SOCK_SETTLE" || return 1
405 427
406 timeout 20 "$MUXA" run --sock "$SOCK_SETTLE" --settle 300 --timeout 10000 'sleep 1' >"$TMP/s1" 2>&1 428 timeout 20 "$MUXA" run --sock "$SOCK_SETTLE" --settle 300 --timeout 10000 'sleep 1' >"$TMP/s1" 2>&1
407 _rc=$? 429 _rc=$?
@@ -622,10 +644,8 @@ run_scenario "quic: a tear with no path back fails with the whole story" scen_te
622 # happened, and the honest answer (still running) would have been one field. 644 # happened, and the honest answer (still running) would have been one field.
623 scen_keepalive() { 645 scen_keepalive() {
624 quic_gate || return $? 646 quic_gate || return $?
625 D_QUIET=$(start_daemon "$TMP/quiet.log" --sock "$SOCK_QUIET" --shell /bin/bash \ 647 start_ready D_QUIET "$TMP/quiet.log" "$SOCK_QUIET" --sock "$SOCK_QUIET" --shell /bin/bash \
626 --quic "127.0.0.1:$PORT_QUIET" --key "$KEY") 648 --quic "127.0.0.1:$PORT_QUIET" --key "$KEY" || return 1
627 [ -n "$D_QUIET" ] || why "daemon never printed an up-line [$(cat "$TMP/quiet.log")]" || return 1
628 wait_ready "$SOCK_QUIET" || why "daemon never answered on $SOCK_QUIET" || return 1
629 649
630 timeout 40 "$MUXA" await --quic "127.0.0.1:$PORT_QUIET" --key "$KEY" --timeout 20000 >"$TMP/q3" 2>&1 650 timeout 40 "$MUXA" await --quic "127.0.0.1:$PORT_QUIET" --key "$KEY" --timeout 20000 >"$TMP/q3" 2>&1
631 _rc=$? 651 _rc=$?