1be12949
test: the refusal leg proves the daemon it means
a73x 2026-08-21 08:33
Commit message
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 ------------------- |