diff --git a/src/agents/bash-tools.exec.pty-cleanup.test.ts b/src/agents/bash-tools.exec.pty-cleanup.test.ts deleted file mode 100644 index da3ac243d92..00000000000 --- a/src/agents/bash-tools.exec.pty-cleanup.test.ts +++ /dev/null @@ -1,102 +0,0 @@ -import { afterEach, beforeAll, beforeEach, expect, test, vi } from "vitest"; -let createExecTool: typeof import("./bash-tools.exec.js").createExecTool; -let resetProcessRegistryForTests: typeof import("./bash-process-registry.js").resetProcessRegistryForTests; - -const { ptySpawnMock } = vi.hoisted(() => ({ - ptySpawnMock: vi.fn(), -})); - -vi.mock("@lydell/node-pty", () => ({ - spawn: (...args: unknown[]) => ptySpawnMock(...args), -})); - -beforeAll(async () => { - ({ createExecTool } = await import("./bash-tools.exec.js")); - ({ resetProcessRegistryForTests } = await import("./bash-process-registry.js")); -}); - -beforeEach(() => { - ptySpawnMock.mockReset(); -}); - -afterEach(() => { - resetProcessRegistryForTests(); - vi.clearAllMocks(); -}); - -test("exec disposes PTY listeners after normal exit", async () => { - const disposeData = vi.fn(); - const disposeExit = vi.fn(); - - ptySpawnMock.mockImplementation(() => ({ - pid: 0, - write: vi.fn(), - onData: (listener: (value: string) => void) => { - listener("ok"); - return { dispose: disposeData }; - }, - onExit: (listener: (event: { exitCode: number; signal?: number }) => void) => { - listener({ exitCode: 0 }); - return { dispose: disposeExit }; - }, - kill: vi.fn(), - })); - - const tool = createExecTool({ - allowBackground: false, - host: "gateway", - security: "full", - ask: "off", - }); - const result = await tool.execute("toolcall", { - command: "echo ok", - pty: true, - }); - - expect(result.details.status).toBe("completed"); - expect(disposeData).toHaveBeenCalledTimes(1); - expect(disposeExit).toHaveBeenCalledTimes(1); -}); - -test("exec tears down PTY resources on timeout", async () => { - const disposeData = vi.fn(); - const disposeExit = vi.fn(); - let exitListener: ((event: { exitCode: number; signal?: number }) => void) | undefined; - const kill = vi.fn(() => { - // Mirror real PTY behavior: process exits shortly after force-kill. - exitListener?.({ exitCode: 137, signal: 9 }); - }); - - ptySpawnMock.mockImplementation(() => ({ - pid: 0, - write: vi.fn(), - onData: () => ({ dispose: disposeData }), - onExit: (listener: (event: { exitCode: number; signal?: number }) => void) => { - exitListener = listener; - return { dispose: disposeExit }; - }, - kill, - })); - - const tool = createExecTool({ - allowBackground: false, - host: "gateway", - security: "full", - ask: "off", - }); - const result = await tool.execute("toolcall", { - command: "sleep 5", - pty: true, - timeout: 0.01, - }); - - expect(result.details).toMatchObject({ - status: "failed", - timedOut: true, - exitCode: 137, - }); - expect((result.content[0] as { text?: string }).text).toMatch(/Command timed out/); - expect(kill).toHaveBeenCalledTimes(1); - expect(disposeData).toHaveBeenCalledTimes(1); - expect(disposeExit).toHaveBeenCalledTimes(1); -}); diff --git a/src/process/supervisor/adapters/pty.test.ts b/src/process/supervisor/adapters/pty.test.ts index b9107284ad5..14525471e7d 100644 --- a/src/process/supervisor/adapters/pty.test.ts +++ b/src/process/supervisor/adapters/pty.test.ts @@ -20,18 +20,22 @@ vi.mock("../../kill-tree.js", () => ({ function createStubPty(pid = 1234) { let exitListener: ((event: { exitCode: number; signal?: number }) => void) | null = null; + const disposeData = vi.fn(); + const disposeExit = vi.fn(); return { pid, write: vi.fn(), - onData: vi.fn(() => ({ dispose: vi.fn() })), + onData: vi.fn(() => ({ dispose: disposeData })), onExit: vi.fn((listener: (event: { exitCode: number; signal?: number }) => void) => { exitListener = listener; - return { dispose: vi.fn() }; + return { dispose: disposeExit }; }), kill: (signal?: string) => ptyKillMock(signal), emitExit: (event: { exitCode: number; signal?: number }) => { exitListener?.(event); }, + disposeData, + disposeExit, }; } @@ -151,6 +155,22 @@ describe("createPtyAdapter", () => { await expect(adapter.wait()).resolves.toEqual({ code: 3, signal: null }); }); + it("disposes PTY listeners", async () => { + const stub = createStubPty(); + spawnMock.mockReturnValue(stub); + + const adapter = await createPtyAdapter({ + shell: "bash", + args: ["-lc", "echo ok"], + }); + adapter.onStdout(() => undefined); + + adapter.dispose(); + + expect(stub.disposeData).toHaveBeenCalledTimes(1); + expect(stub.disposeExit).toHaveBeenCalledTimes(1); + }); + it("keeps inherited env when no override env is provided on non-Linux", async () => { const originalPlatform = Object.getOwnPropertyDescriptor(process, "platform"); Object.defineProperty(process, "platform", { value: "darwin", configurable: true });