Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
9 changes: 7 additions & 2 deletions tests/claude-integration/claude-desktop-picker.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,10 @@ import { createCertificateAuthority } from "../../src/claude/intercept/local-ca"
import { PICKER_CA_COMMON_NAME, PICKER_HOST } from "../../src/claude/intercept/picker-ca";
import { memoryPickerCaStore } from "../helpers/picker-ca-store";
import type { OcxConfig } from "../../src/types";
import { isolateClientLifecycleLock } from "../helpers/client-lifecycle-lock";

let root = "";
let lifecycleLock: ReturnType<typeof isolateClientLifecycleLock>;

function config(extra: Partial<OcxConfig> = {}): OcxConfig {
return {
Expand Down Expand Up @@ -136,8 +138,11 @@ function controllerFor(options: {
});
}

beforeEach(() => { root = mkdtempSync(join(tmpdir(), "ocx-picker-controller-")); });
afterEach(() => { rmSync(root, { recursive: true, force: true }); });
beforeEach(() => {
root = mkdtempSync(join(tmpdir(), "ocx-picker-controller-"));
lifecycleLock = isolateClientLifecycleLock(root);
});
afterEach(() => { lifecycleLock.restore(); rmSync(root, { recursive: true, force: true }); });

test("enable orders trust, fresh recheck, profile, and rearm", async () => {
const events: string[] = [];
Expand Down
4 changes: 4 additions & 0 deletions tests/claude-integration/claude-management-api.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,14 @@ import { installIsolatedCodexHome, type IsolatedCodexHome } from "../helpers/iso
import { MANAGEMENT_JSON_BODY_MAX_BYTES } from "../../src/server/management/body";
import { removeTreeWithRetry } from "../helpers/remove-tree";
import { desktopFirstPartyTarget } from "../../src/claude/desktop-first-party";
import { isolateClientLifecycleLock } from "../helpers/client-lifecycle-lock";

// Full-suite Windows load: startServer + multi-PUT management flows often exceed bun's
// default 5s per-test budget (same flake class as 810fa115 / kiro-oauth).
setDefaultTimeout(30_000);

let testDir = "";
let lifecycleLock: ReturnType<typeof isolateClientLifecycleLock>;
let previousHome: string | undefined;
let previousClaudeConfigDir: string | undefined;
let previousDesktopConfigDir: string | undefined;
Expand All @@ -45,6 +47,7 @@ beforeEach(() => {
previousDesktopConfigDir = process.env.OPENCODEX_CLAUDE_DESKTOP_CONFIG_DIR;
isolatedCodexHome = installIsolatedCodexHome("ocx-claude-mgmt-");
testDir = mkdtempSync(join(tmpdir(), "ocx-claude-mgmt-"));
lifecycleLock = isolateClientLifecycleLock(testDir);
process.env.OPENCODEX_HOME = testDir;
// These API tests intentionally toggle agent injection off. Never let that
// prune the developer's real ~/.claude/agents directory.
Expand All @@ -60,6 +63,7 @@ beforeEach(() => {
});

afterEach(() => {
lifecycleLock.restore();
if (previousHome === undefined) delete process.env.OPENCODEX_HOME;
else process.env.OPENCODEX_HOME = previousHome;
if (previousClaudeConfigDir === undefined) delete process.env.CLAUDE_CONFIG_DIR;
Expand Down
4 changes: 4 additions & 0 deletions tests/claude-integration/claude-picker-relay-intent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,12 @@ import { getClaudePickerRuntime, startClaudeIntercept } from "../../src/claude/i
import type { OcxConfig } from "../../src/types";
import { memoryPickerCaStore } from "../helpers/picker-ca-store";
import { removeTreeWithRetry } from "../helpers/remove-tree";
import { isolateClientLifecycleLock } from "../helpers/client-lifecycle-lock";

const envKeys = ["OPENCODEX_HOME", "OPENCODEX_CLAUDE_DESKTOP_CONFIG_DIR", "CLAUDE_CONFIG_DIR"] as const;
let previous: Array<string | undefined>;
let root: string;
let lifecycleLock: ReturnType<typeof isolateClientLifecycleLock>;
let config: OcxConfig;
let handle: Awaited<ReturnType<typeof startClaudeIntercept>>;
let upstream: Server | undefined;
Expand All @@ -23,6 +25,7 @@ const binds: Array<{ requested: number; port: number; options: ConnectProxyOptio
beforeEach(() => {
previous = envKeys.map(key => process.env[key]);
root = mkdtempSync(join(tmpdir(), "ocx-picker-relay-intent-"));
lifecycleLock = isolateClientLifecycleLock(root);
process.env.OPENCODEX_HOME = root;
process.env.OPENCODEX_CLAUDE_DESKTOP_CONFIG_DIR = join(root, "desktop");
process.env.CLAUDE_CONFIG_DIR = join(root, "claude");
Expand All @@ -38,6 +41,7 @@ afterEach(async () => {
await handle?.stop();
if (upstream) await new Promise<void>((resolve, reject) => upstream!.close(error => error ? reject(error) : resolve()));
} finally {
lifecycleLock.restore();
handle = null;
upstream = undefined;
envKeys.forEach((key, index) => {
Expand Down
51 changes: 33 additions & 18 deletions tests/claude-integration/claude-picker-runtime.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,11 +20,13 @@ import {
type PickerRuntime,
} from "../../src/claude/intercept/picker-runtime";
import type { SecurityRunner } from "../../src/claude/intercept/picker-trust";
import { getClaudePickerRuntime, startClaudeIntercept } from "../../src/claude/intercept/runtime";
import { getClaudePickerController, getClaudePickerRuntime, startClaudeIntercept } from "../../src/claude/intercept/runtime";
import type { OcxConfig } from "../../src/types";
import { removeTreeWithRetry } from "../helpers/remove-tree";
import { isolateClientLifecycleLock } from "../helpers/client-lifecycle-lock";

let root = "";
let lifecycleLock: ReturnType<typeof isolateClientLifecycleLock>;
const previous: Record<string, string | undefined> = {};
const ENV_KEYS = ["OPENCODEX_HOME", "OPENCODEX_CLAUDE_DESKTOP_CONFIG_DIR", "CLAUDE_CONFIG_DIR"] as const;
const running: PickerRuntime[] = [];
Expand Down Expand Up @@ -87,20 +89,26 @@ const INTERCEPT = { kind: "intercept", port: LISTENER_PORT };

beforeEach(() => {
root = mkdtempSync(join(tmpdir(), "ocx-picker-runtime-"));
lifecycleLock = isolateClientLifecycleLock(root);
for (const key of ENV_KEYS) previous[key] = process.env[key];
process.env.OPENCODEX_HOME = root;
process.env.OPENCODEX_CLAUDE_DESKTOP_CONFIG_DIR = join(root, "desktop-library");
process.env.CLAUDE_CONFIG_DIR = join(root, "claude");
});

afterEach(async () => {
for (const created of running.splice(0)) await created.stop();
for (const close of closeFixtures.splice(0)) await close();
for (const key of ENV_KEYS) {
if (previous[key] === undefined) delete process.env[key];
else process.env[key] = previous[key];
try {
for (const created of running.splice(0)) await created.stop();
for (const close of closeFixtures.splice(0)) await close();
} finally {
// A rejected stop or close must not leave the lifecycle spies, environment or root behind.
lifecycleLock.restore();
for (const key of ENV_KEYS) {
if (previous[key] === undefined) delete process.env[key];
else process.env[key] = previous[key];
}
removeTreeWithRetry(root);
Comment thread
lidge-jun marked this conversation as resolved.
Resolved
}
removeTreeWithRetry(root);
});

describe("pickerDesired", () => {
Expand Down Expand Up @@ -300,6 +308,13 @@ async function heldEgress(dialUpstream?: Parameters<typeof startConnectProxy>[1]
return { port: proxy.port, startProxy, close: proxy.close };
}

// A restart with an applied profile starts a detached controller.enable; join it before stopping,
// so it cannot run after afterEach restores the lifecycle spy and reach the account-wide lock.
async function stopAfterPickerSettles(handle: { stop(): Promise<void> } | null | undefined): Promise<void> {
try { await getClaudePickerController()?.transition(async () => {}); }
finally { await handle?.stop(); }
}

const BROWSER_UA = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Claude/2.7032.0";

function connectStatusLine(port: number, host: string, userAgent?: string): Promise<string> {
Expand Down Expand Up @@ -436,7 +451,7 @@ describe("startClaudeIntercept wiring", () => {
expect(names).not.toContain("add-trusted-cert");
expect(getClaudePickerRuntime()?.selectTunnel("claude.ai", 443)).toEqual(INTERCEPT);
} finally {
await handle?.stop();
await stopAfterPickerSettles(handle);
}
expect(getClaudePickerRuntime()).toBeNull();
});
Expand Down Expand Up @@ -469,7 +484,7 @@ describe("startClaudeIntercept wiring", () => {
expect(names).not.toContain("remove-trusted-cert");
expect(names).not.toContain("delete-certificate");
} finally {
await handle?.stop();
await stopAfterPickerSettles(handle);
}
expect(getClaudePickerRuntime()).toBeNull();
});
Expand Down Expand Up @@ -520,7 +535,7 @@ describe("startClaudeIntercept wiring", () => {
expect(pickerCaFingerprints(readFileSync(pickerCaCertPath(root), "utf8")).sha1).not.toBe(foreignSha1);
expect(getClaudePickerRuntime()).toBe(fake);
} finally {
await handle?.stop();
await stopAfterPickerSettles(handle);
}
expect(getClaudePickerRuntime()).toBeNull();
});
Expand Down Expand Up @@ -556,7 +571,7 @@ describe("startClaudeIntercept wiring", () => {
expect(names).not.toContain("remove-trusted-cert");
expect(names).not.toContain("delete-certificate");
} finally {
await handle?.stop();
await stopAfterPickerSettles(handle);
}
expect(getClaudePickerRuntime()).toBeNull();
});
Expand Down Expand Up @@ -628,7 +643,7 @@ describe("startClaudeIntercept wiring", () => {
expect(await canBind(handle!.proxyPort)).toBe(false);
expect(await connectStatusLine(handle!.proxyPort, "api.anthropic.com")).toContain("200");
} finally {
await handle?.stop();
await stopAfterPickerSettles(handle);
await new Promise<void>(resolve => upstream.close(() => resolve()));
}
expect(getClaudePickerRuntime()).toBeNull();
Expand Down Expand Up @@ -669,7 +684,7 @@ describe("startClaudeIntercept wiring", () => {
expect(readFileSync(join(pickerStateDir(root), "profile-state.json"), "utf8")).toBe(stateBefore);
expect(existsSync(pickerCaPendingUntrustPath(root))).toBe(true);
} finally {
await handle?.stop();
await stopAfterPickerSettles(handle);
await new Promise<void>(resolve => squatter.close(() => resolve()));
}
});
Expand Down Expand Up @@ -701,7 +716,7 @@ describe("startClaudeIntercept wiring", () => {
expect(calls).toEqual([]);
expect(readFileSync(pickerCaPendingUntrustPath(root), "utf8")).toBe("{corrupt");
} finally {
await handle?.stop();
await stopAfterPickerSettles(handle);
}
});

Expand Down Expand Up @@ -769,7 +784,7 @@ describe("startClaudeIntercept wiring", () => {
const metadata = JSON.parse(readFileSync(join(library, "_meta.json"), "utf8")) as { entries: { name: string }[] };
expect(metadata.entries.some(entry => entry.name === "opencodex-standard")).toBe(false);
} finally {
await handle?.stop();
await stopAfterPickerSettles(handle);
}
});

Expand Down Expand Up @@ -827,7 +842,7 @@ describe("startClaudeIntercept wiring", () => {
expect(inspectDesktopPickerProfile({ configDir: root, platform: "darwin" }))
.toMatchObject({ kind: "applied", entryId: original.entryId, proxyUrl: original.proxyUrl });
} finally {
await first?.stop();
await stopAfterPickerSettles(first);
}
// The next restart adopts the held relay; the surviving selection restores in place.
retry = true;
Expand All @@ -845,7 +860,7 @@ describe("startClaudeIntercept wiring", () => {
expect(inspectDesktopPickerProfile({ configDir: root, platform: "darwin" }))
.toMatchObject({ kind: "applied", entryId: original.entryId, proxyUrl: restoredUrl });
} finally {
await second?.stop();
await stopAfterPickerSettles(second);
}
} finally { await egress.close(); }
});
Expand Down Expand Up @@ -886,7 +901,7 @@ describe("startClaudeIntercept wiring", () => {
expect(await connectStatusLine(pickerPort, "spoofed-ua.invalid", `${BROWSER_UA} spoofed`)).toContain("502");
expect(asked).toEqual(["picker-probe.invalid", "spoofed-ua.invalid"]);
} finally {
await handle?.stop();
await stopAfterPickerSettles(handle);
}
expect(getClaudePickerRuntime()).toBeNull();
expect(await canBind(pickerPort)).toBe(true);
Expand Down
18 changes: 16 additions & 2 deletions tests/claude-integration/claude-picker-startup.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { afterEach, expect, spyOn, test } from "bun:test";
import { afterEach, beforeEach, expect, spyOn, test } from "bun:test";
import { existsSync, mkdirSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { pathToFileURL } from "node:url";
Expand All @@ -17,8 +17,11 @@ import { flushConfigDirHardeningAndReaps, hardenConfigDir } from "../../src/conf
import { resetHardenedStateForTests, setAsyncIcaclsRunnerForTests, setPlatformForTests } from "../../src/lib/windows-secret-acl";
import { removeTreeWithRetry } from "../helpers/remove-tree";
import type { OcxConfig } from "../../src/types";
import { isolateClientLifecycleLock } from "../helpers/client-lifecycle-lock";

const roots: string[] = [];
let lifecycleLock: ReturnType<typeof isolateClientLifecycleLock>;
let lockRoot = "";
const handles: Array<Awaited<ReturnType<typeof startClaudeIntercept>>> = [];
const priorHome = process.env.OPENCODEX_HOME;
const priorDesktop = process.env.OPENCODEX_CLAUDE_DESKTOP_CONFIG_DIR;
Expand All @@ -29,6 +32,9 @@ const priorDesktop = process.env.OPENCODEX_CLAUDE_DESKTOP_CONFIG_DIR;
// even when an earlier one fails, the environment is always restored, and failures are rethrown.
async function cleanupPickerStartup(remove: (dir: string) => void = removeTreeWithRetry): Promise<void> {
const failures: unknown[] = [];
// A restart with an applied profile starts a detached controller.enable; join it before the
// handles stop and the lifecycle spy is restored, so it cannot reach the account-wide lock.
try { await getClaudePickerController()?.transition(async () => {}); } catch (error) { failures.push(error); }
for (const handle of handles.splice(0)) {
try { await handle?.stop(); } catch (error) { failures.push(error); }
}
Expand All @@ -43,7 +49,15 @@ async function cleanupPickerStartup(remove: (dir: string) => void = removeTreeWi
if (failures.length === 1) throw failures[0];
if (failures.length > 1) throw new AggregateError(failures, "picker-startup cleanup failed");
}
afterEach(() => cleanupPickerStartup());
// Cases own multiple roots; keep their cleanup oracle intact and give the lock its own temp root.
beforeEach(() => {
lockRoot = mkdtempSync(join(tmpdir(), "ocx-picker-startup-lock-"));
lifecycleLock = isolateClientLifecycleLock(lockRoot);
});
afterEach(async () => {
try { await cleanupPickerStartup(); }
finally { lifecycleLock.restore(); removeTreeWithRetry(lockRoot); }
});
const routes = async () => ({ nativeSlugs: [], routedModels: [] });
function setup() {
const root = mkdtempSync(join(tmpdir(), "ocx-picker-startup-")); roots.push(root);
Expand Down
18 changes: 16 additions & 2 deletions tests/claude-integration/claude-picker-windows.test.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { afterEach, expect, test } from "bun:test";
import { afterEach, beforeEach, expect, test } from "bun:test";
import { existsSync, mkdirSync, mkdtempSync, readFileSync, writeFileSync } from "node:fs";
import { createServer as createHttpsServer } from "node:https";
import { connect } from "node:net";
Expand Down Expand Up @@ -26,8 +26,11 @@ import {
import type { SecurityResult, SecurityRunner } from "../../src/claude/intercept/picker-trust";
import { filePickerCaStore, memoryPickerCaStore } from "../helpers/picker-ca-store";
import type { OcxConfig } from "../../src/types";
import { isolateClientLifecycleLock } from "../helpers/client-lifecycle-lock";

const roots: string[] = [];
let lifecycleLock: ReturnType<typeof isolateClientLifecycleLock>;
let lockRoot = "";
const handles: Array<Awaited<ReturnType<typeof startClaudeIntercept>>> = [];
const priorHome = process.env.OPENCODEX_HOME;
const priorDesktop = process.env.OPENCODEX_CLAUDE_DESKTOP_CONFIG_DIR;
Expand All @@ -37,6 +40,9 @@ const priorClaude = process.env.CLAUDE_CONFIG_DIR;
// Drain hardening and reaps before retrying removal; retain failures and restore env.
async function cleanupPickerWindows(remove: (dir: string) => void = removeTreeWithRetry): Promise<void> {
const failures: unknown[] = [];
// A restart with an applied profile starts a detached controller.enable; join it before the
// handles stop and the lifecycle spy is restored, so it cannot reach the account-wide lock.
try { await getClaudePickerController()?.transition(async () => {}); } catch (error) { failures.push(error); }
for (const handle of handles.splice(0)) {
try { await handle?.stop(); } catch (error) { failures.push(error); }
}
Expand All @@ -52,7 +58,15 @@ async function cleanupPickerWindows(remove: (dir: string) => void = removeTreeWi
if (failures.length === 1) throw failures[0];
if (failures.length > 1) throw new AggregateError(failures, "picker-windows cleanup failed");
}
afterEach(() => cleanupPickerWindows());
// Cases own multiple roots; keep their cleanup oracle intact and give the lock its own temp root.
beforeEach(() => {
lockRoot = mkdtempSync(join(tmpdir(), "ocx-picker-windows-lock-"));
lifecycleLock = isolateClientLifecycleLock(lockRoot);
});
afterEach(async () => {
try { await cleanupPickerWindows(); }
finally { lifecycleLock.restore(); removeTreeWithRetry(lockRoot); }
});
Comment thread
coderabbitai[bot] marked this conversation as resolved.
Resolved

const routes = async () => ({ nativeSlugs: [], routedModels: [] });

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,10 @@ import { armClaudeCodeBaseline, saveConfigPreservingClaudeCode } from "../../src
import type { ManagementApiDeps } from "../../src/server/management/context";
import type { OcxConfig } from "../../src/types";
import { removeTreeWithRetry } from "../helpers/remove-tree";
import { isolateClientLifecycleLock } from "../helpers/client-lifecycle-lock";

let root = "";
let lifecycleLock: ReturnType<typeof isolateClientLifecycleLock>;
let library = "";
let previousHome: string | undefined;
let previousLibrary: string | undefined;
Expand Down Expand Up @@ -106,6 +108,7 @@ function oversizedTrackedBody(): {

beforeEach(() => {
root = mkdtempSync(join(tmpdir(), "ocx-desktop-toggle-"));
lifecycleLock = isolateClientLifecycleLock(root);
library = join(root, "desktop-library");
previousHome = process.env.OPENCODEX_HOME;
previousLibrary = process.env.OPENCODEX_CLAUDE_DESKTOP_CONFIG_DIR;
Expand All @@ -117,6 +120,7 @@ beforeEach(() => {
});

afterEach(() => {
lifecycleLock.restore();
if (previousHome === undefined) delete process.env.OPENCODEX_HOME;
else process.env.OPENCODEX_HOME = previousHome;
if (previousLibrary === undefined) delete process.env.OPENCODEX_CLAUDE_DESKTOP_CONFIG_DIR;
Expand Down
Loading