From 66cd2855d9fc7a7a6c45c4e2139d4894388f5c93 Mon Sep 17 00:00:00 2001 From: "github-actions[bot]" <41898282+github-actions[bot]@users.noreply.github.com> Date: Wed, 7 Oct 2026 02:23:21 +0000 Subject: [PATCH 01/11] fix(test): keep the default Bun transpiler cache inside its sandbox Preserve nested-child reuse and explicit overrides, but do not adopt the fixed shared temporary cache. Independent environments start cold. Linux/Bun 1.4.0: nine new regressions pass; cross-user access is denied. Broader run: 124 pass, 3 skips, 1 environment-dependent wrapper failure. Typecheck, privacy, structure and whitespace checks pass. Native cold-start coverage and independent review remain required; keep draft. Fork-only candidate; no merge, release or finding closure. --- scripts/test.ts | 16 ++-- structure/ops/cross-platform-ci.md | 13 ++- tests/ci-workflows/test-runner.test.ts | 125 +++++++++++++++++++++++++ 3 files changed, 142 insertions(+), 12 deletions(-) diff --git a/scripts/test.ts b/scripts/test.ts index 7adeeee74d5..2b87dcd16a9 100644 --- a/scripts/test.ts +++ b/scripts/test.ts @@ -51,6 +51,8 @@ export function createIsolatedTestEnvironment( const opencodexHome = join(root, ".opencodex"); const codexHome = join(root, ".codex"); const containedTemp = createContainedTestTemp(root); + const transpilerCache = baseEnv.BUN_RUNTIME_TRANSPILER_CACHE_PATH ?? join(root, "bun-transpiler-cache"); + if (baseEnv.BUN_RUNTIME_TRANSPILER_CACHE_PATH === undefined) mkdirSync(transpilerCache, { mode: 0o700 }); mkdirSync(opencodexHome, { recursive: true }); mkdirSync(codexHome, { recursive: true }); if (process.platform === "win32") { @@ -88,15 +90,11 @@ export function createIsolatedTestEnvironment( // whichever adapter collected the metadata. Naming the file keeps the sandbox // (git still writes nothing here) while leaving git's own trust decisions intact. GIT_CONFIG_GLOBAL: baseEnv.GIT_CONFIG_GLOBAL ?? join(homedir(), ".gitconfig"), - // Pin Bun's runtime transpiler cache for the same reason. Bun keeps it under the home - // directory (macOS: ~/Library/Caches/bun/@t@), so a sandboxed HOME/USERPROFILE handed every - // batch, and every fixture that gives its child its own HOME, an empty cache: the first child - // re-transpiled each large module (73 src files are over the 50 KB cache threshold). On a busy - // Windows shard that first child took 10-45 s where later ones took 2-5 s, failing whichever - // timed case happened to spawn it. The cache holds transpiled source only, so sharing it keeps - // the sandbox. An explicit value, including "" or "0" to disable it, is kept as given. - BUN_RUNTIME_TRANSPILER_CACHE_PATH: baseEnv.BUN_RUNTIME_TRANSPILER_CACHE_PATH - ?? join(hostTemp, "ocx-test-bun-transpiler-cache"), + // Cached JavaScript is executable input. Keep the default beneath the exclusively created + // private root; nested sandboxes and fixture children retain this path even when HOME moves. + // Independent runs start cold. A protected explicit cache can share across runs; "" and "0" + // still disable caching. Never adopt, repair or reclaim the old shared host-TEMP cache. + BUN_RUNTIME_TRANSPILER_CACHE_PATH: transpilerCache, HOME: root, USERPROFILE: root, OPENCODEX_HOME: opencodexHome, diff --git a/structure/ops/cross-platform-ci.md b/structure/ops/cross-platform-ci.md index 177dd0ec9c3..49f7dcc5165 100644 --- a/structure/ops/cross-platform-ci.md +++ b/structure/ops/cross-platform-ci.md @@ -72,9 +72,16 @@ release that requires Windows proof still dispatches it for the exact publish SH Test sandboxes keep the runner's `LOCALAPPDATA`, so Windows PowerShell 5.1 children reuse the image's warm module-analysis cache. Replacing it with a freshly built seed made every child re-analyze modules and timed out seven shards on the first run of #6670. -The sandbox also pins `BUN_RUNTIME_TRANSPILER_CACHE_PATH` to one shared directory: Bun keeps that cache -under the home directory, so a sandboxed home made the first child of every batch or fixture -re-transpile each large module, 10-45 s on a busy Windows shard against 2-5 s warm. +`scripts/test.ts` pins the default `BUN_RUNTIME_TRANSPILER_CACHE_PATH` beneath its exclusively +created test root, in a `bun-transpiler-cache` directory (mode 0700 on POSIX). Cached JavaScript is +executable input, so the runner never adopts, repairs, migrates or deletes the old fixed host-TEMP +cache. Nested sandboxes and fixture children inherit the owning environment's cache even when +HOME changes; only that owner's cleanup removes it. Separate environments and CI batches start +with separate caches. This gives up automatic cross-batch reuse: cold Windows startup coverage +must be evaluated without raising test deadlines or weakening assertions. An explicit override, +including "" or "0" to disable caching, is preserved and remains the operator's protected-path +responsibility. `tests/ci-workflows/test-runner.test.ts` covers isolation, legacy-path refusal, +cleanup ownership, overrides and actual Bun children with different homes. Startup ACL reads use .NET, never module-autoloaded `Get-Acl`/`Set-Acl` (`tests/ci-workflows/ci-review-lanes.test.ts` scans `src/`). Across the jobs, the workflow runs: diff --git a/tests/ci-workflows/test-runner.test.ts b/tests/ci-workflows/test-runner.test.ts index db18e231f3b..dee4331fd0d 100644 --- a/tests/ci-workflows/test-runner.test.ts +++ b/tests/ci-workflows/test-runner.test.ts @@ -2,6 +2,7 @@ import { beforeAll, describe, expect, spyOn, test } from "bun:test"; import { spawnSync } from "node:child_process"; import { existsSync, + lstatSync, mkdirSync, mkdtempSync, readFileSync, @@ -387,6 +388,130 @@ describe("test runner isolation", () => { ); }); +describe("test runner transpiler cache isolation", () => { + test("places the default executable cache inside its owned sandbox", () => { + const isolated = createIsolatedTestEnvironment({}); + try { + const cache = isolated.env.BUN_RUNTIME_TRANSPILER_CACHE_PATH!; + expect(cache).toBe(join(isolated.root, "bun-transpiler-cache")); + const entry = lstatSync(cache); + expect(entry.isDirectory()).toBe(true); + expect(entry.isSymbolicLink()).toBe(false); + if (process.platform !== "win32") { + expect(entry.uid).toBe(process.geteuid!()); + expect(entry.mode & 0o777).toBe(0o700); + expect(lstatSync(isolated.root).mode & 0o777).toBe(0o700); + } + } finally { isolated.cleanup(); } + expect(existsSync(isolated.root)).toBe(false); + }); + + test("independent sandboxes neither reuse nor remove another default cache", () => { + const first = createIsolatedTestEnvironment({}); + const second = createIsolatedTestEnvironment({}); + try { + const firstCache = first.env.BUN_RUNTIME_TRANSPILER_CACHE_PATH!; + const secondCache = second.env.BUN_RUNTIME_TRANSPILER_CACHE_PATH!; + expect(firstCache).not.toBe(secondCache); + writeFileSync(join(firstCache, "fixture"), "first"); + writeFileSync(join(secondCache, "fixture"), "second"); + first.cleanup(); + expect(existsSync(firstCache)).toBe(false); + expect(readFileSync(join(secondCache, "fixture"), "utf8")).toBe("second"); + } finally { first.cleanup(); second.cleanup(); } + }); + + test("nested home isolation inherits the parent's cache and leaves its lifetime to the owner", () => { + const parent = createIsolatedTestEnvironment({}); + const child = createIsolatedTestEnvironment(parent.env); + try { + const cache = parent.env.BUN_RUNTIME_TRANSPILER_CACHE_PATH!; + expect(child.root).not.toBe(parent.root); + expect(child.env.BUN_RUNTIME_TRANSPILER_CACHE_PATH).toBe(cache); + writeFileSync(join(cache, "fixture"), "parent-owned"); + child.cleanup(); + expect(readFileSync(join(cache, "fixture"), "utf8")).toBe("parent-owned"); + } finally { child.cleanup(); parent.cleanup(); } + }); + + test("keeps an explicit cache path without creating or reclaiming it", () => { + const fixture = mkdtempSync(join(tmpdir(), "ocx-cache-override-")); + const override = join(fixture, "operator-selected"); + const isolated = createIsolatedTestEnvironment({ BUN_RUNTIME_TRANSPILER_CACHE_PATH: override }); + try { + expect(isolated.env.BUN_RUNTIME_TRANSPILER_CACHE_PATH).toBe(override); + expect(existsSync(override)).toBe(false); + expect(existsSync(join(isolated.root, "bun-transpiler-cache"))).toBe(false); + mkdirSync(override); + writeFileSync(join(override, "fixture"), "operator-owned"); + isolated.cleanup(); + expect(readFileSync(join(override, "fixture"), "utf8")).toBe("operator-owned"); + } finally { isolated.cleanup(); removeTreeWithRetry(fixture); } + }); + + test.each(["", "0"])("preserves cache-disable value %j without allocating a default cache", value => { + const isolated = createIsolatedTestEnvironment({ BUN_RUNTIME_TRANSPILER_CACHE_PATH: value }); + try { + expect(isolated.env.BUN_RUNTIME_TRANSPILER_CACHE_PATH).toBe(value); + expect(existsSync(join(isolated.root, "bun-transpiler-cache"))).toBe(false); + } finally { isolated.cleanup(); } + }); + + test.each(["directory", "symlink"])("does not adopt or alter a pre-existing shared cache %s", kind => { + const fixture = mkdtempSync(join(tmpdir(), "ocx-cache-legacy-")); + const legacy = join(fixture, "ocx-test-bun-transpiler-cache"); + const target = join(fixture, "foreign-cache"); + mkdirSync(target); + writeFileSync(join(target, "fixture"), "leave-unchanged"); + if (kind === "symlink") symlinkSync(target, legacy, process.platform === "win32" ? "junction" : "dir"); + else { mkdirSync(legacy); writeFileSync(join(legacy, "fixture"), "leave-unchanged"); } + const before = lstatSync(legacy); + const oldEnv = { TMPDIR: process.env.TMPDIR, TMP: process.env.TMP, TEMP: process.env.TEMP }; + let isolated: ReturnType | undefined; + try { + process.env.TMPDIR = fixture; process.env.TMP = fixture; process.env.TEMP = fixture; + isolated = createIsolatedTestEnvironment({}); + expect(dirname(isolated.root)).toBe(fixture); + expect(isolated.env.BUN_RUNTIME_TRANSPILER_CACHE_PATH).toBe(join(isolated.root, "bun-transpiler-cache")); + isolated.cleanup(); + expect(readFileSync(join(legacy, "fixture"), "utf8")).toBe("leave-unchanged"); + expect(lstatSync(legacy).ino).toBe(before.ino); + expect(lstatSync(legacy).mode).toBe(before.mode); + expect(lstatSync(legacy).isSymbolicLink()).toBe(kind === "symlink"); + } finally { + isolated?.cleanup(); + for (const [name, value] of Object.entries(oldEnv)) { + if (value === undefined) delete process.env[name]; else process.env[name] = value; + } + removeTreeWithRetry(fixture); + } + }); + + test("Bun children with different homes reuse only the selected private cache", () => { + const isolated = createIsolatedTestEnvironment({ ...process.env, BUN_RUNTIME_TRANSPILER_CACHE_PATH: undefined }); + try { + const cache = isolated.env.BUN_RUNTIME_TRANSPILER_CACHE_PATH!; + const source = join(isolated.root, "cacheable-fixture.ts"); + // Above the pinned Bun runtime's cache threshold; no production code or credential is loaded. + writeFileSync(source, `const value: string = ${JSON.stringify("x".repeat(70_000))}; console.log(value.length);`); + for (const name of ["first-home", "second-home"]) { + const home = join(isolated.root, name); + mkdirSync(home); + const child = Bun.spawnSync([process.execPath, source], { + cwd: isolated.root, + env: { ...isolated.env, HOME: home, USERPROFILE: home }, + stdout: "pipe", stderr: "pipe", timeout: SPAWN_BUDGET_MS, + }); + expect(child.exitCode, new TextDecoder().decode(child.stderr)).toBe(0); + expect(new TextDecoder().decode(child.stdout).trim()).toBe("70000"); + expect(readdirSync(home)).toEqual([]); + } + expect(readdirSync(cache).some(name => name.endsWith(".pile"))).toBe(true); + expect(dirname(cache)).toBe(isolated.root); + } finally { isolated.cleanup(); } + }, { timeout: SPAWN_BUDGET_MS * 3 }); +}); + describe("Windows test TEMP recovery", () => { const age = (path: string, milliseconds: number) => { const date = new Date(milliseconds); From 11592cf57f11b2618a9ca266b5224edf376e18bf Mon Sep 17 00:00:00 2001 From: "devin-ai-integration[bot]" Date: Wed, 7 Oct 2026 02:36:22 +0000 Subject: [PATCH 02/11] test: prove the second Bun child hits the sealed private cache Co-Authored-By: Epinephrine --- tests/ci-workflows/test-runner.test.ts | 26 +++++++++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/tests/ci-workflows/test-runner.test.ts b/tests/ci-workflows/test-runner.test.ts index dee4331fd0d..fe6161f6daa 100644 --- a/tests/ci-workflows/test-runner.test.ts +++ b/tests/ci-workflows/test-runner.test.ts @@ -494,6 +494,15 @@ describe("test runner transpiler cache isolation", () => { const source = join(isolated.root, "cacheable-fixture.ts"); // Above the pinned Bun runtime's cache threshold; no production code or credential is loaded. writeFileSync(source, `const value: string = ${JSON.stringify("x".repeat(70_000))}; console.log(value.length);`); + // A hit only reads; a miss for the identical input must add or rewrite an entry. Seal the + // first home's entries at a sentinel mtime, then the second home proves reuse by adding + // nothing, rewriting nothing, and leaving no cache anywhere else in the sandbox. + const sealedAt = new Date("2001-01-01T00:00:00Z"); + const sandboxPiles = () => readdirSync(isolated.root, { recursive: true }) + .map(String) + .filter(entry => entry.endsWith(".pile")) + .sort(); + let sealed: string[] | undefined; for (const name of ["first-home", "second-home"]) { const home = join(isolated.root, name); mkdirSync(home); @@ -505,8 +514,23 @@ describe("test runner transpiler cache isolation", () => { expect(child.exitCode, new TextDecoder().decode(child.stderr)).toBe(0); expect(new TextDecoder().decode(child.stdout).trim()).toBe("70000"); expect(readdirSync(home)).toEqual([]); + const piles = sandboxPiles(); + if (sealed === undefined) { + expect(piles.length).toBeGreaterThan(0); + for (const entry of piles) { + const file = join(isolated.root, entry); + expect(pathIsContainedBy(cache, file, process.platform === "win32" ? "win32" : "posix")) + .toBe(true); + utimesSync(file, sealedAt, sealedAt); + } + sealed = piles; + } else { + expect(piles).toEqual(sealed); + for (const entry of sealed) { + expect(statSync(join(isolated.root, entry)).mtimeMs).toBe(sealedAt.getTime()); + } + } } - expect(readdirSync(cache).some(name => name.endsWith(".pile"))).toBe(true); expect(dirname(cache)).toBe(isolated.root); } finally { isolated.cleanup(); } }, { timeout: SPAWN_BUDGET_MS * 3 }); From 3f274dbcb385a25308e6d8801f2ee4dad4e8cb89 Mon Sep 17 00:00:00 2001 From: "devin-ai-integration[bot]" <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 8 Oct 2026 05:02:44 +0000 Subject: [PATCH 03/11] test(claude-desktop): attach response bodies to status assertions A 500 from these routes was previously reported as a bare "Expected: 200 / Received: 500", leaving the underlying reason unclassified (the windows 2/9 failure on 11592cf5 needed a manual diagnosis to identify ca_unavailable). Status assertions now carry the response body via an expectStatus helper, so a hosted-runner flake classifies itself in CI output. Co-Authored-By: Epinephrine --- .../claude-desktop-first-party.test.ts | 48 +++++++++++-------- 1 file changed, 27 insertions(+), 21 deletions(-) diff --git a/tests/claude-integration/claude-desktop-first-party.test.ts b/tests/claude-integration/claude-desktop-first-party.test.ts index e56ae92b87d..6427cd4536f 100644 --- a/tests/claude-integration/claude-desktop-first-party.test.ts +++ b/tests/claude-integration/claude-desktop-first-party.test.ts @@ -52,6 +52,12 @@ async function dispatch(path: string, init?: RequestInit, inputConfig: OcxConfig return { status: response!.status, body: await response!.json() as Record }; } +// Attach the response body to every status failure so a hosted-runner flake classifies +// itself (an environmental ca_unavailable, say, instead of a bare "Expected: 200 / Received: 500"). +function expectStatus(result: { status: number; body: unknown }, status: number): void { + expect(result.status, `body=${JSON.stringify(result.body)}`).toBe(status); +} + beforeEach(() => { root = mkdtempSync(join(tmpdir(), "ocx-desktop-1p-")); library = join(root, "desktop-library"); @@ -163,7 +169,7 @@ test("first-party apply refuses foreign proxy env and disabled intercept", () => test("POST /api/claude-desktop/apply defaults to gateway; first-party is explicit and carries the account-risk notice", async () => { const byDefault = await dispatch("/api/claude-desktop/apply", { method: "POST" }); - expect(byDefault.status).toBe(200); + expectStatus(byDefault, 200); expect(byDefault.body.riskWarning).toBeUndefined(); expect(existsSync(join(claudeDir, "settings.json"))).toBe(false); const afterDefault = JSON.parse(readFileSync(join(root, "config.json"), "utf8")) as OcxConfig; @@ -174,7 +180,7 @@ test("POST /api/claude-desktop/apply defaults to gateway; first-party is explici expect(gatewayStatus.body.riskWarning).toBeNull(); const first = await dispatch("/api/claude-desktop/apply", { method: "POST", body: JSON.stringify({ mode: "first-party" }) }, afterDefault); - expect(first.status).toBe(200); + expectStatus(first, 200); expect(first.body).toMatchObject({ ok: true, mode: "first-party", @@ -202,7 +208,7 @@ test("POST /api/claude-desktop/apply defaults to gateway; first-party is explici expect(status.body.health.ok).toBe(true); const gateway = await dispatch("/api/claude-desktop/apply", { method: "POST", body: JSON.stringify({ mode: "gateway" }) }, saved); - expect(gateway.status).toBe(200); + expectStatus(gateway, 200); expect(settings().env?.HTTPS_PROXY).toBeUndefined(); expect(settings().env?.NODE_EXTRA_CA_CERTS).toBeUndefined(); const afterGateway = JSON.parse(readFileSync(join(root, "config.json"), "utf8")) as OcxConfig; @@ -211,7 +217,7 @@ test("POST /api/claude-desktop/apply defaults to gateway; first-party is explici // Switching back replaces the gateway profile with the first-party env in one apply. const back = await dispatch("/api/claude-desktop/apply", { method: "POST", body: JSON.stringify({ mode: "first-party" }) }, afterGateway); - expect(back.status).toBe(200); + expectStatus(back, 200); expect(back.body).toMatchObject({ ok: true, mode: "first-party", applied: true, gatewayRemoved: true }); expect(settings().env?.HTTPS_PROXY).toBe(expectedProxyUrl(10200)); const afterBack = await dispatch("/api/claude-desktop/status", {}, afterGateway); @@ -230,7 +236,7 @@ test("POST /api/claude-desktop/apply defaults to gateway; first-party is explici test("native toggle: enable applies gateway by default and never writes first-party env", async () => { const enabled = await dispatch("/api/native-integrations/claude-desktop", { method: "PUT", body: JSON.stringify({ enabled: true }) }); - expect(enabled.status).toBe(200); + expectStatus(enabled, 200); expect(enabled.body).toMatchObject({ ok: true, state: "current", desiredEnabled: true }); expect(enabled.body.message).not.toContain("suspend"); expect(existsSync(join(claudeDir, "settings.json"))).toBe(false); @@ -242,7 +248,7 @@ test("native toggle: explicit first-party enable warns about the account risk an const chosen = config({ claudeCode: { desktopMode: "first-party" } }); writeFileSync(join(root, "config.json"), JSON.stringify(chosen)); const enabled = await dispatch("/api/native-integrations/claude-desktop", { method: "PUT", body: JSON.stringify({ enabled: true }) }, chosen); - expect(enabled.status).toBe(200); + expectStatus(enabled, 200); expect(enabled.body).toMatchObject({ ok: true, changed: true, state: "current", desiredEnabled: true }); expect(enabled.body.message).toContain("suspend the account"); expect(settings().env?.HTTPS_PROXY).toBe(expectedProxyUrl(10200)); @@ -252,7 +258,7 @@ test("native toggle: explicit first-party enable warns about the account risk an expect(desktop?.state).toBe("current"); const disabled = await dispatch("/api/native-integrations/claude-desktop", { method: "PUT", body: JSON.stringify({ enabled: false }) }, chosen); - expect(disabled.status).toBe(200); + expectStatus(disabled, 200); expect(disabled.body).toMatchObject({ ok: true, changed: true, state: "absent", desiredEnabled: false }); expect(settings().env?.HTTPS_PROXY).toBeUndefined(); }); @@ -261,7 +267,7 @@ test("native first-party ON pins the committed mode on a stale live config", asy writeFileSync(join(root, "config.json"), JSON.stringify(config({ claudeCode: { desktopMode: "first-party" } }))); const live = config({ claudeCode: { desktopMode: "gateway" } }); const enabled = await dispatch("/api/native-integrations/claude-desktop", { method: "PUT", body: JSON.stringify({ enabled: true }) }, live); - expect(enabled.status).toBe(200); + expectStatus(enabled, 200); expect(enabled.body).toMatchObject({ ok: true, state: "current", desiredEnabled: true }); expect(live.claudeCode?.desktopMode).toBe("first-party"); expect(firstPartyDesired(live).desktop).toBe(true); @@ -269,7 +275,7 @@ test("native first-party ON pins the committed mode on a stale live config", asy test("native toggle: enabling into explicit first-party pivots an applied gateway profile and saves the mode marker", async () => { const gateway = await dispatch("/api/claude-desktop/apply", { method: "POST", body: JSON.stringify({ mode: "gateway" }) }); - expect(gateway.status).toBe(200); + expectStatus(gateway, 200); const afterGateway = JSON.parse(readFileSync(join(root, "config.json"), "utf8")) as OcxConfig; expect(afterGateway.claudeCode?.desktopProfile?.appliedFingerprint).toBeTruthy(); // The operator chose first-party in config while the gateway profile is still on disk. @@ -277,7 +283,7 @@ test("native toggle: enabling into explicit first-party pivots an applied gatewa writeFileSync(join(root, "config.json"), JSON.stringify(chosen)); const enabled = await dispatch("/api/native-integrations/claude-desktop", { method: "PUT", body: JSON.stringify({ enabled: true }) }, chosen); - expect(enabled.status).toBe(200); + expectStatus(enabled, 200); expect(enabled.body).toMatchObject({ ok: true, changed: true, state: "current", desiredEnabled: true }); expect(settings().env?.HTTPS_PROXY).toBe(expectedProxyUrl(10200)); const saved = JSON.parse(readFileSync(join(root, "config.json"), "utf8")) as OcxConfig; @@ -294,7 +300,7 @@ test("native toggle: enabling into gateway saves the gateway mode marker like th const chosen = config({ claudeCode: { intercept: { enabled: false } } }); writeFileSync(join(root, "config.json"), JSON.stringify(chosen)); const enabled = await dispatch("/api/native-integrations/claude-desktop", { method: "PUT", body: JSON.stringify({ enabled: true }) }, chosen); - expect(enabled.status).toBe(200); + expectStatus(enabled, 200); expect(enabled.body).toMatchObject({ ok: true, state: "current", message: "Claude Desktop integration enabled." }); expect(existsSync(join(claudeDir, "settings.json"))).toBe(false); const saved = JSON.parse(readFileSync(join(root, "config.json"), "utf8")) as OcxConfig; @@ -327,7 +333,7 @@ test("first-party apply rebases the Claude hand-edit guard after its scoped mode armClaudeCodeBaseline(snapshot); const applied = await dispatch("/api/claude-desktop/apply", { method: "POST", body: JSON.stringify({ mode: "first-party" }) }, snapshot); - expect(applied.status).toBe(200); + expectStatus(applied, 200); expect(applied.body).toMatchObject({ mode: "first-party", saved: true }); const handEdited = JSON.parse(readFileSync(join(root, "config.json"), "utf8")) as OcxConfig; @@ -505,7 +511,7 @@ for (const surface of ["cli", "api"] as const) { for (const failure of ["intercept_disabled", "foreign_env", "unreadable", "ca_unavailable"] as const) { test(`${surface} failed first-party ${failure} leaves the gateway profile active`, async () => { const gateway = await dispatch("/api/claude-desktop/apply", { method: "POST", body: JSON.stringify({ mode: "gateway" }) }); - expect(gateway.status).toBe(200); + expectStatus(gateway, 200); const saved = JSON.parse(readFileSync(join(root, "config.json"), "utf8")) as OcxConfig; const appliedFingerprint = saved.claudeCode!.desktopProfile!.appliedFingerprint!; const before = inspectDesktop3pConfigLibrary({ appliedFingerprint }); @@ -543,7 +549,7 @@ test("CLI gateway apply refusal leaves the first-party connection intact", async }); test("a refused gateway cleanup restores the previous settings env", async () => { - expect((await dispatch("/api/claude-desktop/apply", { method: "POST", body: JSON.stringify({ mode: "gateway" }) })).status).toBe(200); + expectStatus(await dispatch("/api/claude-desktop/apply", { method: "POST", body: JSON.stringify({ mode: "gateway" }) }), 200); const saved = JSON.parse(readFileSync(join(root, "config.json"), "utf8")) as OcxConfig; mkdirSync(claudeDir, { recursive: true }); const before = { theme: "dark", env: { FOO: "keep" } }; @@ -551,14 +557,14 @@ test("a refused gateway cleanup restores the previous settings env", async () => const result = await dispatch("/api/claude-desktop/apply", { method: "POST", body: JSON.stringify({ mode: "first-party" }) }, saved, { removeDesktop3pStandardPivot: () => ({ ok: false, changed: false, kind: "unsafe", libraryPath: library, reason: "metadata_unreadable" }), }); - expect(result.status).toBe(409); + expectStatus(result, 409); expect(result.body.code).toBe("claude_desktop_gateway_removal_failed"); expect(settings()).toEqual(before); expect(inspectDesktop3pConfigLibrary({ appliedFingerprint: saved.claudeCode!.desktopProfile!.appliedFingerprint }).kind).toBe("gateway_ours"); }); test("a completed standard pivot keeps first-party active when credential cleanup is incomplete", async () => { - expect((await dispatch("/api/claude-desktop/apply", { method: "POST", body: JSON.stringify({ mode: "gateway" }) })).status).toBe(200); + expectStatus(await dispatch("/api/claude-desktop/apply", { method: "POST", body: JSON.stringify({ mode: "gateway" }) }), 200); const saved = JSON.parse(readFileSync(join(root, "config.json"), "utf8")) as OcxConfig; const result = await dispatch("/api/claude-desktop/apply", { method: "POST", body: JSON.stringify({ mode: "first-party" }) }, saved, { removeDesktop3pStandardPivot: options => { @@ -567,7 +573,7 @@ test("a completed standard pivot keeps first-party active when credential cleanu return { ok: false, changed: true, kind: "cleanup_incomplete", libraryPath: library, residualPaths: ["fixture-residue"] }; }, }); - expect(result.status).toBe(500); + expectStatus(result, 500); expect(result.body.reason).toBe("cleanup_incomplete"); expect(inspectDesktopFirstParty(config()).applied).toBe(true); const after = JSON.parse(readFileSync(join(root, "config.json"), "utf8")) as OcxConfig; @@ -593,7 +599,7 @@ for (const surface of ["api", "native", "cli"] as const) { const result = surface === "api" ? await dispatch("/api/claude-desktop/apply", { method: "POST", body: JSON.stringify({ mode: "gateway" }) }, initial) : await dispatch("/api/native-integrations/claude-desktop", { method: "PUT", body: JSON.stringify({ enabled: true }) }, initial); - expect(result.status).toBe(500); + expectStatus(result, 500); if (surface === "api") expect(result.body).toMatchObject({ applied: true, saved: true, mode: "gateway" }); else expect(result.body.message).toContain("Gateway applied"); } @@ -641,9 +647,9 @@ test("a deleted token flips an applied env to stale and inspection does not recr test("the status routes never mint the proxy token", async () => { const status = await dispatch("/api/claude-desktop/status"); - expect(status.status).toBe(200); + expectStatus(status, 200); const list = await dispatch("/api/native-integrations"); - expect(list.status).toBe(200); + expectStatus(list, 200); expect(existsSync(claudeInterceptProxyTokenPath(root))).toBe(false); }); @@ -652,7 +658,7 @@ test("first-party apply and native enable refuse a changed configured port befor writeFileSync(join(root, "config.json"), JSON.stringify(live)); for (const [path, body] of [["/api/claude-desktop/apply", { mode: "first-party" }], ["/api/native-integrations/claude-desktop", { enabled: true }]] as const) { const result = await dispatch(path, { method: path.endsWith("apply") ? "POST" : "PUT", body: JSON.stringify(body) }, live); - expect(result.status).toBe(409); + expectStatus(result, 409); expect(result.body.code).toBe("port_mismatch"); expect(result.body.bound).toBe(10200); expect(result.body.configured).toBe(10300); From eadccf7abd7a76353f75e55a753faf19ae7ffb7e Mon Sep 17 00:00:00 2001 From: "devin-ai-integration[bot]" <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 8 Oct 2026 05:09:38 +0000 Subject: [PATCH 04/11] test: retry transient Windows removal codes in process-spawning fixtures The windows 1/9 rerun failed on EBUSY in claude-picker-startup's afterEach: a still-exiting spawned child held the test root while bare rmSync had no tolerance. Eleven fixtures that spawn real children but removed roots with raw rmSync now use the shared removeTreeWithRetry helper (EPERM/EBUSY/ENOTEMPTY retry, plus the protected-tree refusal every caller shares). File-level rmSync calls inside those files are unchanged. Co-Authored-By: Epinephrine --- tests/claude-integration/claude-cli-picker.test.ts | 5 +++-- tests/claude-integration/claude-cli.test.ts | 7 ++++--- .../claude-intercept-on-demand.test.ts | 5 +++-- tests/claude-integration/claude-picker-ca-store.test.ts | 3 ++- tests/claude-integration/claude-picker-recovery.test.ts | 9 +++++---- tests/claude-integration/claude-picker-startup.test.ts | 5 +++-- tests/server/link-management-routes.test.ts | 5 +++-- tests/server/local-account-switch-ingress.test.ts | 5 +++-- tests/server/plaintext-v2-agent-messages-server.test.ts | 5 +++-- tests/server/restart-replacement.test.ts | 5 +++-- tests/server/startup-health-packaged-probe.test.ts | 5 +++-- tests/server/system-restart.test.ts | 5 +++-- 12 files changed, 38 insertions(+), 26 deletions(-) diff --git a/tests/claude-integration/claude-cli-picker.test.ts b/tests/claude-integration/claude-cli-picker.test.ts index 68e6cdc1884..16f579050ed 100644 --- a/tests/claude-integration/claude-cli-picker.test.ts +++ b/tests/claude-integration/claude-cli-picker.test.ts @@ -1,6 +1,7 @@ // INV-CLIPICKER-01: cc catalog rows only for CLI-classified, CLI-first-party requests; registry-decodable aliases only; fail-open. import { afterAll, expect, test } from "bun:test"; -import { existsSync, mkdirSync, mkdtempSync, readdirSync, rmSync, writeFileSync } from "node:fs"; +import { removeTreeWithRetry } from "../helpers/remove-tree"; +import { existsSync, mkdirSync, mkdtempSync, readdirSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { activeDesktop3pAlias, buildDesktop3pRegistry, resolveDesktop3pAlias } from "../../src/claude/desktop-3p"; @@ -31,7 +32,7 @@ afterAll(async () => { function tempDir(prefix: string): string { const dir = mkdtempSync(join(tmpdir(), prefix)); - cleanups.push(() => rmSync(dir, { recursive: true, force: true })); + cleanups.push(() => removeTreeWithRetry(dir)); return dir; } diff --git a/tests/claude-integration/claude-cli.test.ts b/tests/claude-integration/claude-cli.test.ts index 78a4cb24973..963258f4fcc 100644 --- a/tests/claude-integration/claude-cli.test.ts +++ b/tests/claude-integration/claude-cli.test.ts @@ -1,4 +1,5 @@ import { describe, expect, test } from "bun:test"; +import { removeTreeWithRetry } from "../helpers/remove-tree"; import { buildClaudeEnv as buildClaudeEnvWithIo, buildNativeClaudeEnv, @@ -16,7 +17,7 @@ import { } from "../../src/cli/claude"; import { buildClaudeContextWindows } from "../../src/claude/context-windows"; import { commandInvocation } from "../../src/lib/win-exec"; -import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { mkdtempSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import type { LivenessIo, LiveProxy } from "../../src/server/proxy-liveness"; @@ -231,7 +232,7 @@ describe("ocx claude native fallback", () => { expect(windows["ocx-claude-native--gpt-5.6-sol"]).toBe(272_000); expect(windows["claude-ocx-native--gpt-5.6-sol"]).toBe(272_000); } finally { - rmSync(dir, { recursive: true, force: true }); + removeTreeWithRetry(dir); } }); @@ -269,7 +270,7 @@ describe("ocx claude native fallback", () => { expect(warnings).toHaveLength(1); } finally { console.warn = realWarn; - rmSync(dir, { recursive: true, force: true }); + removeTreeWithRetry(dir); } }); }); diff --git a/tests/claude-integration/claude-intercept-on-demand.test.ts b/tests/claude-integration/claude-intercept-on-demand.test.ts index ab893307341..1566f804d7a 100644 --- a/tests/claude-integration/claude-intercept-on-demand.test.ts +++ b/tests/claude-integration/claude-intercept-on-demand.test.ts @@ -1,5 +1,6 @@ import { afterEach, beforeEach, expect, test } from "bun:test"; -import { mkdtempSync, rmSync } from "node:fs"; +import { removeTreeWithRetry } from "../helpers/remove-tree"; +import { mkdtempSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { createClaudeInterceptLifecycle } from "../../src/server/index/claude-intercept-lifecycle"; @@ -28,7 +29,7 @@ afterEach(async () => { if (previousHome === undefined) delete process.env.OPENCODEX_HOME; else process.env.OPENCODEX_HOME = previousHome; if (previousClaude === undefined) delete process.env.CLAUDE_CONFIG_DIR; else process.env.CLAUDE_CONFIG_DIR = previousClaude; if (previousDesktop === undefined) delete process.env.OPENCODEX_CLAUDE_DESKTOP_CONFIG_DIR; else process.env.OPENCODEX_CLAUDE_DESKTOP_CONFIG_DIR = previousDesktop; - rmSync(root, { recursive: true, force: true }); + removeTreeWithRetry(root); }); function config(): OcxConfig { return { port: 10100, providers: { mock: { adapter: "openai-chat", baseUrl: "https://example.test/v1", models: ["test"], liveModels: false } }, defaultProvider: "mock", claudeCode: { enabled: false } } as OcxConfig; diff --git a/tests/claude-integration/claude-picker-ca-store.test.ts b/tests/claude-integration/claude-picker-ca-store.test.ts index 6a12ab894ab..0d113b19ff6 100644 --- a/tests/claude-integration/claude-picker-ca-store.test.ts +++ b/tests/claude-integration/claude-picker-ca-store.test.ts @@ -9,10 +9,11 @@ import { join } from "node:path"; import { pathToFileURL } from "node:url"; import { discardPickerCaKey, ensurePickerCa, pickerCaCertPath, pickerCaFingerprints, readPendingPickerCaUntrust } from "../../src/claude/intercept/picker-ca"; import { memoryPickerCaStore } from "../helpers/picker-ca-store"; +import { removeTreeWithRetry } from "../helpers/remove-tree"; const roots: string[] = []; const root = () => { const value = mkdtempSync(join(tmpdir(), "ocx-picker-store-")); roots.push(value); return value; }; -afterEach(() => { for (const dir of roots.splice(0)) rmSync(dir, { recursive: true, force: true }); }); +afterEach(() => { for (const dir of roots.splice(0)) removeTreeWithRetry(dir); }); /** Keep non-link assertions active; only a native Windows file-link privilege gap is unavailable. */ function fileSymlink(target: string, path: string): boolean { try { symlinkSync(target, path, "file"); return true; } diff --git a/tests/claude-integration/claude-picker-recovery.test.ts b/tests/claude-integration/claude-picker-recovery.test.ts index 91d6c90333b..951ab1e9067 100644 --- a/tests/claude-integration/claude-picker-recovery.test.ts +++ b/tests/claude-integration/claude-picker-recovery.test.ts @@ -1,6 +1,7 @@ // INV-PICKER-02: the outgoing public picker CA survives process replacement until a confirmed untrust clears it. import { expect, test } from "bun:test"; -import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { removeTreeWithRetry } from "../helpers/remove-tree"; +import { mkdirSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { pathToFileURL } from "node:url"; @@ -100,7 +101,7 @@ test("a replacement process retries the recorded predecessor before rotating and expect(readPendingPickerCaUntrust(root)).toBeNull(); } finally { occupied.stop(true); - rmSync(root, { recursive: true, force: true }); + removeTreeWithRetry(root); } }); @@ -128,7 +129,7 @@ test("controller enable with pending cleanup does not request trust", async () = expect(calls).toEqual([]); expect(readPendingPickerCaUntrust(root)).not.toBeNull(); } finally { - rmSync(root, { recursive: true, force: true }); + removeTreeWithRetry(root); } }); @@ -145,6 +146,6 @@ test("replacement defers a pending certificate still published by a live owner", expect(readPendingPickerCaUntrust(root)).toEqual(pending); expect(pickerCaFingerprints(readFileSync(pickerCaCertPath(root), "utf8")).sha1).toBe(pending.sha1); } finally { - rmSync(root, { recursive: true, force: true }); + removeTreeWithRetry(root); } }); diff --git a/tests/claude-integration/claude-picker-startup.test.ts b/tests/claude-integration/claude-picker-startup.test.ts index 9603173247e..2c34eb1e64a 100644 --- a/tests/claude-integration/claude-picker-startup.test.ts +++ b/tests/claude-integration/claude-picker-startup.test.ts @@ -1,5 +1,5 @@ import { afterEach, expect, spyOn, test } from "bun:test"; -import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs"; +import { existsSync, mkdirSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { pathToFileURL } from "node:url"; import { join } from "node:path"; @@ -11,6 +11,7 @@ import { createCertificateAuthority } from "../../src/claude/intercept/local-ca" import { PICKER_CA_COMMON_NAME, PICKER_HOST } from "../../src/claude/intercept/picker-ca"; import { pickerCaCertPath, pickerCaFingerprints } from "../../src/claude/intercept/picker-ca"; import { watchdogMs } from "../helpers/ci-watchdog"; +import { removeTreeWithRetry } from "../helpers/remove-tree"; import { memoryPickerCaStore } from "../helpers/picker-ca-store"; import { saveConfig } from "../../src/config"; import type { OcxConfig } from "../../src/types"; @@ -21,7 +22,7 @@ const priorHome = process.env.OPENCODEX_HOME; const priorDesktop = process.env.OPENCODEX_CLAUDE_DESKTOP_CONFIG_DIR; afterEach(async () => { for (const handle of handles.splice(0)) await handle?.stop(); - for (const dir of roots.splice(0)) rmSync(dir, { recursive: true, force: true }); + for (const dir of roots.splice(0)) removeTreeWithRetry(dir); if (priorHome === undefined) delete process.env.OPENCODEX_HOME; else process.env.OPENCODEX_HOME = priorHome; if (priorDesktop === undefined) delete process.env.OPENCODEX_CLAUDE_DESKTOP_CONFIG_DIR; else process.env.OPENCODEX_CLAUDE_DESKTOP_CONFIG_DIR = priorDesktop; }); diff --git a/tests/server/link-management-routes.test.ts b/tests/server/link-management-routes.test.ts index 63f4b721c02..512f1ed2771 100644 --- a/tests/server/link-management-routes.test.ts +++ b/tests/server/link-management-routes.test.ts @@ -1,5 +1,6 @@ import { afterEach, describe, expect, test } from "bun:test"; -import { existsSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; +import { removeTreeWithRetry } from "../helpers/remove-tree"; +import { existsSync, mkdtempSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { handleManagementAPI } from "../../src/server/management-api"; @@ -145,7 +146,7 @@ function versionRunner(remote: { probeCode: number; probeStderr: string; code: n } afterEach(() => { - if (temp) rmSync(temp, { recursive: true, force: true }); + if (temp) removeTreeWithRetry(temp); temp = ""; }); diff --git a/tests/server/local-account-switch-ingress.test.ts b/tests/server/local-account-switch-ingress.test.ts index b82c37e8da1..0f15fb0fec3 100644 --- a/tests/server/local-account-switch-ingress.test.ts +++ b/tests/server/local-account-switch-ingress.test.ts @@ -1,7 +1,8 @@ // Moved out of server-management-auth.test.ts, which sits at the 2000-line ratchet threshold. // Proves the account-switch capability survives the real server ingress and reaches the handler. import { afterEach, beforeEach, expect, test } from "bun:test"; -import { mkdtempSync, rmSync } from "node:fs"; +import { removeTreeWithRetry } from "../helpers/remove-tree"; +import { mkdtempSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { flushNativeMainStartupReleases } from "../../src/codex/native-profile-startup"; @@ -53,7 +54,7 @@ afterEach(async () => { restore("OPENCODEX_HOME", previous.home); restore("OPENCODEX_API_AUTH_TOKEN", previous.dataToken); restore("OPENCODEX_ADMIN_AUTH_TOKEN", previous.adminToken); - if (testHome) rmSync(testHome, { recursive: true, force: true, maxRetries: 5, retryDelay: 50 }); + if (testHome) removeTreeWithRetry(testHome); testHome = ""; }); diff --git a/tests/server/plaintext-v2-agent-messages-server.test.ts b/tests/server/plaintext-v2-agent-messages-server.test.ts index 56125cbd192..af663eeae6a 100644 --- a/tests/server/plaintext-v2-agent-messages-server.test.ts +++ b/tests/server/plaintext-v2-agent-messages-server.test.ts @@ -1,6 +1,6 @@ import { warnPlaintextV2AgentMessagesStartup } from "../../src/server"; import { afterEach, beforeEach, describe, expect, test } from "bun:test"; -import { mkdtempSync, rmSync } from "node:fs"; +import { mkdtempSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { saveCodexAccountCredential } from "../../src/codex/account-store"; @@ -16,6 +16,7 @@ import { clearResponseStateForTests, expandPreviousResponseInput } from "../../s import { handleResponses } from "../../src/server/responses"; import type { OcxConfig } from "../../src/types"; import { acquireOwnedSpendHome } from "../helpers/owned-spend-home"; +import { removeTreeWithRetry } from "../helpers/remove-tree"; const originalFetch = globalThis.fetch; let releaseInheritedSpendHome: (() => void) | undefined; @@ -122,7 +123,7 @@ async function withPoolHome(run: () => Promise): Promise { clearCodexUpstreamHealth(); clearThreadAccountMap(); clearAccountQuota(); - rmSync(home, { recursive: true, force: true }); + removeTreeWithRetry(home); if (previousOpencodexHome === undefined) delete process.env.OPENCODEX_HOME; else process.env.OPENCODEX_HOME = previousOpencodexHome; if (previousCodexHome === undefined) delete process.env.CODEX_HOME; diff --git a/tests/server/restart-replacement.test.ts b/tests/server/restart-replacement.test.ts index 9fca5e4e52d..578346de90b 100644 --- a/tests/server/restart-replacement.test.ts +++ b/tests/server/restart-replacement.test.ts @@ -9,8 +9,9 @@ * The scripted children print fixed lines, so what a real `ocx start` prints is not covered here. */ import { afterEach, describe, expect, test } from "bun:test"; +import { removeTreeWithRetry } from "../helpers/remove-tree"; import { spawn, type ChildProcess } from "node:child_process"; -import { closeSync, constants, lstatSync, mkdtempSync, openSync, readFileSync, rmSync, statSync, symlinkSync, writeFileSync, writeSync } from "node:fs"; +import { closeSync, constants, lstatSync, mkdtempSync, openSync, readFileSync, statSync, symlinkSync, writeFileSync, writeSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { @@ -33,7 +34,7 @@ afterEach(() => { for (const child of children.splice(0)) { try { child.kill("SIGKILL"); } catch { /* already gone */ } } - for (const dir of dirs.splice(0)) rmSync(dir, { recursive: true, force: true }); + for (const dir of dirs.splice(0)) removeTreeWithRetry(dir); }); function tempDir(): string { diff --git a/tests/server/startup-health-packaged-probe.test.ts b/tests/server/startup-health-packaged-probe.test.ts index c3df5f4883f..a8b5e19d3a4 100644 --- a/tests/server/startup-health-packaged-probe.test.ts +++ b/tests/server/startup-health-packaged-probe.test.ts @@ -1,8 +1,9 @@ import { expect, test } from "bun:test"; -import { copyFileSync, mkdirSync, mkdtempSync, rmSync } from "node:fs"; +import { copyFileSync, mkdirSync, mkdtempSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { helperPath, repoRoot } from "../helpers/repo-root"; +import { removeTreeWithRetry } from "../helpers/remove-tree"; // Exercise the default cache reader's real subprocess from a compiled executable. // A unit test of selfLaunchArgv alone misses a caller that still passes $bunfs source. @@ -36,6 +37,6 @@ test("packaged startup probe is fresh before and after replacing its bundled exe expect(health).toMatchObject({ status: "native", diagnosticStale: false, rebootSafe: true }); } } finally { - rmSync(scratch, { recursive: true, force: true }); + removeTreeWithRetry(scratch); } }, 120_000); diff --git a/tests/server/system-restart.test.ts b/tests/server/system-restart.test.ts index 1a7c67ff046..4de18fae2e5 100644 --- a/tests/server/system-restart.test.ts +++ b/tests/server/system-restart.test.ts @@ -2,7 +2,7 @@ * #563 — memory-card drain-and-restart acceptance + respawn policy. */ import { afterEach, describe, expect, test } from "bun:test"; -import { mkdtempSync, rmSync } from "node:fs"; +import { mkdtempSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { consumeSiblingHandoff } from "../../src/codex/sibling-handoff"; @@ -62,7 +62,7 @@ test("a sibling replacement environment carries a one-use handoff before restart removeRuntimePort(process.pid); if (previousHome === undefined) delete process.env.OPENCODEX_HOME; else process.env.OPENCODEX_HOME = previousHome; - rmSync(home, { recursive: true, force: true }); + removeTreeWithRetry(home); } }); @@ -972,3 +972,4 @@ describe("POST /api/system/restart", () => { }); }); import { ManagementRequest as Request } from "../helpers/management-auth"; +import { removeTreeWithRetry } from "../helpers/remove-tree"; From c94eaff4522a47110b8137e4879bccbad512010a Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 8 Oct 2026 05:28:41 +0000 Subject: [PATCH 05/11] test: warm the readiness eval graph before its timed children The private per-sandbox transpiler cache means the first --eval child in a batch pays its cold module-graph load inside its own 3000ms spawnSync bound; windows 3/9 killed it with ETIMEDOUT. Register the file's union eval script with warmModuleGraph so the cold load happens in setup, and switch the file:// hrefs to absolute paths so the warm-up's import scan can see them. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../codex-shim-path-readiness.test.ts | 49 ++++++++++++------- 1 file changed, 30 insertions(+), 19 deletions(-) diff --git a/tests/codex-integration/codex-shim-path-readiness.test.ts b/tests/codex-integration/codex-shim-path-readiness.test.ts index 4f1b87b198e..d9c4a5c0623 100644 --- a/tests/codex-integration/codex-shim-path-readiness.test.ts +++ b/tests/codex-integration/codex-shim-path-readiness.test.ts @@ -1,10 +1,12 @@ -import { afterEach, describe, expect, mock, spyOn, test } from "bun:test"; +import { afterEach, beforeAll, describe, expect, mock, spyOn, test } from "bun:test"; import { spawnSync } from "node:child_process"; import * as fs from "node:fs"; import { tmpdir } from "node:os"; import { delimiter, join } from "node:path"; +import { fileURLToPath } from "node:url"; import { findFirstCodexOnPath } from "../../src/codex/shim-path-resolution"; import { buildUnixCodexShim, buildWindowsCodexShim, buildWindowsPowerShellCodexShim } from "../../src/codex/shim-templates"; +import { COLD_SPAWN_WARMUP_HOOK_BUDGET_MS, warmModuleGraph } from "../helpers/cold-spawn-warmup"; import { removeTreeWithRetry } from "../helpers/remove-tree"; const roots: string[] = []; @@ -28,7 +30,29 @@ function fixture() { return { root, first, later, command, fallback, pathValue }; } +// Absolute filesystem paths, not file:// hrefs: the warm-up's import scan keeps specifiers +// that are absolute paths, and the spawned --eval children resolve either form identically. +const shimModule = fileURLToPath(new URL("../../src/codex/shim.ts", import.meta.url)); +const scannerModule = fileURLToPath(new URL("../../src/codex/shim-path-resolution.ts", import.meta.url)); +const readinessModule = fileURLToPath(new URL("../../src/cli/codex-shim-readiness.ts", import.meta.url)); + +// The union of the repository module graphs this file's --eval children load; the other eval +// scripts below import a subset of the same three specifiers. +const READINESS_EVAL_SCRIPT = ` + const { diagnoseCodexShim } = await import(${JSON.stringify(shimModule)}); + const { findFirstCodexOnPath } = await import(${JSON.stringify(scannerModule)}); + const { inspectCodexShimForConnect } = await import(${JSON.stringify(readinessModule)}); + console.log(JSON.stringify({ diagnosis: diagnoseCodexShim(), command: findFirstCodexOnPath(), readiness: inspectCodexShimForConnect() })); +`; + describe("connect readiness PATH inspection", () => { + // The file's --eval children load the readiness graph under the run's private transpiler + // cache, so the first one's cold module load lands inside its own 3000ms bound. Pay it + // once in setup instead; see tests/helpers/cold-spawn-warmup.ts. + beforeAll(async () => { + await warmModuleGraph({ graph: "codex-shim-path-readiness/eval", source: READINESS_EVAL_SCRIPT }); + }, COLD_SPAWN_WARMUP_HOOK_BUDGET_MS); + test.skipIf(process.platform === "win32").each([ { kind: "ordinary file", shim: false, symlink: false, executable: false }, { kind: "shim file", shim: true, symlink: false, executable: false }, @@ -50,11 +74,9 @@ describe("connect readiness PATH inspection", () => { const expectedPath = executable ? f.command : f.fallback; const expectedShim = executable ? shim : !shim; expect(shell.stdout.trim()).toBe(expectedPath); - const scanner = new URL("../../src/codex/shim-path-resolution.ts", import.meta.url).href; - const readiness = new URL("../../src/cli/codex-shim-readiness.ts", import.meta.url).href; const script = ` - const { findFirstCodexOnPath } = await import(${JSON.stringify(scanner)}); - const { inspectCodexShimForConnect } = await import(${JSON.stringify(readiness)}); + const { findFirstCodexOnPath } = await import(${JSON.stringify(scannerModule)}); + const { inspectCodexShimForConnect } = await import(${JSON.stringify(readinessModule)}); console.log(JSON.stringify({ candidate: findFirstCodexOnPath({ wsl: false }), result: inspectCodexShimForConnect({ @@ -116,16 +138,7 @@ describe("connect readiness PATH inspection", () => { expect(shell.error).toBeUndefined(); expect(shell.status).not.toBe(0); } - const shim = new URL("../../src/codex/shim.ts", import.meta.url).href; - const scanner = new URL("../../src/codex/shim-path-resolution.ts", import.meta.url).href; - const readiness = new URL("../../src/cli/codex-shim-readiness.ts", import.meta.url).href; - const script = ` - const { diagnoseCodexShim } = await import(${JSON.stringify(shim)}); - const { findFirstCodexOnPath } = await import(${JSON.stringify(scanner)}); - const { inspectCodexShimForConnect } = await import(${JSON.stringify(readiness)}); - console.log(JSON.stringify({ diagnosis: diagnoseCodexShim(), command: findFirstCodexOnPath(), readiness: inspectCodexShimForConnect() })); - `; - const child = spawnSync(process.execPath, ["--eval", script], { env, encoding: "utf8", timeout: 3000, killSignal: "SIGKILL" }); + const child = spawnSync(process.execPath, ["--eval", READINESS_EVAL_SCRIPT], { env, encoding: "utf8", timeout: 3000, killSignal: "SIGKILL" }); expect(child.error).toBeUndefined(); expect(child.status).toBe(0); const output = JSON.parse(child.stdout); @@ -143,13 +156,11 @@ describe("connect readiness PATH inspection", () => { if (mode === "fifo") fs.renameSync(fifo, f.command); else if (mode === "symlink") fs.symlinkSync(fifo, f.command); else fs.writeFileSync(f.command, "ordinary launcher", { mode: 0o755 }); - const scanner = new URL("../../src/codex/shim-path-resolution.ts", import.meta.url).href; - const readiness = new URL("../../src/cli/codex-shim-readiness.ts", import.meta.url).href; const script = ` import * as fs from "node:fs"; import { spyOn } from "bun:test"; - const { findFirstCodexOnPath } = await import(${JSON.stringify(scanner)}); - const { inspectCodexShimForConnect } = await import(${JSON.stringify(readiness)}); + const { findFirstCodexOnPath } = await import(${JSON.stringify(scannerModule)}); + const { inspectCodexShimForConnect } = await import(${JSON.stringify(readinessModule)}); const command = ${JSON.stringify(f.command)}; let replaced = false, rejectedDescriptor = false, closedRejectedDescriptor = false; if (${JSON.stringify(mode)} === "replacement") { From 79ea0f37f486f1a65897b8811aa67107cfa2b55a Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 8 Oct 2026 05:31:48 +0000 Subject: [PATCH 06/11] test(ci-workflows): bound ci-gui-typecheck-gate's spawned children test 1/4 batch 4 pinned for the full 120s shard deadline in run 37730984813: the file's spawnSync children carry no timeout, so a wedged bun child froze the test inside a synchronous wait the per-test timeout cannot interrupt. The 60s test timeout killed one dangling process and the wait still never settled. Every spawnSync in the file now runs under INTERNAL_DEADLINE_MS, with SIGKILL for the bun children per the cli-connect-readiness shape, and cold-spawn-warmup records the unwarmed disposition the deadline oracle requires: the children run a script importing only node builtins, so an import scan has nothing to warm. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- tests/ci-workflows/ci-gui-typecheck-gate.test.ts | 14 ++++++++++++-- tests/ci-workflows/cold-spawn-warmup.test.ts | 8 ++++++++ 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/tests/ci-workflows/ci-gui-typecheck-gate.test.ts b/tests/ci-workflows/ci-gui-typecheck-gate.test.ts index d462432510f..ef22cc92bb0 100644 --- a/tests/ci-workflows/ci-gui-typecheck-gate.test.ts +++ b/tests/ci-workflows/ci-gui-typecheck-gate.test.ts @@ -4,8 +4,14 @@ import { tmpdir } from "node:os"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; import { repoPath } from "../helpers/repo-root"; +import { INTERNAL_DEADLINE_MS } from "../helpers/test-budget"; /** + * Every spawnSync below is bounded by the shared spawned-child deadline: Bun's test + * timeout cannot interrupt a synchronous wait, so an unbounded child that wedges on + * a lock or a runner stall pins its whole batch until the shard deadline cuts it + * (Linux test 1/4, run 37730984813). A bounded child fails this one test instead. + * * Local push validation ("bun run prepush") typechecks the root project only: * tsconfig.json has no project references, so "bun x tsc --noEmit" never parses * gui/. A gui compile error therefore ships silently and first surfaces in CI's @@ -33,6 +39,7 @@ describe("gui typecheck if-changed gate", () => { const dryRun = (files: string): string => { const probe = Bun.spawnSync([process.execPath, typecheckGuiIfChangedScript], { env: { ...process.env, TYPECHECK_DRY_RUN: "1", TYPECHECK_FILES: files }, + timeout: INTERNAL_DEADLINE_MS, killSignal: "SIGKILL", }); return probe.stdout.toString().trim(); }; @@ -49,6 +56,7 @@ describe("gui typecheck if-changed gate", () => { TYPECHECK_FILES: "gui/src/App.tsx", TYPECHECK_CMD: stubCompiler, }, + timeout: INTERNAL_DEADLINE_MS, killSignal: "SIGKILL", }); expect(failing.exitCode).toBe(23); @@ -59,6 +67,7 @@ describe("gui typecheck if-changed gate", () => { TYPECHECK_FILES: "gui/src/App.tsx", TYPECHECK_CMD: JSON.stringify(["ocx-gui-typecheck-missing-compiler"]), }, + timeout: INTERNAL_DEADLINE_MS, killSignal: "SIGKILL", }); expect(missing.exitCode).toBe(1); @@ -68,6 +77,7 @@ describe("gui typecheck if-changed gate", () => { TYPECHECK_FILES: "scripts/x.ts\nREADME.md", TYPECHECK_CMD: stubCompiler, }, + timeout: INTERNAL_DEADLINE_MS, killSignal: "SIGKILL", }); expect(skipping.exitCode).toBe(0); }); @@ -90,7 +100,7 @@ describe("gui typecheck if-changed gate", () => { const dir = mkdtempSync(join(tmpdir(), "ocx-gui-typecheck-")); fixtures.push(dir); const git = (...args: string[]): void => { - const run = Bun.spawnSync(["git", ...args], { cwd: dir, env: gitEnv() }); + const run = Bun.spawnSync(["git", ...args], { cwd: dir, env: gitEnv(), timeout: INTERNAL_DEADLINE_MS }); if (run.exitCode !== 0) throw new Error(`git ${args.join(" ")}: ${run.stderr.toString()}`); }; const commit = (path: string): void => { @@ -108,7 +118,7 @@ describe("gui typecheck if-changed gate", () => { const decide = (): string => { const env = { ...gitEnv(), TYPECHECK_DRY_RUN: "1" }; delete env.TYPECHECK_FILES; - const probe = Bun.spawnSync([process.execPath, join(dir, "scripts", "typecheck-gui-if-changed.ts")], { cwd: dir, env }); + const probe = Bun.spawnSync([process.execPath, join(dir, "scripts", "typecheck-gui-if-changed.ts")], { cwd: dir, env, timeout: INTERNAL_DEADLINE_MS, killSignal: "SIGKILL" }); return probe.stdout.toString().trim(); }; return { dir, git, commit, decide }; diff --git a/tests/ci-workflows/cold-spawn-warmup.test.ts b/tests/ci-workflows/cold-spawn-warmup.test.ts index 7a3cdb62024..e04890fb0ed 100644 --- a/tests/ci-workflows/cold-spawn-warmup.test.ts +++ b/tests/ci-workflows/cold-spawn-warmup.test.ts @@ -55,6 +55,14 @@ type Disposition = WarmupDisposition; * `tests/helpers/cold-spawn-warmup.ts`. `warmed: false` needs a reason that survives review. */ const DISPOSITIONS: Readonly> = { + "tests/ci-workflows/ci-gui-typecheck-gate.test.ts": { + warmed: false, + why: + "Its children run scripts/typecheck-gui-if-changed.ts, which imports only node builtins, " + + "or git itself, so an import scan has no repository module graph to warm and a registered " + + "warm-up would fail closed. The spawnSync bounds exist because an unbounded child pinned " + + "Linux test 1/4 batch 4 until the 120s shard deadline cut it (run 37730984813).", + }, "tests/ci-workflows/test-runner.test.ts": { warmed: true, why: "one throwaway lane pays Bun's test-runner bootstrap before the captured-output lane is timed", From a81c8bcb0e82dbcef7e07a7da4ec707b632073ed Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 8 Oct 2026 05:47:21 +0000 Subject: [PATCH 07/11] test(server): close the routing-history index before removing the auth test home windows 8/9 run 37730984813 job 113160397742: the afterEach removal of ocx-management-auth-* lost the whole 15s retry budget to EPERM on the root rmdir. A routed request opens the process-wide request-history SQLite index under OPENCODEX_HOME and nothing in this file closes it, so the home's files leave delete-pending handles the same process cannot release while Bun.sleepSync blocks the loop. Same signature and same fix as 1c27b3f17: call closeRequestHistoryIndex between the config-dir flight drain and the reaps barrier, matching settleServerAuthFixture's drain order. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- tests/server/server-management-auth.test.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/server/server-management-auth.test.ts b/tests/server/server-management-auth.test.ts index cb392a5b667..5cc84825dd8 100644 --- a/tests/server/server-management-auth.test.ts +++ b/tests/server/server-management-auth.test.ts @@ -92,6 +92,7 @@ import { createGuiPairingGrant, } from "../../src/server/gui-session"; import { setSystemRestartIoForTests } from "../../src/server/management/system-restart"; +import { closeRequestHistoryIndex } from "../../src/routing/history/indexer"; import { removeTreeWithRetry } from "../helpers/remove-tree"; const previousHome = process.env.OPENCODEX_HOME; @@ -201,6 +202,10 @@ afterEach(async () => { // Flush all homes before restoring environment variables, including startup // rollback flights that no successfully returned server could have awaited. await flushConfigDirHardeningForTests(); + // The process-wide routing-history index keeps its SQLite under this home open + // after stop; close it before the reaps barrier or the removal below meets a + // delete-pending handle for the whole retry budget. + closeRequestHistoryIndex(); // The caller-facing ACL timeout may settle before icacls actually exits. // Deletion waits for actual reaps, after every producer above has settled. await flushWindowsSecretAclReapsBeforeRemoval(testHome); From 3390e7dbf00a3193ab64b50469aa197d6cd0e9f6 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 8 Oct 2026 06:00:02 +0000 Subject: [PATCH 08/11] test(server): move management-auth server fixtures into tests/helpers test 2/4 run 37734155690 job 113170258422: the file-size ratchet reported NEW_OVERSIZED on tests/server/server-management-auth.test.ts after a81c8bcb0's five lines took it from 1999 to 2004. Move remoteConfig, hubConfig, startEphemeralHubServer, and websocketHandshakeOpens unchanged into tests/helpers/management-auth-fixture.ts; none of them read the file-scoped fixture state, so behavior is identical and the file is back under the 2000-line threshold at 1931. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- tests/helpers/management-auth-fixture.ts | 82 ++++++++++++++++++++ tests/server/server-management-auth.test.ts | 85 ++------------------- 2 files changed, 88 insertions(+), 79 deletions(-) create mode 100644 tests/helpers/management-auth-fixture.ts diff --git a/tests/helpers/management-auth-fixture.ts b/tests/helpers/management-auth-fixture.ts new file mode 100644 index 00000000000..7fba14e6b16 --- /dev/null +++ b/tests/helpers/management-auth-fixture.ts @@ -0,0 +1,82 @@ +import { expect, spyOn } from "bun:test"; +import { startServer } from "../../src/server"; +import type { OcxConfig } from "../../src/types"; + +export function remoteConfig(): OcxConfig { + return { + port: 0, + hostname: "0.0.0.0", + defaultProvider: "test", + providers: { + test: { + adapter: "openai-chat", + baseUrl: "https://example.test/v1", + disabled: true, + models: ["gpt-test"], + }, + }, + }; +} + +export function hubConfig(publicOrigin = "https://hub.example.test"): OcxConfig { + return { + ...remoteConfig(), + runtimeRole: "hub", + hub: { managementPublicOrigin: publicOrigin }, + remoteGui: { allowedTailscaleUsers: ["alice@example.test"] }, + corsAllowOrigins: ["https://dashboard.example.test"], + }; +} + +/** Keep real ingress/handlers while the kernel allocates both ports at the actual bind. */ +export async function startEphemeralHubServer(deps: Parameters[1]) { + const nativeServe = Bun.serve.bind(Bun); + const listeners: Array> = []; + const hostnames: unknown[] = []; + const serveSpy = spyOn(Bun, "serve").mockImplementation((options) => { + const listener = nativeServe({ ...options, port: 0 } as Parameters[0]); + listeners.push(listener); + hostnames.push("hostname" in options ? options.hostname : undefined); + return listener; + }); + try { + let server: ReturnType; + try { + server = startServer(0, deps); + } finally { + // startServer is synchronous; restore before requests or any awaited cleanup. + serveSpy.mockRestore(); + } + expect(listeners).toHaveLength(2); + expect(listeners[0]).toBe(server); + expect(hostnames).toEqual(["0.0.0.0", "127.0.0.1"]); + const managementPort = listeners[1]?.port; + if (!managementPort || managementPort === server.port) throw new Error("expected distinct live ingress ports"); + return { server, managementPort }; + } catch (error) { + await Promise.allSettled(listeners.map(async listener => { await listener.stop(true); })); + throw error; + } +} + +export function websocketHandshakeOpens(url: URL, token: string): Promise { + return new Promise(resolve => { + const target = new URL("/v1/responses", url); + target.protocol = target.protocol === "https:" ? "wss:" : "ws:"; + const socket = new WebSocket(target, { + headers: { "X-OpenCodex-API-Key": token }, + } as unknown as string[]); + let settled = false; + const finish = (opened: boolean) => { + if (settled) return; + settled = true; + clearTimeout(timer); + try { socket.close(); } catch { /* already closed */ } + resolve(opened); + }; + socket.addEventListener("open", () => finish(true)); + socket.addEventListener("error", () => finish(false)); + socket.addEventListener("close", () => finish(false)); + const timer = setTimeout(() => finish(false), 5_000); + }); +} diff --git a/tests/server/server-management-auth.test.ts b/tests/server/server-management-auth.test.ts index 5cc84825dd8..32b7632046f 100644 --- a/tests/server/server-management-auth.test.ts +++ b/tests/server/server-management-auth.test.ts @@ -93,6 +93,12 @@ import { } from "../../src/server/gui-session"; import { setSystemRestartIoForTests } from "../../src/server/management/system-restart"; import { closeRequestHistoryIndex } from "../../src/routing/history/indexer"; +import { + hubConfig, + remoteConfig, + startEphemeralHubServer, + websocketHandshakeOpens, +} from "../helpers/management-auth-fixture"; import { removeTreeWithRetry } from "../helpers/remove-tree"; const previousHome = process.env.OPENCODEX_HOME; @@ -106,85 +112,6 @@ function enableContextRelay(): void { resetContextRelayActivationForTests(); } -function remoteConfig(): OcxConfig { - return { - port: 0, - hostname: "0.0.0.0", - defaultProvider: "test", - providers: { - test: { - adapter: "openai-chat", - baseUrl: "https://example.test/v1", - disabled: true, - models: ["gpt-test"], - }, - }, - }; -} - -function hubConfig(publicOrigin = "https://hub.example.test"): OcxConfig { - return { - ...remoteConfig(), - runtimeRole: "hub", - hub: { managementPublicOrigin: publicOrigin }, - remoteGui: { allowedTailscaleUsers: ["alice@example.test"] }, - corsAllowOrigins: ["https://dashboard.example.test"], - }; -} - -/** Keep real ingress/handlers while the kernel allocates both ports at the actual bind. */ -async function startEphemeralHubServer(deps: Parameters[1]) { - const nativeServe = Bun.serve.bind(Bun); - const listeners: Array> = []; - const hostnames: unknown[] = []; - const serveSpy = spyOn(Bun, "serve").mockImplementation((options) => { - const listener = nativeServe({ ...options, port: 0 } as Parameters[0]); - listeners.push(listener); - hostnames.push("hostname" in options ? options.hostname : undefined); - return listener; - }); - try { - let server: ReturnType; - try { - server = startServer(0, deps); - } finally { - // startServer is synchronous; restore before requests or any awaited cleanup. - serveSpy.mockRestore(); - } - expect(listeners).toHaveLength(2); - expect(listeners[0]).toBe(server); - expect(hostnames).toEqual(["0.0.0.0", "127.0.0.1"]); - const managementPort = listeners[1]?.port; - if (!managementPort || managementPort === server.port) throw new Error("expected distinct live ingress ports"); - return { server, managementPort }; - } catch (error) { - await Promise.allSettled(listeners.map(async listener => { await listener.stop(true); })); - throw error; - } -} - -function websocketHandshakeOpens(url: URL, token: string): Promise { - return new Promise(resolve => { - const target = new URL("/v1/responses", url); - target.protocol = target.protocol === "https:" ? "wss:" : "ws:"; - const socket = new WebSocket(target, { - headers: { "X-OpenCodex-API-Key": token }, - } as unknown as string[]); - let settled = false; - const finish = (opened: boolean) => { - if (settled) return; - settled = true; - clearTimeout(timer); - try { socket.close(); } catch { /* already closed */ } - resolve(opened); - }; - socket.addEventListener("open", () => finish(true)); - socket.addEventListener("error", () => finish(false)); - socket.addEventListener("close", () => finish(false)); - const timer = setTimeout(() => finish(false), 5_000); - }); -} - beforeEach(() => { testHome = mkdtempSync(join(tmpdir(), "ocx-management-auth-")); process.env.OPENCODEX_HOME = testHome; From 622c42c3fe46d8ad3b098a6df69df253a1cd6444 Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 8 Oct 2026 06:13:15 +0000 Subject: [PATCH 09/11] test: quarantine ci-gui-typecheck-gate and warm catalog-remote-pull's eval children test 1/4 batch 4 timed out three runs in a row inside the 12-file batch while every case passed alone (37730984813, 37732846946, 37735238354), the same multi-file process-state class as the other serial-lane entries; move ci-gui-typecheck-gate to SERIAL_FULL_SUITE_FILES. windows 3/9 killed both real-Bun-transport children at their 10s deadline on a cold catalog-remote graph load. Register the graph with warmModuleGraph so the cold load happens in setup, pass BUN_RUNTIME_TRANSPILER_CACHE_PATH into the child env so the warm transfers, and use an absolute path the import scan can see. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- scripts/test.ts | 4 +++ .../catalog-remote-pull.test.ts | 25 ++++++++++++++++--- 2 files changed, 26 insertions(+), 3 deletions(-) diff --git a/scripts/test.ts b/scripts/test.ts index 2b87dcd16a9..959698d440b 100644 --- a/scripts/test.ts +++ b/scripts/test.ts @@ -371,6 +371,10 @@ export const SERIAL_FULL_SUITE_FILES = [ "providers/cursor/cursor-native-exec-shell.test.ts", "codex-integration/issue-452-empty-503.test.ts", "adapters/openai/openai-provider-option-e2e.test.ts", + // Three Linux runs in a row timed out mid-file inside a 12-file batch while + // every case passed alone (37730984813, 37732846946, 37735238354, all test + // 1/4 batch 4): the same multi-file process-state class as the entries below. + "ci-workflows/ci-gui-typecheck-gate.test.ts", "ci-workflows/release-helper.test.ts", // The full macOS isolate pool stalled in the structure gate's synchronous Git // child after earlier files; fresh-process execution retains the same assertions. diff --git a/tests/codex-integration/catalog-remote-pull.test.ts b/tests/codex-integration/catalog-remote-pull.test.ts index 1082a4c6f46..7cedbfac6a2 100644 --- a/tests/codex-integration/catalog-remote-pull.test.ts +++ b/tests/codex-integration/catalog-remote-pull.test.ts @@ -1,7 +1,8 @@ -import { afterEach, describe, expect, mock, test } from "bun:test"; +import { afterEach, beforeAll, describe, expect, mock, test } from "bun:test"; import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; +import { fileURLToPath } from "node:url"; import { Database } from "bun:sqlite"; import { @@ -13,6 +14,7 @@ import { } from "../../src/codex/catalog/remote"; import { resolveCodexCatalogSerializationDatabasePath, resolveEffectiveUserIdentity } from "../../src/codex/user-identity"; import { withCatalogWriteSerialization } from "../../src/codex/catalog-write-serialization"; +import { COLD_SPAWN_WARMUP_HOOK_BUDGET_MS, warmModuleGraph } from "../helpers/cold-spawn-warmup"; import { removeTreeWithRetry } from "../helpers/remove-tree"; /** Every K acquisition states its intent (#6529); these tests exercise the lock, not the intent. */ @@ -51,7 +53,24 @@ async function withProxyEnv(env: Record, action: () => Promise { + // The --eval children load the catalog-remote graph in a fresh Bun process bounded by a + // 10s kill deadline; the cold module load can exceed it on a loaded Windows runner (run + // 37735238354, windows 3/9). Pay it once in setup, into the same transpiler cache the + // children inherit via BUN_RUNTIME_TRANSPILER_CACHE_PATH below. + beforeAll(async () => { + await warmModuleGraph({ graph: "catalog-remote-pull/transport-eval", source: REMOTE_TRANSPORT_EVAL_SCRIPT }); + }, COLD_SPAWN_WARMUP_HOOK_BUDGET_MS); + test("accepts HTTPS and loopback HTTP but rejects credentials and insecure remote HTTP", () => { expect(validateRemoteCatalogUrl("https://hub.example.com/v1/catalog").href).toBe("https://hub.example.com/v1/catalog"); expect(validateRemoteCatalogUrl("http://127.0.0.1:10100/v1/catalog").protocol).toBe("http:"); @@ -167,7 +186,7 @@ describe("remote catalog acquisition", () => { // Inherit process-launch necessities and test provenance only, never host credentials. const env: Record = {}; for (const key of ["PATH", "Path", "SystemRoot", "WINDIR", "COMSPEC", "PATHEXT", "TEMP", "TMP", - "OCX_TEST_HOME_GUARD", "OCX_TEST_RUN_ID"]) { + "OCX_TEST_HOME_GUARD", "OCX_TEST_RUN_ID", "BUN_RUNTIME_TRANSPILER_CACHE_PATH"]) { const value = process.env[key]; if (value !== undefined) env[key] = value; } @@ -179,7 +198,7 @@ describe("remote catalog acquisition", () => { writeFileSync(join(env.OPENCODEX_HOME, ".env"), "no_proxy=*\n"); env.HTTP_PROXY = `http://127.0.0.1:${proxy.port}`; env.NO_PROXY = bypass; - const source = new URL("../../src/codex/catalog/remote.ts", import.meta.url).href; + const source = remoteModule; const script = ` const { fetchRemoteCatalog } = await import(${JSON.stringify(source)}); try { From 4f4af6be14f587fc267e5645f4299828a893b75c Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 8 Oct 2026 06:24:11 +0000 Subject: [PATCH 10/11] test(ci-workflows): bound release-version-sources' spawned children test 1/4 batch 6 pinned for the full 120s shard deadline in run 37736425700: the file's spawnSync children carry no timeout, so a wedged bun child froze the test inside a synchronous wait the per-test timeout cannot interrupt. The 60s dangling-process sweep killed one child and the wait still never settled. Both spawnSync sites now run under INTERNAL_DEADLINE_MS, with SIGKILL for the bun child per the cli-connect-readiness shape, and cold-spawn-warmup records the unwarmed disposition the deadline oracle requires: the children run a script importing only node builtins, or a bash -c fragment, so an import scan has nothing to warm. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- tests/ci-workflows/cold-spawn-warmup.test.ts | 9 +++++++++ tests/ci-workflows/release-version-sources.test.ts | 9 ++++++++- 2 files changed, 17 insertions(+), 1 deletion(-) diff --git a/tests/ci-workflows/cold-spawn-warmup.test.ts b/tests/ci-workflows/cold-spawn-warmup.test.ts index e04890fb0ed..af886c29f9f 100644 --- a/tests/ci-workflows/cold-spawn-warmup.test.ts +++ b/tests/ci-workflows/cold-spawn-warmup.test.ts @@ -67,6 +67,15 @@ const DISPOSITIONS: Readonly> = { warmed: true, why: "one throwaway lane pays Bun's test-runner bootstrap before the captured-output lane is timed", }, + "tests/ci-workflows/release-version-sources.test.ts": { + warmed: false, + why: + "Its children run scripts/release-version-sources.ts, which imports only node builtins, " + + "or a bash -c guard fragment, so an import scan has no repository module graph to warm " + + "and a registered warm-up would fail closed. The spawnSync bounds exist because an " + + "unbounded child pinned Linux test 1/4 batch 6 until the 120s shard deadline cut it " + + "(run 37736425700).", + }, "tests/cli/cli-connect-readiness.test.ts": { warmed: true, why: "two graphs: the connect eval, and the observed ladder that also loads src/codex/runtime", diff --git a/tests/ci-workflows/release-version-sources.test.ts b/tests/ci-workflows/release-version-sources.test.ts index 36947fd84d1..8c8bad5768b 100644 --- a/tests/ci-workflows/release-version-sources.test.ts +++ b/tests/ci-workflows/release-version-sources.test.ts @@ -8,8 +8,14 @@ import { writeVersionSources, } from "../../scripts/release-version-sources"; import { repoPath, repoRoot } from "../helpers/repo-root"; +import { INTERNAL_DEADLINE_MS } from "../helpers/test-budget"; /** + * Every spawnSync below is bounded by the shared spawned-child deadline: Bun's test + * timeout cannot interrupt a synchronous wait, so an unbounded child that wedges on + * a lock or a runner stall pins its whole batch until the shard deadline cuts it + * (Linux test 1/4 batch 6, run 37736425700). A bounded child fails this one test instead. + * * The npm package and the desktop app read their version from different files. When only * package.json moved, dev carried 2.62.0 for npm while tauri.conf.json, Cargo.toml and the * opencodex-desktop Cargo.lock entry still said 2.61.0, so a release would have shipped an app @@ -39,7 +45,7 @@ function snapshot(root: string): Record { } function runCli(...args: string[]) { - const proc = Bun.spawnSync([process.execPath, CLI, ...args]); + const proc = Bun.spawnSync([process.execPath, CLI, ...args], { timeout: INTERNAL_DEADLINE_MS, killSignal: "SIGKILL" }); return { exitCode: proc.exitCode, stderr: new TextDecoder().decode(proc.stderr) }; } @@ -217,6 +223,7 @@ describe("every version move covers all four sources", () => { const verdict = (changed: string) => Bun.spawnSync( ["bash", "-c", 'set -euo pipefail\nbranch=codex/dev-version-9.9.9\nchanged_files="$1"\n' + guard + "\necho accepted", "guard", changed], + { timeout: INTERNAL_DEADLINE_MS }, ).exitCode; expect(verdict("package.json")).toBe(0); From 2d948e1bfaa80e4f3b1f62b34980ddcd243fd48d Mon Sep 17 00:00:00 2001 From: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> Date: Thu, 8 Oct 2026 06:27:51 +0000 Subject: [PATCH 11/11] test: warm the CLI module graph before cli-ready-subprocess's timed children The private per-sandbox transpiler cache means the first spawned child in a batch pays its cold module-graph load inside its own bound; windows 9/9 run 37736425700 killed the ready --wait child at its 10s deadline before it ever reached /healthz (healthzHits 0 vs 1). Register src/cli/index.ts's entry graph with warmModuleGraph so the cold load happens in setup, matching the cli-models precedent. Co-Authored-By: Devin AI <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- tests/cli/cli-ready-subprocess.test.ts | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/tests/cli/cli-ready-subprocess.test.ts b/tests/cli/cli-ready-subprocess.test.ts index 54a5416a62e..f2bbaeec858 100644 --- a/tests/cli/cli-ready-subprocess.test.ts +++ b/tests/cli/cli-ready-subprocess.test.ts @@ -5,11 +5,12 @@ * prove that the top-level CLI preserves terminal-failed and pre-parse behavior * with isolated homes and an actual discovered proxy fixture. */ -import { describe, expect, test } from "bun:test"; +import { beforeAll, describe, expect, test } from "bun:test"; import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { dirname, join } from "node:path"; import { fileURLToPath } from "node:url"; +import { COLD_SPAWN_WARMUP_HOOK_BUDGET_MS, warmModuleGraph } from "../helpers/cold-spawn-warmup"; import { removeTreeWithRetry } from "../helpers/remove-tree"; const repoRoot = dirname(fileURLToPath(new URL("../../package.json", import.meta.url))); @@ -70,6 +71,15 @@ function writeRuntimePort(opencodexHome: string, port: number, pid: number): voi } describe("ocx ready real subprocess", () => { + // src/cli/index.ts has roughly fifty top-level imports, so the runCli children below all pay + // the same static graph. This file's first spawned child is the one that loads it, and under + // the run's private transpiler cache that cold load lands inside the child's own kill bound + // (windows 9/9 run 37736425700 killed it at 10s before it reached /healthz). Pay it once in + // setup instead; see tests/helpers/cold-spawn-warmup.ts. + beforeAll(async () => { + await warmModuleGraph({ graph: "cli-index/ready-subprocess", entry: cliPath }); + }, COLD_SPAWN_WARMUP_HOOK_BUDGET_MS); + test("released-process protocol skew matrix rejects before any local write", async () => { const homes = isolatedHomes("ocx-protocol-skew-subprocess-"); const script = `