From 83da9500ae2c3c87f3f011168dedd0ae8248e035 Mon Sep 17 00:00:00 2001 From: Wynne668 Date: Sat, 18 Jul 2026 09:24:02 +0800 Subject: [PATCH] fix(node-host): guard Claude CLI pipe errors (#109794) * fix(node-host): guard Claude CLI stdout/stderr pipe errors Co-authored-by: Cursor * fix(node-host): make request mock generic to fix test-types CI failure * test(node-host): harden child pipe regression --------- Co-authored-by: Cursor Co-authored-by: Peter Steinberger --- ...invoke-agent-cli-claude-pipe-error.test.ts | 69 +++++++++++++++++++ src/node-host/invoke-agent-cli-claude.ts | 4 ++ 2 files changed, 73 insertions(+) create mode 100644 src/node-host/invoke-agent-cli-claude-pipe-error.test.ts diff --git a/src/node-host/invoke-agent-cli-claude-pipe-error.test.ts b/src/node-host/invoke-agent-cli-claude-pipe-error.test.ts new file mode 100644 index 000000000000..f4167ac021ab --- /dev/null +++ b/src/node-host/invoke-agent-cli-claude-pipe-error.test.ts @@ -0,0 +1,69 @@ +import { afterEach, describe, expect, it, vi } from "vitest"; + +const spawnMock = vi.hoisted(() => vi.fn()); + +vi.mock("node:child_process", () => ({ + spawn: spawnMock, +})); + +import type { NodeHostClient } from "./client.js"; +import type { NodeInvokeRequestPayload } from "./invoke.js"; + +function frame(params: unknown): NodeInvokeRequestPayload { + return { + id: "invoke-pipe-error", + nodeId: "node-pipe-error", + command: "agent.cli.claude.run.v1", + paramsJSON: JSON.stringify(params), + }; +} + +function client(): NodeHostClient { + return { + async request>() { + return {} as T; + }, + }; +} + +describe("Claude CLI node command pipe errors", () => { + let realChild: import("node:child_process").ChildProcessWithoutNullStreams | undefined; + + afterEach(() => { + if (realChild && !realChild.killed) { + realChild.kill("SIGKILL"); + } + realChild = undefined; + spawnMock.mockReset(); + vi.resetModules(); + }); + + it("guards real child stdout/stderr pipe error events", async () => { + const childProcess = + await vi.importActual("node:child_process"); + realChild = childProcess.spawn(process.execPath, ["-e", "setInterval(() => {}, 1000)"], { + stdio: ["pipe", "pipe", "pipe"], + }); + spawnMock.mockReturnValueOnce(realChild as never); + const { runClaudeCliNodeCommand } = await import("./invoke-agent-cli-claude.js"); + + const request = { argv: ["-p"], idleTimeoutMs: 100, timeoutMs: 5_000 }; + + const run = runClaudeCliNodeCommand({ + client: client(), + frame: frame(request), + request, + argv: [process.execPath, ...request.argv], + cwd: undefined, + env: process.env as Record, + timeoutMs: request.timeoutMs, + }); + + expect(() => realChild?.stdout.emit("error", new Error("stdout pipe failure"))).not.toThrow(); + expect(() => realChild?.stderr.emit("error", new Error("stderr pipe failure"))).not.toThrow(); + realChild.kill("SIGKILL"); + + await expect(run).resolves.toMatchObject({ success: false }); + expect(spawnMock).toHaveBeenCalledOnce(); + }); +}); diff --git a/src/node-host/invoke-agent-cli-claude.ts b/src/node-host/invoke-agent-cli-claude.ts index ef44db3c69e4..1691cc62ddfc 100644 --- a/src/node-host/invoke-agent-cli-claude.ts +++ b/src/node-host/invoke-agent-cli-claude.ts @@ -165,6 +165,10 @@ export async function runClaudeCliNodeCommand(params: { terminalLineTouchesTruncation = false; } }; + // Output pipes can fail independently; child close/error remains authoritative. + const ignoreOutputStreamError = () => {}; + child.stdout.on("error", ignoreOutputStreamError); + child.stderr.on("error", ignoreOutputStreamError); child.stdout.on("data", (raw: Buffer) => { const retained = retain(raw); if (retained.length > 0) {