From 050919a493878045ed4ade0cd1cbb391174920b8 Mon Sep 17 00:00:00 2001 From: jinhyuk9714 Date: Sat, 6 Jun 2026 00:31:11 +0900 Subject: [PATCH] fix(nodejs): handle stdio stdin errors --- nodejs/src/client.ts | 14 ++++++++++---- nodejs/test/client.test.ts | 16 ++++++++++++++++ 2 files changed, 26 insertions(+), 4 deletions(-) diff --git a/nodejs/src/client.ts b/nodejs/src/client.ts index 8dc35b8d7..aa2e7807b 100644 --- a/nodejs/src/client.ts +++ b/nodejs/src/client.ts @@ -2089,10 +2089,16 @@ export class CopilotClient { throw new Error("CLI process not started"); } - // Add error handler to stdin to prevent unhandled rejections during forceStop - this.cliProcess.stdin?.on("error", (err) => { - if (!this.forceStopping) { - throw err; + // Keep stdin pipe errors inside the normal JSON-RPC teardown path. + this.cliProcess.stdin?.on("error", () => { + if (this.forceStopping) { + return; + } + this.state = "error"; + try { + this.connection?.dispose(); + } catch { + // The connection may already be closing after the child process exited. } }); diff --git a/nodejs/test/client.test.ts b/nodejs/test/client.test.ts index 657ec7c9c..b60541d70 100644 --- a/nodejs/test/client.test.ts +++ b/nodejs/test/client.test.ts @@ -1,5 +1,6 @@ /* eslint-disable @typescript-eslint/no-explicit-any */ import { describe, expect, it, onTestFinished, vi } from "vitest"; +import { PassThrough } from "stream"; import { approveAll, CopilotClient, @@ -13,6 +14,21 @@ import { defaultJoinSessionPermissionHandler } from "../src/types.js"; // This file is for unit tests. Where relevant, prefer to add e2e tests in e2e/*.test.ts instead describe("CopilotClient", () => { + it("disposes the stdio connection when child stdin emits an error", async () => { + const client = new CopilotClient(); + onTestFinished(() => client.forceStop()); + + const stdin = new PassThrough(); + const stdout = new PassThrough(); + (client as any).cliProcess = { stdin, stdout }; + await (client as any).connectToChildProcessViaStdio(); + + const dispose = vi.spyOn((client as any).connection, "dispose"); + + expect(() => stdin.emit("error", new Error("broken pipe"))).not.toThrow(); + expect(dispose).toHaveBeenCalledOnce(); + }); + it("does not respond to v3 permission requests when handler returns no-result", async () => { const session = new CopilotSession("session-1", {} as any); session.registerPermissionHandler(() => ({ kind: "no-result" }));