Skip to content

Commit 2881b8e

Browse files
committed
Isolate tests with temporary CODEX_HOME
1 parent 90ecc21 commit 2881b8e

3 files changed

Lines changed: 130 additions & 151 deletions

File tree

src/CodexJsonRpcConnection.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,11 @@ export interface CodexConnection {
1111
readonly process: ChildProcessWithoutNullStreams;
1212
}
1313

14-
export function startCodexConnection(codexPath: string): CodexConnection {
14+
export function startCodexConnection(codexPath: string, env?: NodeJS.ProcessEnv): CodexConnection {
15+
const spawnEnv = env ?? process.env;
1516
const codex: ChildProcessWithoutNullStreams = process.platform === 'win32'
16-
? spawn(`"${codexPath}" app-server`, { shell: true })
17-
: spawn(codexPath, ['app-server']);
17+
? spawn(`"${codexPath}" app-server`, { shell: true, env: spawnEnv })
18+
: spawn(codexPath, ['app-server'], { env: spawnEnv });
1819

1920
attachLogs(codex);
2021

src/__tests__/CodexACPAgent/CodexAcpClient.test.ts

Lines changed: 97 additions & 147 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,6 @@
11
// noinspection ES6RedundantAwait
22

33
import {describe, expect, it, vi, beforeEach} from 'vitest';
4-
import fs from "node:fs";
5-
import os from "node:os";
6-
import path from "node:path";
74
import type {CodexAuthRequest} from "../../CodexAuthMethod";
85
import type * as acp from "@agentclientprotocol/sdk";
96
import {createTestFixture, createCodexMockTestFixture, createTestSessionState, type TestFixture} from "../acp-test-utils";
@@ -14,46 +11,6 @@ import type {ListMcpServerStatusResponse, Model, SkillsListResponse} from "../..
1411
import type {RateLimitsMap} from "../../RateLimitsMap";
1512
import {ModelId} from "../../ModelId";
1613

17-
const CODEX_HOME_ENV = "CODEX_HOME";
18-
19-
async function overrideCodexHome<T>(configToml: string, run: () => Promise<T>): Promise<T> {
20-
const previousCodexHome = process.env[CODEX_HOME_ENV];
21-
const codexHome = fs.mkdtempSync(path.join(os.tmpdir(), "codex-acp-codex-home-"));
22-
fs.writeFileSync(path.join(codexHome, "config.toml"), configToml, "utf8");
23-
process.env[CODEX_HOME_ENV] = codexHome;
24-
25-
try {
26-
return await run();
27-
} finally {
28-
if (previousCodexHome === undefined) {
29-
delete process.env[CODEX_HOME_ENV];
30-
} else {
31-
process.env[CODEX_HOME_ENV] = previousCodexHome;
32-
}
33-
await removeDirectoryWithRetry(codexHome);
34-
}
35-
}
36-
37-
async function removeDirectoryWithRetry(directory: string): Promise<void> {
38-
let lastError: NodeJS.ErrnoException | null = null;
39-
for (let attempt = 0; attempt < 5; attempt += 1) {
40-
try {
41-
fs.rmSync(directory, { recursive: true, force: true });
42-
return;
43-
} catch (error) {
44-
const err = error as NodeJS.ErrnoException;
45-
if (err.code !== "ENOTEMPTY" && err.code !== "EBUSY") {
46-
throw err;
47-
}
48-
lastError = err;
49-
await new Promise((resolve) => setTimeout(resolve, 50 * (attempt + 1)));
50-
}
51-
}
52-
if (lastError) {
53-
throw lastError;
54-
}
55-
}
56-
5714
describe('ACP server test', { timeout: 40_000 }, () => {
5815

5916
let fixture: TestFixture;
@@ -65,126 +22,119 @@ describe('ACP server test', { timeout: 40_000 }, () => {
6522
const ignoredFields = ["thread", "cwd", "id", "createdAt", "path", "threadId", "userAgent", "sandbox", "conversationId", "origins", "supportedReasoningEfforts", "reasoningEffort", "model", "readOnlyAccess", "approvalsReviewer"];
6623

6724
it('should throw error without authentication', async () => {
68-
await overrideCodexHome('cli_auth_credentials_store = "file"', async () => {
69-
const authFixture = createTestFixture();
70-
const codexAcpAgent = authFixture.getCodexAcpAgent();
25+
const authFixture = createTestFixture();
26+
const codexAcpAgent = authFixture.getCodexAcpAgent();
7127

72-
await codexAcpAgent.initialize({protocolVersion: 1});
73-
await authFixture.getCodexAcpClient().logout();
74-
authFixture.clearCodexConnectionDump();
28+
await codexAcpAgent.initialize({protocolVersion: 1});
29+
await authFixture.getCodexAcpClient().logout();
30+
authFixture.clearCodexConnectionDump();
7531

76-
await expect(
77-
codexAcpAgent.newSession({cwd: "", mcpServers: []})
78-
).rejects.toThrow("Authentication required");
32+
await expect(
33+
codexAcpAgent.newSession({cwd: "", mcpServers: []})
34+
).rejects.toThrow("Authentication required");
7935

80-
const transportDump = authFixture.getCodexConnectionDump(ignoredFields);
81-
await expect(transportDump).toMatchFileSnapshot("data/auth-failed.json");
82-
});
36+
const transportDump = authFixture.getCodexConnectionDump(ignoredFields);
37+
await expect(transportDump).toMatchFileSnapshot("data/auth-failed.json");
8338
});
8439

8540
it('should authenticate with key', async () => {
86-
// In sandboxed environments Codex may fail when trying to write to the OS keychain (`Operation not permitted`).
87-
await overrideCodexHome('cli_auth_credentials_store = "file"', async () => {
88-
const keyFixture = createTestFixture();
89-
const codexAcpAgent = keyFixture.getCodexAcpAgent();
90-
91-
await codexAcpAgent.initialize({protocolVersion: 1});
92-
await keyFixture.getCodexAcpClient().logout();
93-
94-
95-
const unauthenticatedResponse = await keyFixture.getCodexAcpAgent().extMethod("authentication/status", {});
96-
expect(unauthenticatedResponse).toEqual({type: "unauthenticated"});
97-
98-
keyFixture.clearCodexConnectionDump();
99-
100-
const authRequest: CodexAuthRequest = { methodId: "api-key", _meta: { "api-key": { apiKey: "TOKEN" }}}
101-
await codexAcpAgent.authenticate(authRequest);
102-
const newSessionResponse = await codexAcpAgent.newSession({cwd: "", mcpServers: []});
103-
expect(newSessionResponse.sessionId).toBeDefined()
104-
105-
const transportEvents = keyFixture.getCodexConnectionEvents([...ignoredFields, "upgrade"]);
106-
const transportMethods = transportEvents.flatMap(event => "method" in event ? [event.method] : []);
107-
const loginRequest = transportEvents.find(event =>
108-
event.eventType === "request" &&
109-
"method" in event &&
110-
event.method === "account/login/start"
111-
);
112-
const loginResponse = transportEvents.find(event =>
113-
event.eventType === "response" &&
114-
"type" in event &&
115-
event.type === "apiKey"
116-
);
117-
const threadStartResponse = transportEvents.find(event =>
118-
event.eventType === "response" &&
119-
"modelProvider" in event &&
120-
"approvalPolicy" in event
121-
);
122-
expect(transportMethods).toEqual([
123-
"account/login/start",
124-
"account/read",
125-
"account/updated",
126-
"thread/start",
127-
"model/list",
128-
"thread/started",
129-
"account/read",
130-
"skills/list",
131-
]);
132-
expect(loginRequest).toEqual({
133-
eventType: "request",
134-
method: "account/login/start",
135-
params: {
136-
type: "apiKey",
137-
apiKey: "TOKEN",
138-
}
139-
});
140-
expect(loginResponse).toEqual({
141-
eventType: "response",
41+
const keyFixture = createTestFixture();
42+
const codexAcpAgent = keyFixture.getCodexAcpAgent();
43+
44+
await codexAcpAgent.initialize({protocolVersion: 1});
45+
await keyFixture.getCodexAcpClient().logout();
46+
47+
48+
const unauthenticatedResponse = await keyFixture.getCodexAcpAgent().extMethod("authentication/status", {});
49+
expect(unauthenticatedResponse).toEqual({type: "unauthenticated"});
50+
51+
keyFixture.clearCodexConnectionDump();
52+
53+
const authRequest: CodexAuthRequest = { methodId: "api-key", _meta: { "api-key": { apiKey: "TOKEN" }}}
54+
await codexAcpAgent.authenticate(authRequest);
55+
const newSessionResponse = await codexAcpAgent.newSession({cwd: "", mcpServers: []});
56+
expect(newSessionResponse.sessionId).toBeDefined()
57+
58+
const transportEvents = keyFixture.getCodexConnectionEvents([...ignoredFields, "upgrade"]);
59+
const transportMethods = transportEvents.flatMap(event => "method" in event ? [event.method] : []);
60+
const loginRequest = transportEvents.find(event =>
61+
event.eventType === "request" &&
62+
"method" in event &&
63+
event.method === "account/login/start"
64+
);
65+
const loginResponse = transportEvents.find(event =>
66+
event.eventType === "response" &&
67+
"type" in event &&
68+
event.type === "apiKey"
69+
);
70+
const threadStartResponse = transportEvents.find(event =>
71+
event.eventType === "response" &&
72+
"modelProvider" in event &&
73+
"approvalPolicy" in event
74+
);
75+
expect(transportMethods).toEqual([
76+
"account/login/start",
77+
"account/read",
78+
"account/updated",
79+
"thread/start",
80+
"model/list",
81+
"thread/started",
82+
"account/read",
83+
"skills/list",
84+
]);
85+
expect(loginRequest).toEqual({
86+
eventType: "request",
87+
method: "account/login/start",
88+
params: {
14289
type: "apiKey",
143-
});
144-
expect(threadStartResponse).toMatchObject({
145-
eventType: "response",
146-
modelProvider: "openai",
147-
approvalPolicy: "on-request",
148-
approvalsReviewer: "approvalsReviewer",
149-
});
150-
const authenticatedResponse = await keyFixture.getCodexAcpAgent().extMethod("authentication/status", {});
151-
expect(authenticatedResponse).toEqual({type: "api-key"});
152-
153-
await keyFixture.getCodexAcpAgent().extMethod("authentication/logout", {});
154-
const logoutResponse = await keyFixture.getCodexAcpAgent().extMethod("authentication/status", {});
155-
expect(logoutResponse).toEqual({type: "unauthenticated"});
90+
apiKey: "TOKEN",
91+
}
15692
});
93+
expect(loginResponse).toEqual({
94+
eventType: "response",
95+
type: "apiKey",
96+
});
97+
expect(threadStartResponse).toMatchObject({
98+
eventType: "response",
99+
modelProvider: "openai",
100+
approvalPolicy: "on-request",
101+
approvalsReviewer: "approvalsReviewer",
102+
});
103+
const authenticatedResponse = await keyFixture.getCodexAcpAgent().extMethod("authentication/status", {});
104+
expect(authenticatedResponse).toEqual({type: "api-key"});
105+
106+
await keyFixture.getCodexAcpAgent().extMethod("authentication/logout", {});
107+
const logoutResponse = await keyFixture.getCodexAcpAgent().extMethod("authentication/status", {});
108+
expect(logoutResponse).toEqual({type: "unauthenticated"});
157109
});
158110

159111
it('should authenticate with a gateway', async () => {
160-
await overrideCodexHome('cli_auth_credentials_store = "file"', async () => {
161-
const gatewayFixture = createTestFixture();
162-
const codexAcpAgent = gatewayFixture.getCodexAcpAgent();
163-
164-
await codexAcpAgent.initialize({protocolVersion: 1});
165-
await gatewayFixture.getCodexAcpClient().logout();
166-
167-
const authRequest: CodexAuthRequest = {
168-
methodId: "gateway",
169-
_meta: {
170-
"gateway": {
171-
baseUrl: "https://www.example.com",
172-
headers: {
173-
"Custom-Auth-Header": "TOKEN"
174-
}
112+
const gatewayFixture = createTestFixture();
113+
const codexAcpAgent = gatewayFixture.getCodexAcpAgent();
114+
115+
await codexAcpAgent.initialize({protocolVersion: 1});
116+
await gatewayFixture.getCodexAcpClient().logout();
117+
118+
const authRequest: CodexAuthRequest = {
119+
methodId: "gateway",
120+
_meta: {
121+
"gateway": {
122+
baseUrl: "https://www.example.com",
123+
headers: {
124+
"Custom-Auth-Header": "TOKEN"
175125
}
176126
}
177-
};
127+
}
128+
};
178129

179-
await codexAcpAgent.authenticate(authRequest);
180-
expect(await gatewayFixture.getCodexAcpClient().authRequired()).toBe(false);
130+
await codexAcpAgent.authenticate(authRequest);
131+
expect(await gatewayFixture.getCodexAcpClient().authRequired()).toBe(false);
181132

182-
const authenticatedResponse = await gatewayFixture.getCodexAcpAgent().extMethod("authentication/status", {});
183-
expect(authenticatedResponse).toEqual({type: "gateway", name: "custom-gateway"});
133+
const authenticatedResponse = await gatewayFixture.getCodexAcpAgent().extMethod("authentication/status", {});
134+
expect(authenticatedResponse).toEqual({type: "gateway", name: "custom-gateway"});
184135

185-
const newSessionResponse = await codexAcpAgent.newSession({cwd: "", mcpServers: []});
186-
expect(newSessionResponse.sessionId).toBeDefined()
187-
});
136+
const newSessionResponse = await codexAcpAgent.newSession({cwd: "", mcpServers: []});
137+
expect(newSessionResponse.sessionId).toBeDefined()
188138
})
189139

190140
it('prefetches session additional skill roots before thread start', async () => {

src/__tests__/acp-test-utils.ts

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import type {ServerNotification} from "../app-server";
77
import type {MessageConnection} from "vscode-jsonrpc/node";
88
import path from "node:path";
99
import fs from "node:fs";
10+
import os from "node:os";
1011
import {AgentMode} from "../AgentMode";
1112
import {expect, vi} from "vitest";
1213

@@ -153,14 +154,41 @@ export function createTestFixture(): TestFixture {
153154
throw new Error(`Codex binary not found at ${pathToCodex}. Did you run 'npm install'?`);
154155
}
155156

156-
const codexConnection = startCodexConnection(pathToCodex);
157+
const codexHome = createTestCodexHome();
158+
const codexConnection = startCodexConnection(pathToCodex, {
159+
...process.env,
160+
CODEX_HOME: codexHome,
161+
});
162+
codexConnection.process.on("exit", () => {
163+
removeDirectoryWithRetry(codexHome);
164+
});
157165

158166
return createBaseTestFixture({
159167
connection: codexConnection.connection,
160168
getExitCode: () => codexConnection.process.exitCode
161169
});
162170
}
163171

172+
function createTestCodexHome(): string {
173+
const codexHome = fs.mkdtempSync(path.join(os.tmpdir(), "codex-acp-codex-home-"));
174+
fs.writeFileSync(path.join(codexHome, "config.toml"), 'cli_auth_credentials_store = "file"\n', "utf8");
175+
return codexHome;
176+
}
177+
178+
function removeDirectoryWithRetry(directory: string): void {
179+
for (let attempt = 0; attempt < 5; attempt += 1) {
180+
try {
181+
fs.rmSync(directory, { recursive: true, force: true });
182+
return;
183+
} catch (error) {
184+
const err = error as NodeJS.ErrnoException;
185+
if (err.code !== "ENOTEMPTY" && err.code !== "EBUSY") {
186+
return;
187+
}
188+
}
189+
}
190+
}
191+
164192
export interface CodexMockTestFixture extends TestFixture {
165193
sendServerNotification(notification: ServerNotification | Record<string, unknown>): void,
166194
sendServerRequest<T>(method: string, params: unknown): Promise<T>,

0 commit comments

Comments
 (0)