feat(ts-sdk): allow overriding CLI environment (#6648)

## Summary
- add an `env` option for the TypeScript Codex client and plumb it into
`CodexExec` so the CLI can run without inheriting `process.env`
- extend the test spy to capture spawn environments, add coverage for
the new option, and document how to use it

## Testing
- `pnpm test` *(fails: corepack cannot download pnpm because outbound
network access is blocked in the sandbox)*

------
[Codex
Task](https://chatgpt.com/codex/tasks/task_i_6916b2d7c7548322a72d61d91a2dac85)
This commit is contained in:
Ryan Lopopolo
2025-11-14 11:44:19 -08:00
committed by GitHub
parent 37fba28ac3
commit 936650001f
6 changed files with 88 additions and 6 deletions

View File

@@ -9,18 +9,26 @@ const actualChildProcess =
jest.requireActual<typeof import("node:child_process")>("node:child_process");
const spawnMock = child_process.spawn as jest.MockedFunction<typeof actualChildProcess.spawn>;
export function codexExecSpy(): { args: string[][]; restore: () => void } {
export function codexExecSpy(): {
args: string[][];
envs: (Record<string, string> | undefined)[];
restore: () => void;
} {
const previousImplementation = spawnMock.getMockImplementation() ?? actualChildProcess.spawn;
const args: string[][] = [];
const envs: (Record<string, string> | undefined)[] = [];
spawnMock.mockImplementation(((...spawnArgs: Parameters<typeof child_process.spawn>) => {
const commandArgs = spawnArgs[1];
args.push(Array.isArray(commandArgs) ? [...commandArgs] : []);
const options = spawnArgs[2] as child_process.SpawnOptions | undefined;
envs.push(options?.env as Record<string, string> | undefined);
return previousImplementation(...spawnArgs);
}) as typeof actualChildProcess.spawn);
return {
args,
envs,
restore: () => {
spawnMock.mockClear();
spawnMock.mockImplementation(previousImplementation);

View File

@@ -348,6 +348,49 @@ describe("Codex", () => {
}
});
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 { 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");
}
expect(spawnEnv.CUSTOM_ENV).toBe("custom");
expect(spawnEnv.CODEX_ENV_SHOULD_NOT_LEAK).toBeUndefined();
expect(spawnEnv.OPENAI_BASE_URL).toBe(url);
expect(spawnEnv.CODEX_API_KEY).toBe("test");
expect(spawnEnv.CODEX_INTERNAL_ORIGINATOR_OVERRIDE).toBeDefined();
} finally {
delete process.env.CODEX_ENV_SHOULD_NOT_LEAK;
restore();
await close();
}
});
it("passes additionalDirectories as repeated flags", async () => {
const { url, close } = await startResponsesTestProxy({
statusCode: 200,