| import { describe, expect, it, vi } from "vitest"; |
| import type { MessageEvent } from "#/types/agent-server/core"; |
| import { |
| loadCompleteTranscriptEvents, |
| TRANSCRIPT_HISTORY_PAGE_SIZE, |
| } from "./load-complete-events"; |
|
|
| const timestamp = "2026-07-10T12:34:56.000Z"; |
|
|
| const makeMessage = (index: number): MessageEvent => ({ |
| id: `event-${index.toString().padStart(3, "0")}`, |
| timestamp: new Date(Date.UTC(2026, 6, 10, 0, 0, index)).toISOString(), |
| source: "user", |
| llm_message: { |
| role: "user", |
| content: [{ type: "text", text: `Message ${index}` }], |
| }, |
| activated_skills: [], |
| extended_content: [], |
| }); |
|
|
| describe("loadCompleteTranscriptEvents", () => { |
| it("paginates beyond the 50 events initially loaded by the chat", async () => { |
| const allEvents = Array.from({ length: 225 }, (_, index) => |
| makeMessage(index), |
| ); |
| const loadedEvents = allEvents.slice(-50); |
| const descendingEvents = allEvents.slice().reverse(); |
| const searchEvents = vi.fn( |
| async ({ limit, pageId }: { limit: number; pageId?: string }) => { |
| const offset = Number(pageId ?? 0); |
| const items = descendingEvents.slice(offset, offset + limit); |
| const nextOffset = offset + items.length; |
| return { |
| items, |
| next_page_id: |
| nextOffset < descendingEvents.length ? String(nextOffset) : null, |
| }; |
| }, |
| ); |
|
|
| const result = await loadCompleteTranscriptEvents( |
| loadedEvents, |
| searchEvents, |
| ); |
|
|
| expect(result).toEqual(allEvents); |
| expect(searchEvents).toHaveBeenCalledTimes(3); |
| expect(searchEvents).toHaveBeenNthCalledWith(1, { |
| limit: TRANSCRIPT_HISTORY_PAGE_SIZE, |
| sortOrder: "TIMESTAMP_DESC", |
| strictPagination: true, |
| }); |
| }); |
|
|
| it("rejects a repeated full page when completeness cannot be proven", async () => { |
| const page = Array.from( |
| { length: TRANSCRIPT_HISTORY_PAGE_SIZE }, |
| (_, index) => makeMessage(index), |
| ).reverse(); |
| const searchEvents = vi.fn().mockResolvedValue({ items: page }); |
|
|
| await expect( |
| loadCompleteTranscriptEvents(page, searchEvents), |
| ).rejects.toThrow("cannot prove that all events were loaded"); |
| expect(searchEvents).toHaveBeenCalledTimes(1); |
| }); |
|
|
| it("uses cursors without dropping events at a shared timestamp boundary", async () => { |
| const allEvents = Array.from({ length: 125 }, (_, index) => ({ |
| ...makeMessage(index), |
| timestamp, |
| })); |
| const descendingEvents = allEvents.slice().reverse(); |
| const searchEvents = vi.fn( |
| async ({ limit, pageId }: { limit: number; pageId?: string }) => { |
| const offset = Number(pageId ?? 0); |
| const items = descendingEvents.slice(offset, offset + limit); |
| return { |
| items, |
| next_page_id: |
| offset + items.length < descendingEvents.length |
| ? String(offset + items.length) |
| : null, |
| }; |
| }, |
| ); |
|
|
| const result = await loadCompleteTranscriptEvents([], searchEvents); |
|
|
| expect(result).toEqual(allEvents); |
| expect(searchEvents).toHaveBeenNthCalledWith(2, { |
| limit: TRANSCRIPT_HISTORY_PAGE_SIZE, |
| pageId: "100", |
| sortOrder: "TIMESTAMP_DESC", |
| strictPagination: true, |
| }); |
| }); |
|
|
| it("does not let live-only store events mask unfetched persisted history", async () => { |
| const persistedEvents = Array.from({ length: 150 }, (_, index) => |
| makeMessage(index), |
| ); |
| const liveEvents = Array.from({ length: 50 }, (_, index) => |
| makeMessage(index + 150), |
| ); |
| const searchEvents = vi.fn( |
| async ({ |
| limit, |
| timestampLt, |
| }: { |
| limit: number; |
| timestampLt?: string; |
| }) => ({ |
| items: persistedEvents |
| .filter((event) => |
| timestampLt ? event.timestamp < timestampLt : true, |
| ) |
| .slice() |
| .reverse() |
| .slice(0, limit), |
| next_page_id: null, |
| }), |
| ); |
|
|
| const result = await loadCompleteTranscriptEvents( |
| liveEvents, |
| searchEvents, |
| persistedEvents.length, |
| ); |
|
|
| expect(result).toEqual([...persistedEvents, ...liveEvents]); |
| expect(searchEvents).toHaveBeenCalledTimes(2); |
| }); |
|
|
| it("rejects malformed history pages", async () => { |
| await expect( |
| loadCompleteTranscriptEvents([], async () => ({ items: null as never })), |
| ).rejects.toThrow("expected page.items to be an array"); |
| }); |
|
|
| it("rejects a partial export when the server reports more events", async () => { |
| await expect( |
| loadCompleteTranscriptEvents( |
| [], |
| async () => ({ items: [makeMessage(1)], next_page_id: null }), |
| 2, |
| ), |
| ).rejects.toThrow("Transcript history is incomplete"); |
| }); |
| }); |
|
|