mirror of
https://github.com/openai/codex.git
synced 2026-04-29 17:06:51 +00:00
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:
@@ -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();
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
});
|
||||
|
||||
28
sdk/typescript/tests/setupCodexHome.ts
Normal file
28
sdk/typescript/tests/setupCodexHome.ts
Normal 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 });
|
||||
}
|
||||
});
|
||||
94
sdk/typescript/tests/testCodex.ts
Normal file
94
sdk/typescript/tests/testCodex.ts
Normal 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;
|
||||
}
|
||||
Reference in New Issue
Block a user