test(coding-agent): hardened spawn-based suites against Bun pipe EBADF

- Bun 1.4 canary's posix_spawn intermittently rejects pipe-backed child
  stdio (EBADF) inside test workers, failing telemetry-export and
  shell-snapshot probes before their assertions ran.
- Probes now spawn with ignored stdio and capture output via exit status
  or temp-file redirection; all behavioral assertions retained.
This commit is contained in:
can1357
2026-07-31 19:27:26 +02:00
parent c458b6e4bc
commit 9663df02cf
@@ -106,9 +106,47 @@ describe("sanitizeSnapshotForBrush", () => {
// function but discard the sidecar var, so the replay shell ran
// `command "" "$@"` and died with `command: command not found:` (issue #3470).
async function readStream(stream: ReadableStream<Uint8Array> | null): Promise<string> {
if (!stream) return "";
return await new Response(stream).text();
async function runBashWithCapturedOutput(
script: string,
options: { env?: Record<string, string | undefined>; stdin?: string } = {},
): Promise<{ exitCode: number | null; stdout: string; stderr: string }> {
const ioDir = await fs.mkdtemp(path.join(os.tmpdir(), "omp-snap-stdio-"));
const stdinPath = path.join(ioDir, "stdin");
const stdoutPath = path.join(ioDir, "stdout");
const stderrPath = path.join(ioDir, "stderr");
await Promise.all([fs.writeFile(stdinPath, options.stdin ?? ""), fs.writeFile(stdoutPath, ""), fs.writeFile(stderrPath, "")]);
try {
// Bun 1.4's test worker can inherit an invalid IPC descriptor on macOS,
// which makes posix_spawn reject pipe-backed stdio with EBADF. Redirect
// inside bash and keep the spawn's own stdio detached from that descriptor.
const child = Bun.spawn(
[
REAL_BASH,
"--noprofile",
"--norc",
"-c",
`{ ${script}\n} < "$OMP_TEST_STDIN" > "$OMP_TEST_STDOUT" 2> "$OMP_TEST_STDERR"`,
],
{
env: {
...options.env,
OMP_TEST_STDIN: stdinPath,
OMP_TEST_STDOUT: stdoutPath,
OMP_TEST_STDERR: stderrPath,
},
stdin: "ignore",
stdout: "ignore",
stderr: "ignore",
},
);
await child.exited;
const [stdout, stderr] = await Promise.all([fs.readFile(stdoutPath, "utf8"), fs.readFile(stderrPath, "utf8")]);
return { exitCode: child.exitCode, stdout, stderr };
} finally {
await fs.rm(ioDir, { recursive: true, force: true });
}
}
describe("shell-snapshot fn-env helper", () => {
@@ -123,22 +161,17 @@ describe("shell-snapshot fn-env helper", () => {
``,
].join("\n");
const child = Bun.spawn(["bash", "-c", `${fnEnvHelper}\n__omp_emit_referenced_exports`], {
const result = await runBashWithCapturedOutput(`${fnEnvHelper}\n__omp_emit_referenced_exports`, {
env: {
PATH: process.env.PATH ?? "/usr/bin:/bin",
__MISE_EXE: "/opt/echo",
FOO_TEST_DIR: "/opt/dir",
LC_ALL: "C",
},
stdin: "pipe",
stdout: "pipe",
stderr: "pipe",
stdin: funcs,
});
child.stdin.write(funcs);
await child.stdin.end();
const out = await readStream(child.stdout as ReadableStream<Uint8Array> | null);
await child.exited;
expect(child.exitCode).toBe(0);
const out = result.stdout;
expect(result.exitCode).toBe(0);
expect(out).toContain("export __MISE_EXE='/opt/echo'");
expect(out).toContain("export FOO_TEST_DIR='/opt/dir'");
@@ -164,7 +197,7 @@ describe("shell-snapshot fn-env helper", () => {
``,
].join("\n");
const child = Bun.spawn(["bash", "-c", `${fnEnvHelper}\n__omp_emit_referenced_exports`], {
const result = await runBashWithCapturedOutput(`${fnEnvHelper}\n__omp_emit_referenced_exports`, {
env: {
PATH: process.env.PATH ?? "/usr/bin:/bin",
GITHUB_TOKEN: "ghp_REDACTED",
@@ -177,15 +210,10 @@ describe("shell-snapshot fn-env helper", () => {
AZURE_CREDENTIAL: "xyz",
__MISE_EXE: "/opt/echo",
},
stdin: "pipe",
stdout: "pipe",
stderr: "ignore",
stdin: funcs,
});
child.stdin.write(funcs);
await child.stdin.end();
const out = await readStream(child.stdout as ReadableStream<Uint8Array> | null);
await child.exited;
expect(child.exitCode).toBe(0);
const out = result.stdout;
expect(result.exitCode).toBe(0);
for (const secret of [
"GITHUB_TOKEN",
@@ -210,32 +238,25 @@ describe("shell-snapshot fn-env helper", () => {
it("single-quote-escapes values containing apostrophes and preserves newlines", async () => {
const funcs = `shout () { echo "$TRICKY_VAL $NL_VAL"; }\n`;
const child = Bun.spawn(["bash", "-c", `${fnEnvHelper}\n__omp_emit_referenced_exports`], {
const result = await runBashWithCapturedOutput(`${fnEnvHelper}\n__omp_emit_referenced_exports`, {
env: {
PATH: process.env.PATH ?? "/usr/bin:/bin",
TRICKY_VAL: "it's 'tricky'",
NL_VAL: "line1\nline2",
},
stdin: "pipe",
stdout: "pipe",
stderr: "ignore",
stdin: funcs,
});
child.stdin.write(funcs);
await child.stdin.end();
const out = await readStream(child.stdout as ReadableStream<Uint8Array> | null);
await child.exited;
const out = result.stdout;
expect(out).toContain(`export TRICKY_VAL='it'\\''s '\\''tricky'\\'''`);
expect(out).toContain(`export NL_VAL='line1\nline2'`);
// Eval the emitted lines and verify the round-trip values match.
const round = Bun.spawn(
["bash", "-c", `eval "$1"; printf '%s\\n' "$TRICKY_VAL"; printf '%s\\n' "$NL_VAL"`, "_", out],
{ stdout: "pipe", stderr: "ignore" },
const round = await runBashWithCapturedOutput(
`__omp_exports=$(cat); eval "$__omp_exports"; printf '%s\\n' "$TRICKY_VAL"; printf '%s\\n' "$NL_VAL"`,
{ env: { PATH: process.env.PATH ?? "/usr/bin:/bin" }, stdin: out },
);
const echoed = await readStream(round.stdout as ReadableStream<Uint8Array> | null);
await round.exited;
expect(echoed).toBe("it's 'tricky'\nline1\nline2\n");
expect(round.stdout).toBe("it's 'tricky'\nline1\nline2\n");
});
});
@@ -274,15 +295,11 @@ describe("getOrCreateSnapshot", () => {
// Replay the snapshot in a fresh bash with `set -u` and confirm the
// mise() function resolves cleanly instead of dying on the empty var.
const replay = Bun.spawn(
[realBash, "--noprofile", "--norc", "-c", `set -u; source "$1"; mise hello world; shout`, "_", snapshotPath!],
{ stdout: "pipe", stderr: "pipe" },
);
const stdout = await readStream(replay.stdout as ReadableStream<Uint8Array> | null);
const stderr = await readStream(replay.stderr as ReadableStream<Uint8Array> | null);
await replay.exited;
expect({ exitCode: replay.exitCode, stderr }).toEqual({ exitCode: 0, stderr: "" });
expect(stdout).toBe("hello world\n/opt/foo\n");
const replay = await runBashWithCapturedOutput(`set -u; source "$OMP_TEST_SNAPSHOT"; mise hello world; shout`, {
env: { ...process.env, OMP_TEST_SNAPSHOT: snapshotPath! },
});
expect({ exitCode: replay.exitCode, stderr: replay.stderr }).toEqual({ exitCode: 0, stderr: "" });
expect(replay.stdout).toBe("hello world\n/opt/foo\n");
// PR-review hardening: snapshot file must be group/world-unreadable since
// it now inlines env-var values. Directory must be 0700 for the same