Prefer websockets when providers support them (#13592)

Remove all flags and model settings.

---------

Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
pakrym-oai
2026-03-17 19:46:44 -07:00
committed by GitHub
parent d950543e65
commit 770616414a
34 changed files with 348 additions and 303 deletions

View File

@@ -1,9 +1,5 @@
import path from "node:path";
import { describe, expect, it } from "@jest/globals";
import { Codex } from "../src/codex";
import {
assistantMessage,
responseCompleted,
@@ -13,8 +9,7 @@ import {
SseResponseBody,
startResponsesTestProxy,
} from "./responsesProxy";
const codexExecPath = path.join(process.cwd(), "..", "..", "codex-rs", "target", "debug", "codex");
import { createMockClient } from "./testCodex";
function* infiniteShellCall(): Generator<SseResponseBody> {
while (true) {
@@ -28,9 +23,9 @@ describe("AbortSignal support", () => {
statusCode: 200,
responseBodies: infiniteShellCall(),
});
const { client, cleanup } = createMockClient(url);
try {
const client = new Codex({ codexPathOverride: codexExecPath, baseUrl: url, apiKey: "test" });
const thread = client.startThread();
// Create an abort controller and abort it immediately
@@ -40,6 +35,7 @@ describe("AbortSignal support", () => {
// The operation should fail because the signal is already aborted
await expect(thread.run("Hello, world!", { signal: controller.signal })).rejects.toThrow();
} finally {
cleanup();
await close();
}
});
@@ -49,9 +45,9 @@ describe("AbortSignal support", () => {
statusCode: 200,
responseBodies: infiniteShellCall(),
});
const { client, cleanup } = createMockClient(url);
try {
const client = new Codex({ codexPathOverride: codexExecPath, baseUrl: url, apiKey: "test" });
const thread = client.startThread();
// Create an abort controller and abort it immediately
@@ -78,6 +74,7 @@ describe("AbortSignal support", () => {
expect(error).toBeDefined();
}
} finally {
cleanup();
await close();
}
});
@@ -87,9 +84,9 @@ describe("AbortSignal support", () => {
statusCode: 200,
responseBodies: infiniteShellCall(),
});
const { client, cleanup } = createMockClient(url);
try {
const client = new Codex({ codexPathOverride: codexExecPath, baseUrl: url, apiKey: "test" });
const thread = client.startThread();
const controller = new AbortController();
@@ -103,6 +100,7 @@ describe("AbortSignal support", () => {
// The operation should fail
await expect(runPromise).rejects.toThrow();
} finally {
cleanup();
await close();
}
});
@@ -112,9 +110,9 @@ describe("AbortSignal support", () => {
statusCode: 200,
responseBodies: infiniteShellCall(),
});
const { client, cleanup } = createMockClient(url);
try {
const client = new Codex({ codexPathOverride: codexExecPath, baseUrl: url, apiKey: "test" });
const thread = client.startThread();
const controller = new AbortController();
@@ -137,6 +135,7 @@ describe("AbortSignal support", () => {
})(),
).rejects.toThrow();
} finally {
cleanup();
await close();
}
});
@@ -146,9 +145,9 @@ describe("AbortSignal support", () => {
statusCode: 200,
responseBodies: [sse(responseStarted(), assistantMessage("Hi!"), responseCompleted())],
});
const { client, cleanup } = createMockClient(url);
try {
const client = new Codex({ codexPathOverride: codexExecPath, baseUrl: url, apiKey: "test" });
const thread = client.startThread();
const controller = new AbortController();
@@ -159,6 +158,7 @@ describe("AbortSignal support", () => {
expect(result.finalResponse).toBe("Hi!");
expect(result.items).toHaveLength(1);
} finally {
cleanup();
await close();
}
});

View File

