* fix(cli): stop quietly when the stdout reader closes early
`rea ... | head` crashed once head closed the pipe. TOON and YAML output
raised an unhandled EPIPE 'error' event with a Node stack trace. JSON
output reported {"code":"UNKNOWN","message":"write EPIPE"} on stderr.
Both exited 1, as if the analysis had failed.
The CLI entry point owns process.stdout, so it now treats EPIPE as the
reader declining the rest of the output. It stops writing, suppresses
the failure report and exits 141 like a SIGPIPE-terminated writer, also
when Incur exits through its own failure path. Other stdout errors are
handled as before.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
* fix(cli): guard mcp doctor output and cover midstream reader closure
The closed-reader guard was only installed for ordinary CLI commands. As a
result `rea mcp doctor | head` still crashed on EPIPE. The guard now lives
in one entry-point function shared by both CLI branches; MCP stdio keeps
its own lifecycle.
The tests now also close the reader after the first chunk of a 4 MiB
streamed JSON result, so later writes in writeJsonOutput are covered.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
85 lines
2.8 KiB
TypeScript
85 lines
2.8 KiB
TypeScript
import { join, resolve } from "node:path";
|
|
import { DatabaseSync } from "node:sqlite";
|
|
|
|
import { describe, expect } from "vitest";
|
|
|
|
import { createTestTempDirectory } from "../../fixtures/temporaryDirectory.js";
|
|
import { cliTest } from "../../support/cli/cliFixture.js";
|
|
import type { TestProcesses } from "../../support/process/processFixture.js";
|
|
|
|
/** Run the CLI, closing its stdout reader after `bytes` (0: before any write). */
|
|
const runWithClosedReader = async (
|
|
processes: TestProcesses,
|
|
arguments_: readonly string[],
|
|
bytes: number,
|
|
) => {
|
|
const child = processes.spawn(
|
|
process.execPath,
|
|
[resolve("scripts/rea.mjs"), ...arguments_],
|
|
{ env: { PATH: process.env.PATH ?? "", REA_LOG_LEVEL: "silent" } },
|
|
);
|
|
let received = 0;
|
|
if (bytes === 0) child.stdout.destroy();
|
|
else
|
|
child.stdout.on("data", (chunk: Buffer) => {
|
|
received += chunk.length;
|
|
if (received >= bytes) child.stdout.destroy();
|
|
});
|
|
child.stdin.end();
|
|
let stderr = "";
|
|
child.stderr.setEncoding("utf8");
|
|
child.stderr.on("data", (chunk: string) => (stderr += chunk));
|
|
const code = await new Promise<number | null>((done) =>
|
|
child.once("close", done),
|
|
);
|
|
return { code, stderr, received };
|
|
};
|
|
|
|
// Windows reports a closed pipe reader through a different error path.
|
|
describe.skipIf(process.platform === "win32")(
|
|
"CLI with a closed stdout",
|
|
() => {
|
|
cliTest.for([
|
|
["toon", ["capabilities", "--format", "toon"]],
|
|
["json", ["capabilities", "--format", "json"]],
|
|
["mcp doctor", ["mcp", "doctor", "--json"]],
|
|
] as const)(
|
|
"stops quietly with the SIGPIPE status when the reader closes first ($0)",
|
|
async ([, arguments_], { processes }) => {
|
|
const result = await runWithClosedReader(processes, arguments_, 0);
|
|
expect(result).toMatchObject({ code: 141, stderr: "" });
|
|
},
|
|
);
|
|
|
|
cliTest(
|
|
"stops quietly when the reader closes during streamed JSON",
|
|
async ({ processes }) => {
|
|
const directory = await createTestTempDirectory("rea-closed-stdout-");
|
|
const database = join(directory, "rows.db");
|
|
const writer = new DatabaseSync(database);
|
|
writer.exec("CREATE TABLE t (v TEXT)");
|
|
const insert = writer.prepare("INSERT INTO t VALUES (?)");
|
|
// 4 MiB of rows outlasts any pipe buffer, so writes continue after close.
|
|
for (let row = 0; row < 64; row += 1) insert.run("x".repeat(65_536));
|
|
writer.close();
|
|
const result = await runWithClosedReader(
|
|
processes,
|
|
[
|
|
"inspect-sqlite-database",
|
|
database,
|
|
"--table",
|
|
"t",
|
|
"--row-limit",
|
|
"64",
|
|
"--format",
|
|
"json",
|
|
],
|
|
1,
|
|
);
|
|
expect(result.received).toBeGreaterThan(0);
|
|
expect(result).toMatchObject({ code: 141, stderr: "" });
|
|
},
|
|
);
|
|
},
|
|
);
|