diff --git a/plugins/codex/scripts/lib/process.mjs b/plugins/codex/scripts/lib/process.mjs index dd8fc3751..78bb9a20e 100644 --- a/plugins/codex/scripts/lib/process.mjs +++ b/plugins/codex/scripts/lib/process.mjs @@ -1,13 +1,16 @@ import { spawnSync } from "node:child_process"; import process from "node:process"; +// Git metadata can exceed Node's 1 MiB spawnSync default. Keep a generous explicit bound. +const DEFAULT_MAX_BUFFER = 256 * 1024 * 1024; + export function runCommand(command, args = [], options = {}) { const result = spawnSync(command, args, { cwd: options.cwd, env: options.env, encoding: "utf8", input: options.input, - maxBuffer: options.maxBuffer, + maxBuffer: options.maxBuffer ?? DEFAULT_MAX_BUFFER, stdio: options.stdio ?? "pipe", shell: options.shell ?? (process.platform === "win32" ? (process.env.SHELL || true) : false), windowsHide: true diff --git a/tests/process.test.mjs b/tests/process.test.mjs index 80e0715b0..13e70ecf0 100644 --- a/tests/process.test.mjs +++ b/tests/process.test.mjs @@ -1,7 +1,29 @@ import test from "node:test"; import assert from "node:assert/strict"; -import { terminateProcessTree } from "../plugins/codex/scripts/lib/process.mjs"; +import { runCommand, terminateProcessTree } from "../plugins/codex/scripts/lib/process.mjs"; + +test("runCommand allows output larger than Node's default maxBuffer", () => { + const outputBytes = 2 * 1024 * 1024; + const result = runCommand(process.execPath, [ + "-e", + `process.stdout.write("x".repeat(${outputBytes}))` + ], { shell: false }); + + assert.equal(result.error, null); + assert.equal(result.status, 0); + assert.equal(Buffer.byteLength(result.stdout), outputBytes); +}); + +test("runCommand preserves an explicit maxBuffer override", () => { + const result = runCommand( + process.execPath, + ["-e", `process.stdout.write("x".repeat(${2 * 1024}))`], + { maxBuffer: 1024, shell: false } + ); + + assert.equal(result.error?.code, "ENOBUFS"); +}); test("terminateProcessTree uses taskkill on Windows", () => { let captured = null; @@ -53,3 +75,18 @@ test("terminateProcessTree treats missing Windows processes as already stopped", assert.equal(outcome.result.status, 128); assert.match(outcome.result.stdout, /not found/i); }); + + +for (const [stream, character] of [["stdout", "é"], ["stderr", "😀"]]) { + test(`runCommand retains large Unicode ${stream} output`, () => { + const text = character.repeat(600_000); + const result = runCommand(process.execPath, [ + "-e", + `process.${stream}.write(${JSON.stringify(character)}.repeat(600000))` + ], { shell: false }); + assert.equal(result.status, 0); + assert.equal(result.error, null); + assert.equal(result[stream], text); + assert.equal(Buffer.byteLength(result[stream]), Buffer.byteLength(text)); + }); +}