@@ -93,4 +93,54 @@ describe("CodexExec", () => {
expect(imageIndex).toBeGreaterThan(-1);
expect(resumeIndex).toBeLessThan(imageIndex);
});
it("allows overriding the env passed to the Codex CLI", async () => {
const { CodexExec } = await import("../src/exec");
spawnMock.mockClear();
const child = new FakeChildProcess();
spawnMock.mockReturnValue(child as unknown as child_process.ChildProcess);
setImmediate(() => {
child.stdout.end();
child.stderr.end();
child.emit("exit", 0, null);
});
process.env.CODEX_ENV_SHOULD_NOT_LEAK = "leak";
try {
const exec = new CodexExec("codex", {
CODEX_HOME: "/tmp/codex-home",
CUSTOM_ENV: "custom",
});
for await (const _ of exec.run({
input: "custom env",
apiKey: "test",
baseUrl: "https://example.test",
})) {
// no-op
}
const commandArgs = spawnMock.mock.calls[0]?.[1] as string[] | undefined;
expect(commandArgs).toBeDefined();
const spawnOptions = spawnMock.mock.calls[0]?.[2] as child_process.SpawnOptions | undefined;
const spawnEnv = spawnOptions?.env as Record<string, string> | undefined;
expect(spawnEnv).toBeDefined();
if (!spawnEnv || !commandArgs) {
throw new Error("Spawn args missing");
}
expect(spawnEnv.CODEX_HOME).toBe("/tmp/codex-home");
expect(spawnEnv.CUSTOM_ENV).toBe("custom");
expect(spawnEnv.CODEX_ENV_SHOULD_NOT_LEAK).toBeUndefined();
expect(spawnEnv.OPENAI_BASE_URL).toBeUndefined();
expect(spawnEnv.CODEX_API_KEY).toBe("test");
expect(spawnEnv.CODEX_INTERNAL_ORIGINATOR_OVERRIDE).toBeDefined();
expect(commandArgs).toContain("--config");
expect(commandArgs).toContain(`openai_base_url=${JSON.stringify("https://example.test")}`);
} finally {
delete process.env.CODEX_ENV_SHOULD_NOT_LEAK;
}
});
});

View File

