File size: 4,750 Bytes
3201ca6
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
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");
  });
});