import { afterEach, expect, test, vi } from "vitest"; import { resetDiagnosticSessionStateForTest } from "../logging/diagnostic-session-state.js"; 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"; 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, }; } function createProcessSessionHarness(sessionId: string) { const processTool = createProcessTool(); const session = createBackgroundSession(sessionId); addSession(session); return { processTool, session }; } async function pollSession( processTool: ReturnType, callId: string, sessionId: string, timeout?: number | string, ) { return processTool.execute(callId, { action: "poll", sessionId, ...(timeout === undefined ? {} : { timeout }), }); } function retryMs(result: Awaited["execute"]>>) { return (result.details as { retryInMs?: number }).retryInMs; } function pollStatus(result: Awaited["execute"]>>) { return (result.details as { status?: string }).status; } test("process poll waits for completion when timeout is provided", async () => { vi.useFakeTimers(); try { const sessionId = "sess"; const { processTool, session } = createProcessSessionHarness(sessionId); setTimeout(() => { appendOutput(session, "stdout", "done\n"); markExited(session, 0, null, "completed"); }, 10); const pollPromise = pollSession(processTool, "toolcall", sessionId, 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 sessionId = "sess-2"; const { processTool, session } = createProcessSessionHarness(sessionId); setTimeout(() => { appendOutput(session, "stdout", "done\n"); markExited(session, 0, null, "completed"); }, 10); const pollPromise = pollSession(processTool, "toolcall", sessionId, "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 sessionId = "sess-retry"; const { processTool } = createProcessSessionHarness(sessionId); const polls = await Promise.all([ pollSession(processTool, "toolcall-1", sessionId), pollSession(processTool, "toolcall-2", sessionId), pollSession(processTool, "toolcall-3", sessionId), pollSession(processTool, "toolcall-4", sessionId), pollSession(processTool, "toolcall-5", sessionId), ]); expect(polls.map((poll) => retryMs(poll))).toEqual([5000, 10000, 30000, 60000, 60000]); }); test("process poll resets retryInMs when output appears and clears on completion", async () => { const sessionId = "sess-reset"; const { processTool, session } = createProcessSessionHarness(sessionId); const poll1 = await pollSession(processTool, "toolcall-1", sessionId); const poll2 = await pollSession(processTool, "toolcall-2", sessionId); expect(retryMs(poll1)).toBe(5000); expect(retryMs(poll2)).toBe(10000); appendOutput(session, "stdout", "step complete\n"); const pollWithOutput = await pollSession(processTool, "toolcall-output", sessionId); expect(retryMs(pollWithOutput)).toBe(5000); markExited(session, 0, null, "completed"); const pollCompleted = await pollSession(processTool, "toolcall-completed", sessionId); expect(pollStatus(pollCompleted)).toBe("completed"); expect(retryMs(pollCompleted)).toBeUndefined(); const pollFinished = await pollSession(processTool, "toolcall-finished", sessionId); expect(pollStatus(pollFinished)).toBe("completed"); expect(retryMs(pollFinished)).toBeUndefined(); });