import { describe, expect, it, vi } from "vitest"; import { createMutationRecovery } from "./mutation-recovery"; const page = (next: string, through: string, hasMore = false) => ({ messages: [{ id: "m", sequence: "1", revision: Number(next) }], next, through, hasMore, }); describe("mutation recovery", () => { it("drains a fixed high water, then resumes independently of creation sequences", async () => { const fetch = vi.fn().mockResolvedValueOnce(page("2", "4", true)).mockResolvedValueOnce(page("4", "4")).mockResolvedValueOnce(page("5", "5")); const apply = vi.fn(); const recovery = createMutationRecovery(fetch, apply); await recovery.run(); await recovery.run(); expect(fetch.mock.calls.map(([cursor]) => cursor)).toEqual([{ after: "0" }, { after: "2", through: "4" }, { after: "4" }]); expect(apply).toHaveBeenCalledTimes(3); }); it("retries the same cursor after projection application fails", async () => { const fetch = vi.fn().mockResolvedValue(page("1", "1")); const apply = vi.fn().mockImplementationOnce(() => { throw new Error("merge failed"); }); const recovery = createMutationRecovery(fetch, apply); await expect(recovery.run()).rejects.toThrow("merge failed"); await recovery.run(); expect(fetch.mock.calls.map(([cursor]) => cursor)).toEqual([{ after: "0" }, { after: "0" }]); }); it("ignores a response after scope disposal", async () => { let resolve!: (value: ReturnType) => void; const fetch = vi.fn(() => new Promise>((done) => { resolve = done; })); const apply = vi.fn(); const recovery = createMutationRecovery(fetch, apply); const pending = recovery.run(); recovery.stop(); resolve(page("1", "1")); await pending; expect(apply).not.toHaveBeenCalled(); }); it("rejects stalled pages instead of spinning", async () => { const recovery = createMutationRecovery(async () => page("0", "2", true), vi.fn()); await expect(recovery.run()).rejects.toThrow(); }); it("advances empty completed windows without looping", async () => { const fetch = vi.fn().mockResolvedValue({ ...page("0", "0"), messages: [] }); const recovery = createMutationRecovery(fetch, vi.fn()); await recovery.run(); expect(fetch).toHaveBeenCalledTimes(1); }); });