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
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import { setSyntheticWindowsPrincipalForTests } from "../../src/lib/windows-user
import { writeServiceApiTokenFile } from "../../src/lib/service-secrets";
import type { OcxConfig } from "../../src/types";
import { removeTreeWithRetry } from "../helpers/remove-tree";
import { isolateClientLifecycleLock } from "../helpers/client-lifecycle-lock";

const success: IcaclsResult = { success: true, exitCode: 0, timedOut: false, stdout: "" };
const turn = () => new Promise<void>(done => setImmediate(done));
Expand All @@ -31,10 +32,12 @@ function deferred() {
}
const keys = ["OPENCODEX_HOME", "CODEX_HOME", "CLAUDE_CONFIG_DIR", "OPENCODEX_CLAUDE_DESKTOP_CONFIG_DIR", "OPENCODEX_ACL_TIMEOUT_MS", "OPENCODEX_ACL_VERIFY_EXISTING"] as const;
let root: string;
let lifecycleLock: ReturnType<typeof isolateClientLifecycleLock>;
let previous: (string | undefined)[];
let restores: Array<() => void>;
beforeEach(() => {
root = mkdtempSync(join(tmpdir(), "ocx-config-writer-barrier-"));
lifecycleLock = isolateClientLifecycleLock(root);
previous = keys.map(key => process.env[key]);
process.env.OPENCODEX_HOME = root;
process.env.CODEX_HOME = join(root, "codex");
Expand All @@ -53,6 +56,7 @@ beforeEach(() => {
afterEach(async () => {
await paths.flushConfigDirHardeningAndReaps(root);
for (const restore of restores.reverse()) restore();
lifecycleLock.restore();
setAsyncIcaclsRunnerForTests(null);
setAsyncIcaclsBeltSchedulerForTests(null);
setNowForTests(null);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import { setSyntheticWindowsPrincipalForTests } from "../../src/lib/windows-user
import { writeServiceApiTokenFile } from "../../src/lib/service-secrets";
import type { OcxConfig } from "../../src/types";
import { removeTreeWithRetry } from "../helpers/remove-tree";
import { isolateClientLifecycleLock } from "../helpers/client-lifecycle-lock";

const success: IcaclsResult = { success: true, exitCode: 0, timedOut: false, stdout: "" };
const turn = () => new Promise<void>(done => setImmediate(done));
Expand All @@ -26,10 +27,12 @@ function deferred() {
}
const keys = ["OPENCODEX_HOME", "CLAUDE_CONFIG_DIR", "OPENCODEX_CLAUDE_DESKTOP_CONFIG_DIR", "OPENCODEX_ACL_TIMEOUT_MS", "OPENCODEX_ACL_VERIFY_EXISTING"] as const;
let root: string;
let lifecycleLock: ReturnType<typeof isolateClientLifecycleLock>;
let previous: (string | undefined)[];
let restores: Array<() => void>;
beforeEach(() => {
root = mkdtempSync(join(tmpdir(), "ocx-config-writer-barrier-"));
lifecycleLock = isolateClientLifecycleLock(root);
previous = keys.map(key => process.env[key]);
process.env.OPENCODEX_HOME = root;
process.env.CLAUDE_CONFIG_DIR = join(root, "claude");
Expand All @@ -45,6 +48,7 @@ beforeEach(() => {
afterEach(async () => {
await paths.flushConfigDirHardeningAndReaps(root);
for (const restore of restores.reverse()) restore();
lifecycleLock.restore();
setAsyncIcaclsRunnerForTests(null);
setAsyncIcaclsBeltSchedulerForTests(null);
setNowForTests(null);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,14 @@ import { syncEnabledClientIntegrations } from "../../src/server/management/confi
import type { CatalogModel } from "../../src/codex/catalog";
import type { OcxConfig } from "../../src/types";
import { removeTreeWithRetry } from "../helpers/remove-tree";
import { isolateClientLifecycleLock } from "../helpers/client-lifecycle-lock";

// Gateway is the default Desktop mode. These guards keep that default from ever writing a
// gateway profile behind a Desktop that runs first-party, and keep foreign proxy env from being
// read as a first-party install.

let root = "";
let lifecycleLock: ReturnType<typeof isolateClientLifecycleLock>;
let library = "";
let claudeDir = "";
const previous: Record<string, string | undefined> = {};
Expand Down Expand Up @@ -78,6 +80,7 @@ function gatedDiscovery(models: CatalogModel[]) {

beforeEach(() => {
root = mkdtempSync(join(tmpdir(), "ocx-desktop-1p-guards-"));
lifecycleLock = isolateClientLifecycleLock(root);
library = join(root, "desktop-library");
claudeDir = join(root, "claude");
for (const key of ENV_KEYS) previous[key] = process.env[key];
Expand All @@ -88,6 +91,7 @@ beforeEach(() => {
});

afterEach(() => {
lifecycleLock.restore();
resetSiblingStartForTests();
for (const key of ENV_KEYS) {
if (previous[key] === undefined) delete process.env[key];
Expand Down
33 changes: 31 additions & 2 deletions tests/claude-integration/claude-desktop-first-party.test.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
import { afterEach, beforeEach, expect, test } from "bun:test";
import { afterEach, beforeEach, expect, spyOn, test } from "bun:test";
import { existsSync, mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { isAbsolute, join, relative, sep } from "node:path";
import * as userIdentity from "../../src/codex/user-identity";
import { isolateClientLifecycleLock } from "../helpers/client-lifecycle-lock";
import {
applyDesktopFirstParty,
inspectDesktopFirstParty,
Expand All @@ -25,6 +27,7 @@ import { removeTreeWithRetry } from "../helpers/remove-tree";
import { flushConfigDirHardeningAndReaps } from "../../src/config/paths";

let root = "";
let lifecycleLock: ReturnType<typeof isolateClientLifecycleLock>;
let library = "";
let claudeDir = "";
const previous: Record<string, string | undefined> = {};
Expand Down Expand Up @@ -61,6 +64,7 @@ function expectStatus(result: { status: number; body: unknown }, status: number)

beforeEach(() => {
root = mkdtempSync(join(tmpdir(), "ocx-desktop-1p-"));
lifecycleLock = isolateClientLifecycleLock(root);
library = join(root, "desktop-library");
claudeDir = join(root, "claude");
for (const key of ENV_KEYS) previous[key] = process.env[key];
Expand All @@ -72,6 +76,7 @@ beforeEach(() => {

afterEach(async () => {
await flushConfigDirHardeningAndReaps(root);
lifecycleLock.restore();
for (const key of ENV_KEYS) {
if (previous[key] === undefined) delete process.env[key];
else process.env[key] = previous[key];
Expand Down Expand Up @@ -237,6 +242,30 @@ test("POST /api/claude-desktop/apply defaults to gateway; first-party is explici
expect(resolveClaudeDesktopMode({ claudeCode: { ...savedBack.claudeCode, desktopMode: undefined } }, { ownedFirstPartySettings: true })).toBe("first-party");
});

test("management gateway and first-party apply acquire only fixture lifecycle locks", async () => {
// If fixture injection regresses, refuse before opening the real user's lock.
const productionRoot = spyOn(userIdentity, "resolveEffectiveUserRuntimeRoot").mockImplementation(() => {
throw new Error("management fixture reached the production lifecycle namespace");
});
try {
for (const mode of ["gateway", "first-party"] as const) {
lifecycleLock.acquisitions.length = 0;
const result = await dispatch("/api/claude-desktop/apply", {
method: "POST", body: JSON.stringify({ mode }),
}, JSON.parse(readFileSync(join(root, "config.json"), "utf8")), { fetchAllModels: async () => [] });
expect(lifecycleLock.acquisitions.length).toBeGreaterThan(0);
for (const { path } of lifecycleLock.acquisitions) {
const within = relative(root, path);
expect(within).not.toBe("");
expect(isAbsolute(within)).toBe(false);
expect(within === ".." || within.startsWith(`..${sep}`)).toBe(false);
}
expectStatus(result, 200);
}
expect(productionRoot).not.toHaveBeenCalled();
} finally { productionRoot.mockRestore(); }
});

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 }) });
expectStatus(enabled, 200);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,13 @@ import { getClaudePickerRuntime, startClaudeIntercept, type ClaudeInterceptHandl
import { handleManagementAPI } from "../../src/server/management-api";
import type { OcxConfig } from "../../src/types";
import { removeTreeWithRetry } from "../helpers/remove-tree";
import { isolateClientLifecycleLock } from "../helpers/client-lifecycle-lock";

// End to end over the management routes: a real picker runtime and controller behind the
// dedicated egress proxy, with the keychain, the claude.ai listener and discovery faked.

let root = "";
let lifecycleLock: ReturnType<typeof isolateClientLifecycleLock>;
let handle: ClaudeInterceptHandle | null = null;
const previous: Record<string, string | undefined> = {};
const ENV_KEYS = ["OPENCODEX_HOME", "OPENCODEX_CLAUDE_DESKTOP_CONFIG_DIR", "CLAUDE_CONFIG_DIR"] as const;
Expand Down Expand Up @@ -124,6 +126,7 @@ test("picker egress uses HTTPS Desktop entrypoint after a UA-less CONNECT", asyn

beforeEach(() => {
root = mkdtempSync(join(tmpdir(), "ocx-picker-routes-"));
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");
Expand All @@ -135,6 +138,7 @@ beforeEach(() => {
afterEach(async () => {
await handle?.stop();
handle = null;
lifecycleLock.restore();
for (const key of ENV_KEYS) {
if (previous[key] === undefined) delete process.env[key];
else process.env[key] = previous[key];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ import { reconcileClaudeFirstPartySettings } from "../../src/claude/first-party-
import { handleManagementAPI } from "../../src/server/management-api";
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 claudeDir = "";
let config: OcxConfig;
const previous: Record<string, string | undefined> = {};
Expand Down Expand Up @@ -60,6 +62,7 @@ async function dispatch(target: "cli" | "desktop", enabled = true, deps: Managem

beforeEach(() => {
root = mkdtempSync(join(tmpdir(), "ocx-first-party-agents-"));
lifecycleLock = isolateClientLifecycleLock(root);
claudeDir = join(root, "claude");
for (const key of ENV_KEYS) previous[key] = process.env[key];
process.env.OPENCODEX_HOME = root;
Expand All @@ -77,6 +80,7 @@ beforeEach(() => {
});

afterEach(() => {
lifecycleLock.restore();
for (const key of ENV_KEYS) {
if (previous[key] === undefined) delete process.env[key];
else process.env[key] = previous[key];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import { handleManagementAPI } from "../../src/server/management-api";
import { previewClaudeModelDefaults } from "../../src/claude/model-default-settings";
import type { OcxConfig } from "../../src/types";
import { removeTreeWithRetry } from "../helpers/remove-tree";
import { isolateClientLifecycleLock } from "../helpers/client-lifecycle-lock";

const success: IcaclsResult = { success: true, exitCode: 0, timedOut: false, stdout: "" };
const sid = "S-1-5-21-1-2-3-1001";
Expand All @@ -38,11 +39,13 @@ function deferred() {
}
const keys = ["OPENCODEX_HOME", "CLAUDE_CONFIG_DIR", "OPENCODEX_CLAUDE_DESKTOP_CONFIG_DIR", "OPENCODEX_ACL_VERIFY_EXISTING", "OPENCODEX_ACL_TIMEOUT_MS"] as const;
let root: string;
let lifecycleLock: ReturnType<typeof isolateClientLifecycleLock>;
let previous: (string | undefined)[];
let entrySpy: ReturnType<typeof spyOn> | undefined;

beforeEach(() => {
root = mkdtempSync(join(tmpdir(), "ocx-intercept-acl-barrier-"));
lifecycleLock = isolateClientLifecycleLock(root);
previous = keys.map(key => process.env[key]);
process.env.OPENCODEX_HOME = root;
process.env.CLAUDE_CONFIG_DIR = join(root, "claude");
Expand Down Expand Up @@ -77,6 +80,7 @@ afterEach(async () => {
await flushConfigDirHardeningAndReaps(root);
entrySpy?.mockRestore();
entrySpy = undefined;
lifecycleLock.restore();
setAsyncIcaclsRunnerForTests(null);
setAsyncIcaclsBeltSchedulerForTests(null);
setNowForTests(null);
Expand Down
4 changes: 4 additions & 0 deletions tests/claude-integration/claude-messages-endpoint.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,7 @@ import { estimateTokens } from "../../src/lib/token-estimate";
import type { OcxConfig } from "../../src/types";
import { installIsolatedCodexHome, type IsolatedCodexHome } from "../helpers/isolated-codex-home";
import { removeTreeWithRetry } from "../helpers/remove-tree";
import { isolateClientLifecycleLock } from "../helpers/client-lifecycle-lock";
import { acquireOwnedSpendHome } from "../helpers/owned-spend-home";
import { SERVER_BUDGET_MS } from "../helpers/test-budget";
import { createTestTranslatorBudget } from "../helpers/translator-budget";
Expand All @@ -49,6 +50,7 @@ import {
} from "../../src/codex/native-profile-startup";

let testDir = "";
let lifecycleLock: ReturnType<typeof isolateClientLifecycleLock>;
let previousHome: string | undefined;
let previousDesktopConfigDir: string | undefined;
let isolatedCodexHome: IsolatedCodexHome | null = null;
Expand All @@ -58,6 +60,7 @@ beforeEach(() => {
previousHome = process.env.OPENCODEX_HOME;
isolatedCodexHome = installIsolatedCodexHome("ocx-claude-endpoint-");
testDir = mkdtempSync(join(tmpdir(), "ocx-claude-endpoint-"));
lifecycleLock = isolateClientLifecycleLock(testDir);
process.env.OPENCODEX_HOME = testDir;
previousDesktopConfigDir = process.env.OPENCODEX_CLAUDE_DESKTOP_CONFIG_DIR;
process.env.OPENCODEX_CLAUDE_DESKTOP_CONFIG_DIR = join(testDir, "claude-desktop");
Expand All @@ -66,6 +69,7 @@ beforeEach(() => {

afterEach(async () => {
if (testDir) await flushConfigDirHardeningAndReaps(testDir);
lifecycleLock.restore();
if (isolatedCodexHome) await flushConfigDirHardeningAndReaps(isolatedCodexHome.path);
if (previousHome === undefined) delete process.env.OPENCODEX_HOME;
else process.env.OPENCODEX_HOME = previousHome;
Expand Down
26 changes: 26 additions & 0 deletions tests/helpers/client-lifecycle-lock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { spyOn } from "bun:test";
import { join, resolve } from "node:path";
import * as clientLifecycle from "../../src/client/lifecycle-lock";

const realSync = clientLifecycle.withClientLifecycleSync;
const realAsync = clientLifecycle.withClientLifecycle;

/** Keep real SQLite exclusion and leases, without sharing the OS user's Desktop lock. */
export function isolateClientLifecycleLock(root: string) {
const acquisitions: Array<{ kind: "sync" | "async"; path: string }> = [];
function isolatedDeps(kind: "sync" | "async", deps?: clientLifecycle.ClientLifecycleLockDeps) {
const lockPath = deps?.lockPath ?? join(root, "locks", "lifecycle.sqlite");
acquisitions.push({ kind, path: resolve(lockPath) });
return { ...deps, lockPath };
}
const sync = spyOn(clientLifecycle, "withClientLifecycleSync").mockImplementation(
(work, deps) => realSync(work, isolatedDeps("sync", deps)),
);
const async = spyOn(clientLifecycle, "withClientLifecycle").mockImplementation(
(work, deps) => realAsync(work, isolatedDeps("async", deps)),
);
return {
acquisitions,
restore() { async.mockRestore(); sync.mockRestore(); },
};
}
Loading