mirror of
https://github.com/openai/codex.git
synced 2026-04-24 22:54:54 +00:00
fix: handle early codex exec exit (#8825)
Fixes CodexExec to avoid missing early process exits by registering the exit handler up front and deferring the error until after stdout is drained, and adds a regression test that simulates a fast-exit child while still producing output so hangs are caught.
This commit is contained in:
committed by
GitHub
parent
4cef89a122
commit
0d788e6263
70
sdk/typescript/tests/exec.test.ts
Normal file
70
sdk/typescript/tests/exec.test.ts
Normal file
@@ -0,0 +1,70 @@
|
||||
import * as child_process from "node:child_process";
|
||||
import { EventEmitter } from "node:events";
|
||||
import { PassThrough } from "node:stream";
|
||||
|
||||
import { describe, expect, it } from "@jest/globals";
|
||||
|
||||
jest.mock("node:child_process", () => {
|
||||
const actual = jest.requireActual<typeof import("node:child_process")>("node:child_process");
|
||||
return { ...actual, spawn: jest.fn() };
|
||||
});
|
||||
|
||||
const _actualChildProcess =
|
||||
jest.requireActual<typeof import("node:child_process")>("node:child_process");
|
||||
const spawnMock = child_process.spawn as jest.MockedFunction<typeof _actualChildProcess.spawn>;
|
||||
|
||||
class FakeChildProcess extends EventEmitter {
|
||||
stdin = new PassThrough();
|
||||
stdout = new PassThrough();
|
||||
stderr = new PassThrough();
|
||||
killed = false;
|
||||
|
||||
kill(): boolean {
|
||||
this.killed = true;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
function createEarlyExitChild(exitCode = 2): FakeChildProcess {
|
||||
const child = new FakeChildProcess();
|
||||
setImmediate(() => {
|
||||
child.stderr.write("boom");
|
||||
child.emit("exit", exitCode, null);
|
||||
setImmediate(() => {
|
||||
child.stdout.end();
|
||||
child.stderr.end();
|
||||
});
|
||||
});
|
||||
return child;
|
||||
}
|
||||
|
||||
const delay = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms));
|
||||
|
||||
describe("CodexExec", () => {
|
||||
it("rejects when exit happens before stdout closes", async () => {
|
||||
const { CodexExec } = await import("../src/exec");
|
||||
const child = createEarlyExitChild();
|
||||
spawnMock.mockReturnValue(child as unknown as child_process.ChildProcess);
|
||||
|
||||
const exec = new CodexExec("codex");
|
||||
const runPromise = (async () => {
|
||||
for await (const _ of exec.run({ input: "hi" })) {
|
||||
// no-op
|
||||
}
|
||||
})().then(
|
||||
() => ({ status: "resolved" as const }),
|
||||
(error) => ({ status: "rejected" as const, error }),
|
||||
);
|
||||
|
||||
const result = await Promise.race([
|
||||
runPromise,
|
||||
delay(500).then(() => ({ status: "timeout" as const })),
|
||||
]);
|
||||
|
||||
expect(result.status).toBe("rejected");
|
||||
if (result.status === "rejected") {
|
||||
expect(result.error).toBeInstanceOf(Error);
|
||||
expect(result.error.message).toMatch(/Codex Exec exited/);
|
||||
}
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user