a73x

1be12949

test: the refusal leg proves the daemon it means

a73x   2026-08-21 08:33

Commit message
test: the refusal leg proves the daemon it means

The negative leg's client dialed a socket whose daemon had already died
with its last session, so mux auto-started whatever muxd ambient PATH
offered — an installed release with no agent code, making every
assertion pass for the wrong reason. The leg now starts its daemon
explicitly, pins non-vacuity with an expansion-only agtsock probe, and
the suite prepends the build's bin dir so an accidental auto-start can
never again run anything but the binary under test.

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

test/e2e.sh
Old New
@@ -21,6 +21,17 @@ MUXWEB="$8"
21 # does is test/agent.sh's business. 21 # does is test/agent.sh's business.
22 MUXA="$9" 22 MUXA="$9"
23 23
24 # Every muxd this suite reaches for must be the one it was handed. A client
25 # whose daemon is not already up AUTO-STARTS one by name, off $PATH — so on
26 # a box with a release installed, a leg that forgot its `muxd run &` silently
27 # grades the INSTALLED binary and passes on features the build under test is
28 # the only one to have. (That is not hypothetical: the agent-refusal leg did
29 # exactly this against a v0.0.1-10 with no agent code in it.) Prepending the
30 # build's own bin directory makes the accident harmless — the daemon that
31 # comes up is still the one under test.
32 PATH="$(cd "$(dirname "$MUXD")" && pwd):$PATH"
33 export PATH
34
24 # The one thing this suite needs that it does not build: a real editor, for 35 # The one thing this suite needs that it does not build: a real editor, for
25 # the paste scenario far below. Checked HERE rather than there because there 36 # the paste scenario far below. Checked HERE rather than there because there
26 # is the whole suite, several daemons and a QUIC leg away — minutes of work 37 # is the whole suite, several daemons and a QUIC leg away — minutes of work
@@ -423,10 +434,11 @@ SOCK47="${TMPDIR:-/tmp}/muxd-e2e-exitsem-$$.sock"
423 XESTATE="${TMPDIR:-/tmp}/mux-e2e-exitsem-state-$$" 434 XESTATE="${TMPDIR:-/tmp}/mux-e2e-exitsem-state-$$"
424 D47PID="" 435 D47PID=""
425 436
426 # Agent forwarding: one daemon serving both legs, because the negative one 437 # Agent forwarding: one socket path, two daemons in turn. The positive leg
427 # is ABOUT the positive one's daemon — a session on it that nobody offered 438 # ends by typing `exit`, which ends that daemon's only session and takes the
428 # an agent to. A second daemon would prove only that a daemon with no 439 # daemon with it — so the refusal leg below starts its own on the freed path
429 # clients refuses, which is not the case anyone is in. 440 # rather than inheriting one that is already gone. Both are $MUXD; neither
441 # may be auto-started (see the $PATH note at the top of this file).
430 # 442 #
431 # The agent lives beside the sockets rather than under $OUT: a unix path is 443 # The agent lives beside the sockets rather than under $OUT: a unix path is
432 # capped at 107 bytes by sun_path, and this one is dialled from inside a 444 # capped at 107 bytes by sun_path, and this one is dialled from inside a
@@ -1300,7 +1312,7 @@ cleanup() {
1300 # when it exits cleanly, so it is named here as well. 1312 # when it exits cleanly, so it is named here as well.
1301 rm -f "$SOCK48" "$AGENT48" "$AGENT48KEY" "$AGENT48KEY.pub" \ 1313 rm -f "$SOCK48" "$AGENT48" "$AGENT48KEY" "$AGENT48KEY.pub" \
1302 "$OUT.agt.d" "$OUT.agt.env" "$OUT.agt" "$OUT.agt.err" "$OUT.agt.log" \ 1314 "$OUT.agt.d" "$OUT.agt.env" "$OUT.agt" "$OUT.agt.err" "$OUT.agt.log" \
1303 "$OUT.agtn" "$OUT.agtn.err" "$OUT.agtn.log" "$OUT.agtstop" 1315 "$OUT.agtn" "$OUT.agtn.err" "$OUT.agtn.log" "$OUT.agtn.d" "$OUT.agtstop"
1304 rm -f "$SOCK49" "$AGENT49A" "$AGENT49B" \ 1316 rm -f "$SOCK49" "$AGENT49A" "$AGENT49B" \
1305 "$AGENT49AKEY" "$AGENT49AKEY.pub" "$AGENT49BKEY" "$AGENT49BKEY.pub" \ 1317 "$AGENT49AKEY" "$AGENT49AKEY.pub" "$AGENT49BKEY" "$AGENT49BKEY.pub" \
1306 "$OUT.flip.d" \ 1318 "$OUT.flip.d" \
@@ -7030,23 +7042,34 @@ ok "agent forwarding: ssh-add -l in the session lists the client's key"
7030 7042
7031 # --- ...and a session nobody offered an agent to refuses, fast ------------- 7043 # --- ...and a session nobody offered an agent to refuses, fast -------------
7032 # 7044 #
7033 # The same daemon, deliberately: the interesting no-offerer case is a 7045 # A fresh daemon on the freed path, and the spawn is load-bearing: the leg
7034 # session sitting beside one that HAS an answerer, not a daemon with no 7046 # above took its daemon down with its last session, so a client dialling
7035 # clients at all. The client above has exited, so its offer is gone; this 7047 # $SOCK48 now would AUTO-START one off $PATH instead — a daemon that need
7036 # one attaches without -A and with no agent of its own to offer. 7048 # not be the one under test, and against a release with no agent code in it
7049 # every assertion below passes having witnessed nothing.
7050 #
7051 # The sibling case — a session beside one that HAS an answerer — is the unit
7052 # suite's; what this leg witnesses is the plainer shape, a live daemon
7053 # holding a single session nobody has offered an agent to.
7037 # 7054 #
7038 # SSH_AUTH_SOCK is still set inside the session — every session is born with 7055 # SSH_AUTH_SOCK is still set inside that session — every session is born
7039 # one, whether or not anyone has offered — and it points at the daemon, 7056 # with one, whether or not anyone has offered — and it points at the daemon,
7040 # which accepts the dial and closes it. That is the designed answer, not a 7057 # which accepts the dial and closes it. That is the designed answer, not a
7041 # gap: ssh reads a closed agent socket as "agent refused operation" and 7058 # gap: ssh reads a closed agent socket as "agent refused operation" and
7042 # falls straight through to its other methods, where a connection accepted 7059 # falls straight through to its other methods, where a connection accepted
7043 # and left silent would make it wait out a timeout on every dial. 7060 # and left silent would make it wait out a timeout on every dial.
7061 wait_pid_gone "$D42PID" "agent forwarding: the positive leg's daemon outlived its last session"
7062 "$MUXD" run --sock "$SOCK48" --shell /bin/sh > "$OUT.agtn.d" 2>&1 &
7063 D42PID=$!
7064 wait_sock "$SOCK48" "$OUT.agtn.d" "agent-refusal daemon never bound"
7044 AR0=$(date +%s%N) 7065 AR0=$(date +%s%N)
7045 set +e 7066 set +e
7046 timeout 40 "$PTYCLIENT" --cols 100 --rows 30 --out "$OUT.agtn" --err "$OUT.agtn.err" \ 7067 timeout 40 "$PTYCLIENT" --cols 100 --rows 30 --out "$OUT.agtn" --err "$OUT.agtn.err" \
7047 -- "$MUX" --sock "$SOCK48" --session noagent > "$OUT.agtn.log" 2>&1 <<'EOF' 7068 -- "$MUX" --sock "$SOCK48" --session noagent > "$OUT.agtn.log" 2>&1 <<'EOF'
7048 expect \x1b[?1049h 15000 7069 expect \x1b[?1049h 15000
7049 settle 400 15000 7070 settle 400 15000
7071 send echo agtsock=${SSH_AUTH_SOCK:+present}\n
7072 expect agtsock=present 15000
7050 send ssh-add -l; echo agtrc=$?\n 7073 send ssh-add -l; echo agtrc=$?\n
7051 expect agtrc= 15000 7074 expect agtrc= 15000
7052 settle 400 15000 7075 settle 400 15000
@@ -7063,6 +7086,14 @@ AMS=$(( (AR1 - AR0) / 1000000 ))
7063 # Quoted heredoc, unlike the leg above: `$?` has to reach the SESSION's 7086 # Quoted heredoc, unlike the leg above: `$?` has to reach the SESSION's
7064 # shell as two characters, not be spent by this one. 7087 # shell as two characters, not be spent by this one.
7065 # 7088 #
7089 # `expect agtsock=present` is the leg's non-vacuity pin, and unlike the
7090 # needle below it IS an assertion: the typed line spells the parameter
7091 # expansion, so only a shell that ran it can put the word `present` on its
7092 # own line. Everything else here would pass just as well against a daemon
7093 # that had never heard of agents — no SSH_AUTH_SOCK at all also means no
7094 # key, no signature and a nonzero status. This verb is what separates
7095 # "accepted the dial and closed it" from "nothing was ever offered".
7096 #
7066 # `expect agtrc=` is a wait and not an assertion — the shell echoes the line 7097 # `expect agtrc=` is a wait and not an assertion — the shell echoes the line
7067 # it was typed, so that needle matches before ssh-add has run. The settle 7098 # it was typed, so that needle matches before ssh-add has run. The settle
7068 # after it is what waits for the answer; these two greps are the assertion, 7099 # after it is what waits for the answer; these two greps are the assertion,
@@ -7087,10 +7118,11 @@ grep -qE "agtrc=[1-9]" "$OUT.agtn" || {
7087 # 7118 #
7088 # 10000 for the reason the key-mismatch leg above picked the same number: it 7119 # 10000 for the reason the key-mismatch leg above picked the same number: it
7089 # separates "refused" from "sat in a loop" and is not asked to do anything 7120 # separates "refused" from "sat in a loop" and is not asked to do anything
7090 # finer. The whole leg — attach, two 400ms settles, a shell's startup and 7121 # finer. The whole leg — attach, two 400ms settles, the agtsock round trip,
7091 # the refusal itself — measured 832-881ms across the grading runs, so a 7122 # a shell's startup and the refusal itself — measured 816-837ms across the
7092 # tighter ceiling would catch nothing this one misses while making a check 7123 # grading runs against the daemon started above, so a tighter ceiling would
7093 # that is nobody's bottleneck the suite's most load-sensitive one. 7124 # catch nothing this one misses while making a check that is nobody's
7125 # bottleneck the suite's most load-sensitive one.
7094 [ "$AMS" -lt 10000 ] || { 7126 [ "$AMS" -lt 10000 ] || {
7095 echo "e2e FAIL: agent-refusal: the leg took ${AMS}ms — a dial with nobody to" 7127 echo "e2e FAIL: agent-refusal: the leg took ${AMS}ms — a dial with nobody to"
7096 echo " answer it must be refused, not retried" 7128 echo " answer it must be refused, not retried"
@@ -7102,7 +7134,11 @@ D42PID=""
7102 # teardown. The trap still names it, for every run that does not reach here. 7134 # teardown. The trap still names it, for every run that does not reach here.
7103 kill "$AGENT48PID" 2>/dev/null || true 7135 kill "$AGENT48PID" 2>/dev/null || true
7104 AGENT48PID="" 7136 AGENT48PID=""
7105 ok "agent forwarding: no offerer means a fast refusal, not a hang" 7137 # The measurement is printed on a PASSING run too, not just quoted in the
7138 # failure above: the band this leg's ceiling was chosen against lives in
7139 # docs/decisions.md, and a number nobody can see on a green run is a number
7140 # that quietly stops being true.
7141 ok "agent forwarding: no offerer means a fast refusal, not a hang (${AMS}ms)"
7106 7142
7107 7143
7108 # --- ...and the agent that answers is whoever typed last ------------------- 7144 # --- ...and the agent that answers is whoever typed last -------------------