Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
31 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
c12838f
merge(dev): clear conflicts so Cross-platform CI can run again
Wibias Aug 1, 2026
a7cb033
Merge remote-tracking branch 'upstream/dev' into fix/macos-isolate-wo…
Wibias Aug 1, 2026
b031cd1
merge(dev): absorb #827 isolate settle and ACL stub fixes
Wibias Aug 1, 2026
8ddf196
test(storage): budget satellite restore-failure cleanup on Windows
Wibias Aug 1, 2026
ebdf535
Merge remote-tracking branch 'upstream/dev' into fix/macos-isolate-wo…
Wibias Aug 1, 2026
4d7ee28
fix(storage): lengthen Windows Worker OS-join settle under isolate
Wibias Aug 1, 2026
9e7cc30
test(storage): budget multi-satellite quarantine restore on Windows
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 @@ -336,9 +336,10 @@ function waitForBackgroundShellClose(entry: BackgroundShellEntry): Promise<boole
return new Promise(resolveWait => {
let settled = false;
// 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.
const timer = backgroundShellRuntime.setTimer(() => {
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
2 changes: 1 addition & 1 deletion src/oauth/store.ts
Original file line number Diff line number Diff line change
Expand Up @@ -419,7 +419,7 @@ function serializeMutation<T>(work: () => Promise<T>, retainedValues: readonly u
// Only unref the long default wait. Short waitMs (tests) must stay ref'd:
// on Windows Bun under `bun test --isolate`, an unref'd timer can fail to
// fire while the head mutation holds an unresolved Promise, hanging the
// waiter forever (#827).
// waiter forever (#827 / full admission queue hang on windows-latest).
if (waitMs >= OAUTH_MUTATION_WAIT_MS) entry.timeout.unref?.();
mutationWaiters.push(entry);
drainOAuthMutations();
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
8 changes: 5 additions & 3 deletions src/storage/worker-lifecycle.ts
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,12 @@ let spawnCancelEpoch = 0;
/**
* OS-join gap after the `close` event on platforms where Bun's Worker reclaim
* races the isolate/file boundary (not a CI job-timeout bump).
* Windows GHA at 250ms still left `workers_spawned(N) workers_terminated(N-1)`
* panics under isolate; 750ms covers deferred reclaim. Darwin uses 250ms.
* Windows GHA at 250ms and 750ms still left `workers_spawned(N)
* workers_terminated(N-1)` panics under isolate (seen mid
* `storage-mutation-race` with 11/10). 1500ms covers deferred reclaim under
* stacked policy/restore workers. Darwin uses 250ms.
*/
const WORKER_OS_JOIN_MS = process.platform === "win32" ? 750 : 250;
const WORKER_OS_JOIN_MS = process.platform === "win32" ? 1_500 : 250;

function needsWorkerOsJoinSettle(): boolean {
return process.platform === "win32" || process.platform === "darwin";
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 @@ -1615,7 +1615,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);
}, 5_000); // waitMs is 10; keep a hard ceiling so an unref regression cannot hang CI.
}, { 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
23 changes: 6 additions & 17 deletions tests/server-management-auth.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -474,7 +474,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 @@ -492,25 +492,13 @@ describe("management and data-plane credential separation", () => {
return { success: true, exitCode: 0, timedOut: false, 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 @@ -555,7 +543,8 @@ describe("management and data-plane credential separation", () => {
setPlatformForTests("win32");
// Env-token init never needs file ACL. Time out management-token paths so a
// broken file-backed ACL cannot be what made management available; allow
// other file hardens so startServer → saveConfig works on real win32.
// other file hardens so startServer → saveConfig works on real win32
// (config-mutation directory harden soft-fails home timeouts).
setIcaclsRunnerForTests(args => {
const target = args[0] ?? "";
if (target === testHome || target.endsWith("admin-api-token")) {
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