1import { MessageId, RunId } from "@t3tools/contracts";
2import { describe, expect, it } from "vite-plus/test";
3import type { TimelineEntry } from "../../session-logic";
4import {
5 type CitedThreadSnapshot,
6 citedThreadSnapshot,
7 loadCitedHistory,
8 resolveCitationPhase,
9} from "./citationProcedure";
10import type { MessagesTimelineRow } from "./MessagesTimeline.logic";
11
12function scriptedHistory(snapshots: ReadonlyArray<CitedThreadSnapshot | null>) {
13 const controller = new AbortController();
14 let reads = 0;
15 let loads = 0;
16 return {
17 controller,
18 get loads() {
19 return loads;
20 },
21 run: (results: ReadonlyArray<"loaded" | "noop" | "error"> = []) =>
22 loadCitedHistory({
23 signal: controller.signal,
24 readThread: async () => snapshots[Math.min(reads++, snapshots.length - 1)] ?? null,
25 loadThrough: async () => {
26 const result = results[loads++] ?? "loaded";
27 return result === "error" ? { _tag: "error", message: "offline" } : { _tag: result };
28 },
29 }),
30 };
31}
32
33const notLoaded = { message: null, hasMoreHistory: true } satisfies CitedThreadSnapshot;
34
35describe("loadCitedHistory", () => {
36 it("finds a cited response that is already loaded without paging", async () => {
37 const history = scriptedHistory([{ message: "assistant", hasMoreHistory: true }]);
38 await expect(history.run()).resolves.toEqual({ _tag: "found" });
39 expect(history.loads).toBe(0);
40 });
41
42 it("pages history toward a response outside the loaded window", async () => {
43 const history = scriptedHistory([
44 notLoaded,
45 notLoaded,
46 { message: "assistant", hasMoreHistory: true },
47 ]);
48 await expect(history.run()).resolves.toEqual({ _tag: "found" });
49 expect(history.loads).toBe(2);
50 });
51
52 it("reports an unavailable response once the thread has no more history", async () => {
53 const history = scriptedHistory([notLoaded, { message: null, hasMoreHistory: false }]);
54 await expect(history.run()).resolves.toMatchObject({
55 _tag: "failed",
56 title: "The cited response is unavailable",
57 });
58 });
59
60 it("rejects a citation that names a user message", async () => {
61 const history = scriptedHistory([{ message: "other", hasMoreHistory: false }]);
62 await expect(history.run()).resolves.toMatchObject({
63 _tag: "failed",
64 title: "The citation does not refer to an assistant response",
65 });
66 });
67
68 it("stops on a failed page", async () => {
69 const history = scriptedHistory([notLoaded]);
70 await expect(history.run(["error"])).resolves.toMatchObject({
71 _tag: "failed",
72 title: "Could not load the cited response",
73 });
74 expect(history.loads).toBe(1);
75 });
76
77 it("reports an unavailable response when the store has nothing older to page", async () => {
78 const history = scriptedHistory([notLoaded]);
79 await expect(history.run(["noop"])).resolves.toMatchObject({
80 _tag: "failed",
81 title: "The cited response is unavailable",
82 });
83 expect(history.loads).toBe(1);
84 });
85
86 it("gives up after a bounded number of pages that never reach the response", async () => {
87 const history = scriptedHistory([notLoaded]);
88 await expect(history.run()).resolves.toMatchObject({
89 _tag: "failed",
90 title: "Could not load the cited response",
91 });
92 expect(history.loads).toBe(20);
93 });
94
95 it("ends quietly when the citation is left mid-load", async () => {
96 const history = scriptedHistory([notLoaded]);
97 const outcome = history.run();
98 history.controller.abort();
99 await expect(outcome).resolves.toEqual({ _tag: "aborted" });
100 });
101});
102
103describe("citedThreadSnapshot", () => {
104 const settledHistory = {
105 historyCursor: null,
106 hasMoreHistory: false,
107 loading: false,
108 error: null,
109 expanded: false,
110 latestLocalTurnOrdinal: null,
111 };
112 const unloaded = {
113 thread: null,
114 history: settledHistory,
115 status: "synchronizing",
116 error: null,
117 messageId: "cited",
118 } satisfies Parameters<typeof citedThreadSnapshot>[0];
119
120 it("keeps waiting while a thread that has not loaded yet is still synchronizing", () => {
121 expect(citedThreadSnapshot(unloaded)).toBeNull();
122 });
123
124 it("fails the citation when the thread could not load at all", async () => {
125 const snapshot = citedThreadSnapshot({ ...unloaded, error: "Thread not found." });
126 expect(snapshot).not.toBeNull();
127 await expect(scriptedHistory([snapshot]).run()).resolves.toMatchObject({
128 _tag: "failed",
129 title: "Could not load the cited response",
130 });
131 });
132});
133
134describe("resolveCitationPhase", () => {
135 const at = "2026-01-01T00:00:00Z";
136 const messageId = MessageId.make("cited");
137 const runId = RunId.make("run-1");
138 const entry: TimelineEntry = {
139 kind: "message",
140 id: "cited-entry",
141 createdAt: at,
142 message: {
143 id: messageId,
144 role: "assistant",
145 text: "Answer",
146 runId,
147 streaming: false,
148 createdAt: at,
149 updatedAt: at,
150 },
151 };
152 const row = {
153 kind: "message",
154 id: "cited-row",
155 message: entry.message,
156 } as MessagesTimelineRow;
157 const phase = (input: {
158 history?: "loading" | "found" | "failed";
159 entries?: ReadonlyArray<TimelineEntry>;
160 rows?: ReadonlyArray<MessagesTimelineRow>;
161 }) =>
162 resolveCitationPhase({
163 history: { _tag: input.history ?? "loading" },
164 messageId,
165 entries: input.entries ?? [],
166 rows: input.rows ?? [],
167 });
168
169 it("waits while the cited response is not in the timeline yet", () => {
170 expect(phase({ history: "found" })).toEqual({ _tag: "loading" });
171 });
172
173 it("expands the turn that folds the cited response", () => {
174 expect(phase({ entries: [entry] })).toEqual({ _tag: "expanding" });
175 });
176
177 it("is ready once the cited response has a row, even before paging settles", () => {
178 expect(phase({ entries: [entry], rows: [row] })).toEqual({
179 _tag: "ready",
180 rowId: "cited-row",
181 });
182 });
183
184 it("fails for a citation that names a user message", () => {
185 const userEntry: TimelineEntry = {
186 ...entry,
187 message: { ...entry.message, role: "user" },
188 } as TimelineEntry;
189 expect(phase({ entries: [userEntry] })).toEqual({ _tag: "failed" });
190 });
191
192 it("fails when the history procedure failed", () => {
193 expect(phase({ history: "failed", entries: [entry], rows: [row] })).toEqual({
194 _tag: "failed",
195 });
196 });
197});