import type { AgentMessage } from "@mariozechner/pi-agent-core"; import type { ExtensionContext } from "@mariozechner/pi-coding-agent"; import * as piCodingAgent from "@mariozechner/pi-coding-agent"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { retryAsync } from "../infra/retry.js"; // Mock the external generateSummary function vi.mock("@mariozechner/pi-coding-agent", async (importOriginal) => { const actual = await importOriginal(); return { ...actual, generateSummary: vi.fn(), }; }); const mockGenerateSummary = vi.mocked(piCodingAgent.generateSummary); describe("compaction retry integration", () => { beforeEach(() => { mockGenerateSummary.mockClear(); }); afterEach(() => { vi.clearAllTimers(); vi.useRealTimers(); }); const testMessages = [ { role: "user", content: "Test message" }, { role: "assistant", content: "Test response" }, ] as unknown as AgentMessage[]; const testModel = { provider: "anthropic", model: "claude-3-opus", } as unknown as NonNullable; const invokeGenerateSummary = (signal = new AbortController().signal) => mockGenerateSummary(testMessages, testModel, 1000, "test-api-key", signal); const runSummaryRetry = (options: Parameters[1]) => retryAsync(() => invokeGenerateSummary(), options); it("should successfully call generateSummary with retry wrapper", async () => { mockGenerateSummary.mockResolvedValueOnce("Test summary"); const result = await runSummaryRetry({ attempts: 3, minDelayMs: 500, maxDelayMs: 5000, jitter: 0.2, label: "compaction/generateSummary", }); expect(result).toBe("Test summary"); expect(mockGenerateSummary).toHaveBeenCalledTimes(1); }); it("should retry on transient error and succeed", async () => { mockGenerateSummary .mockRejectedValueOnce(new Error("Network timeout")) .mockResolvedValueOnce("Success after retry"); const result = await runSummaryRetry({ attempts: 3, minDelayMs: 0, maxDelayMs: 0, label: "compaction/generateSummary", }); expect(result).toBe("Success after retry"); expect(mockGenerateSummary).toHaveBeenCalledTimes(2); }); it("should NOT retry on user abort", async () => { const abortErr = new Error("aborted"); abortErr.name = "AbortError"; (abortErr as { cause?: unknown }).cause = { source: "user" }; mockGenerateSummary.mockRejectedValueOnce(abortErr); await expect( retryAsync(() => invokeGenerateSummary(), { attempts: 3, minDelayMs: 0, label: "compaction/generateSummary", shouldRetry: (err: unknown) => !(err instanceof Error && err.name === "AbortError"), }), ).rejects.toThrow("aborted"); // Should NOT retry on user cancellation (AbortError filtered by shouldRetry) expect(mockGenerateSummary).toHaveBeenCalledTimes(1); }); it("should retry up to 3 times and then fail", async () => { mockGenerateSummary.mockRejectedValue(new Error("Persistent API error")); await expect( runSummaryRetry({ attempts: 3, minDelayMs: 0, maxDelayMs: 0, label: "compaction/generateSummary", }), ).rejects.toThrow("Persistent API error"); expect(mockGenerateSummary).toHaveBeenCalledTimes(3); }); it("should apply exponential backoff", async () => { vi.useFakeTimers(); mockGenerateSummary .mockRejectedValueOnce(new Error("Error 1")) .mockRejectedValueOnce(new Error("Error 2")) .mockResolvedValueOnce("Success on 3rd attempt"); const delays: number[] = []; const promise = runSummaryRetry({ attempts: 3, minDelayMs: 500, maxDelayMs: 5000, jitter: 0, label: "compaction/generateSummary", onRetry: (info) => delays.push(info.delayMs), }); await vi.runAllTimersAsync(); const result = await promise; expect(result).toBe("Success on 3rd attempt"); expect(mockGenerateSummary).toHaveBeenCalledTimes(3); // First retry: 500ms, second retry: 1000ms expect(delays[0]).toBe(500); expect(delays[1]).toBe(1000); vi.useRealTimers(); }); });