From 668d2832a796478c6a027b4de87095bec6dfc0a3 Mon Sep 17 00:00:00 2001 From: bitkyc08-arch Date: Mon, 31 Aug 2026 17:50:13 +0900 Subject: [PATCH 1/3] test(shutdown): give the launcher startup wait room on a loaded runner The three signal cases in tests/shutdown-launcher.test.ts each cold-spawn `node bin/ocx.mjs start` and wait for /healthz, and they run back to back. The wait was capped at 20s, which on a loaded runner is the failure rather than a bound on one: observed failures land at 20061ms and 20168ms, exactly the budget. Reproduced on a Linux host at origin/dev (0844dc9a9) 1-in-3 in isolation, and it failed CI test 2/4 on PR #3057, a branch that does not touch this file. Startup latency is not what any assertion here is about. The test exists to prove that signalling only the launcher PID tears down the Bun proxy, frees the port, and restores the Codex config. Raise the startup wait to 90s and the per-test ceiling from 45s to 130s so the wait can actually elapse; every assertion is unchanged. --- tests/shutdown-launcher.test.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tests/shutdown-launcher.test.ts b/tests/shutdown-launcher.test.ts index 08316ea588..1364ee9677 100644 --- a/tests/shutdown-launcher.test.ts +++ b/tests/shutdown-launcher.test.ts @@ -107,7 +107,11 @@ describe.skipIf(!runnable)("ocx launcher graceful shutdown", () => { child.on("exit", () => { exited = true; }); // 1. Proxy comes up + injected the Codex config (Design B root override on loopback). - const up = await waitUntil(() => healthy(port), 20_000); + // Startup is a cold `node bin/ocx.mjs start` spawn, and this file runs three of them + // back to back. On a loaded runner the 20s ceiling was the failure: observed failures + // land at 20061ms and 20168ms, i.e. the budget itself, not a hang. Startup time is not + // what any assertion here is about — the teardown behaviour after the signal is. + const up = await waitUntil(() => healthy(port), 90_000); expect(up).toBe(true); expect(existsSync(join(home, "ocx.pid"))).toBe(true); const injected = readFileSync(codexConfig, "utf8"); @@ -131,7 +135,9 @@ describe.skipIf(!runnable)("ocx launcher graceful shutdown", () => { expect(existsSync(join(home, "runtime-port.json"))).toBe(false); expect(readFileSync(codexConfig, "utf8")).not.toContain("opencodex"); }, - 45_000, + // Raised with the startup budget above: a 45s per-test ceiling could not contain a + // 90s wait, so the two have to move together or the wait is decorative. + 130_000, ); } }); From b579bf38fe80794865fba78829d562a6c0356aa0 Mon Sep 17 00:00:00 2001 From: bitkyc08-arch Date: Mon, 31 Aug 2026 18:15:51 +0900 Subject: [PATCH 2/3] test(shutdown): capture the launcher output when it never becomes healthy The test spawned the launcher with stdio ignored, so a startup failure surfaced only as expect(up).toBe(true) with Received: false and no evidence whatsoever. A CI failure that consumed the full 90s wait could not be distinguished from a slow start, which is exactly the ambiguity that made this flake expensive to chase. Pipe stdout/stderr and print them when the health wait expires. --- tests/shutdown-launcher.test.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/tests/shutdown-launcher.test.ts b/tests/shutdown-launcher.test.ts index 1364ee9677..687d44436c 100644 --- a/tests/shutdown-launcher.test.ts +++ b/tests/shutdown-launcher.test.ts @@ -91,7 +91,11 @@ describe.skipIf(!runnable)("ocx launcher graceful shutdown", () => { writeFileSync(codexConfig, 'model = "gpt-5.1"\n'); const child = spawn("node", [BIN_OCX, "start", "--port", String(port)], { - stdio: "ignore", + // Captured rather than discarded: when this test fails in CI it fails on + // `expect(up).toBe(true)` with no evidence at all, which cost a full + // investigation cycle. The launcher's own output is the only thing that + // can distinguish a slow start from a refused one. + stdio: ["ignore", "pipe", "pipe"], env: { ...process.env, HOME: identity.homeDir, @@ -102,6 +106,9 @@ describe.skipIf(!runnable)("ocx launcher graceful shutdown", () => { }, }); spawned.push(child); + let launcherOutput = ""; + child.stdout?.on("data", chunk => { launcherOutput += chunk; }); + child.stderr?.on("data", chunk => { launcherOutput += chunk; }); let exited = false; child.on("exit", () => { exited = true; }); @@ -112,6 +119,9 @@ describe.skipIf(!runnable)("ocx launcher graceful shutdown", () => { // land at 20061ms and 20168ms, i.e. the budget itself, not a hang. Startup time is not // what any assertion here is about — the teardown behaviour after the signal is. const up = await waitUntil(() => healthy(port), 90_000); + if (!up) { + console.error(`launcher never became healthy on port ${port}; its output was:\n${launcherOutput || "(nothing)"}`); + } expect(up).toBe(true); expect(existsSync(join(home, "ocx.pid"))).toBe(true); const injected = readFileSync(codexConfig, "utf8"); From 872bf64e4f674cc7fb9a3413620031856072464c Mon Sep 17 00:00:00 2001 From: bitkyc08-arch Date: Mon, 31 Aug 2026 18:43:06 +0900 Subject: [PATCH 3/3] test(shutdown): record whether the launcher exited when health never arrives MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The macOS CI failure printed "(nothing)" for the launcher output across the full 90s wait, and the next case in the same file came up in 820ms. That rules out a slow or loaded machine: the process never reached its startup banner. Also report whether the child had exited, which separates a crashed launcher from one still running but never binding. The likeliest remaining cause is the freePort() gap — it binds :0, reads the port, then closes, so another process on the runner can claim it before the proxy does. --- tests/shutdown-launcher.test.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/tests/shutdown-launcher.test.ts b/tests/shutdown-launcher.test.ts index 687d44436c..ca884b1490 100644 --- a/tests/shutdown-launcher.test.ts +++ b/tests/shutdown-launcher.test.ts @@ -120,7 +120,17 @@ describe.skipIf(!runnable)("ocx launcher graceful shutdown", () => { // what any assertion here is about — the teardown behaviour after the signal is. const up = await waitUntil(() => healthy(port), 90_000); if (!up) { - console.error(`launcher never became healthy on port ${port}; its output was:\n${launcherOutput || "(nothing)"}`); + // A launcher that produced NO output for the whole wait did not start slowly; it + // never got far enough to print its banner. The observed macOS case looks exactly + // like that, and the next case in the same file then came up in 820ms — so the + // machine was not busy. The remaining suspect is the port: freePort() binds :0, + // reads the number, then closes, so anything on the runner can take that port in + // the gap before the proxy binds it. + console.error( + `launcher never became healthy on port ${port} after ${Math.round(90_000 / 1000)}s` + + `; exited=${exited}` + + `; its output was:\n${launcherOutput || "(nothing)"}`, + ); } expect(up).toBe(true); expect(existsSync(join(home, "ocx.pid"))).toBe(true);