Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
58ed4c8
test(storage): cap macOS isolate worker churn to avoid Bun segfault
Wibias Aug 1, 2026
871e96f
fix(ci): skip bun --isolate on macOS to avoid Bun 1.3.14 segfault
Wibias Aug 1, 2026
be8110e
fix(storage): settle Worker OS join on darwin under isolate
Wibias Aug 1, 2026
bef8e9c
test(ci): bind macOS isolate skip assertions to full step blocks
Wibias Aug 1, 2026
655ecaa
test(catalog): raise restore spawnSync case budget under isolate
Wibias Aug 1, 2026
e7f4ec7
fix(ci): keep macOS --isolate; skip darwin Worker hammer cases
Wibias Aug 1, 2026
0c6ef39
fix(storage): disarm terminate timeout before OS-join settle
Wibias Aug 1, 2026
1029e37
test(subagent): raise fallback suite budget for Windows ACL hooks
Wibias Aug 1, 2026
1ce6a2c
test(subagent): clarify Windows ACL hook timeout comment
Wibias Aug 1, 2026
81a010e
merge(dev): resolve storage-worker-lifecycle test import conflict
Wibias Aug 1, 2026
974ecc0
test(oauth): raise mutation admission case budget and always release …
Wibias Aug 1, 2026
63b6050
test(storage): assert close-wins regression covers the registered path
Wibias Aug 1, 2026
ca3f025
chore(ci): comment to retrigger Cross-platform CI on tip
Wibias Aug 1, 2026
8da1d7e
merge(dev): keep oauth admission gate cleanup with STORE_BUDGET_MS
Wibias Aug 1, 2026
e07ba88
fix(oauth): keep mutation wait timers ref'd under isolate
Wibias Aug 1, 2026
ed92918
fix(responses): record dir-fsync after best-effort attempt on Windows
Wibias Aug 1, 2026
aacbf89
test(storage): drop unused darwin settle sleeps in teardown isolate
Wibias Aug 1, 2026
a3f2fbc
fix(cursor): keep background-shell term-grace timers ref'd under isolate
Wibias Aug 1, 2026
94628ec
merge(dev): keep oauth wait-timer and shell grace-timer isolate fixes
Wibias Aug 1, 2026
7e041d7
fix(auth): stop management ACL timeout tests from poisoning startServer
Wibias Aug 1, 2026
14a73cf
test(config): expect config-mutation ACL timeoutMemoKey
Wibias Aug 1, 2026
34a1ac4
test: cover dir-fsync failure path and clear oauth race deadline
Wibias Aug 1, 2026
7c5b49d
ci: nudge Cross-platform CI after missed synchronize runs
Wibias Aug 1, 2026
54aa3ed
merge(dev): restore mergeability so Cross-platform CI can run
Wibias Aug 1, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ on:
permissions:
contents: read

