import { afterEach, expect, test } from "vitest"; import { createExecTool } from "./bash-tools.exec"; import { getFinishedSession, getSession, resetProcessRegistryForTests, } from "./bash-process-registry"; import { killProcessTree } from "./shell-utils"; const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve, ms)); afterEach(() => { resetProcessRegistryForTests(); }); test("background exec is not killed when tool signal aborts", async () => { const tool = createExecTool({ allowBackground: true, backgroundMs: 0 }); const abortController = new AbortController(); const result = await tool.execute( "toolcall", { command: 'node -e "setTimeout(() => {}, 5000)"', background: true }, abortController.signal, ); expect(result.details.status).toBe("running"); const sessionId = (result.details as { sessionId: string }).sessionId; abortController.abort(); await sleep(150); const running = getSession(sessionId); const finished = getFinishedSession(sessionId); try { expect(finished).toBeUndefined(); expect(running?.exited).toBe(false); } finally { const pid = running?.pid; if (pid) killProcessTree(pid); } }); test("background exec still times out after tool signal abort", async () => { const tool = createExecTool({ allowBackground: true, backgroundMs: 0 }); const abortController = new AbortController(); const result = await tool.execute( "toolcall", { command: 'node -e "setTimeout(() => {}, 5000)"', background: true, timeout: 0.2, }, abortController.signal, ); expect(result.details.status).toBe("running"); const sessionId = (result.details as { sessionId: string }).sessionId; abortController.abort(); let finished = getFinishedSession(sessionId); const deadline = Date.now() + (process.platform === "win32" ? 10_000 : 2_000); while (!finished && Date.now() < deadline) { await sleep(20); finished = getFinishedSession(sessionId); } const running = getSession(sessionId); try { expect(finished).toBeTruthy(); expect(finished?.status).toBe("failed"); } finally { const pid = running?.pid; if (pid) killProcessTree(pid); } }); test("yielded background exec is not killed when tool signal aborts", async () => { const tool = createExecTool({ allowBackground: true, backgroundMs: 10 }); const abortController = new AbortController(); const result = await tool.execute( "toolcall", { command: 'node -e "setTimeout(() => {}, 5000)"', yieldMs: 5 }, abortController.signal, ); expect(result.details.status).toBe("running"); const sessionId = (result.details as { sessionId: string }).sessionId; abortController.abort(); await sleep(150); const running = getSession(sessionId); const finished = getFinishedSession(sessionId); try { expect(finished).toBeUndefined(); expect(running?.exited).toBe(false); } finally { const pid = running?.pid; if (pid) killProcessTree(pid); } }); test("yielded background exec still times out", async () => { const tool = createExecTool({ allowBackground: true, backgroundMs: 10 }); const result = await tool.execute("toolcall", { command: 'node -e "setTimeout(() => {}, 5000)"', yieldMs: 5, timeout: 0.2, }); expect(result.details.status).toBe("running"); const sessionId = (result.details as { sessionId: string }).sessionId; let finished = getFinishedSession(sessionId); const deadline = Date.now() + (process.platform === "win32" ? 10_000 : 2_000); while (!finished && Date.now() < deadline) { await sleep(20); finished = getFinishedSession(sessionId); } const running = getSession(sessionId); try { expect(finished).toBeTruthy(); expect(finished?.status).toBe("failed"); } finally { const pid = running?.pid; if (pid) killProcessTree(pid); } });