Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions nodejs/src/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
}
});

Expand Down
16 changes: 16 additions & 0 deletions nodejs/test/client.test.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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" }));
Expand Down