1import {
2 CheckpointId,
3 CheckpointRef,
4 CheckpointScopeId,
5 MessageId,
6 NodeId,
7 RunId,
8 ThreadId,
9 type OrchestrationV2Checkpoint,
10 type OrchestrationV2ConversationMessage,
11} from "@t3tools/contracts";
12import * as DateTime from "effect/DateTime";
13import { describe, expect, it } from "vite-plus/test";
14
15import { deriveThreadCheckpointSummaries } from "./threadCheckpoints.ts";
16
17function checkpoint(
18 run: string,
19 overrides: Partial<OrchestrationV2Checkpoint> = {},
20): OrchestrationV2Checkpoint {
21 return {
22 id: CheckpointId.make(`checkpoint-${run}`),
23 threadId: ThreadId.make("thread"),
24 scopeId: CheckpointScopeId.make("scope"),
25 runId: RunId.make(run),
26 nodeId: NodeId.make(`node-${run}`),
27 parentCheckpointId: null,
28 ordinalWithinScope: 1,
29 appRunOrdinal: 1,
30 ref: CheckpointRef.make(`ref-${run}`),
31 status: "ready",
32 files: [],
33 capturedAt: DateTime.makeUnsafe("2026-10-26T09:30:00.123+11:00"),
34 ...overrides,
35 };
36}
37
38function message(
39 run: string,
40 id: string,
41 overrides: Partial<OrchestrationV2ConversationMessage> = {},
42): OrchestrationV2ConversationMessage {
43 return {
44 id: MessageId.make(id),
45 threadId: ThreadId.make("thread"),
46 runId: RunId.make(run),
47 nodeId: null,
48 role: "assistant",
49 text: "Answer",
50 attachments: [],
51 streaming: false,
52 createdBy: "agent",
53 creationSource: "provider",
54 createdAt: DateTime.makeUnsafe("2026-10-26T00:00:00.000Z"),
55 updatedAt: DateTime.makeUnsafe("2026-10-26T00:00:00.000Z"),
56 ...overrides,
57 };
58}
59
60describe("deriveThreadCheckpointSummaries", () => {
61 it("preserves checkpoint order, status, file references and ISO dates while filtering null keys", () => {
62 const files = Object.freeze([
63 { path: "src/one.ts", kind: "modified", additions: 10, deletions: 2 },
64 ]);
65 const eligible = ["stale", "error", "missing", "ready"].map((status, index) =>
66 checkpoint(`run-${index}`, {
67 status: status as OrchestrationV2Checkpoint["status"],
68 appRunOrdinal: 4 - index,
69 files,
70 }),
71 );
72 const checkpoints = Object.freeze([
73 checkpoint("no-ordinal", { appRunOrdinal: null }),
74 ...eligible.slice(0, 2),
75 checkpoint("no-run", { runId: null }),
76 ...eligible.slice(2),
77 ]);
78 const summaries = deriveThreadCheckpointSummaries({ checkpoints, messages: [] });
79
80 expect(summaries).toEqual(
81 eligible.map((entry) => ({
82 checkpointId: entry.id,
83 scopeId: entry.scopeId,
84 runId: entry.runId,
85 checkpointTurnCount: entry.appRunOrdinal,
86 checkpointRef: entry.ref,
87 status: entry.status,
88 files,
89 assistantMessageId: null,
90 completedAt: "2026-10-25T22:30:00.123Z",
91 })),
92 );
93 for (const summary of summaries) expect(summary.files).toBe(files);
94 });
95
96 it("selects the last assistant in array order across interleaved runs and duplicate checkpoints", () => {
97 const newer = DateTime.makeUnsafe("2026-10-26T01:00:00.000Z");
98 const messages = Object.freeze([
99 message("a", "newer-timestamp", { createdAt: newer, updatedAt: newer }),
100 message("b", "b-first"),
101 message("a", "last-in-array"),
102 message("b", "b-last"),
103 message("a", "user-tail", { role: "user" }),
104 message("a", "system-tail", { role: "system" }),
105 message("a", "unscoped-tail", { runId: null }),
106 ]);
107 const summaries = deriveThreadCheckpointSummaries({
108 checkpoints: [checkpoint("b"), checkpoint("a"), checkpoint("b", { appRunOrdinal: 3 })],
109 messages,
110 });
111
112 expect(summaries.map((summary) => summary.assistantMessageId)).toEqual([
113 "b-last",
114 "last-in-array",
115 "b-last",
116 ]);
117 expect(messages.map((entry) => entry.id)).toEqual([
118 "newer-timestamp",
119 "b-first",
120 "last-in-array",
121 "b-last",
122 "user-tail",
123 "system-tail",
124 "unscoped-tail",
125 ]);
126 });
127
128 it("returns null for absent runs and runs containing only user or system messages", () => {
129 const summaries = deriveThreadCheckpointSummaries({
130 checkpoints: [checkpoint("absent"), checkpoint("user-only"), checkpoint("system-only")],
131 messages: [
132 message("other", "other-assistant"),
133 message("user-only", "user", { role: "user" }),
134 message("system-only", "system", { role: "system" }),
135 ],
136 });
137 expect(summaries.map((summary) => summary.assistantMessageId)).toEqual([null, null, null]);
138 });
139
140 it.each([null, undefined])("does not fall back past a last assistant with ID %s", (id) => {
141 const { id: _id, ...withoutId } = message("a", "last");
142 // Preserve the runtime fallback even for legacy objects outside the decoded schema.
143 const last = (
144 id === undefined ? withoutId : { ...withoutId, id }
145 ) as OrchestrationV2ConversationMessage;
146 const summaries = deriveThreadCheckpointSummaries({
147 checkpoints: [checkpoint("a"), checkpoint("a")],
148 messages: [message("a", "earlier-valid-id"), last],
149 });
150 expect(summaries.map((summary) => summary.assistantMessageId)).toEqual([null, null]);
151 });
152
153 it.each([
154 { checkpoints: [] },
155 {
156 checkpoints: [
157 checkpoint("no-ordinal", { appRunOrdinal: null }),
158 checkpoint("no-run", { runId: null }),
159 ],
160 },
161 ])("does not read messages with no eligible checkpoints: %j", ({ checkpoints }) => {
162 expect(
163 deriveThreadCheckpointSummaries({
164 checkpoints,
165 get messages(): ReadonlyArray<OrchestrationV2ConversationMessage> {
166 throw new Error("Ineligible checkpoints must not read message history");
167 },
168 }),
169 ).toEqual([]);
170 });
171
172 it("rebuilds associations for message and checkpoint replacements without retaining stale IDs", () => {
173 const original = message("a", "original");
174 const checkpoints = [checkpoint("a")];
175 const first = deriveThreadCheckpointSummaries({ checkpoints, messages: [original] });
176 expect(first[0]?.assistantMessageId).toBe("original");
177 expect(
178 deriveThreadCheckpointSummaries({
179 checkpoints,
180 messages: [original, message("a", "replacement")],
181 })[0]?.assistantMessageId,
182 ).toBe("replacement");
183 expect(
184 deriveThreadCheckpointSummaries({ checkpoints, messages: [] })[0]?.assistantMessageId,
185 ).toBeNull();
186 expect(
187 deriveThreadCheckpointSummaries({
188 checkpoints: [checkpoint("b")],
189 messages: [original, message("b", "new-checkpoint")],
190 })[0]?.assistantMessageId,
191 ).toBe("new-checkpoint");
192 expect(first[0]?.assistantMessageId).toBe("original");
193 });
194
195 it("stops at the newest requested assistant when only tail checkpoints are needed", () => {
196 const history = Array.from({ length: 1_000 }, (_, index) => ({
197 ...message(`old-${index}`, `old-${index}`),
198 get runId(): RunId {
199 throw new Error("Tail checkpoints must not traverse older history");
200 },
201 }));
202 const summaries = deriveThreadCheckpointSummaries({
203 checkpoints: [checkpoint("latest"), checkpoint("latest")],
204 messages: [...history, message("latest", "latest-answer")],
205 });
206 expect(summaries.map((summary) => summary.assistantMessageId)).toEqual([
207 "latest-answer",
208 "latest-answer",
209 ]);
210 });
211
212 it("does not index assistant IDs from unrelated runs", () => {
213 const unrelated = Array.from({ length: 1_000 }, (_, index) => ({
214 ...message(`other-${index}`, `other-${index}`),
215 get id(): MessageId {
216 throw new Error("Unrequested runs must not be indexed");
217 },
218 }));
219 const summaries = deriveThreadCheckpointSummaries({
220 checkpoints: [checkpoint("requested")],
221 messages: [message("requested", "requested-answer"), ...unrelated],
222 });
223 expect(summaries[0]?.assistantMessageId).toBe("requested-answer");
224 });
225
226 it.each([20, 100, 1_000])(
227 "visits message run metadata at most once for %i eligible checkpoints",
228 (count) => {
229 let metadataVisits = 0;
230 const messages = Array.from({ length: count }, (_, index) => [
231 message(`run-${index}`, `user-${index}`, { role: "user" }),
232 message(`run-${index}`, `assistant-${index}`),
233 ])
234 .flat()
235 .concat(message("active-run", "active-assistant"))
236 .map((entry) => ({
237 ...entry,
238 get runId() {
239 metadataVisits++;
240 return entry.runId;
241 },
242 get text(): string {
243 throw new Error("Checkpoint summaries must not read message bodies");
244 },
245 }));
246 const summaries = deriveThreadCheckpointSummaries({
247 checkpoints: Array.from({ length: count }, (_, index) => checkpoint(`run-${index}`)),
248 messages,
249 });
250
251 expect(summaries.map((summary) => summary.assistantMessageId)).toEqual(
252 Array.from({ length: count }, (_, index) => `assistant-${index}`),
253 );
254 expect(metadataVisits).toBeLessThanOrEqual(messages.length);
255 },
256 );
257});