@@ -5,8 +5,6 @@ import path from "node:path";
import { codexExecSpy } from "./codexExecSpy";
import { describe, expect, it } from "@jest/globals";
import { Codex } from "../src/codex";
import {
assistantMessage,
responseCompleted,
@@ -16,8 +14,7 @@ import {
startResponsesTestProxy,
SseResponseBody,
} from "./responsesProxy";
const codexExecPath = path.join(process.cwd(), "..", "..", "codex-rs", "target", "debug", "codex");
import { createMockClient, createTestClient } from "./testCodex";
describe("Codex", () => {
it("returns thread events", async () => {
@@ -25,10 +22,9 @@ describe("Codex", () => {
statusCode: 200,
responseBodies: [sse(responseStarted(), assistantMessage("Hi!"), responseCompleted())],
});
const { client, cleanup } = createMockClient(url);
try {
const client = new Codex({ codexPathOverride: codexExecPath, baseUrl: url, apiKey: "test" });
const thread = client.startThread();
const result = await thread.run("Hello, world!");
@@ -47,6 +43,7 @@ describe("Codex", () => {
});
expect(thread.id).toEqual(expect.any(String));
} finally {
cleanup();
await close();
}
});
@@ -67,10 +64,9 @@ describe("Codex", () => {
),
],
});
const { client, cleanup } = createMockClient(url);
try {
const client = new Codex({ codexPathOverride: codexExecPath, baseUrl: url, apiKey: "test" });
const thread = client.startThread();
await thread.run("first input");
await thread.run("second input");
@@ -90,6 +86,7 @@ describe("Codex", () => {
)?.text;
expect(assistantText).toBe("First response");
} finally {
cleanup();
await close();
}
});
@@ -110,10 +107,9 @@ describe("Codex", () => {
),
],
});
const { client, cleanup } = createMockClient(url);
try {
const client = new Codex({ codexPathOverride: codexExecPath, baseUrl: url, apiKey: "test" });
const thread = client.startThread();
await thread.run("first input");
await thread.run("second input");
@@ -134,6 +130,7 @@ describe("Codex", () => {
)?.text;
expect(assistantText).toBe("First response");
} finally {
cleanup();
await close();
}
});
@@ -154,10 +151,9 @@ describe("Codex", () => {
),
],
});
const { client, cleanup } = createMockClient(url);
try {
const client = new Codex({ codexPathOverride: codexExecPath, baseUrl: url, apiKey: "test" });
const originalThread = client.startThread();
await originalThread.run("first input");
@@ -181,6 +177,7 @@ describe("Codex", () => {
)?.text;
expect(assistantText).toBe("First response");
} finally {
cleanup();
await close();
}
});
@@ -198,10 +195,9 @@ describe("Codex", () => {
});
const { args: spawnArgs, restore } = codexExecSpy();
const { client, cleanup } = createMockClient(url);
try {
const client = new Codex({ codexPathOverride: codexExecPath, baseUrl: url, apiKey: "test" });
const thread = client.startThread({
model: "gpt-test-1",
sandboxMode: "workspace-write",
@@ -219,6 +215,7 @@ describe("Codex", () => {
expectPair(commandArgs, ["--sandbox", "workspace-write"]);
expectPair(commandArgs, ["--model", "gpt-test-1"]);
} finally {
cleanup();
restore();
await close();
}
@@ -237,10 +234,9 @@ describe("Codex", () => {
});
const { args: spawnArgs, restore } = codexExecSpy();
const { client, cleanup } = createMockClient(url);
try {
const client = new Codex({ codexPathOverride: codexExecPath, baseUrl: url, apiKey: "test" });
const thread = client.startThread({
modelReasoningEffort: "high",
});
@@ -250,6 +246,7 @@ describe("Codex", () => {
expect(commandArgs).toBeDefined();
expectPair(commandArgs, ["--config", 'model_reasoning_effort="high"']);
} finally {
cleanup();
restore();
await close();
}
@@ -268,10 +265,9 @@ describe("Codex", () => {
});
const { args: spawnArgs, restore } = codexExecSpy();
const { client, cleanup } = createMockClient(url);
try {
const client = new Codex({ codexPathOverride: codexExecPath, baseUrl: url, apiKey: "test" });
const thread = client.startThread({
networkAccessEnabled: true,
});
@@ -281,6 +277,7 @@ describe("Codex", () => {
expect(commandArgs).toBeDefined();
expectPair(commandArgs, ["--config", "sandbox_workspace_write.network_access=true"]);
} finally {
cleanup();
restore();
await close();
}
@@ -299,10 +296,9 @@ describe("Codex", () => {
});
const { args: spawnArgs, restore } = codexExecSpy();
const { client, cleanup } = createMockClient(url);
try {
const client = new Codex({ codexPathOverride: codexExecPath, baseUrl: url, apiKey: "test" });
const thread = client.startThread({
webSearchEnabled: true,
});
@@ -312,6 +308,7 @@ describe("Codex", () => {
expect(commandArgs).toBeDefined();
expectPair(commandArgs, ["--config", 'web_search="live"']);
} finally {
cleanup();
restore();
await close();
}
@@ -330,10 +327,9 @@ describe("Codex", () => {
});
const { args: spawnArgs, restore } = codexExecSpy();
const { client, cleanup } = createMockClient(url);
try {
const client = new Codex({ codexPathOverride: codexExecPath, baseUrl: url, apiKey: "test" });
const thread = client.startThread({
webSearchMode: "cached",
});
@@ -343,6 +339,7 @@ describe("Codex", () => {
expect(commandArgs).toBeDefined();
expectPair(commandArgs, ["--config", 'web_search="cached"']);
} finally {
cleanup();
restore();
await close();
}
@@ -361,10 +358,9 @@ describe("Codex", () => {
});
const { args: spawnArgs, restore } = codexExecSpy();
const { client, cleanup } = createMockClient(url);
try {
const client = new Codex({ codexPathOverride: codexExecPath, baseUrl: url, apiKey: "test" });
const thread = client.startThread({
webSearchEnabled: false,
});
@@ -374,6 +370,7 @@ describe("Codex", () => {
expect(commandArgs).toBeDefined();
expectPair(commandArgs, ["--config", 'web_search="disabled"']);
} finally {
cleanup();
restore();
await close();
}
@@ -392,10 +389,9 @@ describe("Codex", () => {
});
const { args: spawnArgs, restore } = codexExecSpy();
const { client, cleanup } = createMockClient(url);
try {
const client = new Codex({ codexPathOverride: codexExecPath, baseUrl: url, apiKey: "test" });
const thread = client.startThread({
approvalPolicy: "on-request",
});
@@ -405,6 +401,7 @@ describe("Codex", () => {
expect(commandArgs).toBeDefined();
expectPair(commandArgs, ["--config", 'approval_policy="on-request"']);
} finally {
cleanup();
restore();
await close();
}
@@ -423,20 +420,18 @@ describe("Codex", () => {
});
const { args: spawnArgs, restore } = codexExecSpy();
const { client, cleanup } = createTestClient({
baseUrl: url,
apiKey: "test",
config: {
approval_policy: "never",
sandbox_workspace_write: { network_access: true },
retry_budget: 3,
tool_rules: { allow: ["git status", "git diff"] },
},
});
try {
const client = new Codex({
codexPathOverride: codexExecPath,
baseUrl: url,
apiKey: "test",
config: {
approval_policy: "never",
sandbox_workspace_write: { network_access: true },
retry_budget: 3,
tool_rules: { allow: ["git status", "git diff"] },
},
});
const thread = client.startThread();
await thread.run("apply config overrides");
@@ -447,6 +442,7 @@ describe("Codex", () => {
expectPair(commandArgs, ["--config", "retry_budget=3"]);
expectPair(commandArgs, ["--config", 'tool_rules.allow=["git status", "git diff"]']);
} finally {
cleanup();
restore();
await close();
}
@@ -465,15 +461,13 @@ describe("Codex", () => {
});
const { args: spawnArgs, restore } = codexExecSpy();
const { client, cleanup } = createTestClient({
baseUrl: url,
apiKey: "test",
config: { approval_policy: "never" },
});
try {
const client = new Codex({
codexPathOverride: codexExecPath,
baseUrl: url,
apiKey: "test",
config: { approval_policy: "never" },
});
const thread = client.startThread({ approvalPolicy: "on-request" });
await thread.run("override approval policy");
@@ -485,56 +479,7 @@ describe("Codex", () => {
]);
expect(approvalPolicyOverrides.at(-1)).toBe('approval_policy="on-request"');
} finally {
restore();
await close();
}
});
it("allows overriding the env passed to the Codex CLI", async () => {
const { url, close } = await startResponsesTestProxy({
statusCode: 200,
responseBodies: [
sse(
responseStarted("response_1"),
assistantMessage("Custom env", "item_1"),
responseCompleted("response_1"),
),
],
});
const { args: spawnArgs, envs: spawnEnvs, restore } = codexExecSpy();
process.env.CODEX_ENV_SHOULD_NOT_LEAK = "leak";
try {
const client = new Codex({
codexPathOverride: codexExecPath,
baseUrl: url,
apiKey: "test",
env: { CUSTOM_ENV: "custom" },
});
const thread = client.startThread();
await thread.run("custom env");
const spawnEnv = spawnEnvs[0];
expect(spawnEnv).toBeDefined();
if (!spawnEnv) {
throw new Error("Spawn env missing");
}
const commandArgs = spawnArgs[0];
expect(commandArgs).toBeDefined();
if (!commandArgs) {
throw new Error("Command args missing");
}
expect(spawnEnv.CUSTOM_ENV).toBe("custom");
expect(spawnEnv.CODEX_ENV_SHOULD_NOT_LEAK).toBeUndefined();
expect(spawnEnv.OPENAI_BASE_URL).toBeUndefined();
expect(spawnEnv.CODEX_API_KEY).toBe("test");
expect(spawnEnv.CODEX_INTERNAL_ORIGINATOR_OVERRIDE).toBeDefined();
expect(commandArgs).toContain("--config");
expect(commandArgs).toContain(`openai_base_url=${JSON.stringify(url)}`);
} finally {
delete process.env.CODEX_ENV_SHOULD_NOT_LEAK;
cleanup();
restore();
await close();
}
@@ -553,10 +498,9 @@ describe("Codex", () => {
});
const { args: spawnArgs, restore } = codexExecSpy();
const { client, cleanup } = createMockClient(url);
try {
const client = new Codex({ codexPathOverride: codexExecPath, baseUrl: url, apiKey: "test" });
const thread = client.startThread({
additionalDirectories: ["../backend", "/tmp/shared"],
});
@@ -577,6 +521,7 @@ describe("Codex", () => {
}
expect(addDirArgs).toEqual(["../backend", "/tmp/shared"]);
} finally {
cleanup();
restore();
await close();
}
@@ -605,9 +550,9 @@ describe("Codex", () => {
additionalProperties: false,
} as const;
try {
const client = new Codex({ codexPathOverride: codexExecPath, baseUrl: url, apiKey: "test" });
const { client, cleanup } = createMockClient(url);
try {
const thread = client.startThread();
await thread.run("structured", { outputSchema: schema });
@@ -634,6 +579,7 @@ describe("Codex", () => {
}
expect(fs.existsSync(schemaPath)).toBe(false);
} finally {
cleanup();
restore();
await close();
}
@@ -649,10 +595,9 @@ describe("Codex", () => {
),
],
});
const { client, cleanup } = createMockClient(url);
try {
const client = new Codex({ codexPathOverride: codexExecPath, baseUrl: url, apiKey: "test" });
const thread = client.startThread();
await thread.run([
{ type: "text", text: "Describe file changes" },
@@ -664,6 +609,7 @@ describe("Codex", () => {
const lastUser = payload!.json.input.at(-1);
expect(lastUser?.content?.[0]?.text).toBe("Describe file changes\n\nFocus on impacted tests");
} finally {
cleanup();
await close();
}
});
@@ -688,10 +634,9 @@ describe("Codex", () => {
imagesDirectoryEntries.forEach((image, index) => {
fs.writeFileSync(image, `image-${index}`);
});
const { client, cleanup } = createMockClient(url);
try {
const client = new Codex({ codexPathOverride: codexExecPath, baseUrl: url, apiKey: "test" });
const thread = client.startThread();
await thread.run([
{ type: "text", text: "describe the images" },
@@ -709,6 +654,7 @@ describe("Codex", () => {
}
expect(forwardedImages).toEqual(imagesDirectoryEntries);
} finally {
cleanup();
fs.rmSync(tempDir, { recursive: true, force: true });
restore();
await close();
@@ -727,15 +673,13 @@ describe("Codex", () => {
});
const { args: spawnArgs, restore } = codexExecSpy();
const workingDirectory = fs.mkdtempSync(path.join(os.tmpdir(), "codex-working-dir-"));
const { client, cleanup } = createTestClient({
baseUrl: url,
apiKey: "test",
});
try {
const workingDirectory = fs.mkdtempSync(path.join(os.tmpdir(), "codex-working-dir-"));
const client = new Codex({
codexPathOverride: codexExecPath,
baseUrl: url,
apiKey: "test",
});
const thread = client.startThread({
workingDirectory,
skipGitRepoCheck: true,
@@ -745,6 +689,8 @@ describe("Codex", () => {
const commandArgs = spawnArgs[0];
expectPair(commandArgs, ["--cd", workingDirectory]);
} finally {
cleanup();
fs.rmSync(workingDirectory, { recursive: true, force: true });
restore();
await close();
}
@@ -761,15 +707,13 @@ describe("Codex", () => {
),
],
});
const workingDirectory = fs.mkdtempSync(path.join(os.tmpdir(), "codex-working-dir-"));
const { client, cleanup } = createTestClient({
baseUrl: url,
apiKey: "test",
});
try {
const workingDirectory = fs.mkdtempSync(path.join(os.tmpdir(), "codex-working-dir-"));
const client = new Codex({
codexPathOverride: codexExecPath,
baseUrl: url,
apiKey: "test",
});
const thread = client.startThread({
workingDirectory,
});
@@ -777,6 +721,8 @@ describe("Codex", () => {
/Not inside a trusted directory/,
);
} finally {
cleanup();
fs.rmSync(workingDirectory, { recursive: true, force: true });
await close();
}
});
@@ -786,10 +732,9 @@ describe("Codex", () => {
statusCode: 200,
responseBodies: [sse(responseStarted(), assistantMessage("Hi!"), responseCompleted())],
});
const { client, cleanup } = createMockClient(url);
try {
const client = new Codex({ codexPathOverride: codexExecPath, baseUrl: url, apiKey: "test" });
const thread = client.startThread();
await thread.run("Hello, originator!");
@@ -801,6 +746,7 @@ describe("Codex", () => {
expect(originatorHeader).toBe("codex_sdk_ts");
}
} finally {
cleanup();
await close();
}
});
@@ -814,12 +760,13 @@ describe("Codex", () => {
}
})(),
});
const { client, cleanup } = createMockClient(url);
try {
const client = new Codex({ codexPathOverride: codexExecPath, baseUrl: url, apiKey: "test" });
const thread = client.startThread();
await expect(thread.run("fail")).rejects.toThrow("stream disconnected before completion:");
} finally {
cleanup();
await close();
}
}, 10000); // TODO(pakrym): remove timeout

View File

@@ -1,8 +1,5 @@
import path from "node:path";
import { describe, expect, it } from "@jest/globals";
import { Codex } from "../src/codex";
import { ThreadEvent } from "../src/index";
import {
@@ -12,8 +9,7 @@ import {
sse,
startResponsesTestProxy,
} from "./responsesProxy";
const codexExecPath = path.join(process.cwd(), "..", "..", "codex-rs", "target", "debug", "codex");
import { createMockClient } from "./testCodex";
describe("Codex", () => {
it("returns thread events", async () => {
@@ -21,10 +17,9 @@ describe("Codex", () => {
statusCode: 200,
responseBodies: [sse(responseStarted(), assistantMessage("Hi!"), responseCompleted())],
});
const { client, cleanup } = createMockClient(url);
try {
const client = new Codex({ codexPathOverride: codexExecPath, baseUrl: url, apiKey: "test" });
const thread = client.startThread();
const result = await thread.runStreamed("Hello, world!");
@@ -60,6 +55,7 @@ describe("Codex", () => {
]);
expect(thread.id).toEqual(expect.any(String));
} finally {
cleanup();
await close();
}
});
@@ -80,10 +76,9 @@ describe("Codex", () => {
),
],
});
const { client, cleanup } = createMockClient(url);
try {
const client = new Codex({ codexPathOverride: codexExecPath, baseUrl: url, apiKey: "test" });
const thread = client.startThread();
const first = await thread.runStreamed("first input");
await drainEvents(first.events);
@@ -106,6 +101,7 @@ describe("Codex", () => {
)?.text;
expect(assistantText).toBe("First response");
} finally {
cleanup();
await close();
}
});
@@ -126,10 +122,9 @@ describe("Codex", () => {
),
],
});
const { client, cleanup } = createMockClient(url);
try {
const client = new Codex({ codexPathOverride: codexExecPath, baseUrl: url, apiKey: "test" });
const originalThread = client.startThread();
const first = await originalThread.runStreamed("first input");
await drainEvents(first.events);
@@ -154,6 +149,7 @@ describe("Codex", () => {
)?.text;
expect(assistantText).toBe("First response");
} finally {
cleanup();
await close();
}
});
@@ -169,6 +165,7 @@ describe("Codex", () => {
),
],
});
const { client, cleanup } = createMockClient(url);
const schema = {
type: "object",
@@ -180,8 +177,6 @@ describe("Codex", () => {
} as const;
try {
const client = new Codex({ codexPathOverride: codexExecPath, baseUrl: url, apiKey: "test" });
const thread = client.startThread();
const streamed = await thread.runStreamed("structured", { outputSchema: schema });
await drainEvents(streamed.events);
@@ -198,6 +193,7 @@ describe("Codex", () => {
schema,
});
} finally {
cleanup();
await close();
}
});

View File

@@ -0,0 +1,28 @@
import fs from "node:fs/promises";
import os from "node:os";
import path from "node:path";
import { afterEach, beforeEach } from "@jest/globals";
const originalCodexHome = process.env.CODEX_HOME;
let currentCodexHome: string | undefined;
beforeEach(async () => {
currentCodexHome = await fs.mkdtemp(path.join(os.tmpdir(), "codex-sdk-test-"));
process.env.CODEX_HOME = currentCodexHome;
});
afterEach(async () => {
const codexHomeToDelete = currentCodexHome;
currentCodexHome = undefined;
if (originalCodexHome === undefined) {
delete process.env.CODEX_HOME;
} else {
process.env.CODEX_HOME = originalCodexHome;
}
if (codexHomeToDelete) {
await fs.rm(codexHomeToDelete, { recursive: true, force: true });
}
});

View File

@@ -0,0 +1,94 @@
import path from "node:path";
import { Codex } from "../src/codex";
import type { CodexConfigObject } from "../src/codexOptions";
export const codexExecPath = path.join(process.cwd(), "..", "..", "codex-rs", "target", "debug", "codex");
type CreateTestClientOptions = {
apiKey?: string;
baseUrl?: string;
config?: CodexConfigObject;
env?: Record<string, string>;
inheritEnv?: boolean;
};
export type TestClient = {
cleanup: () => void;
client: Codex;
};
export function createMockClient(url: string): TestClient {
return createTestClient({
config: {
model_provider: "mock",
model_providers: {
mock: {
name: "Mock provider for test",
base_url: url,
wire_api: "responses",
supports_websockets: false,
},
},
},
});
}
export function createTestClient(options: CreateTestClientOptions = {}): TestClient {
const env =
options.inheritEnv === false ? { ...options.env } : { ...getCurrentEnv(), ...options.env };
return {
cleanup: () => {},
client: new Codex({
codexPathOverride: codexExecPath,
baseUrl: options.baseUrl,
apiKey: options.apiKey,
config: mergeTestProviderConfig(options.baseUrl, options.config),
env,
}),
};
}
function mergeTestProviderConfig(
baseUrl: string | undefined,
config: CodexConfigObject | undefined,
): CodexConfigObject | undefined {
if (!baseUrl || hasExplicitProviderConfig(config)) {
return config;
}
// Built-in providers are merged before user config, so tests need a custom
// provider entry to force SSE against the local mock server.
return {
...config,
model_provider: "mock",
model_providers: {
mock: {
name: "Mock provider for test",
base_url: baseUrl,
wire_api: "responses",
supports_websockets: false,
},
},
};
}
function hasExplicitProviderConfig(config: CodexConfigObject | undefined): boolean {
return config?.model_provider !== undefined || config?.model_providers !== undefined;
}
function getCurrentEnv(): Record<string, string> {
const env: Record<string, string> = {};
for (const [key, value] of Object.entries(process.env)) {
if (key === "CODEX_INTERNAL_ORIGINATOR_OVERRIDE") {
continue;
}
if (value !== undefined) {
env[key] = value;
}
}
return env;
}