Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 16 additions & 4 deletions dist/main.js

Large diffs are not rendered by default.

19 changes: 16 additions & 3 deletions src/runCodexExec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -312,14 +312,27 @@ export async function runCodexExec({
await new Promise((resolve, reject) => {
const child = spawn(program, command, {
env,
stdio: ["pipe", "inherit", "inherit"],
stdio: ["pipe", "pipe", "pipe"],
});
child.stdout.pipe(process.stdout, { end: false });
child.stderr.pipe(process.stderr, { end: false });
child.stdin.write(input);
child.stdin.end();

child.on("error", reject);
const closeOutputStreams = () => {
child.stdout.unpipe(process.stdout);
child.stderr.unpipe(process.stderr);
child.stdout.destroy();
child.stderr.destroy();
};

child.on("close", async (code) => {
child.once("error", (err) => {
closeOutputStreams();
reject(err);
});

child.once("exit", async (code) => {
closeOutputStreams();
if (code !== 0) {
reject(new Error(`${program} exited with code ${code}`));
return;
Expand Down
26 changes: 25 additions & 1 deletion test/runCodexExec.test.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -25,21 +25,33 @@ function runCodexExecWithFakeCodex({
permissionProfile = "",
extraArgs = "",
safetyStrategy = "unsafe",
holdStdioOpen = false,
} = {}) {
const tempDir = mkdtempSync(path.join(tmpdir(), "codex-action-permissions-"));
const capturePath = path.join(tempDir, "args.json");
const outputPath = path.join(tempDir, "output.txt");
const fakeCodexPath = path.join(tempDir, "codex.mjs");
writeFileSync(
fakeCodexPath,
`import { writeFileSync } from "node:fs";
`import { spawn } from "node:child_process";
import { writeFileSync } from "node:fs";
const args = process.argv.slice(2);
writeFileSync(process.env.CODEX_CAPTURE_ARGS, JSON.stringify(args));
const outputIndex = args.indexOf("--output-last-message");
if (outputIndex < 0 || outputIndex + 1 >= args.length) {
throw new Error("missing --output-last-message");
}
writeFileSync(args[outputIndex + 1], "fake final message\\n");
if (process.env.CODEX_HOLD_STDIO_OPEN === "1") {
console.log("fake codex stdout");
console.error("fake codex stderr");
const descendant = spawn(
process.execPath,
["-e", "setTimeout(() => {}, 5000)"],
{ stdio: "inherit" }
);
descendant.unref();
}
`,
"utf8"
);
Expand Down Expand Up @@ -97,7 +109,9 @@ writeFileSync(args[outputIndex + 1], "fake final message\\n");
...process.env,
PATH: `${tempDir}${path.delimiter}${process.env.PATH ?? ""}`,
CODEX_CAPTURE_ARGS: capturePath,
CODEX_HOLD_STDIO_OPEN: holdStdioOpen ? "1" : "0",
},
timeout: holdStdioOpen ? 2_000 : undefined,
}
);

Expand All @@ -111,6 +125,16 @@ writeFileSync(args[outputIndex + 1], "fake final message\\n");
return { result, capturedArgs };
}

test("does not wait for descendants holding stdio open", () => {
const { result } = runCodexExecWithFakeCodex({ holdStdioOpen: true });

assert.equal(result.error, undefined);
assert.equal(result.status, 0, result.error?.message ?? result.stderr);
assert.match(result.stdout, /fake codex stdout/);
assert.match(result.stderr, /fake codex stderr/);
assert.match(result.stdout, /fake final message/);
});

test("preserves workspace-write as the default legacy sandbox", () => {
const { result, capturedArgs } = runCodexExecWithFakeCodex();

Expand Down
Loading