import { afterEach, expect, test, vi } from "vitest"; import type { ProcessSession } from "./bash-process-registry.js"; import { addSession, appendOutput, markExited, resetProcessRegistryForTests, } from "./bash-process-registry.js"; import { createProcessTool } from "./bash-tools.process.js"; import { resetDiagnosticSessionStateForTest } from "../logging/diagnostic-session-state.js"; afterEach(() => { resetProcessRegistryForTests(); resetDiagnosticSessionStateForTest(); }); function createBackgroundSession(id: string): ProcessSession { return { id, command: "test", startedAt: Date.now(), cwd: "/tmp", maxOutputChars: 10_000, pendingMaxOutputChars: 30_000, totalOutputChars: 0, pendingStdout: [], pendingStderr: [], pendingStdoutChars: 0, pendingStderrChars: 0, aggregated: "", tail: "", exited: false, exitCode: undefined, exitSignal: undefined, truncated: false, backgrounded: true, }; } test("process poll waits for completion when timeout is provided", async () => { vi.useFakeTimers(); try { const processTool = createProcessTool(); const sessionId = "sess"; const session = createBackgroundSession(sessionId); addSession(session); setTimeout(() => { appendOutput(session, "stdout", "done\n"); markExited(session, 0, null, "completed"); }, 10); const pollPromise = processTool.execute("toolcall", { action: "poll", sessionId, timeout: 2000, }); let resolved = false; void pollPromise.finally(() => { resolved = true; }); await vi.advanceTimersByTimeAsync(200); expect(resolved).toBe(false); await vi.advanceTimersByTimeAsync(100); const poll = await pollPromise; const details = poll.details as { status?: string; aggregated?: string }; expect(details.status).toBe("completed"); expect(details.aggregated ?? "").toContain("done"); } finally { vi.useRealTimers(); } }); test("process poll accepts string timeout values", async () => { vi.useFakeTimers(); try { const processTool = createProcessTool(); const sessionId = "sess-2"; const session = createBackgroundSession(sessionId); addSession(session); setTimeout(() => { appendOutput(session, "stdout", "done\n"); markExited(session, 0, null, "completed"); }, 10); const pollPromise = processTool.execute("toolcall", { action: "poll", sessionId, timeout: "2000", }); await vi.advanceTimersByTimeAsync(350); const poll = await pollPromise; const details = poll.details as { status?: string; aggregated?: string }; expect(details.status).toBe("completed"); expect(details.aggregated ?? "").toContain("done"); } finally { vi.useRealTimers(); } }); test("process poll exposes adaptive retryInMs for repeated no-output polls", async () => { const processTool = createProcessTool(); const sessionId = "sess-retry"; const session = createBackgroundSession(sessionId); addSession(session); const poll1 = await processTool.execute("toolcall-1", { action: "poll", sessionId, }); const poll2 = await processTool.execute("toolcall-2", { action: "poll", sessionId, }); const poll3 = await processTool.execute("toolcall-3", { action: "poll", sessionId, }); const poll4 = await processTool.execute("toolcall-4", { action: "poll", sessionId, }); const poll5 = await processTool.execute("toolcall-5", { action: "poll", sessionId, }); expect((poll1.details as { retryInMs?: number }).retryInMs).toBe(5000); expect((poll2.details as { retryInMs?: number }).retryInMs).toBe(10000); expect((poll3.details as { retryInMs?: number }).retryInMs).toBe(30000); expect((poll4.details as { retryInMs?: number }).retryInMs).toBe(60000); expect((poll5.details as { retryInMs?: number }).retryInMs).toBe(60000); }); test("process poll resets retryInMs when output appears and clears on completion", async () => { const processTool = createProcessTool(); const sessionId = "sess-reset"; const session = createBackgroundSession(sessionId); addSession(session); const poll1 = await processTool.execute("toolcall-1", { action: "poll", sessionId, }); const poll2 = await processTool.execute("toolcall-2", { action: "poll", sessionId, }); expect((poll1.details as { retryInMs?: number }).retryInMs).toBe(5000); expect((poll2.details as { retryInMs?: number }).retryInMs).toBe(10000); appendOutput(session, "stdout", "step complete\n"); const pollWithOutput = await processTool.execute("toolcall-output", { action: "poll", sessionId, }); expect((pollWithOutput.details as { retryInMs?: number }).retryInMs).toBe(5000); markExited(session, 0, null, "completed"); const pollCompleted = await processTool.execute("toolcall-completed", { action: "poll", sessionId, }); const completedDetails = pollCompleted.details as { status?: string; retryInMs?: number }; expect(completedDetails.status).toBe("completed"); expect(completedDetails.retryInMs).toBeUndefined(); const pollFinished = await processTool.execute("toolcall-finished", { action: "poll", sessionId, }); const finishedDetails = pollFinished.details as { status?: string; retryInMs?: number }; expect(finishedDetails.status).toBe("completed"); expect(finishedDetails.retryInMs).toBeUndefined(); });