Skip to content
Source
Sign inSign up

threadHistoryMerge.test.ts

509 lines16.2 KB

1import type { OrchestrationV2ProjectedTurnItem } from "@t3tools/contracts";
2import { describe, expect, it } from "@effect/vitest";
3import * as DateTime from "effect/DateTime";
4​
5import { v2Projection, v2ThreadId } from "./orchestrationV2TestFixtures.ts";
6import { mergeOlderHistoryIntoProjection } from "./threadHistoryMerge.ts";
7import { applyOrchestrationV2ProjectionEvent } from "./orchestrationV2Projection.ts";
8import { derivePendingThreadRequests } from "./threadRequests.ts";
9​
10const NOW = DateTime.makeUnsafe("2026-06-20T00:00:00.000Z");
11​
12function row(index: number): OrchestrationV2ProjectedTurnItem {
13 const id = `item-${index}` as never;
14 return {
15 position: index,
16 visibility: "local",
17 sourceThreadId: v2ThreadId,
18 sourceItemId: id,
19 item: {
20 id,
21 type: "command_execution",
22 threadId: v2ThreadId,
23 runId: null,
24 nodeId: null,
25 providerThreadId: null,
26 providerTurnId: null,
27 nativeItemRef: null,
28 parentItemId: null,
29 ordinal: index + 1,
30 status: "completed",
31 title: `Command ${index}`,
32 input: `cmd-${index}`,
33 output: `out-${index}`,
34 exitCode: 0,
35 startedAt: NOW,
36 completedAt: NOW,
37 updatedAt: NOW,
38 },
39 };
41​
42describe("threadHistoryMerge", () => {
43 it("fills activity around partial messages in canonical order and preserves live edits", () => {
44 const initial = { ...v2Projection, turnItems: [row(4).item], visibleTurnItems: [row(4)] };
45 const preview = [row(1), row(3)];
46 const partial = mergeOlderHistoryIntoProjection(initial, preview);
47 const updated = { ...row(1), item: { ...row(1).item, title: "newer live title" } };
48 const live = {
49 ...partial,
50 visibleTurnItems: partial.visibleTurnItems.map((entry) =>
51 entry.sourceItemId === updated.sourceItemId ? updated : entry,
52 ),
53 turnItems: partial.turnItems.map((item) =>
54 item.id === updated.item.id ? updated.item : item,
55 ),
56 };
57 const complete = mergeOlderHistoryIntoProjection(live, [row(0), row(1), row(2), row(3)]);
58 expect(complete.visibleTurnItems.map((entry) => entry.sourceItemId)).toEqual([
59 "item-0",
60 "item-1",
61 "item-2",
62 "item-3",
63 "item-4",
64 ]);
65 expect(complete.visibleTurnItems[1]?.item).toBe(updated.item);
66 expect(complete.visibleTurnItems.map((entry) => entry.position)).toEqual([0, 1, 2, 3, 4]);
67 expect(new Set(complete.turnItems.map((item) => item.id)).size).toBe(5);
68 const anchorItem = {
69 ...updated.item,
70 deferredWork: { cursor: "work", itemCount: 4, lastOrdinal: 5 },
71 };
72 const deferred = {
73 ...live,
74 turnItems: live.turnItems.map((item) => (item.id === anchorItem.id ? anchorItem : item)),
75 visibleTurnItems: live.visibleTurnItems.map((entry) =>
76 entry.item.id === anchorItem.id ? { ...entry, item: anchorItem } : entry,
77 ),
78 };
79 const loadedWork = mergeOlderHistoryIntoProjection(deferred, [row(1), row(2), row(3)], {
80 anchorId: anchorItem.id,
81 nextCursor: null,
82 });
83 expect(loadedWork.visibleTurnItems.map((entry) => entry.sourceItemId)).toEqual([
84 "item-1",
85 "item-2",
86 "item-3",
87 "item-4",
88 ]);
89 expect(loadedWork.visibleTurnItems[0]?.item.title).toBe("newer live title");
90 expect(loadedWork.visibleTurnItems[0]?.item.deferredWork).toBeUndefined();
91 const hidden = {
92 ...partial,
93 visibleTurnItems: partial.visibleTurnItems.filter(
94 (entry) => entry.sourceItemId !== preview[0]!.sourceItemId,
95 ),
96 };
97 const hiddenComplete = mergeOlderHistoryIntoProjection(hidden, [
98 row(0),
99 row(1),
100 row(2),
101 row(3),
102 ]);
103 expect(hiddenComplete.visibleTurnItems.map((entry) => entry.sourceItemId)).toEqual([
104 "item-0",
105 "item-2",
106 "item-3",
107 "item-4",
108 ]);
109 });
110​
111 it("prepends older rows and dedupes by source identity", () => {
112 const recent = [row(2), row(3)];
113 const projection = {
114 ...v2Projection,
115 turnItems: recent.map((entry) => entry.item),
116 visibleTurnItems: recent.map((entry, position) => ({ ...entry, position })),
117 };
118 const older = [row(0), row(1), row(2)];
119 const merged = mergeOlderHistoryIntoProjection(projection, older);
120 expect(merged.visibleTurnItems.map((entry) => entry.sourceItemId)).toEqual([
121 "item-0",
122 "item-1",
123 "item-2",
124 "item-3",
125 ]);
126 expect(merged.visibleTurnItems.map((entry) => entry.position)).toEqual([0, 1, 2, 3]);
127 expect(merged.turnItems.map((item) => item.id)).toEqual([
128 "item-2",
129 "item-3",
130 "item-0",
131 "item-1",
132 ]);
133 });
134​
135 it("fills inherited activity without confusing identical item IDs from different source threads", () => {
136 const inherited = (index: number) => ({
137 ...row(index),
138 visibility: "inherited" as const,
139 sourceThreadId: "fork-source" as never,
140 });
141 const local = row(1);
142 const preview = { ...inherited(1), item: { ...row(1).item, title: "newer inherited title" } };
143 const projection = {
144 ...v2Projection,
145 turnItems: [local.item],
146 visibleTurnItems: [preview, local],
147 };
148 const merged = mergeOlderHistoryIntoProjection(projection, [inherited(0), inherited(1)]);
149 expect(
150 merged.visibleTurnItems.map((entry) => [entry.sourceThreadId, entry.sourceItemId]),
151 ).toEqual([
152 ["fork-source", "item-0"],
153 ["fork-source", "item-1"],
154 [v2ThreadId, "item-1"],
155 ]);
156 expect(merged.visibleTurnItems[1]?.item).toBe(preview.item);
157 expect(merged.turnItems).toEqual([local.item]);
158 expect(mergeOlderHistoryIntoProjection(merged, [inherited(0), inherited(1)])).toBe(merged);
159 });
160​
161 it("retains live rows that arrived after the older page was fetched", () => {
162 const live = [row(5), row(6)];
163 const projection = {
164 ...v2Projection,
165 turnItems: live.map((entry) => entry.item),
166 visibleTurnItems: live.map((entry, position) => ({ ...entry, position })),
167 };
168 const older = [row(3), row(4)];
169 const merged = mergeOlderHistoryIntoProjection(projection, older);
170 expect(merged.visibleTurnItems.map((entry) => entry.sourceItemId)).toEqual([
171 "item-3",
172 "item-4",
173 "item-5",
174 "item-6",
175 ]);
176 });
177​
178 it("does not resurrect a local item hidden while an older page was in flight", () => {
179 const stalePageRow = row(1);
180 const currentItem = {
181 ...stalePageRow.item,
182 output: "newer live output",
183 updatedAt: DateTime.makeUnsafe("2026-06-20T00:00:01.000Z"),
184 };
185 const projection = {
186 ...v2Projection,
187 turnItems: [currentItem],
188 visibleTurnItems: [],
189 };
190​
191 const merged = mergeOlderHistoryIntoProjection(projection, [stalePageRow]);
192​
193 expect(merged).toBe(projection);
194 expect(merged.turnItems).toEqual([currentItem]);
195 expect(merged.visibleTurnItems).toEqual([]);
196 });
197​
198 it("retains merged older rows when a live turn-item update arrives", () => {
199 const recent = [row(2), row(3)];
200 const projection = {
201 ...v2Projection,
202 turnItems: recent.map((entry) => entry.item),
203 visibleTurnItems: recent.map((entry, position) => ({ ...entry, position })),
204 };
205 const merged = mergeOlderHistoryIntoProjection(projection, [row(0), row(1)]);
206 const liveItem = {
207 ...row(3).item,
208 output: "streamed",
209 };
210 const next = applyOrchestrationV2ProjectionEvent(merged, {
211 id: "event-live-item" as never,
212 type: "turn-item.updated",
213 threadId: v2ThreadId,
214 occurredAt: NOW,
215 payload: liveItem,
216 } as never);
217 expect(next?.visibleTurnItems.map((entry) => entry.sourceItemId)).toEqual([
218 "item-0",
219 "item-1",
220 "item-2",
221 "item-3",
222 ]);
223 expect(next?.visibleTurnItems[3]?.item).toEqual(liveItem);
224 });
225​
226 // regression: #25806 a pending question card retained outside the window shows when paged in
227 it.each(["completed", "rolled_back"] as const)(
228 "keeps a pending question actionable when paging a %s run",
229 (status) => {
230 const recent = row(5);
231 const runId = "question-run" as never;
232 const nodeId = "question-node" as never;
233 const card = {
234 id: "item-question" as never,
235 type: "user_input_request" as const,
236 threadId: v2ThreadId,
237 runId,
238 nodeId,
239 providerThreadId: null,
240 providerTurnId: null,
241 nativeItemRef: null,
242 parentItemId: null,
243 ordinal: 1,
244 status: "running" as const,
245 title: null,
246 requestId: "request-question" as never,
247 responseMode: "message" as const,
248 questions: [{ id: "scope", header: "Scope", question: "Which scope?", options: [] }],
249 startedAt: NOW,
250 completedAt: null,
251 updatedAt: NOW,
252 };
253 const coldOpen = {
254 ...v2Projection,
255 runs: [
256 {
257 id: runId,
258 threadId: v2ThreadId,
259 ordinal: 1,
260 providerInstanceId: v2Projection.thread.providerInstanceId,
261 modelSelection: v2Projection.thread.modelSelection,
262 providerThreadId: null,
263 userMessageId: "question-message" as never,
264 rootNodeId: nodeId,
265 activeAttemptId: null,
266 status,
267 requestedAt: NOW,
268 startedAt: NOW,
269 completedAt: NOW,
270 checkpointId: null,
271 contextHandoffId: null,
272 },
273 ],
274 runtimeRequests: [
275 {
276 id: card.requestId,
277 nodeId,
278 providerTurnId: null,
279 nativeRequestRef: null,
280 kind: "user_input" as const,
281 status: "pending" as const,
282 responseCapability: { type: "message" as const },
283 createdAt: NOW,
284 resolvedAt: null,
285 },
286 ],
287 turnItems: [card, recent.item],
288 visibleTurnItems: [{ ...recent, position: 0 }],
289 };
290​
291 const merged = mergeOlderHistoryIntoProjection(coldOpen, [
292 { ...row(0), position: 0 },
293 {
294 position: 1,
295 visibility: "local",
296 sourceThreadId: v2ThreadId,
297 sourceItemId: card.id,
298 item: card,
299 },
300 ]);
301​
302 expect(merged.visibleTurnItems.map((entry) => String(entry.sourceItemId))).toEqual(
303 status === "rolled_back" ? ["item-0", "item-5"] : ["item-0", "item-question", "item-5"],
304 );
305 expect(merged.turnItems).toContain(card);
306 const pending = derivePendingThreadRequests(merged).userInputs;
307 expect(pending).toEqual(derivePendingThreadRequests(coldOpen).userInputs);
308 expect(pending).toHaveLength(1);
309 expect(pending[0]).toMatchObject({
310 requestId: card.requestId,
311 dismissible: true,
312 questions: card.questions.map((question) => ({ ...question, multiSelect: false })),
313 });
314 },
315 );
316​
317 it("keeps a history-page interrupt result visible when its request was retained at cold open", () => {
318 const runId = "run-interrupt-cold" as never;
319 const nodeId = "node-interrupt-cold" as never;
320 const recent = row(5);
321 const requestItem = {
322 id: "item-interrupt-request" as never,
323 type: "run_interrupt_request" as const,
324 threadId: v2ThreadId,
325 runId,
326 nodeId,
327 providerThreadId: null,
328 providerTurnId: null,
329 nativeItemRef: null,
330 parentItemId: null,
331 ordinal: 1,
332 status: "completed" as const,
333 title: null,
334 message: "Stopping",
335 startedAt: NOW,
336 completedAt: NOW,
337 updatedAt: NOW,
338 };
339 const resultItem = {
340 id: "item-interrupt-result" as never,
341 type: "run_interrupt_result" as const,
342 threadId: v2ThreadId,
343 runId,
344 nodeId,
345 providerThreadId: null,
346 providerTurnId: null,
347 nativeItemRef: null,
348 parentItemId: null,
349 ordinal: 2,
350 status: "completed" as const,
351 title: null,
352 message: "Stopped",
353 startedAt: NOW,
354 completedAt: NOW,
355 updatedAt: NOW,
356 };
357 const resultRow = {
358 position: 0,
359 visibility: "local" as const,
360 sourceThreadId: v2ThreadId,
361 sourceItemId: resultItem.id,
362 item: resultItem,
363 };
364 const coldOpen = {
365 ...v2Projection,
366 runs: [
367 {
368 id: runId,
369 threadId: v2ThreadId,
370 ordinal: 1,
371 providerInstanceId: "codex" as never,
372 modelSelection: { instanceId: "codex" as never, model: "gpt-5" },
373 providerThreadId: null,
374 userMessageId: "message-1" as never,
375 rootNodeId: nodeId,
376 activeAttemptId: null,
377 status: "running" as const,
378 requestedAt: NOW,
379 startedAt: NOW,
380 completedAt: null,
381 checkpointId: null,
382 contextHandoffId: null,
383 },
384 ],
385 attempts: [
386 {
387 id: "attempt-1" as never,
388 runId,
389 attemptOrdinal: 1,
390 rootNodeId: nodeId,
391 providerInstanceId: "codex" as never,
392 providerThreadId: "provider-thread-1" as never,
393 providerTurnId: null,
394 reason: "initial" as const,
395 status: "running" as const,
396 startedAt: NOW,
397 completedAt: null,
398 },
399 ],
400 turnItems: [requestItem, recent.item],
401 visibleTurnItems: [{ ...recent, position: 0 }],
402 };
403​
404 const requestMerged = mergeOlderHistoryIntoProjection(coldOpen, [
405 {
406 position: 0,
407 visibility: "local",
408 sourceThreadId: v2ThreadId,
409 sourceItemId: requestItem.id,
410 item: requestItem,
411 },
412 ]);
413 expect(requestMerged.visibleTurnItems.map((entry) => String(entry.sourceItemId))).toEqual([
414 "item-interrupt-request",
415 "item-5",
416 ]);
417​
418 const rolledBackRequestMerged = mergeOlderHistoryIntoProjection(
419 {
420 ...coldOpen,
421 runs: coldOpen.runs.map((run) => ({
422 ...run,
423 status: "rolled_back" as const,
424 completedAt: NOW,
425 })),
426 },
427 [
428 {
429 position: 0,
430 visibility: "local",
431 sourceThreadId: v2ThreadId,
432 sourceItemId: requestItem.id,
433 item: requestItem,
434 },
435 ],
436 );
437 expect(
438 rolledBackRequestMerged.visibleTurnItems.map((entry) => String(entry.sourceItemId)),
439 ).toEqual(["item-5"]);
440​
441 const merged = mergeOlderHistoryIntoProjection(coldOpen, [resultRow]);
442 expect(merged.visibleTurnItems.map((entry) => String(entry.sourceItemId))).toEqual([
443 "item-interrupt-result",
444 "item-5",
445 ]);
446 expect(merged.turnItems.some((item) => item.type === "run_interrupt_request")).toBe(true);
447 expect(merged.turnItems.some((item) => item.type === "run_interrupt_result")).toBe(true);
448​
449 const attemptSuperseded = applyOrchestrationV2ProjectionEvent(
450 merged,
451 {
452 id: "event-attempt-superseded" as never,
453 type: "run-attempt.updated",
454 threadId: v2ThreadId,
455 runId,
456 occurredAt: NOW,
457 payload: {
458 id: "attempt-1" as never,
459 runId,
460 attemptOrdinal: 1,
461 rootNodeId: nodeId,
462 providerInstanceId: "codex" as never,
463 providerThreadId: "provider-thread-1" as never,
464 providerTurnId: null,
465 reason: "initial",
466 status: "superseded",
467 startedAt: NOW,
468 completedAt: NOW,
469 },
470 } as never,
471 { partialTimeline: true },
472 );
473​
474 expect(attemptSuperseded?.visibleTurnItems.map((entry) => String(entry.sourceItemId))).toEqual([
475 "item-interrupt-result",
476 "item-5",
477 ]);
478​
479 const withoutRequest = {
480 ...merged,
481 turnItems: merged.turnItems.filter((item) => item.type !== "run_interrupt_request"),
482 };
483 const hidden = applyOrchestrationV2ProjectionEvent(
484 withoutRequest,
485 {
486 id: "event-attempt-superseded-missing" as never,
487 type: "run-attempt.updated",
488 threadId: v2ThreadId,
489 runId,
490 occurredAt: NOW,
491 payload: {
492 id: "attempt-1" as never,
493 runId,
494 attemptOrdinal: 1,
495 rootNodeId: nodeId,
496 providerInstanceId: "codex" as never,
497 providerThreadId: "provider-thread-1" as never,
498 providerTurnId: null,
499 reason: "initial",
500 status: "superseded",
501 startedAt: NOW,
502 completedAt: NOW,
503 },
504 } as never,
505 { partialTimeline: true },
506 );
507 expect(hidden?.visibleTurnItems.map((entry) => String(entry.sourceItemId))).toEqual(["item-5"]);
508 });
509});