Submatter/apps/web/components/chat/mutation-recovery.test.ts

48 lines
2.3 KiB
TypeScript
Raw Normal View History

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<typeof page>) => void;
const fetch = vi.fn(() => new Promise<ReturnType<typeof page>>((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);
});
});