1import { describe, expect, it } from "vite-plus/test";
2import {
3 type ThreadPullRequestLink,
4 type OrchestrationV2DomainEvent,
5 type OrchestrationV2Run,
6 type OrchestrationV2ThreadProjection,
7 type OrchestrationV2TurnItem,
8 MessageId,
9 ProjectId,
10 ProviderInstanceId,
11 NodeId,
12 ProviderThreadId,
13 ProviderTurnId,
14 RunId,
15 ThreadId,
16 TurnItemId,
17} from "@t3tools/contracts";
18import * as DateTime from "effect/DateTime";
19
20import { applyOrchestrationV2ProjectionEvent } from "./orchestrationV2Projection.ts";
21
22const now = DateTime.makeUnsafe("2026-06-20T00:00:00.000Z");
23const threadId = ThreadId.make("thread-reducer");
24const runId = RunId.make("run-reducer");
25const run = {
26 id: runId,
27 threadId,
28 ordinal: 1,
29 providerInstanceId: ProviderInstanceId.make("codex"),
30 modelSelection: { instanceId: ProviderInstanceId.make("codex"), model: "gpt-5.4" },
31 providerThreadId: null,
32 userMessageId: MessageId.make("message-reducer"),
33 rootNodeId: null,
34 activeAttemptId: null,
35 status: "completed",
36 requestedAt: now,
37 startedAt: now,
38 completedAt: now,
39 checkpointId: null,
40 contextHandoffId: null,
41} satisfies OrchestrationV2Run;
42
43function commandItem(id: string, output = "done", ordinal = 1): OrchestrationV2TurnItem {
44 return {
45 id: TurnItemId.make(id),
46 threadId,
47 runId,
48 nodeId: null,
49 providerThreadId: null,
50 providerTurnId: null,
51 nativeItemRef: null,
52 parentItemId: null,
53 ordinal,
54 status: "completed",
55 title: null,
56 startedAt: now,
57 completedAt: now,
58 updatedAt: now,
59 type: "command_execution",
60 input: "pwd",
61 output,
62 exitCode: 0,
63 };
64}
65const emptyProjection = {
66 thread: {
67 id: threadId,
68 projectId: ProjectId.make("project-reducer"),
69 title: "Reducer",
70 providerInstanceId: ProviderInstanceId.make("codex"),
71 modelSelection: { instanceId: ProviderInstanceId.make("codex"), model: "gpt-5.4" },
72 runtimeMode: "full-access",
73 interactionMode: "default",
74 branch: null,
75 worktreePath: null,
76 activeProviderThreadId: null,
77 lineage: { rootThreadId: threadId, parentThreadId: null, relationshipToParent: null },
78 forkedFrom: null,
79 createdBy: "user",
80 creationSource: "web",
81 createdAt: now,
82 updatedAt: now,
83 archivedAt: null,
84 settledOverride: null,
85 settledAt: null,
86 lastVisitedAt: null,
87 deletedAt: null,
88 },
89 runs: [],
90 attempts: [],
91 nodes: [],
92 subagents: [],
93 providerSessions: [],
94 providerThreads: [],
95 providerTurns: [],
96 runtimeRequests: [],
97 messages: [],
98 plans: [],
99 turnItems: [],
100 checkpointScopes: [],
101 checkpoints: [],
102 contextHandoffs: [],
103 contextTransfers: [],
104 visibleTurnItems: [],
105 updatedAt: now,
106} as OrchestrationV2ThreadProjection;
107
108describe("applyOrchestrationV2ProjectionEvent", () => {
109 it("applies a compact PR sync without replacing other links or thread metadata", () => {
110 const link = (number: number): ThreadPullRequestLink => ({
111 host: "github.com",
112 repository: "pingdotgg/t3code",
113 number,
114 url: `https://github.com/pingdotgg/t3code/pull/${number}`,
115 source: "manual",
116 linkedAt: DateTime.formatIso(now),
117 snapshot: null,
118 stack: null,
119 });
120 const links = [link(1), link(2)];
121 const projection = {
122 ...emptyProjection,
123 thread: { ...emptyProjection.thread, pullRequests: links },
124 };
125 const updated = { ...links[1]!, source: "agent" as const };
126 const next = applyOrchestrationV2ProjectionEvent(projection, {
127 id: "compact-sync" as OrchestrationV2DomainEvent["id"],
128 type: "thread.pull-request-link-updated",
129 threadId,
130 occurredAt: now,
131 payload: updated,
132 });
133 expect(next?.thread).toEqual({ ...projection.thread, pullRequests: [links[0], updated] });
134 expect(next?.thread.pullRequests?.[0]).toBe(links[0]);
135 });
136
137 it("keeps live token usage when the terminal provider turn omits it", () => {
138 const providerTurnId = ProviderTurnId.make("provider-turn-reducer");
139 const running = {
140 id: providerTurnId,
141 providerThreadId: ProviderThreadId.make("provider-thread-reducer"),
142 nodeId: NodeId.make("provider-node-reducer"),
143 runAttemptId: null,
144 nativeTurnRef: null,
145 ordinal: 1,
146 status: "running" as const,
147 startedAt: now,
148 completedAt: null,
149 tokenUsage: {
150 usedTokens: 50_000,
151 maxTokens: 200_000,
152 updatedAt: "2026-08-29T00:00:00.000Z",
153 },
154 };
155 const projection = { ...emptyProjection, providerTurns: [running] };
156 const event = {
157 id: "event-provider-turn-terminal",
158 type: "provider-turn.updated",
159 threadId,
160 driver: "codex",
161 occurredAt: now,
162 payload: {
163 ...running,
164 status: "completed",
165 completedAt: now,
166 tokenUsage: undefined,
167 },
168 } as OrchestrationV2DomainEvent;
169
170 const next = applyOrchestrationV2ProjectionEvent(projection, event);
171
172 expect(next?.providerTurns[0]?.status).toBe("completed");
173 expect(next?.providerTurns[0]?.tokenUsage).toEqual(running.tokenUsage);
174 });
175
176 it("applies thread lifecycle payloads instead of leaving stale metadata", () => {
177 const archivedAt = DateTime.makeUnsafe("2026-06-20T01:00:00.000Z");
178 const event = {
179 id: "event-archive",
180 type: "thread.archived",
181 threadId,
182 occurredAt: archivedAt,
183 payload: { ...emptyProjection.thread, archivedAt, updatedAt: archivedAt },
184 } as OrchestrationV2DomainEvent;
185
186 const next = applyOrchestrationV2ProjectionEvent(emptyProjection, event);
187 expect(next?.thread.archivedAt).toEqual(archivedAt);
188 expect(next?.updatedAt).toEqual(archivedAt);
189 });
190
191 it("preserves visible row identity when run updates do not change membership", () => {
192 const item = commandItem("item-stable");
193 const visibleTurnItems = [
194 {
195 position: 0,
196 visibility: "local" as const,
197 sourceThreadId: threadId,
198 sourceItemId: item.id,
199 item,
200 },
201 ];
202 const projection = {
203 ...emptyProjection,
204 runs: [run],
205 turnItems: [item],
206 visibleTurnItems,
207 };
208 const event = {
209 id: "event-run-update",
210 type: "run.updated",
211 threadId,
212 runId,
213 occurredAt: now,
214 payload: { ...run, status: "completed" },
215 } as OrchestrationV2DomainEvent;
216
217 const next = applyOrchestrationV2ProjectionEvent(projection, event);
218 expect(next?.visibleTurnItems).toBe(visibleTurnItems);
219 expect(next?.visibleTurnItems[0]).toBe(visibleTurnItems[0]);
220 });
221
222 it("replaces only the updated visible item and retains its deferred work cursor", () => {
223 const deferredWork = { cursor: "remaining-work", itemCount: 100, lastOrdinal: 101 };
224 const first = { ...commandItem("item-first", "first"), deferredWork };
225 const second = commandItem("item-second", "second");
226 const firstRow = {
227 position: 0,
228 visibility: "local" as const,
229 sourceThreadId: threadId,
230 sourceItemId: first.id,
231 item: first,
232 };
233 const secondRow = {
234 position: 1,
235 visibility: "local" as const,
236 sourceThreadId: threadId,
237 sourceItemId: second.id,
238 item: second,
239 };
240 const updated = commandItem("item-first", "streamed output");
241 const projection = {
242 ...emptyProjection,
243 runs: [run],
244 turnItems: [first, second],
245 visibleTurnItems: [firstRow, secondRow],
246 };
247 const event = {
248 id: "event-item-update",
249 type: "turn-item.updated",
250 threadId,
251 runId,
252 occurredAt: now,
253 payload: updated,
254 } as OrchestrationV2DomainEvent;
255
256 const next = applyOrchestrationV2ProjectionEvent(projection, event);
257 expect(next?.visibleTurnItems[0]?.item.deferredWork).toEqual(deferredWork);
258 expect(next?.visibleTurnItems).not.toBe(projection.visibleTurnItems);
259 expect(next?.visibleTurnItems[0]).not.toBe(firstRow);
260 expect(next?.visibleTurnItems[0]?.item).toEqual({ ...updated, deferredWork });
261 expect(next?.visibleTurnItems[1]).toBe(secondRow);
262 });
263
264 it("inserts live turn items by authoritative ordinal", () => {
265 const queuedFuture = commandItem("item-queued-future", "queued", 300);
266 const activeAssistant = commandItem("item-active-assistant", "done", 201);
267 const queuedRow = {
268 position: 0,
269 visibility: "local" as const,
270 sourceThreadId: threadId,
271 sourceItemId: queuedFuture.id,
272 item: queuedFuture,
273 };
274 const projection = {
275 ...emptyProjection,
276 runs: [run],
277 turnItems: [queuedFuture],
278 visibleTurnItems: [queuedRow],
279 };
280 const event = {
281 id: "event-active-assistant",
282 type: "turn-item.updated",
283 threadId,
284 runId,
285 occurredAt: now,
286 payload: activeAssistant,
287 } as OrchestrationV2DomainEvent;
288
289 const next = applyOrchestrationV2ProjectionEvent(projection, event);
290 expect(next?.visibleTurnItems.map((row) => row.item.id)).toEqual([
291 activeAssistant.id,
292 queuedFuture.id,
293 ]);
294 expect(next?.visibleTurnItems.map((row) => row.position)).toEqual([0, 1]);
295 });
296
297 it("moves an updated item to its new ordinal and renumbers the rows it passes", () => {
298 const inherited = {
299 ...commandItem("item-inherited", "parent", 0),
300 threadId: ThreadId.make("parent"),
301 };
302 const items = [
303 commandItem("item-a", "a", 1),
304 commandItem("item-b", "b", 2),
305 commandItem("item-c", "c", 2),
306 ];
307 const rows = [
308 {
309 position: 0,
310 visibility: "inherited" as const,
311 sourceThreadId: inherited.threadId,
312 sourceItemId: inherited.id,
313 item: inherited,
314 },
315 ...items.map((item, index) => ({
316 position: index + 1,
317 visibility: "local" as const,
318 sourceThreadId: threadId,
319 sourceItemId: item.id,
320 item,
321 })),
322 ];
323 const moved = commandItem("item-a", "moved", 2);
324 const next = applyOrchestrationV2ProjectionEvent(
325 { ...emptyProjection, runs: [run], turnItems: items, visibleTurnItems: rows },
326 {
327 id: "event-move",
328 type: "turn-item.updated",
329 threadId,
330 runId,
331 occurredAt: now,
332 payload: moved,
333 } as OrchestrationV2DomainEvent,
334 );
335
336 // Equal ordinals order by id, so item-a stays ahead of item-b.
337 expect(next?.visibleTurnItems.map((row) => [row.item.id, row.position])).toEqual([
338 ["item-inherited", 0],
339 ["item-a", 1],
340 ["item-b", 2],
341 ["item-c", 3],
342 ]);
343 expect(next?.visibleTurnItems[0]).toBe(rows[0]);
344 expect(next?.visibleTurnItems[1]?.item).toBe(moved);
345 expect(next?.turnItems.map((item) => item.id)).toEqual(["item-a", "item-b", "item-c"]);
346
347 const later = commandItem("item-a", "later", 3);
348 const last = applyOrchestrationV2ProjectionEvent(next, {
349 id: "event-move-later",
350 type: "turn-item.updated",
351 threadId,
352 runId,
353 occurredAt: now,
354 payload: later,
355 } as OrchestrationV2DomainEvent);
356 expect(last?.visibleTurnItems.map((row) => [row.item.id, row.position])).toEqual([
357 ["item-inherited", 0],
358 ["item-b", 1],
359 ["item-c", 2],
360 ["item-a", 3],
361 ]);
362 expect(last?.turnItems.at(0)).toBe(later);
363 });
364
365 describe("partial timeline", () => {
366 const queuedRunId = RunId.make("run-queued");
367 const steerRunId = RunId.make("run-steer");
368 const queuedRun = { ...run, id: queuedRunId, ordinal: 2, status: "queued" } as const;
369 const steerRun = { ...run, id: steerRunId, ordinal: 3, status: "completed" } as const;
370 const runItem = (id: string, itemRunId: RunId, ordinal: number) => ({
371 ...commandItem(id, id, ordinal),
372 runId: itemRunId,
373 });
374 const windowOf = (
375 runs: ReadonlyArray<OrchestrationV2Run>,
376 items: ReadonlyArray<OrchestrationV2TurnItem>,
377 ): OrchestrationV2ThreadProjection => ({
378 ...emptyProjection,
379 runs,
380 turnItems: items,
381 visibleTurnItems: items.map((item, position) => ({
382 position,
383 visibility: "local" as const,
384 sourceThreadId: threadId,
385 sourceItemId: item.id,
386 item,
387 })),
388 });
389 const itemEvent = (payload: OrchestrationV2TurnItem) =>
390 ({
391 id: `event-${payload.id}`,
392 type: "turn-item.updated",
393 threadId,
394 occurredAt: now,
395 payload,
396 }) as OrchestrationV2DomainEvent;
397 const runEvent = (payload: OrchestrationV2Run) =>
398 ({
399 id: `event-${payload.id}-${payload.status}`,
400 type: "run.updated",
401 threadId,
402 occurredAt: now,
403 payload,
404 }) as OrchestrationV2DomainEvent;
405
406 it("shows a queued run that starts after a newer steer raised the watermark", () => {
407 const steerItems = [
408 runItem("item-steer-user", steerRunId, 3_000_001),
409 runItem("item-steer-reply", steerRunId, 3_000_002),
410 ];
411 let projection: OrchestrationV2ThreadProjection | null = windowOf(
412 [run, queuedRun, steerRun],
413 steerItems,
414 );
415 let latestLocalTurnOrdinal = 3_000_002;
416 const queuedUser = runItem("item-queued-user", queuedRunId, 2_000_001);
417 const queuedReply = runItem("item-queued-reply", queuedRunId, 2_000_002);
418 for (const event of [
419 itemEvent(queuedUser),
420 runEvent({ ...queuedRun, status: "running" }),
421 itemEvent(queuedReply),
422 runEvent({ ...queuedRun, status: "completed" }),
423 ]) {
424 projection = applyOrchestrationV2ProjectionEvent(projection, event, {
425 partialTimeline: true,
426 latestLocalTurnOrdinal,
427 });
428 if (event.type === "turn-item.updated") {
429 latestLocalTurnOrdinal = Math.max(latestLocalTurnOrdinal, event.payload.ordinal);
430 }
431 }
432
433 expect(projection?.visibleTurnItems.map((row) => row.item.id)).toEqual([
434 queuedUser.id,
435 queuedReply.id,
436 ...steerItems.map((item) => item.id),
437 ]);
438 expect(projection?.turnItems.map((item) => item.id)).toContain(queuedReply.id);
439 });
440
441 it("keeps a running run's item when the window starts after its earlier items", () => {
442 const runningRun = { ...queuedRun, status: "running" } as const;
443 const steerItems = [
444 runItem("item-steer-user", steerRunId, 3_000_001),
445 runItem("item-steer-reply", steerRunId, 3_000_002),
446 ];
447 const projection = windowOf([run, runningRun, steerRun], steerItems);
448 const reply = runItem("item-running-reply", queuedRunId, 2_000_003);
449
450 const next = applyOrchestrationV2ProjectionEvent(projection, itemEvent(reply), {
451 partialTimeline: true,
452 latestLocalTurnOrdinal: 3_000_002,
453 });
454
455 expect(next?.visibleTurnItems.map((row) => row.item.id)).toEqual([
456 reply.id,
457 ...steerItems.map((item) => item.id),
458 ]);
459 });
460
461 it.each([
462 { name: "keeps an item of a run already in the window", itemRunId: steerRunId, kept: true },
463 { name: "drops an item of a finished run outside the window", itemRunId: runId, kept: false },
464 ])("$name that arrives below the watermark", ({ itemRunId, kept }) => {
465 const first = runItem("item-first", steerRunId, 3_000_001);
466 const later = runItem("item-later", steerRunId, 3_000_003);
467 const late = runItem("item-late", itemRunId, 3_000_002);
468 const projection = windowOf([run, steerRun], [first, later]);
469
470 const next = applyOrchestrationV2ProjectionEvent(projection, itemEvent(late), {
471 partialTimeline: true,
472 latestLocalTurnOrdinal: later.ordinal,
473 });
474 if (kept) {
475 expect(next?.visibleTurnItems.map((row) => row.item.id)).toEqual([
476 first.id,
477 late.id,
478 later.id,
479 ]);
480 } else {
481 expect(next).toBe(projection);
482 }
483 });
484 });
485
486 it("removes only hidden local items while preserving inherited rows", () => {
487 const inherited = commandItem("item-inherited");
488 const local = commandItem("item-local");
489 const inheritedRow = {
490 position: 0,
491 visibility: "inherited" as const,
492 sourceThreadId: ThreadId.make("thread-source"),
493 sourceItemId: inherited.id,
494 item: inherited,
495 };
496 const localRow = {
497 position: 1,
498 visibility: "local" as const,
499 sourceThreadId: threadId,
500 sourceItemId: local.id,
501 item: local,
502 };
503 const projection = {
504 ...emptyProjection,
505 runs: [run],
506 turnItems: [local],
507 visibleTurnItems: [inheritedRow, localRow],
508 };
509 const event = {
510 id: "event-run-rollback",
511 type: "run.updated",
512 threadId,
513 runId,
514 occurredAt: now,
515 payload: { ...run, status: "rolled_back" },
516 } as OrchestrationV2DomainEvent;
517
518 const next = applyOrchestrationV2ProjectionEvent(projection, event);
519 expect(next?.visibleTurnItems).toEqual([inheritedRow]);
520 expect(next?.visibleTurnItems[0]).toBe(inheritedRow);
521 });
522});
523
524it("does not scan every row against every run for a streaming item update", () => {
525 let runReads = 0;
526 const runs = Array.from({ length: 100 }, (_, index) => ({
527 ...run,
528 get id() {
529 runReads++;
530 return RunId.make(`run-${index}`);
531 },
532 }));
533 const items = Array.from({ length: 1000 }, (_, index) =>
534 commandItem(`item-${index}`, "before", index),
535 );
536 const projection = {
537 ...emptyProjection,
538 runs,
539 turnItems: items,
540 visibleTurnItems: items.map((item, position) => ({
541 item,
542 position,
543 visibility: "local" as const,
544 sourceThreadId: threadId,
545 sourceItemId: item.id,
546 })),
547 };
548 const payload = commandItem("item-999", "after", 999);
549 const next = applyOrchestrationV2ProjectionEvent(projection, {
550 id: "stream-update",
551 type: "turn-item.updated",
552 threadId,
553 occurredAt: now,
554 payload,
555 } as OrchestrationV2DomainEvent);
556 expect(next?.visibleTurnItems.at(-1)?.item).toBe(payload);
557 expect(next?.visibleTurnItems[0]).toBe(projection.visibleTurnItems[0]);
558 expect(runReads).toBeLessThanOrEqual(100);
559});
560
561it("does not scan ordering or positions for a streamed replacement", () => {
562 const size = 100;
563 let ordinalReads = 0;
564 let positionReads = 0;
565 const items = Array.from({ length: size }, (_, index) => ({
566 ...commandItem(`item-${index}`, "before", index),
567 get ordinal() {
568 ordinalReads++;
569 return index;
570 },
571 }));
572 const rows = items.map((item, index) => ({
573 item,
574 get position() {
575 positionReads++;
576 return index;
577 },
578 visibility: "local" as const,
579 sourceThreadId: threadId,
580 sourceItemId: item.id,
581 }));
582 const payload = commandItem(`item-${size - 1}`, "after", size - 1);
583 const next = applyOrchestrationV2ProjectionEvent(
584 { ...emptyProjection, runs: [run], turnItems: items, visibleTurnItems: rows },
585 {
586 id: "stable-replacement",
587 type: "turn-item.updated",
588 threadId,
589 occurredAt: now,
590 payload,
591 } as OrchestrationV2DomainEvent,
592 );
593
594 expect(ordinalReads).toBeLessThanOrEqual(2);
595 expect(positionReads).toBeLessThanOrEqual(2);
596 expect(next?.visibleTurnItems.at(-1)).toMatchObject({ item: payload, position: size - 1 });
597 expect(next?.visibleTurnItems[0]).toBe(rows[0]);
598 expect(items.at(-1)).toMatchObject({ output: "before" });
599 expect(rows.at(-1)?.item).toBe(items.at(-1));
600});
601
602it("rechecks visibility when an existing item changes runs without changing ordinal", () => {
603 const first = commandItem("item-first", "first", 1);
604 const second = commandItem("item-second", "second", 2);
605 const rolledBackRun = { ...run, id: RunId.make("rolled-back"), status: "rolled_back" as const };
606 const projection = {
607 ...emptyProjection,
608 runs: [run, rolledBackRun],
609 turnItems: [first, second],
610 visibleTurnItems: [first, second].map((item, position) => ({
611 item,
612 position,
613 visibility: "local" as const,
614 sourceThreadId: threadId,
615 sourceItemId: item.id,
616 })),
617 };
618 const update = (payload: OrchestrationV2TurnItem) =>
619 ({
620 id: "visibility-update",
621 type: "turn-item.updated",
622 threadId,
623 occurredAt: now,
624 payload,
625 }) as OrchestrationV2DomainEvent;
626 const hidden = applyOrchestrationV2ProjectionEvent(
627 projection,
628 update({
629 ...first,
630 runId: rolledBackRun.id,
631 }),
632 );
633 expect(hidden?.visibleTurnItems.map((row) => [row.item.id, row.position])).toEqual([
634 [second.id, 0],
635 ]);
636 const restored = applyOrchestrationV2ProjectionEvent(hidden, update(first));
637 expect(restored?.visibleTurnItems.map((row) => [row.item.id, row.position])).toEqual([
638 [first.id, 0],
639 [second.id, 1],
640 ]);
641 expect(projection.visibleTurnItems[0]?.item).toBe(first);
642});
643
644it("restores deduped bodies from the records it already holds, also in a partial timeline", () => {
645 const messageId = MessageId.make("message-deduped");
646 const subagentId = NodeId.make("node-deduped");
647 const reply = {
648 ...commandItem("item-reply", "", 400),
649 type: "assistant_message",
650 messageId,
651 text: "the full reply",
652 streaming: false,
653 } as unknown as OrchestrationV2TurnItem;
654 const subagent = {
655 id: subagentId,
656 threadId,
657 runId,
658 parentNodeId: NodeId.make("node-root"),
659 origin: "app_owned",
660 createdBy: "agent",
661 driver: "codex",
662 providerInstanceId: ProviderInstanceId.make("codex"),
663 providerThreadId: null,
664 childThreadId: null,
665 nativeTaskRef: null,
666 prompt: "the task brief",
667 title: null,
668 model: null,
669 status: "running",
670 result: null,
671 startedAt: now,
672 completedAt: null,
673 updatedAt: now,
674 } as OrchestrationV2ThreadProjection["subagents"][number];
675 const projection = {
676 ...emptyProjection,
677 runs: [run],
678 messages: [
679 {
680 id: messageId,
681 threadId,
682 runId,
683 nodeId: null,
684 role: "assistant",
685 text: "the full reply",
686 attachments: [],
687 streaming: false,
688 createdBy: "agent",
689 creationSource: "provider",
690 createdAt: now,
691 updatedAt: now,
692 },
693 ],
694 subagents: [subagent],
695 } as OrchestrationV2ThreadProjection;
696 const event = (type: string, payload: unknown) =>
697 ({
698 id: `event-${type}`,
699 type,
700 threadId,
701 runId,
702 occurredAt: now,
703 payload,
704 }) as OrchestrationV2DomainEvent;
705
706 for (const options of [undefined, { partialTimeline: true, latestLocalTurnOrdinal: 300 }]) {
707 const withReply = applyOrchestrationV2ProjectionEvent(
708 projection,
709 event("turn-item.updated", { ...reply, text: "", textOmitted: true }),
710 options,
711 );
712 expect(withReply?.turnItems).toEqual([reply]);
713 expect(withReply?.visibleTurnItems.map((row) => row.item)).toEqual([reply]);
714 }
715 const progressed = applyOrchestrationV2ProjectionEvent(
716 projection,
717 event("subagent.updated", { ...subagent, prompt: "", promptOmitted: true, progress: "step 2" }),
718 );
719 expect(progressed?.subagents).toEqual([{ ...subagent, progress: "step 2" }]);
720});