Skip to content

Commit 4d11bb7

Browse files
committed
tests: Add helpers for configuring MCP via codex config
1 parent 708fb3e commit 4d11bb7

3 files changed

Lines changed: 50 additions & 22 deletions

File tree

src/__tests__/CodexACPAgent/e2e/acp-e2e-test-utils.ts

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import * as acp from "@agentclientprotocol/sdk";
22
import {describe, expect} from "vitest";
33
import {AgentMode} from "../../../AgentMode";
4-
import {createSpawnedAgentFixture, type SpawnedAgentFixture} from "./spawned-agent-fixture";
4+
import {createSpawnedAgentFixture, type SpawnedAgentFixture,} from "./spawned-agent-fixture";
55

66
export {
77
createPermissionResponder,
@@ -46,7 +46,7 @@ export function expectNoPermissionRequests(fixture: SpawnedAgentFixture, session
4646
expectPermissionRequests(fixture, sessionId, { edit: 0, execute: 0 });
4747
}
4848

49-
export async function createAuthenticatedFixture(initialMode?: AgentMode): Promise<SpawnedAgentFixture> {
49+
export async function createAuthenticatedFixture(initialMode?: AgentMode, mcpServers?: acp.McpServerStdio[]): Promise<SpawnedAgentFixture> {
5050
const apiKey = requireLiveApiKey();
5151
const extraEnv = initialMode ? {INITIAL_AGENT_MODE: initialMode.id} : undefined;
5252
return await createSpawnedFixture(async (connection, authMethods) => {
@@ -67,7 +67,7 @@ export async function createAuthenticatedFixture(initialMode?: AgentMode): Promi
6767
if (authenticationStatus["type"] !== "api-key") {
6868
throw new Error(`Unexpected authentication status: ${JSON.stringify(authenticationStatus)}`);
6969
}
70-
}, extraEnv);
70+
}, extraEnv, mcpServers);
7171
}
7272