# Retrigger CI after dir-fsync / oauth deadline follow-ups (tip 34a1ac46).
concurrency:
group: cross-platform-ci-${{ github.ref }}
cancel-in-progress: true
Expand Down
7 changes: 4 additions & 3 deletions src/adapters/cursor/native-exec-shell.ts
Original file line number Diff line number Diff line change
Expand Up @@ -341,9 +341,10 @@ function waitForBackgroundShellClose(entry: BackgroundShellEntry): Promise<boole
resolveWait(false);
}, CURSOR_BACKGROUND_SHELL_TERM_GRACE_MS);
// Deliberately REF'D: this is the bounded kill-grace wait that shutdown
// drain awaits. Bun on Windows can starve unref'd timers when a pending
// promise is the only other work, which would leave drainAndShutdown
// waiting on this resolution forever. The timer self-clears within the
// drain awaits. Bun on Windows / under `bun test --isolate` can starve
// unref'd timers when a pending promise is the only other work, which
// would leave drainAndShutdown waiting forever (same class as oauth
// serializeMutation wait timers). The timer self-clears within the
// 2-second grace window (or earlier on close), so a ref cannot keep the
// process alive beyond that bound.
void entry.closePromise.then(() => {
Expand Down
5 changes: 4 additions & 1 deletion src/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1530,7 +1530,10 @@ function configMutationDatabasePath(): string {
}
if (windowsSecretAclApplies()) {
try {
hardenSecretDir(dir, { required: true });
// Distinct timeout memo from management-token directory harden: a required
// management-dir timeout must not poison config mutation on the same home
// (windows-latest server-management-auth cases).
hardenSecretDir(dir, { required: true, timeoutMemoKey: `${dir}::config-mutation` });
} catch (error) {
if (!warnedConfigMutationDirectoryAcl) {
warnedConfigMutationDirectoryAcl = true;
Expand Down
4 changes: 3 additions & 1 deletion src/oauth/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -416,7 +416,9 @@ function serializeMutation<T>(work: () => Promise<T>, retainedValues: readonly u
release();
rejectResult(new OAuthMutationBusyError("OAuth mutation queue wait timed out"));
}, waitMs);
entry.timeout.unref?.();
// Keep the wait timer ref'd: unref'd waiters can stall forever under bun test
// --isolate after a full admission queue (windows-latest hung oauth-store-multi
// past the 20-minute job ceiling with no further output).
mutationWaiters.push(entry);
drainOAuthMutations();
return result;
Expand Down
24 changes: 19 additions & 5 deletions src/responses/spill-store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,12 @@ export interface ResponseSpillCleanupResult {
export interface ResponseSpillIoForTest {
write?: (fd: number, bytes: Uint8Array) => void;
fsync?: (fd: number) => void;
/** Directory-handle fsync seam for `fsyncDirectoryBestEffort` only. */
fsyncDir?: (fd: number) => void;
/** Directory-handle open seam for `fsyncDirectoryBestEffort` only. */
openDir?: (dir: string) => number;
/** Directory-handle close seam for `fsyncDirectoryBestEffort` only. */
closeDir?: (fd: number) => void;
link?: (tempPath: string, destinationPath: string) => void;
copyFileExcl?: (tempPath: string, destinationPath: string) => void;
unlink?: (path: string) => void;
Expand Down Expand Up @@ -86,14 +92,22 @@ function fsyncDirectoryBestEffort(dir: string): void {
// open/fsync directory handles this way, but callers still cross this seam.
record("dir-fsync");
try {
fd = openSync(dir, "r");
if (spillIoForTest?.fsync) spillIoForTest.fsync(fd);
else fsyncSync(fd);
fd = spillIoForTest?.openDir ? spillIoForTest.openDir(dir) : openSync(dir, "r");
try {
if (spillIoForTest?.fsyncDir) spillIoForTest.fsyncDir(fd);
else if (spillIoForTest?.fsync) spillIoForTest.fsync(fd);
else fsyncSync(fd);
} catch {
// Windows and some filesystems do not support fsync on directory handles.
}
} catch {
// Windows and some filesystems do not support fsync on directory handles.
// Directory missing or unreadable — nothing further to sync.
} finally {
if (fd !== null) {
try { closeSync(fd); } catch { /* best effort */ }
try {
if (spillIoForTest?.closeDir) spillIoForTest.closeDir(fd);
else closeSync(fd);
} catch { /* best effort */ }
}
}
}
Expand Down
12 changes: 7 additions & 5 deletions tests/codex-catalog-restore.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,8 @@ describe("Codex catalog restore", () => {
if (existsSync(opencodexHome)) rmSync(opencodexHome, { recursive: true, force: true });
});

// spawnSync(bun --eval) under `bun test --isolate` on Windows can exceed the
// default 5s case budget when the runner is under load (seen at ~5.4s on GHA).
test("drops routed entries without overwriting user-added native entries", () => {
const catalogPath = join(codexHome, "catalog.json");
writeFileSync(join(codexHome, "config.toml"), 'model_catalog_json = "catalog.json"\n', "utf8");
Expand All @@ -59,7 +61,7 @@ describe("Codex catalog restore", () => {
expect(JSON.parse(r.stdout)).toMatchObject({ removed: 1, kept: 2 });
const slugs = JSON.parse(readFileSync(catalogPath, "utf8")).models.map((m: { slug: string }) => m.slug);
expect(slugs).toEqual(["gpt-5.5", "user-native"]);
});
}, { timeout: 15_000 });

test("uses pristine backup while preserving native entries added after sync", () => {
const catalogPath = join(codexHome, "catalog.json");
Expand Down Expand Up @@ -94,7 +96,7 @@ describe("Codex catalog restore", () => {
{ slug: "codex-mini", priority: 60 },
{ slug: "user-native", priority: 10 },
]);
});
}, { timeout: 15_000 });

test("does not apply generic legacy backup to a custom catalog path", () => {
const catalogPath = join(codexHome, "custom-catalog.json");
Expand All @@ -120,7 +122,7 @@ describe("Codex catalog restore", () => {
expect(JSON.parse(r.stdout)).toMatchObject({ removed: 1, kept: 2 });
const restored = JSON.parse(readFileSync(catalogPath, "utf8")).models as Array<Record<string, unknown>>;
expect(restored.map(m => m.slug)).toEqual(["gpt-5.5", "user-native"]);
});
}, { timeout: 15_000 });

test("sync applies native-only subagent priority selections", () => {
const catalogPath = join(codexHome, "catalog.json");
Expand Down Expand Up @@ -150,7 +152,7 @@ describe("Codex catalog restore", () => {
const synced = JSON.parse(readFileSync(catalogPath, "utf8")).models as Array<Record<string, unknown>>;
expect(synced.find(m => m.slug === "gpt-5.5")?.priority).toBe(0);
expect(synced.find(m => m.slug === "gpt-5.4")?.priority).toBeGreaterThan(100);
});
}, { timeout: 15_000 });

test("sync advertises documented Codex-native additions omitted by the bundled catalog", () => {
const catalogPath = join(codexHome, "catalog.json");
Expand Down Expand Up @@ -196,5 +198,5 @@ describe("Codex catalog restore", () => {
expect(synced.map(m => m.slug)).toContain("gpt-5.6-terra");
expect(synced.map(m => m.slug)).toContain("gpt-5.6-luna");
expect(synced.find(m => m.slug === "gpt-5.4")?.max_context_window).toBe(1_000_000);
});
}, { timeout: 15_000 });
});
5 changes: 4 additions & 1 deletion tests/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1611,7 +1611,10 @@ describe("config.ts – Windows ACL hardening integration", () => {
try {
const spy = spyOn(windowsAcl, "hardenSecretDir").mockReturnValue({ ok: true });
saveConfig(getDefaultConfig());
expect(spy).toHaveBeenCalledWith(testDir, { required: true });
expect(spy).toHaveBeenCalledWith(testDir, {
required: true,
timeoutMemoKey: `${testDir}::config-mutation`,
});
expect(existsSync(getConfigPath())).toBe(true);
spy.mockRestore();
} finally {
Expand Down
78 changes: 53 additions & 25 deletions tests/oauth-store-multi.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
import { INTERNAL_DEADLINE_MS, STORE_BUDGET_MS } from "./helpers/test-budget";
import { existsSync, mkdirSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import {
Expand Down Expand Up @@ -263,6 +264,9 @@ describe("multi-account auth store", () => {
expect(getAccountSet("xai")!.accounts[0]?.needsReauth).toBeUndefined();
});

// Filling the 128-slot admission queue then draining it exceeds the default
// 5s case budget on a loaded Windows isolate runner; an early timeout also
// leaves the gate closed and hangs the file realm until the job ceiling.
test("OAuth mutation 129 rejects before enqueue while every accepted mutation executes once", async () => {
let releaseFirst!: () => void;
let firstStarted!: () => void;
Expand All @@ -274,18 +278,23 @@ describe("multi-account auth store", () => {
firstStarted();
await firstGate;
}, ["provider", "account"] )];
await started;
for (let i = 1; i < 128; i++) {
accepted.push(mutateStore(() => { executions++; }, ["provider", `account-${i}`]));
try {
await started;
for (let i = 1; i < 128; i++) {
accepted.push(mutateStore(() => { executions++; }, ["provider", `account-${i}`]));
}
expect(oauthMutationTailSnapshot().active).toBe(128);
await expect(mutateStore(() => { executions++; }, ["rejected"])).rejects.toBeInstanceOf(OAuthMutationBusyError);
expect(oauthMutationTailSnapshot().active).toBe(128);
releaseFirst();
await Promise.all(accepted);
expect(executions).toBe(128);
expect(oauthMutationTailSnapshot().active).toBe(0);
} finally {
releaseFirst();
await Promise.allSettled(accepted);
}
expect(oauthMutationTailSnapshot().active).toBe(128);
await expect(mutateStore(() => { executions++; }, ["rejected"])).rejects.toBeInstanceOf(OAuthMutationBusyError);
expect(oauthMutationTailSnapshot().active).toBe(128);
releaseFirst();
await Promise.all(accepted);
expect(executions).toBe(128);
expect(oauthMutationTailSnapshot().active).toBe(0);
});
}, { timeout: STORE_BUDGET_MS }); // 128 serialized load-modify-persist store mutations; windows-latest measured ~7.3s against Bun's 5s default.

test("OAuth 30 second wait timeout releases an unstarted lease and never enters the chain", async () => {
let releaseFirst!: () => void;
Expand All @@ -296,24 +305,43 @@ describe("multi-account auth store", () => {
firstStarted();
await firstGate;
}, ["provider", "running-account"]);
await started;
let entered = false;
const timedOut = mutateStore(() => { entered = true; }, ["provider", "waiting-account"], { waitMs: 10 });
// Bun 1.3.14 on Windows does not service the production-unref'ed queue timer
// when the test is otherwise waiting only on promises. Keep a test-owned
// ref'ed timer active, and release the blocker if the assertion itself fails.
const timerWakeup = setInterval(() => {}, 5);
const cleanupFallback = setTimeout(releaseFirst, 1_000);
let timedOut: Promise<unknown> | undefined;
try {
await expect(timedOut).rejects.toBeInstanceOf(OAuthMutationBusyError);
await started;
let entered = false;
timedOut = mutateStore(() => { entered = true; }, ["provider", "waiting-account"], { waitMs: 10 });
// Belt-and-suspenders with the ref'd wait timer in store.ts: if reject still
// never fires under isolate load, fail the case instead of hanging the job.
// Use a clearable setTimeout (not Bun.sleep) so a settled race cannot keep
// the isolate alive for the remainder of INTERNAL_DEADLINE_MS.
let deadlineTimer: ReturnType<typeof setTimeout> | undefined;
try {
await Promise.race([
timedOut.then(
() => {
throw new Error("expected OAuthMutationBusyError from waitMs timeout");
},
(error: unknown) => {
expect(error).toBeInstanceOf(OAuthMutationBusyError);
},
),
new Promise<never>((_, reject) => {
deadlineTimer = setTimeout(() => {
reject(new Error(`OAuth mutation waitMs reject did not fire within ${INTERNAL_DEADLINE_MS}ms`));
}, INTERNAL_DEADLINE_MS);
}),
]);
} finally {
if (deadlineTimer !== undefined) clearTimeout(deadlineTimer);
}
expect(entered).toBe(false);
expect(oauthMutationTailSnapshot().active).toBe(1);
} finally {
clearInterval(timerWakeup);
clearTimeout(cleanupFallback);
releaseFirst();
await blocker;
expect(oauthMutationTailSnapshot().active).toBe(0);
} finally {
releaseFirst();
await Promise.allSettled([blocker, ...(timedOut ? [timedOut] : [])]);
}
expect(oauthMutationTailSnapshot().active).toBe(0);
});
}, { timeout: STORE_BUDGET_MS });
});
34 changes: 34 additions & 0 deletions tests/responses-state.test.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
import { BULK_DURABLE_IO_BUDGET_MS } from "./helpers/test-budget";
import {
closeSync,
existsSync,
linkSync,
mkdirSync,
Expand Down Expand Up @@ -617,6 +618,39 @@ describe("Responses previous_response_id state", () => {
expect(events).toEqual(["dir-fsync"]);
});

test("directory fsync still records and closes when fsync throws", () => {
const events: string[] = [];
const closed: number[] = [];
setSpillIoForTest({
record: event => events.push(event),
fsyncDir: () => {
throw new Error("injected directory fsync failure");
},
closeDir: fd => {
closed.push(fd);
closeSync(fd);
},
});
writeResponseSpillDurably("resp_dir_fsync_throw", { createdAt: Date.now(), items: ["x"] });
expect(events).toContain("dir-fsync");
expect(closed).toHaveLength(1);
});

test("directory fsync records even when directory open fails", () => {
const events: string[] = [];
setSpillIoForTest({
record: event => events.push(event),
openDir: () => {
throw Object.assign(new Error("injected directory open failure"), { code: "ENOENT" });
},
});
writeResponseSpillDurably("resp_dir_open_fail", { createdAt: Date.now(), items: ["x"] });
// Record happens before open so Windows (no directory-handle fsync) still
// observes the durability seam in ordering tests.
expect(events).toContain("dir-fsync");
expect(events).toContain("publish");
});

test("spill temp cleanup forgets successful ACL memos and retains failed removals", () => {
const previousUsername = process.env.USERNAME;
process.env.USERNAME = "ocx-test-user";
Expand Down
30 changes: 13 additions & 17 deletions tests/server-management-auth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -473,7 +473,7 @@ describe("management and data-plane credential separation", () => {
}
});

test("directory ACL timeout keeps management unavailable and names OPENCODEX_ADMIN_AUTH_TOKEN", async () => {
test("directory ACL timeout keeps management unavailable and names OPENCODEX_ADMIN_AUTH_TOKEN", () => {
delete process.env.OPENCODEX_ADMIN_AUTH_TOKEN;
saveConfig(remoteConfig());
const adminToken = `ocx_admin_${"d".repeat(43)}`;
Expand All @@ -488,25 +488,13 @@ describe("management and data-plane credential separation", () => {
return { success: false, exitCode: null, timedOut: true, stdout: "" };
});
resetHardenedStateForTests();
// Probe only: startServer would re-harden the same home for config mutation
// and poison/conflict with this required directory timeout. HTTP 503 coverage
// for ACL timeouts lives in "an icacls timeout keeps the management plane closed".
const state = initializeManagementAuthState(remoteConfig());
expect(state.available).toBe(false);
if (state.available) return;
expect(state.reason).toContain("OPENCODEX_ADMIN_AUTH_TOKEN");

const server = startServer(0);
try {
const settings = await fetch(new URL("/api/settings", server.url), {
headers: { "x-opencodex-api-key": adminToken },
});
expect(settings.status).toBe(503);
const body = await settings.json() as { error?: string; hint?: string; reason?: string };
expect(body.error).toBe("management API unavailable");
expect(body.hint).toContain("OPENCODEX_ADMIN_AUTH_TOKEN");
expect(body.reason).toContain("OPENCODEX_ADMIN_AUTH_TOKEN");
expect((await fetch(new URL("/healthz", server.url))).status).toBe(200);
} finally {
await server.stop(true);
}
});

test("required management harden retries after a soft loadConfig directory timeout", async () => {
Expand Down Expand Up @@ -549,7 +537,15 @@ describe("management and data-plane credential separation", () => {
saveConfig(remoteConfig());
process.env.USERNAME ??= "tester";
setPlatformForTests("win32");
setIcaclsRunnerForTests(() => ({ success: false, exitCode: null, timedOut: true, stdout: "" }));
// Env management auth never needs the token file. Time out only that path so
// startServer's config-mutation directory harden can still succeed on win32.
setIcaclsRunnerForTests(args => {
const target = args[0] ?? "";
if (target.endsWith("admin-api-token")) {
return { success: false, exitCode: null, timedOut: true, stdout: "" };
}
return { success: true, exitCode: 0, timedOut: false, stdout: "" };
});
resetHardenedStateForTests();

const state = initializeManagementAuthState(remoteConfig());
Expand Down
2 changes: 2 additions & 0 deletions tests/shutdown-drain.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,8 @@ describe("background shell shutdown drain", () => {
test("shell drain rejection or unresolved termination still calls server.stop", async () => {
const unresolvedChild = installShutdownShell();
setBackgroundShellRuntimeForTests({
// Collapse grace waits to next tick; keep the timer ref'd so isolate does
// not starve the waiter the way an unref'd setTimeout can.
setTimer(callback) {
// Keep this fixture timer REF'D: Bun on Windows can stop servicing
// unref'd timers while the test's only pending work is a promise,
Expand Down
Loading
Loading