From 6d8b39466b2038cd8fd54caec9c75e1a383dc0da Mon Sep 17 00:00:00 2001 From: Martin Vogel Date: Mon, 27 Jul 2026 04:45:13 +0200 Subject: [PATCH 1/3] fix(smoke): Phase 15 probes the UI a fresh profile can actually serve MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Every ui-variant smoke job failed on every platform in dry run 30226483380 — and it turns out a ui-variant Phase 15 had never legitimately passed anywhere: the old probe (a) never passed --ui=true, but the HTTP UI is a persisted DEFAULT-OFF setting, so a fresh profile (every runner, every smoke HOME) can never serve; (b) fed stdin an empty file, but the UI does not pin the process — stdio EOF ends it cleanly (rc=0) before any poll sees it serve; (c) gave a 10s window to a server measured to bind ~6s after launch on a FAST host; (d) POSTed an MCP initialize at /rpc, which speaks the UI's own narrow query protocol, not MCP. The rebuilt probe mirrors the drive-listing guard's proven invocation (--ui=true --port=N equals-form, held-open stdin), waits 30s, and verifies the API surface with the stateless GET /api/ui-config. Validated red->green against the release-shaped artifact: the full darwin ui artifact lane now passes 15a/15b legitimately. Signed-off-by: Martin Vogel --- scripts/smoke-test.sh | 39 ++++++++++++++++++++++++--------------- 1 file changed, 24 insertions(+), 15 deletions(-) diff --git a/scripts/smoke-test.sh b/scripts/smoke-test.sh index 5a87cb2c0..edd27a8fe 100755 --- a/scripts/smoke-test.sh +++ b/scripts/smoke-test.sh @@ -3362,17 +3362,26 @@ echo "=== Phase 15: UI HTTP server ===" # port made the whole phase read as SKIP. The tiny TOCTOU window between # probe and bind is the residual risk, not the common case. UI_PORT=$(python3 -c 'import socket; s = socket.socket(); s.bind(("127.0.0.1", 0)); print(s.getsockname()[1]); s.close()') -UI_INPUT=$(smoke_mktemp_file) -"$BINARY" --port "$UI_PORT" < "$UI_INPUT" > /dev/null 2>&1 & +# --ui=true is REQUIRED: the HTTP UI is a persisted, default-off setting, so +# on any fresh profile (every CI runner, every smoke HOME) a bare --port +# invocation can never serve — the probe then misread "UI disabled" as "no +# embedded assets" on binaries that carry them (first exposed when the +# ui-variant no-skip guard made Phase 15 mandatory). Stdin must be HELD OPEN: +# the UI does not pin the process, so stdio EOF ends it cleanly (rc=0) before +# the poll can see it serve — the drive-listing guard holds a pipe for the +# same reason. Equals-form flags match that guard's proven invocation. +sleep 300 | "$BINARY" --ui=true --port="$UI_PORT" > /dev/null 2>&1 & UI_PID=$! # Readiness poll instead of a fixed sleep: SKIP is legitimate ONLY when the # process exited (the documented no-embedded-assets case); a slow start on a -# loaded runner must not masquerade as it. +# loaded runner must not masquerade as it. The UI binds ~6s after launch even +# on a fast host (measured against the release artifact), so the window +# matches the drive-listing guard's 25s, not a 10s sprint. UI_READY=0 -for _ in $(seq 1 100); do +for _ in $(seq 1 150); do if ! kill -0 "$UI_PID" 2>/dev/null; then break; fi if curl -sf "http://127.0.0.1:$UI_PORT/" -o /dev/null 2>/dev/null; then UI_READY=1; break; fi - sleep 0.1 + sleep 0.2 done if [ "$UI_READY" -eq 1 ] || kill -0 "$UI_PID" 2>/dev/null; then @@ -3388,17 +3397,18 @@ if [ "$UI_READY" -eq 1 ] || kill -0 "$UI_PID" 2>/dev/null; then exit 1 fi - # 15b: POST /rpc accepts JSON-RPC and returns JSON - RPC_BODY=$(curl -sf -X POST \ - -H "Content-Type: application/json" \ - -d '{"jsonrpc":"2.0","id":1,"method":"initialize","params":{}}' \ - "http://127.0.0.1:$UI_PORT/rpc" 2>/dev/null || echo "") - if echo "$RPC_BODY" | grep -q "jsonrpc"; then - echo "OK 15b: /rpc returns JSON-RPC response" + # 15b: the API surface answers — GET /api/ui-config is stateless and + # session-free, so it proves API reachability on any fresh profile. (The + # old probe POSTed an MCP initialize at /rpc, but the UI's /rpc speaks the + # UI's own narrow query protocol, not MCP — the probe asserted a request + # the endpoint never answered; protocol depth belongs to the UI guards.) + RPC_BODY=$(curl -sf "http://127.0.0.1:$UI_PORT/api/ui-config" 2>/dev/null || echo "") + if echo "$RPC_BODY" | grep -q "{"; then + echo "OK 15b: /api/ui-config returns JSON" elif [ -z "$RPC_BODY" ]; then - smoke_ui_missing "15b" "/rpc not reachable" + smoke_ui_missing "15b" "/api/ui-config not reachable" else - echo "FAIL 15b: /rpc did not return JSON-RPC" + echo "FAIL 15b: /api/ui-config did not return JSON" kill "$UI_PID" 2>/dev/null || true exit 1 fi @@ -3408,7 +3418,6 @@ if [ "$UI_READY" -eq 1 ] || kill -0 "$UI_PID" 2>/dev/null; then else smoke_ui_missing "Phase 15" "binary exited immediately (no UI assets embedded)" fi -rm -f "$UI_INPUT" echo "" echo "=== Phase 16: stdio server leaves no orphan after shutdown ===" From 0c0111eb33083490e4277e0c551fa3c9c01632eb Mon Sep 17 00:00:00 2001 From: Martin Vogel Date: Mon, 27 Jul 2026 04:45:13 +0200 Subject: [PATCH 2/3] fix(smoke): header-only fixture-server readiness polls Hosted Windows runners reset the readiness poll's full-archive GET mid-body (WinError 10054 on the server, ~99 aborted transfers per job) while the identical stack passes on the VM; every windows smoke job died at 'fixture server did not serve' with a healthy server. A HEAD proves the artifact routes without transferring the body per poll; the download phases still own full-body transport and report phase-precise if that is ever broken. Signed-off-by: Martin Vogel --- scripts/smoke-local.sh | 4 +++- test-infrastructure/vm/vm-smoke.sh | 8 +++++++- 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/scripts/smoke-local.sh b/scripts/smoke-local.sh index 9a38dd96d..ae617634a 100755 --- a/scripts/smoke-local.sh +++ b/scripts/smoke-local.sh @@ -161,7 +161,9 @@ READY=0 for _ in $(seq 1 100); do if [ -s "$PORT_FILE" ]; then PORT=$(tr -d '[:space:]' < "$PORT_FILE") - if curl --noproxy '*' -fsS \ + # Header-only readiness (see vm-smoke.sh): proves routing without a + # full body transfer per poll; the download phases own body transport. + if curl --noproxy '*' -fsSI \ "http://127.0.0.1:$PORT/$EXPECTED_ARTIFACT" >/dev/null 2>&1; then READY=1 break diff --git a/test-infrastructure/vm/vm-smoke.sh b/test-infrastructure/vm/vm-smoke.sh index 8d3d7de79..881469366 100755 --- a/test-infrastructure/vm/vm-smoke.sh +++ b/test-infrastructure/vm/vm-smoke.sh @@ -186,7 +186,13 @@ READY=0 for _ in $(seq 1 100); do if [ -s "$PORT_FILE" ]; then PORT=$(tr -d '[:space:]' < "$PORT_FILE") - if curl --noproxy '*' -fsS \ + # Header-only readiness: a HEAD proves the server routes the artifact + # without transferring the archive body on every poll (hosted Windows + # runners reset mid-body full-GET polls — WinError 10054 on the server, + # 99 aborted transfers per job — while the same stack passes on the VM; + # the real download phases still exercise full transfers and report + # phase-precise if body transport is broken). + if curl --noproxy '*' -fsSI \ "http://127.0.0.1:$PORT/$EXPECTED_ARTIFACT" >/dev/null 2>&1; then READY=1 break From 657d4b02849dd2d34d0f12e0fbdc88554857ea9a Mon Sep 17 00:00:00 2001 From: Martin Vogel Date: Mon, 27 Jul 2026 04:45:13 +0200 Subject: [PATCH 3/3] fix(soak): give the first diagnostics snapshot a 30s window MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Both Windows runner soak legs died right after 'server running' while the daemon's own log shows a healthy diagnostics.start with valid paths — the wait polled 10s for a snapshot whose first WRITE lands one 5s interval after start and can exceed 10s on a cold 4-vCPU runner mid-initial-index (the VM's 18 cores never miss it; the x64 leg's SIGPIPE was the same failure path dying inside the pipeline). Budget doctrine: the wait sits above the worst case. Signed-off-by: Martin Vogel --- scripts/soak-test.sh | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/scripts/soak-test.sh b/scripts/soak-test.sh index be5891a44..1aba4d327 100755 --- a/scripts/soak-test.sh +++ b/scripts/soak-test.sh @@ -559,7 +559,13 @@ wait_for_daemon_stop() { wait_for_diagnostics_snapshot() { local after_count="${1:-0}" local previous_path="${2:-}" - local attempts=100 + # 30s, not a 10s sprint: the first snapshot lands one diagnostics interval + # (5s) after start, and a cold 4-vCPU hosted runner mid-initial-index can + # push the first WRITE past 10s (observed: both Windows runner soak legs + # died right after 'server running' while the daemon's own log shows a + # perfectly healthy diagnostics.start — the VM's 18 cores never miss the + # window). Budget doctrine: the wait sits above the worst case. + local attempts=300 while [ "$attempts" -gt 0 ]; do local current_count current_count=$(diagnostics_start_count)