From 96c7d4a7b19b6598f7e15dc3181c4aa22a72ccab Mon Sep 17 00:00:00 2001 From: Alex Lavaee Date: Wed, 21 Oct 2026 15:26:47 -0700 Subject: [PATCH] Start the gateway outage test's outage after the agent's session report lands The test that has a gateway answer 502 while Chopin is away moved its fake clock four seconds for every refused request and ended the outage after eight. It expected all eight to come from the heartbeat, but the agent's session report is sent when the agent starts, and on a fast runner that happened during the outage. Each of the report's retries registered the machine again, was refused, and moved the clock and the count, so the heartbeat printed different attempt numbers and elapsed times, or stopped before its sixth attempt. Chopin now goes away only after it has heard the agent's session, so the heartbeat makes every refused request and the printed lines are the same on every run. Waiting for the agent before the outage makes the test longer, so it gets a 10 second budget, as other slow tests in the file do. Assistant-model: Claude Opus 5.5 (fast) Assistant-workflow: inline (fast subagents) Assistant-verification: the old test failed 19 of 20 runs with the test process repeatedly paused, including the exact attempt and elapsed-time mismatch seen in CI Assistant-verification: the new test passed 50 plain runs, 50 runs with the test process repeatedly paused, and 50 runs pinned to one CPU beside three busy loops Assistant-verification: bun test apps/connector, bun run types and bun run ci pass Co-authored-by: Alex Lavaee --- apps/connector/src/run.test.ts | 86 +++++++++++++++++++--------------- 1 file changed, 47 insertions(+), 39 deletions(-) diff --git a/apps/connector/src/run.test.ts b/apps/connector/src/run.test.ts index 280360af0..117b86d2c 100644 --- a/apps/connector/src/run.test.ts +++ b/apps/connector/src/run.test.ts @@ -784,45 +784,53 @@ async function connected(options: { } } -test("a run behind a gateway answering 502 with no body says HTTP 502 while it retries", async () => { - let abort = new AbortController(); - // The fake clock moves four seconds with each request the gateway refuses. - let clock = 0; - let renewals = 0; - let refused = 0; - let down = false; - let { out } = await connected({ - flags: ["--hang"], - abort, - runOptions: { now: () => clock }, - tool(name) { - if (name !== "renew_implementation_build") return {}; - // Chopin answers the first heartbeat and then goes away; once it is back the run ends. - if (++renewals === 1) down = true; - else abort.abort(); - return { accepted: true }; - }, - // A proxy such as Caddy answers for a Chopin that went away with 502 and Content-Length 0, - // both the heartbeat on the open connection and each registration that follows. - gateway(request, name) { - let registering = new URL(request.url).pathname === "/api/connector/connections"; - if (!down || !registering && name !== "renew_implementation_build") return; - clock += 4000; - if (++refused === 8) down = false; - return new Response(null, { status: 502 }); - }, - }); - let lines = out().split("\n").filter(line => /Chopin/.test(line)); - expect(lines).toEqual([ - " … Lost the connection to Chopin during a run (HTTP 502). Registering this machine again; " - + "the agent keeps working.", - " … Lost contact with Chopin. The agent keeps working; reports wait until it is back.", - " … Still trying to reach Chopin (attempt 3, 12 s, HTTP 502), the run keeps going.", - " … Still trying to reach Chopin (attempt 6, 24 s, HTTP 502), the run keeps going.", - " ✓ Registered this machine with Chopin again; the run goes on.", - " ✓ Back in contact with Chopin after 28 s.", - ]); -}); +// The outage waits for the agent's session, which a busy runner can be slow to start. +test( + "a run behind a gateway answering 502 with no body says HTTP 502 while it retries", + async () => { + let abort = new AbortController(); + // The fake clock moves four seconds with each request the gateway refuses. + let clock = 0; + let reported = false; + let refused = 0; + let down = false; + let { out } = await connected({ + flags: ["--hang"], + abort, + runOptions: { now: () => clock }, + tool(name) { + if (name === "report_implementation_build") reported = true; + if (name !== "renew_implementation_build") return {}; + // Chopin goes away only once it has heard the agent's session, so every request it + // refuses is a heartbeat's: a session report retried during the outage would move the + // clock too. Once it is back the run ends. + if (refused) abort.abort(); + else if (reported) down = true; + return { accepted: true }; + }, + // A proxy such as Caddy answers for a Chopin that went away with 502 and Content-Length + // 0, both the heartbeat on the open connection and each registration that follows. + gateway(request, name) { + let registering = new URL(request.url).pathname === "/api/connector/connections"; + if (!down || !registering && name !== "renew_implementation_build") return; + clock += 4000; + if (++refused === 8) down = false; + return new Response(null, { status: 502 }); + }, + }); + let lines = out().split("\n").filter(line => /Chopin/.test(line)); + expect(lines).toEqual([ + " … Lost the connection to Chopin during a run (HTTP 502). Registering this machine again; " + + "the agent keeps working.", + " … Lost contact with Chopin. The agent keeps working; reports wait until it is back.", + " … Still trying to reach Chopin (attempt 3, 12 s, HTTP 502), the run keeps going.", + " … Still trying to reach Chopin (attempt 6, 24 s, HTTP 502), the run keeps going.", + " ✓ Registered this machine with Chopin again; the run goes on.", + " ✓ Back in contact with Chopin after 28 s.", + ]); + }, + 10_000, +); test("a heartbeat in flight when a run finishes is cancelled without losing the connection", async () => { let abort = new AbortController();