import { describe, expect, it } from "vitest"; import { createActor, fromCallback, waitFor } from "xstate"; import type { UiMessage } from "@/stores/chat/ui-message"; import type { ChatEvent } from "@/stores/chat/chat-events"; import { chatMachine } from "@/stores/chat/chat-machine"; import type { ChatHistoryActorInput, LoadMoreHistoryActorEvent, } from "@/stores/chat/machine/actors/history"; import { createLoadHistoryCallback, createTestChatMachine, TEST_CHAT_MACHINE_INPUT, } from "./chat-machine.test-utils"; describe("chat history flow", () => { it("enters user ready after history is loaded", async () => { const actor = createActor(createTestChatMachine(), { input: TEST_CHAT_MACHINE_INPUT, }).start(); actor.send({ type: "ChatUserLogin", token: "token" }); await waitFor(actor, (snapshot) => snapshot.matches({ userSession: "ready" }), ); const snapshot = actor.getSnapshot(); expect(snapshot.context.historyLoaded).toBe(true); actor.stop(); }); it("renders local history before network history sync finishes", async () => { let resolveNetwork!: () => void; const networkReleased = new Promise((resolve) => { resolveNetwork = resolve; }); const localMessage: UiMessage = { id: "local-msg", content: "cached local message", isFromAI: true, date: "2026-07-02", }; const networkMessage: UiMessage = { id: "network-msg", content: "fresh network message", isFromAI: true, date: "2026-07-02", }; const machine = chatMachine.provide({ actors: { loadHistory: fromCallback< ChatEvent, ChatHistoryActorInput >(({ sendBack }) => { sendBack({ type: "ChatLocalHistoryLoaded", output: { messages: [localMessage], localCount: 1, }, }); void networkReleased.then(() => { sendBack({ type: "ChatNetworkHistoryLoaded", output: { messages: [networkMessage], localOverwritten: true, localCount: 1, networkCount: 1, total: 1, limit: 50, }, }); }); return () => undefined; }), }, }); const actor = createActor(machine, { input: TEST_CHAT_MACHINE_INPUT, }).start(); actor.send({ type: "ChatUserLogin", token: "token" }); await waitFor(actor, (snapshot) => snapshot.matches({ userSession: "ready" }), ); expect(actor.getSnapshot().context.messages).toMatchObject([ { id: "local-msg", content: "cached local message" }, ]); resolveNetwork(); await waitFor( actor, (snapshot) => snapshot.context.messages[0]?.id === "network-msg", ); expect(actor.getSnapshot().context.messages).toMatchObject([ { id: "network-msg", content: "fresh network message" }, ]); actor.stop(); }); it("loads older pages until total is exhausted and keeps existing messages", async () => { const requests: LoadMoreHistoryActorEvent[] = []; const latestMessage: UiMessage = { id: "latest", content: "current unlocked message", isFromAI: true, date: "2026-07-15", }; const machine = chatMachine.provide({ actors: { loadHistory: createLoadHistoryCallback( [latestMessage], [latestMessage], { total: 120, limit: 50 }, ), loadMoreHistory: fromCallback< LoadMoreHistoryActorEvent, ChatHistoryActorInput >( ({ receive, sendBack }) => { receive((event) => { requests.push(event); if (event.offset === 50) { sendBack({ type: "ChatOlderHistoryLoaded", output: { messages: [ createHistoryMessage("older-50"), { ...latestMessage, content: "stale duplicate" }, ], offset: event.offset, total: 120, limit: 50, }, }); return; } sendBack({ type: "ChatOlderHistoryLoaded", output: { messages: [createHistoryMessage("oldest-100")], offset: event.offset, total: 120, limit: 50, }, }); }); return () => undefined; }, ), }, }); const actor = createActor(machine, { input: TEST_CHAT_MACHINE_INPUT, }).start(); actor.send({ type: "ChatUserLogin", token: "token" }); await waitFor(actor, (snapshot) => snapshot.matches({ userSession: "ready" }), ); actor.send({ type: "ChatLoadMoreHistoryRequested" }); await waitFor( actor, (snapshot) => snapshot.context.nextHistoryOffset === 100, ); expect(requests[0]).toMatchObject({ offset: 50, limit: 50 }); expect(actor.getSnapshot().context.messages).toMatchObject([ { id: "older-50" }, { id: "latest", content: "current unlocked message" }, ]); actor.send({ type: "ChatLoadMoreHistoryRequested" }); await waitFor( actor, (snapshot) => snapshot.context.nextHistoryOffset === 150, ); actor.send({ type: "ChatLoadMoreHistoryRequested" }); expect(requests).toHaveLength(2); expect(actor.getSnapshot().context.nextHistoryOffset).toBeGreaterThanOrEqual( actor.getSnapshot().context.historyTotal, ); actor.stop(); }); it("keeps the offset after a failed page and allows retrying", async () => { let requestCount = 0; const machine = chatMachine.provide({ actors: { loadHistory: createLoadHistoryCallback([], [], { total: 75, limit: 50, }), loadMoreHistory: fromCallback< LoadMoreHistoryActorEvent, ChatHistoryActorInput >( ({ receive, sendBack }) => { receive((event) => { requestCount += 1; if (requestCount === 1) { sendBack({ type: "ChatOlderHistoryLoadFailed", error: new Error("network failed"), }); return; } sendBack({ type: "ChatOlderHistoryLoaded", output: { messages: [createHistoryMessage("retry-message")], offset: event.offset, total: 75, limit: 50, }, }); }); return () => undefined; }, ), }, }); const actor = createActor(machine, { input: TEST_CHAT_MACHINE_INPUT, }).start(); actor.send({ type: "ChatGuestLogin" }); await waitFor(actor, (snapshot) => snapshot.matches({ guestSession: "ready" }), ); actor.send({ type: "ChatLoadMoreHistoryRequested" }); await waitFor( actor, (snapshot) => requestCount === 1 && !snapshot.context.isLoadingMoreHistory, ); expect(actor.getSnapshot().context.nextHistoryOffset).toBe(50); actor.send({ type: "ChatLoadMoreHistoryRequested" }); await waitFor( actor, (snapshot) => snapshot.context.nextHistoryOffset === 100, ); expect(requestCount).toBe(2); actor.stop(); }); it("does not request another page when total does not exceed limit", async () => { let requested = false; const machine = chatMachine.provide({ actors: { loadHistory: createLoadHistoryCallback([], [], { total: 50, limit: 50, }), loadMoreHistory: fromCallback< LoadMoreHistoryActorEvent, ChatHistoryActorInput >( ({ receive }) => { receive(() => { requested = true; }); return () => undefined; }, ), }, }); const actor = createActor(machine, { input: TEST_CHAT_MACHINE_INPUT, }).start(); actor.send({ type: "ChatUserLogin", token: "token" }); await waitFor(actor, (snapshot) => snapshot.matches({ userSession: "ready" }), ); actor.send({ type: "ChatLoadMoreHistoryRequested" }); expect(requested).toBe(false); expect(actor.getSnapshot().context.isLoadingMoreHistory).toBe(false); actor.stop(); }); }); function createHistoryMessage(id: string): UiMessage { return { id, content: `Message ${id}`, isFromAI: true, date: "2026-07-15", }; }