7373
export async function createGatewayFixture(
@@ -119,6 +119,7 @@ type Authenticator = (connection: acp.ClientSideConnection, authMethods: acp.Aut
119119
async function createSpawnedFixture(
120120
authenticate: Authenticator,
121121
extraEnv?: NodeJS.ProcessEnv,
122+
mcpServers?: acp.McpServerStdio[],
122123
): Promise<SpawnedAgentFixture> {
123124
return await createSpawnedAgentFixture(async (connection) => {
124125
const initializeResponse = await connection.initialize({
@@ -135,7 +136,7 @@ async function createSpawnedFixture(
135136
}
136137

137138
await authenticate(connection, initializeResponse.authMethods ?? []);
138-
}, extraEnv);
139+
}, extraEnv, mcpServers);
139140
}
140141

141142
export function requireLiveApiKey(): string {

src/__tests__/CodexACPAgent/e2e/spawned-agent-fixture.ts

Lines changed: 21 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -43,26 +43,37 @@ type ConnectionInitializer = (connection: acp.ClientSideConnection) => Promise<v
4343
export async function createSpawnedAgentFixture(
4444
initializeConnection: ConnectionInitializer,
4545
extraEnv?: NodeJS.ProcessEnv,
46-
paths = RuntimePaths.createTemporary(),
47-
client: RecordingClient = new RecordingClient(),
46+
mcpServers?: acp.McpServerStdio[],
47+
paths?: RuntimePaths,
48+
client?: RecordingClient,
4849
): Promise<SpawnedAgentFixture> {
50+
const resolvedPaths = paths ?? RuntimePaths.createTemporary();
51+
const configuredMcpServers = mcpServers ?? [];
52+
writeCodexHomeConfig(resolvedPaths.codexHome, {
53+
model: DEFAULT_TEST_MODEL_ID.model,
54+
model_reasoning_effort: DEFAULT_TEST_MODEL_ID.effort,
55+
web_search: "disabled",
56+
}, configuredMcpServers);
57+
58+
const resolvedClient = client ?? new RecordingClient();
4959
const agentProcess = spawn("npm", ["run", "--silent", "start"], {
5060
cwd: process.cwd(),
5161
env: {
5262
...process.env,
53-
CODEX_HOME: paths.codexHome,
54-
APP_SERVER_LOGS: paths.appServerLogsDir,
63+
CODEX_HOME: resolvedPaths.codexHome,
64+
APP_SERVER_LOGS: resolvedPaths.appServerLogsDir,
5565
...extraEnv,
5666
},
5767
stdio: ["pipe", "pipe", "pipe"],
5868
});
5969

6070
const fixture = new SpawnedAgentFixtureImpl(
61-
client,
71+
resolvedClient,
6272
agentProcess,
63-
paths,
73+
resolvedPaths,
6474
initializeConnection,
65-
extraEnv,
75+
extraEnv ?? {},
76+
configuredMcpServers,
6677
);
6778
await initializeConnection(fixture.connection);
6879
return fixture;
@@ -85,11 +96,6 @@ class RuntimePaths {
8596
for (const dir of [paths.rootDir, paths.codexHome, paths.workspaceDir, paths.appServerLogsDir]) {
8697
fs.mkdirSync(dir, {recursive: true});
8798
}
88-
writeCodexHomeConfig(paths.codexHome, {
89-
model: DEFAULT_TEST_MODEL_ID.model,
90-
model_reasoning_effort: DEFAULT_TEST_MODEL_ID.effort,
91-
web_search: "disabled",
92-
});
9399
return paths;
94100
}
95101
}
@@ -146,7 +152,8 @@ class SpawnedAgentFixtureImpl implements SpawnedAgentFixture {
146152
private readonly agentProcess: ChildProcessWithoutNullStreams,
147153
private readonly paths: RuntimePaths,
148154
private readonly initializeConnection: ConnectionInitializer,
149-
private readonly extraEnv?: NodeJS.ProcessEnv,
155+
private readonly extraEnv: NodeJS.ProcessEnv,
156+
private readonly mcpServers: acp.McpServerStdio[],
150157
) {
151158
const output = Readable.toWeb(agentProcess.stdout) as ReadableStream<Uint8Array>;
152159
this.connection = new acp.ClientSideConnection(
@@ -168,7 +175,7 @@ class SpawnedAgentFixtureImpl implements SpawnedAgentFixture {
168175

169176
async restart(): Promise<SpawnedAgentFixture> {
170177
await this.stopProcess(false);
171-
return await createSpawnedAgentFixture(this.initializeConnection, this.extraEnv, this.paths, this.client);
178+
return await createSpawnedAgentFixture(this.initializeConnection, this.extraEnv, this.mcpServers, this.paths, this.client);
172179
}
173180

174181
writeSkill(skill: TestSkill, rootDir?: string): void {

src/__tests__/acp-test-utils.ts

Lines changed: 24 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1+
import type {AgentSideConnection, McpServerStdio, RequestPermissionResponse} from "@agentclientprotocol/sdk";
12
import {CodexAcpClient} from '../CodexAcpClient';
2-
import {type CodexConnectionEvent, CodexAppServerClient} from '../CodexAppServerClient';
3+
import {CodexAppServerClient, type CodexConnectionEvent} from '../CodexAppServerClient';
34
import {startCodexConnection} from "../CodexJsonRpcConnection";
45
import {CodexAcpServer, type SessionState} from "../CodexAcpServer";
5-
import type {AgentSideConnection, RequestPermissionResponse} from "@agentclientprotocol/sdk";
66
import type {ServerNotification} from "../app-server";
77
import type {MessageConnection} from "vscode-jsonrpc/node";
88
import path from "node:path";
@@ -176,17 +176,37 @@ function createTestCodexHome(): string {
176176
return codexHome;
177177
}
178178

179-
export function writeCodexHomeConfig(codexHome: string, extras: Record<string, string> = {}): void {
179+
export function writeCodexHomeConfig(
180+
codexHome: string,
181+
extras: Record<string, string> = {},
182+
mcpServers: McpServerStdio[] = [],
183+
): void {
180184
const entries: Record<string, string> = {
181185
cli_auth_credentials_store: "file",
182186
...extras,
183187
};
184-
const body = Object.entries(entries)
188+
let body = Object.entries(entries)
185189
.map(([key, value]) => `${key} = "${value}"`)
186190
.join("\n");
191+
for (const server of mcpServers) {
192+
body += `\n\n[mcp_servers."${escapeTOML(server.name)}"]`;
193+
body += `\ncommand = "${escapeTOML(server.command)}"`;
194+
const argsToml = server.args.map(a => `"${escapeTOML(a)}"`).join(", ");
195+
body += `\nargs = [${argsToml}]`;
196+
if (server.env && server.env.length > 0) {
197+
const envPairs = server.env
198+
.map(e => `${e.name} = "${escapeTOML(e.value)}"`)
199+
.join(", ");
200+
body += `\nenv = {${envPairs}}`;
201+
}
202+
}
187203
fs.writeFileSync(path.join(codexHome, "config.toml"), body, "utf8");
188204
}
189205

206+
function escapeTOML(value: string): string {
207+
return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"');
208+
}
209+
190210
export function removeDirectoryWithRetry(directory: string): void {
191211
for (let attempt = 0; attempt < 5; attempt += 1) {
192212
try {

0 commit comments

Comments
 (0)