d225b05c
test: a real key crosses the wire; a keyless session refuses fast
a73x 2026-08-21 08:33
Commit message
test/e2e.sh
| Old | New | ||
|---|---|---|---|
| @@ -52,6 +52,19 @@ command -v less > /dev/null 2>&1 || { | |||
| 52 | echo " a real pager); install it, or lose the only check that a" | 52 | echo " a real pager); install it, or lose the only check that a" |
| 53 | echo " wheel notch moves anything on a TUI screen" | 53 | echo " wheel notch moves anything on a TUI screen" |
| 54 | exit 1; } | 54 | exit 1; } |
| 55 | # And the fourth, for the reason the other three are required rather than | ||
| 56 | # skipped: the agent-forwarding legs are the only place a REAL key crosses | ||
| 57 | # the wire, and a `command -v` skip would let a box print a green line | ||
| 58 | # having tested none of it. openssh rather than a stand-in — the daemon and | ||
| 59 | # the client both carry the agent protocol blind, so the only thing that can | ||
| 60 | # say the pump is right is the program the protocol belongs to. | ||
| 61 | for _agtool in ssh-agent ssh-add ssh-keygen; do | ||
| 62 | command -v "$_agtool" > /dev/null 2>&1 || { | ||
| 63 | echo "e2e FAIL: this suite needs $_agtool (the agent-forwarding legs run a" | ||
| 64 | echo " real ssh-agent holding a real key); install openssh, or lose" | ||
| 65 | echo " the only check that a signature request crosses the wire" | ||
| 66 | exit 1; } | ||
| 67 | done | ||
| 55 | 68 | ||
| 56 | SOCK="${TMPDIR:-/tmp}/muxd-e2e-$$.sock" | 69 | SOCK="${TMPDIR:-/tmp}/muxd-e2e-$$.sock" |
| 57 | OUT="${TMPDIR:-/tmp}/mux-e2e-out-$$" | 70 | OUT="${TMPDIR:-/tmp}/mux-e2e-out-$$" |
| @@ -84,6 +97,13 @@ export SHELL | |||
| 84 | # change which key the handoff announces — and the scenarios would still | 97 | # change which key the handoff announces — and the scenarios would still |
| 85 | # pass, against the wrong key, until one of them did not. | 98 | # pass, against the wrong key, until one of them did not. |
| 86 | unset MUX_KEY_FILE | 99 | unset MUX_KEY_FILE |
| 100 | # ...and SSH_AUTH_SOCK, the same family again and the sharpest case in it. | ||
| 101 | # A daemon that inherited the developer's real agent would hand it to every | ||
| 102 | # session whose own agent socket failed to bind — and the refusal leg, whose | ||
| 103 | # whole subject is a shell finding nobody to sign for it, would then be | ||
| 104 | # asserting against the developer's keyring. The legs that want an agent | ||
| 105 | # export one per command, at a path this file made. | ||
| 106 | unset SSH_AUTH_SOCK | ||
| 87 | # Second daemon, used only by the M7 abort scenario; declared here so the | 107 | # Second daemon, used only by the M7 abort scenario; declared here so the |
| 88 | # trap below can reference them under `set -u` before they are ever started. | 108 | # trap below can reference them under `set -u` before they are ever started. |
| 89 | SOCK2="${TMPDIR:-/tmp}/muxd-e2e-abort-$$.sock" | 109 | SOCK2="${TMPDIR:-/tmp}/muxd-e2e-abort-$$.sock" |
| @@ -403,6 +423,21 @@ SOCK47="${TMPDIR:-/tmp}/muxd-e2e-exitsem-$$.sock" | |||
| 403 | XESTATE="${TMPDIR:-/tmp}/mux-e2e-exitsem-state-$$" | 423 | XESTATE="${TMPDIR:-/tmp}/mux-e2e-exitsem-state-$$" |
| 404 | D47PID="" | 424 | D47PID="" |
| 405 | 425 | ||
| 426 | # Agent forwarding: one daemon serving both legs, because the negative one | ||
| 427 | # is ABOUT the positive one's daemon — a session on it that nobody offered | ||
| 428 | # an agent to. A second daemon would prove only that a daemon with no | ||
| 429 | # clients refuses, which is not the case anyone is in. | ||
| 430 | # | ||
| 431 | # 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 | ||
| 433 | # session shell, so the room it has is the room every other socket in this | ||
| 434 | # file was given. | ||
| 435 | SOCK48="${TMPDIR:-/tmp}/muxd-e2e-agentfwd-$$.sock" | ||
| 436 | AGENT48="${TMPDIR:-/tmp}/mux-e2e-agent-$$.sock" | ||
| 437 | AGENT48PID="" | ||
| 438 | AGENT48KEY="${TMPDIR:-/tmp}/mux-e2e-agentkey-$$" | ||
| 439 | D42PID="" | ||
| 440 | |||
| 406 | # One counter out of a MUX_PREDICT_STATS line. The client prints exactly one | 441 | # One counter out of a MUX_PREDICT_STATS line. The client prints exactly one |
| 407 | # such line on exit; every field is a key=value pair, so a rename or reorder | 442 | # such line on exit; every field is a key=value pair, so a rename or reorder |
| 408 | # in the client shows up here as an empty read rather than a wrong number. | 443 | # in the client shows up here as an empty read rather than a wrong number. |
| @@ -1027,6 +1062,12 @@ cleanup() { | |||
| 1027 | [ -n "$D39PID" ] && kill "$D39PID" 2>/dev/null || true | 1062 | [ -n "$D39PID" ] && kill "$D39PID" 2>/dev/null || true |
| 1028 | [ -n "$D40PID" ] && kill "$D40PID" 2>/dev/null || true | 1063 | [ -n "$D40PID" ] && kill "$D40PID" 2>/dev/null || true |
| 1029 | [ -n "$D41PID" ] && kill "$D41PID" 2>/dev/null || true | 1064 | [ -n "$D41PID" ] && kill "$D41PID" 2>/dev/null || true |
| 1065 | [ -n "$D42PID" ] && kill "$D42PID" 2>/dev/null || true | ||
| 1066 | # The ssh-agent the forwarding legs start. Not a mux process and so not | ||
| 1067 | # the leak sweep's business, but it is a daemon this file forked: left | ||
| 1068 | # alive it outlives the suite holding a private key, which is the one | ||
| 1069 | # kind of leak worth chasing even on a green run. | ||
| 1070 | [ -n "$AGENT48PID" ] && kill "$AGENT48PID" 2>/dev/null || true | ||
| 1030 | # The stops still precede the socket rm below, like SOCK14-17 above: | 1071 | # The stops still precede the socket rm below, like SOCK14-17 above: |
| 1031 | # unlinking a socket first would leave a live daemon nothing could reach | 1072 | # unlinking a socket first would leave a live daemon nothing could reach |
| 1032 | # by path. | 1073 | # by path. |
| @@ -1056,6 +1097,7 @@ cleanup() { | |||
| 1056 | [ -S "$SOCK45" ] && "$MUXD" stop --sock "$SOCK45" 2>/dev/null || true | 1097 | [ -S "$SOCK45" ] && "$MUXD" stop --sock "$SOCK45" 2>/dev/null || true |
| 1057 | [ -S "$SOCK46" ] && "$MUXD" stop --sock "$SOCK46" 2>/dev/null || true | 1098 | [ -S "$SOCK46" ] && "$MUXD" stop --sock "$SOCK46" 2>/dev/null || true |
| 1058 | [ -S "$SOCK47" ] && "$MUXD" stop --sock "$SOCK47" 2>/dev/null || true | 1099 | [ -S "$SOCK47" ] && "$MUXD" stop --sock "$SOCK47" 2>/dev/null || true |
| 1100 | [ -S "$SOCK48" ] && "$MUXD" stop --sock "$SOCK48" 2>/dev/null || true | ||
| 1059 | 1101 | ||
| 1060 | # ---- the leak sweep (hygiene kit, 6a) ---- | 1102 | # ---- the leak sweep (hygiene kit, 6a) ---- |
| 1061 | # Here rather than at the bottom of the file, which `set -e` reaches only | 1103 | # Here rather than at the bottom of the file, which `set -e` reaches only |
| @@ -1070,7 +1112,7 @@ cleanup() { | |||
| 1070 | "$D20PID" "$D21PID" "$D22PID" "$D23PID" "$D24PID" "$D25PID" \ | 1112 | "$D20PID" "$D21PID" "$D22PID" "$D23PID" "$D24PID" "$D25PID" \ |
| 1071 | "$D26PID" "$D27PID" "$D28PID" "$D29PID" "$D30PID" "$D31PID" \ | 1113 | "$D26PID" "$D27PID" "$D28PID" "$D29PID" "$D30PID" "$D31PID" \ |
| 1072 | "$D32PID" "$D33PID" "$D34PID" "$D35PID" "$D36PID" "$D37PID" \ | 1114 | "$D32PID" "$D33PID" "$D34PID" "$D35PID" "$D36PID" "$D37PID" \ |
| 1073 | "$D38PID" "$D39PID" "$D40PID" "$D41PID" | 1115 | "$D38PID" "$D39PID" "$D40PID" "$D41PID" "$D42PID" |
| 1074 | _leak=0 | 1116 | _leak=0 |
| 1075 | leak_sweep "$_rc" || _leak=1 | 1117 | leak_sweep "$_rc" || _leak=1 |
| 1076 | 1118 | ||
| @@ -1229,6 +1271,13 @@ cleanup() { | |||
| 1229 | "$OUT.xecap" "$OUT.xecap.err" "$OUT.xepc" "$OUT.xe2" "$OUT.xe2.err" \ | 1271 | "$OUT.xecap" "$OUT.xecap.err" "$OUT.xepc" "$OUT.xe2" "$OUT.xe2.err" \ |
| 1230 | "$OUT.xestop" | 1272 | "$OUT.xestop" |
| 1231 | rm -rf "$CVSTATE" "$RGSTATE" "$RGOTHER" "$XESTATE" | 1273 | rm -rf "$CVSTATE" "$RGSTATE" "$RGOTHER" "$XESTATE" |
| 1274 | # The agent legs. The private key and its public half go too: this file | ||
| 1275 | # made them, and a keypair left in $TMPDIR is litter with a lifetime. | ||
| 1276 | # The agent's own socket is unlinked by the ssh-agent killed above only | ||
| 1277 | # when it exits cleanly, so it is named here as well. | ||
| 1278 | rm -f "$SOCK48" "$AGENT48" "$AGENT48KEY" "$AGENT48KEY.pub" \ | ||
| 1279 | "$OUT.agt.d" "$OUT.agt.env" "$OUT.agt" "$OUT.agt.err" "$OUT.agt.log" \ | ||
| 1280 | "$OUT.agtn" "$OUT.agtn.err" "$OUT.agtn.log" "$OUT.agtstop" | ||
| 1232 | # The convergence files a FAILING assert_converged leaves behind | 1281 | # The convergence files a FAILING assert_converged leaves behind |
| 1233 | # (.render/.dump/.rvt/.dvt/.diff for that capture) are deliberately not | 1282 | # (.render/.dump/.rvt/.dvt/.diff for that capture) are deliberately not |
| 1234 | # chased here: on a failing run they are the evidence. | 1283 | # chased here: on a failing run they are the evidence. |
| @@ -6879,6 +6928,154 @@ D47PID="" | |||
| 6879 | ok "a session ending under the zoom exits mux alone, and drops to the wall beside company" | 6928 | ok "a session ending under the zoom exits mux alone, and drops to the wall beside company" |
| 6880 | 6929 | ||
| 6881 | 6930 | ||
| 6931 | # --- agent forwarding: a real key in the client's agent answers in the session | ||
| 6932 | # | ||
| 6933 | # Everything under this heading is a real program doing its real job: a real | ||
| 6934 | # ssh-agent holding a real ed25519 key, a real `ssh-add -l` typed into a real | ||
| 6935 | # session shell on a real pty. Nothing here stands in for anything, and that | ||
| 6936 | # is the point — the daemon and the client both carry the agent protocol | ||
| 6937 | # BLIND (server.zig's serviceAgentChan copies bytes it never parses), so a | ||
| 6938 | # stand-in agent would be this suite grading the pump against a second guess | ||
| 6939 | # at a format neither end reads. openssh either recognises what came back or | ||
| 6940 | # it does not, and that answer is the only one worth having. | ||
| 6941 | # | ||
| 6942 | # The needle is a SHA256 fingerprint, and it is echo-proof by the M12 rule | ||
| 6943 | # (tp2, argued at the OSC 52 leg above): the shell echoes what is typed, so | ||
| 6944 | # a needle a keystroke could spell would pass on a client that forwarded | ||
| 6945 | # nothing at all. The typed line is `ssh-add -l`, which cannot spell a | ||
| 6946 | # fingerprint. What puts those 50 bytes on the terminal is the agent on this | ||
| 6947 | # machine answering a request that left the session, crossed the daemon, | ||
| 6948 | # reached the client, and came back. | ||
| 6949 | "$MUXD" run --sock "$SOCK48" --shell /bin/sh > "$OUT.agt.d" 2>&1 & | ||
| 6950 | D42PID=$! | ||
| 6951 | wait_sock "$SOCK48" "$OUT.agt.d" "agent daemon never bound" | ||
| 6952 | |||
| 6953 | # The agent is started AFTER the daemon and its path is never exported, so | ||
| 6954 | # the daemon cannot have inherited it: the only SSH_AUTH_SOCK a session on | ||
| 6955 | # this daemon can hold is one the daemon bound itself. That ordering is what | ||
| 6956 | # makes the fingerprint below evidence of a forward rather than of a leak. | ||
| 6957 | ssh-agent -a "$AGENT48" > "$OUT.agt.env" 2>&1 | ||
| 6958 | AGENT48PID=$(sed -n 's/.*SSH_AGENT_PID=\([0-9]*\).*/\1/p' "$OUT.agt.env") | ||
| 6959 | [ -n "$AGENT48PID" ] || { | ||
| 6960 | echo "e2e FAIL: agent: ssh-agent printed no pid for the trap to hold it by:" | ||
| 6961 | cat "$OUT.agt.env"; exit 1; } | ||
| 6962 | # -C, so `ssh-add -l` prints a short comment instead of this key's whole | ||
| 6963 | # path: the answer has to fit one row of the terminal below, and a line that | ||
| 6964 | # wrapped could split the fingerprint across two rows the capture never | ||
| 6965 | # joins back up. | ||
| 6966 | ssh-keygen -q -t ed25519 -N '' -C mux-e2e-agent -f "$AGENT48KEY" | ||
| 6967 | SSH_AUTH_SOCK="$AGENT48" ssh-add "$AGENT48KEY" > /dev/null 2>&1 | ||
| 6968 | FP48=$(ssh-keygen -lf "$AGENT48KEY" | awk '{print $2}') | ||
| 6969 | # An empty or malformed needle would reach ptyclient as a usage error rather | ||
| 6970 | # than as a failed expectation, and the suite would report the wrong thing. | ||
| 6971 | case "$FP48" in | ||
| 6972 | SHA256:?*) ;; | ||
| 6973 | *) echo "e2e FAIL: agent: ssh-keygen -lf gave no fingerprint to look for: [$FP48]" | ||
| 6974 | exit 1;; | ||
| 6975 | esac | ||
| 6976 | |||
| 6977 | # Unquoted heredoc, like the OSC 52 leg's and for the same reason: $FP48 has | ||
| 6978 | # to interpolate. Nothing else in the script needs expanding — `\n` and | ||
| 6979 | # `\x1b` are not escapes the shell touches. | ||
| 6980 | set +e | ||
| 6981 | SSH_AUTH_SOCK="$AGENT48" timeout 40 "$PTYCLIENT" --cols 100 --rows 30 \ | ||
| 6982 | --out "$OUT.agt" --err "$OUT.agt.err" \ | ||
| 6983 | -- "$MUX" -A --sock "$SOCK48" > "$OUT.agt.log" 2>&1 <<EOF | ||
| 6984 | expect \x1b[?1049h 15000 | ||
| 6985 | settle 400 15000 | ||
| 6986 | send ssh-add -l\n | ||
| 6987 | expect $FP48 15000 | ||
| 6988 | settle 400 15000 | ||
| 6989 | send exit\n | ||
| 6990 | waitexit 10000 | ||
| 6991 | EOF | ||
| 6992 | RC=$? | ||
| 6993 | set -e | ||
| 6994 | # Only the fixture's log, not the capture beside it: ptyclient prints the | ||
| 6995 | # verb that failed and the last 200 bytes it saw, which is the answer. The | ||
| 6996 | # capture is a screenful of escapes that would bury it. | ||
| 6997 | [ "$RC" -eq 0 ] || { | ||
| 6998 | echo "e2e FAIL: agent: the client's key never answered ssh-add -l in the" | ||
| 6999 | echo " session (wanted $FP48):" | ||
| 7000 | cat "$OUT.agt.log"; exit 1; } | ||
| 7001 | ok "agent forwarding: ssh-add -l in the session lists the client's key" | ||
| 7002 | |||
| 7003 | # --- ...and a session nobody offered an agent to refuses, fast ------------- | ||
| 7004 | # | ||
| 7005 | # The same daemon, deliberately: the interesting no-offerer case is a | ||
| 7006 | # session sitting beside one that HAS an answerer, not a daemon with no | ||
| 7007 | # clients at all. The client above has exited, so its offer is gone; this | ||
| 7008 | # one attaches without -A and with no agent of its own to offer. | ||
| 7009 | # | ||
| 7010 | # SSH_AUTH_SOCK is still set inside the session — every session is born with | ||
| 7011 | # one, whether or not anyone has offered — and it points at the daemon, | ||
| 7012 | # which accepts the dial and closes it. That is the designed answer, not a | ||
| 7013 | # gap: ssh reads a closed agent socket as "agent refused operation" and | ||
| 7014 | # falls straight through to its other methods, where a connection accepted | ||
| 7015 | # and left silent would make it wait out a timeout on every dial. | ||
| 7016 | AR0=$(date +%s%N) | ||
| 7017 | set +e | ||
| 7018 | timeout 40 "$PTYCLIENT" --cols 100 --rows 30 --out "$OUT.agtn" --err "$OUT.agtn.err" \ | ||
| 7019 | -- "$MUX" --sock "$SOCK48" --session noagent > "$OUT.agtn.log" 2>&1 <<'EOF' | ||
| 7020 | expect \x1b[?1049h 15000 | ||
| 7021 | settle 400 15000 | ||
| 7022 | send ssh-add -l; echo agtrc=$?\n | ||
| 7023 | expect agtrc= 15000 | ||
| 7024 | settle 400 15000 | ||
| 7025 | send exit\n | ||
| 7026 | waitexit 10000 | ||
| 7027 | EOF | ||
| 7028 | RC=$? | ||
| 7029 | set -e | ||
| 7030 | AR1=$(date +%s%N) | ||
| 7031 | AMS=$(( (AR1 - AR0) / 1000000 )) | ||
| 7032 | [ "$RC" -eq 0 ] || { | ||
| 7033 | echo "e2e FAIL: agent-refusal: the leg exited $RC:" | ||
| 7034 | cat "$OUT.agtn.log"; exit 1; } | ||
| 7035 | # Quoted heredoc, unlike the leg above: `$?` has to reach the SESSION's | ||
| 7036 | # shell as two characters, not be spent by this one. | ||
| 7037 | # | ||
| 7038 | # `expect agtrc=` is a wait and not an assertion — the shell echoes the line | ||
| 7039 | # it was typed, so that needle matches before ssh-add has run. The settle | ||
| 7040 | # after it is what waits for the answer; these two greps are the assertion, | ||
| 7041 | # and both are echo-proof, because the typed line spells `agtrc=$?` and | ||
| 7042 | # neither `agtrc=0` nor a digit can come from echoing it. | ||
| 7043 | grep -q "agtrc=0" "$OUT.agtn" && { | ||
| 7044 | echo "e2e FAIL: agent-refusal: ssh-add succeeded in a session nobody offered" | ||
| 7045 | echo " an agent to — something answered for a key that is not there:" | ||
| 7046 | cat -v "$OUT.agtn"; exit 1; } | ||
| 7047 | grep -qE "agtrc=[1-9]" "$OUT.agtn" || { | ||
| 7048 | echo "e2e FAIL: agent-refusal: ssh-add never reported a status at all — the" | ||
| 7049 | echo " session did not run it, so its failure proves nothing:" | ||
| 7050 | cat -v "$OUT.agtn"; exit 1; } | ||
| 7051 | # Ceiling only, and the floor's absence is the claim: a refusal is a close on | ||
| 7052 | # an already-accepted connection and should cost about nothing, so there is | ||
| 7053 | # no lower bound worth defending. What the ceiling owns is the band this | ||
| 7054 | # leg's own deadlines cannot see. A refusal that HUNG is caught by the | ||
| 7055 | # waitexit above and reported as an exit code; a refusal that came back | ||
| 7056 | # after ten seconds of retrying is not — `expect` matched the echo long | ||
| 7057 | # before, and the settle would sit through the whole retry and still pass. | ||
| 7058 | # This bound is what makes that band a failure. | ||
| 7059 | # | ||
| 7060 | # 10000 for the reason the key-mismatch leg above picked the same number: it | ||
| 7061 | # separates "refused" from "sat in a loop" and is not asked to do anything | ||
| 7062 | # finer. The whole leg — attach, two 400ms settles, a shell's startup and | ||
| 7063 | # the refusal itself — measured 832-881ms across the grading runs, so a | ||
| 7064 | # tighter ceiling would catch nothing this one misses while making a check | ||
| 7065 | # that is nobody's bottleneck the suite's most load-sensitive one. | ||
| 7066 | [ "$AMS" -lt 10000 ] || { | ||
| 7067 | echo "e2e FAIL: agent-refusal: the leg took ${AMS}ms — a dial with nobody to" | ||
| 7068 | echo " answer it must be refused, not retried" | ||
| 7069 | exit 1; } | ||
| 7070 | assert_stopped "$SOCK48" "$D42PID" "agent forwarding" "$OUT.agtstop" | ||
| 7071 | D42PID="" | ||
| 7072 | # The agent has no more work; ended here rather than in the trap so a green | ||
| 7073 | # run does not leave a key-holding process alive for the length of the | ||
| 7074 | # teardown. The trap still names it, for every run that does not reach here. | ||
| 7075 | kill "$AGENT48PID" 2>/dev/null || true | ||
| 7076 | AGENT48PID="" | ||
| 7077 | ok "agent forwarding: no offerer means a fast refusal, not a hang" | ||
| 7078 | |||
| 6882 | 7079 | ||
| 6883 | # The long-lived daemon has served every scenario that wanted it; stop it | 7080 | # The long-lived daemon has served every scenario that wanted it; stop it |
| 6884 | # NOW so its allocator verdict is written while the suite is still running | 7081 | # NOW so its allocator verdict is written while the suite is still running |
| @@ -6973,9 +7170,17 @@ DPID="" | |||
| 6973 | # three of them: what they assert on is a ptyclient capture, a cumulative | 7170 | # three of them: what they assert on is a ptyclient capture, a cumulative |
| 6974 | # attach COUNTER, the wall file and an exit CODE — a screen, a number, a | 7171 | # attach COUNTER, the wall file and an exit CODE — a screen, a number, a |
| 6975 | # file and a status, and the last of them names a session that no longer | 7172 | # file and a status, and the last of them names a session that no longer |
| 6976 | # exists to dump. | 7173 | # exists to dump. The 56th is agent forwarding's positive leg, and no |
| 6977 | [ "$OK_COUNT" = "55" ] || { | 7174 | # convergence point because its subject is one string on a terminal — a |
| 6978 | echo "e2e FAIL: $OK_COUNT scenario checkpoints ran, the pin says 55 —" | 7175 | # fingerprint openssh printed after a round trip through the daemon — and |
| 7176 | # converging the grid that string landed in would only say the client | ||
| 7177 | # painted what the daemon holds, which is a fact every leg above already | ||
| 7178 | # establishes and this one does not doubt. The 57th is its negative twin, | ||
| 7179 | # and no convergence point for the same reason plus the exit-code reason: | ||
| 7180 | # what it asserts on is a shell's `$?` and a wall-clock bound, and a grid | ||
| 7181 | # comparison speaks to neither. | ||
| 7182 | [ "$OK_COUNT" = "57" ] || { | ||
| 7183 | echo "e2e FAIL: $OK_COUNT scenario checkpoints ran, the pin says 57 —" | ||
| 6979 | echo " a scenario was added (update the pin) or silently lost" | 7184 | echo " a scenario was added (update the pin) or silently lost" |
| 6980 | exit 1 | 7185 | exit 1 |
| 6981 | } | 7186 | } |
| @@ -6983,4 +7188,4 @@ DPID="" | |||
| 6983 | echo "e2e FAIL: $CONV_COUNT convergence points ran, the pin says 35" | 7188 | echo "e2e FAIL: $CONV_COUNT convergence points ran, the pin says 35" |
| 6984 | exit 1 | 7189 | exit 1 |
| 6985 | } | 7190 | } |
| 6986 | echo "e2e OK (55 scenarios, 35 convergence points)" | 7191 | echo "e2e OK (57 scenarios, 35 convergence points)" |