1import {
2 EnvironmentId,
3 ProjectId,
4 RunId,
5 RuntimeRequestId,
6 ThreadId,
7 type OrchestrationV2ShellSnapshot,
8 type OrchestrationV2ThreadShell,
9} from "@t3tools/contracts";
10import * as DateTime from "effect/DateTime";
11import * as Option from "effect/Option";
12import { Atom, AtomRegistry } from "effect/reactivity";
13import { describe, expect, it } from "vite-plus/test";
14
15import { PrimaryConnectionTarget } from "../connection/model.ts";
16import { v2Now, v2ShellSnapshot, v2ThreadShell } from "./orchestrationV2TestFixtures.ts";
17import { applyShellStreamEvent } from "./shellReducer.ts";
18import { isThreadWorking } from "./threadInbox.ts";
19import { childThreadInputsTitle, createEnvironmentThreadShellAtoms } from "./threadShell.ts";
20
21const environmentId = EnvironmentId.make("environment-v2");
22const remoteEnvironmentId = EnvironmentId.make("remote-environment-v2");
23const otherProjectId = ProjectId.make("other-project");
24
25function makeHarness(environmentIds: ReadonlyArray<EnvironmentId> = [environmentId]) {
26 const snapshotAtom = Atom.family((_environmentId: EnvironmentId) =>
27 Atom.make<OrchestrationV2ShellSnapshot | null>(v2ShellSnapshot),
28 );
29 const catalogValueAtom = Atom.make({
30 isReady: true,
31 entries: new Map(
32 environmentIds.map((id) => [
33 id,
34 {
35 target: new PrimaryConnectionTarget({
36 environmentId: id,
37 label: "Environment",
38 httpBaseUrl: "https://example.test",
39 wsBaseUrl: "wss://example.test",
40 }),
41 profile: Option.none(),
42 enabled: true,
43 },
44 ]),
45 ),
46 });
47 return {
48 registry: AtomRegistry.make(),
49 snapshotAtom,
50 catalogValueAtom,
51 threads: createEnvironmentThreadShellAtoms({
52 catalogValueAtom,
53 snapshotAtom,
54 hiddenThreadTagsAtom: () => Atom.make(null),
55 }),
56 };
57}
58
59describe("v2 thread shell lists", () => {
60 it("preserves ordered reference arrays when a middle thread changes", () => {
61 const { registry, threads, snapshotAtom } = makeHarness();
62 const snapshot = {
63 ...v2ShellSnapshot,
64 threads: ["a", "b", "c"].map((id) => ({ ...v2ThreadShell, id: ThreadId.make(id) })),
65 };
66 registry.set(snapshotAtom(environmentId), snapshot);
67 const dispose = registry.mount(threads.threadRefsAtom);
68 const before = registry.get(threads.threadRefsAtom);
69 registry.set(
70 snapshotAtom(environmentId),
71 applyShellStreamEvent(snapshot, {
72 kind: "thread.updated",
73 location: "active",
74 sequence: 1,
75 thread: { ...snapshot.threads[1]!, title: "Updated" },
76 }),
77 );
78 expect(registry.get(threads.threadRefsAtom)).toBe(before);
79 dispose();
80 registry.dispose();
81 });
82
83 it("keeps navigation stable on hidden subagent updates and retains user forks", () => {
84 const { registry, threads, snapshotAtom } = makeHarness();
85 const root = v2ThreadShell;
86 const child = {
87 ...root,
88 id: ThreadId.make("child"),
89 lineage: {
90 ...root.lineage,
91 parentThreadId: root.id,
92 relationshipToParent: "subagent" as const,
93 },
94 };
95 const fork = {
96 ...child,
97 id: ThreadId.make("fork"),
98 lineage: { ...child.lineage, relationshipToParent: "fork" as const },
99 };
100 const snapshot = { ...v2ShellSnapshot, threads: [root, child, fork] };
101 registry.set(snapshotAtom(environmentId), snapshot);
102 const dispose = registry.mount(threads.navigationThreadShellsAtom);
103 const before = registry.get(threads.navigationThreadShellsAtom);
104 expect(before.map((thread) => thread.id)).toEqual([root.id, fork.id]);
105 registry.set(snapshotAtom(environmentId), {
106 ...snapshot,
107 threads: [root, { ...child, title: "Child streaming" }, fork],
108 });
109 expect(registry.get(threads.navigationThreadShellsAtom)).toBe(before);
110 expect(registry.get(threads.threadShellsAtom)).toHaveLength(3);
111 dispose();
112 registry.dispose();
113 });
114
115 it("keeps a live subagent question inside the parent without flagging its thread", () => {
116 const { registry, threads, snapshotAtom } = makeHarness();
117 const root = v2ThreadShell;
118 const child = {
119 ...root,
120 id: ThreadId.make("child"),
121 creationSource: "mcp" as const,
122 status: "running" as const,
123 activityRunStatus: "running" as const,
124 lineage: {
125 ...root.lineage,
126 parentThreadId: root.id,
127 relationshipToParent: "subagent" as const,
128 },
129 pendingRuntimeRequest: {
130 id: RuntimeRequestId.make("question"),
131 kind: "user_input" as const,
132 createdAt: v2Now,
133 },
134 };
135 const inputs = threads.childThreadInputsAtom({ environmentId, threadId: root.id });
136 const dispose = registry.mount(inputs);
137 registry.set(snapshotAtom(environmentId), { ...v2ShellSnapshot, threads: [root, child] });
138 expect(registry.get(inputs).map((thread) => thread.id)).toEqual([child.id]);
139 expect(
140 registry.get(threads.threadShellAtom({ environmentId, threadId: root.id }))
141 ?.pendingSubagentRequest,
142 ).toBeNull();
143 registry.set(snapshotAtom(environmentId), {
144 ...v2ShellSnapshot,
145 threads: [root, { ...child, status: "queued" as const }],
146 });
147 expect(registry.get(inputs).map((thread) => thread.id)).toEqual([child.id]);
148 registry.set(snapshotAtom(environmentId), {
149 ...v2ShellSnapshot,
150 threads: [root, { ...child, status: "completed" as const, activityRunStatus: null }],
151 });
152 expect(registry.get(inputs)).toEqual([]);
153 const { activityRunStatus: _omitted, ...legacyChild } = child;
154 registry.set(snapshotAtom(environmentId), {
155 ...v2ShellSnapshot,
156 threads: [root, { ...legacyChild, status: "completed" as const }],
157 });
158 expect(registry.get(inputs)).toEqual([]);
159 dispose();
160 registry.dispose();
161 });
162
163 it("rolls a side chat's questions and approvals up to its parent row", () => {
164 const { registry, threads, snapshotAtom } = makeHarness();
165 const root = v2ThreadShell;
166 const sideChat = {
167 ...root,
168 id: ThreadId.make("side-chat"),
169 status: "running" as const,
170 activityRunStatus: "running" as const,
171 latestRunId: RunId.make("side-chat-run"),
172 lineage: {
173 ...root.lineage,
174 parentThreadId: root.id,
175 relationshipToParent: "fork" as const,
176 sideChat: true,
177 },
178 };
179 const request = { id: RuntimeRequestId.make("ask"), createdAt: v2Now };
180 const parentRef = { environmentId, threadId: root.id };
181 const parent = threads.threadShellAtom(parentRef);
182 const inputs = threads.childThreadInputsAtom(parentRef);
183 const disposeParent = registry.mount(parent);
184 const disposeInputs = registry.mount(inputs);
185 const disposeNavigation = registry.mount(threads.navigationThreadShellsAtom);
186 const withRequest = (
187 kind: "user_input" | "command",
188 options: { readonly blocking?: boolean } = {},
189 ) =>
190 registry.set(snapshotAtom(environmentId), {
191 ...v2ShellSnapshot,
192 threads: [
193 root,
194 {
195 ...sideChat,
196 pendingRuntimeRequest: {
197 ...request,
198 kind,
199 ...(options.blocking === undefined ? {} : { blocking: options.blocking }),
200 },
201 },
202 ],
203 });
204
205 withRequest("user_input");
206 expect(registry.get(parent)?.pendingSubagentRequest).toBe("user_input");
207 expect(childThreadInputsTitle(registry.get(inputs))).toBe("Side chat needs input");
208 expect(registry.get(threads.navigationThreadShellsAtom).map((thread) => thread.id)).toEqual([
209 root.id,
210 ]);
211
212 withRequest("user_input", { blocking: false });
213 expect(registry.get(parent)?.pendingSubagentRequest).toBe("user_input");
214
215 withRequest("command");
216 expect(registry.get(parent)?.pendingSubagentRequest).toBe("approval");
217 expect(childThreadInputsTitle(registry.get(inputs))).toBe("Side chat needs approval");
218
219 const helper = {
220 ...sideChat,
221 id: ThreadId.make("side-chat-helper"),
222 creationSource: "mcp" as const,
223 lineage: {
224 ...sideChat.lineage,
225 parentThreadId: sideChat.id,
226 relationshipToParent: "subagent" as const,
227 sideChat: false,
228 },
229 pendingRuntimeRequest: { ...request, kind: "command" as const },
230 };
231 const sideChats = threads.sideChatsAtom(parentRef);
232 const disposeSideChats = registry.mount(sideChats);
233 registry.set(snapshotAtom(environmentId), {
234 ...v2ShellSnapshot,
235 threads: [root, sideChat, helper],
236 });
237 expect(registry.get(parent)?.pendingSubagentRequest).toBe("approval");
238 expect(
239 registry.get(sideChats).map((thread) => [thread.id, thread.pendingSubagentRequest]),
240 ).toEqual([[sideChat.id, "approval"]]);
241
242 registry.set(snapshotAtom(environmentId), { ...v2ShellSnapshot, threads: [root, sideChat] });
243 expect(registry.get(parent)?.pendingSubagentRequest).toBeNull();
244 expect(registry.get(sideChats).map((thread) => thread.pendingSubagentRequest)).toEqual([null]);
245 expect(registry.get(inputs)).toEqual([]);
246 disposeSideChats();
247 disposeNavigation();
248 disposeInputs();
249 disposeParent();
250 registry.dispose();
251 });
252
253 it("brings a parent back from Working while a nested subagent waits for approval", () => {
254 const { registry, threads, snapshotAtom } = makeHarness();
255 const child = {
256 ...v2ThreadShell,
257 id: ThreadId.make("review"),
258 creationSource: "mcp" as const,
259 status: "running" as const,
260 activityRunStatus: "running" as const,
261 lineage: {
262 ...v2ThreadShell.lineage,
263 parentThreadId: v2ThreadShell.id,
264 relationshipToParent: "subagent" as const,
265 },
266 };
267 const grandchild = {
268 ...child,
269 id: ThreadId.make("review-helper"),
270 lineage: { ...child.lineage, parentThreadId: child.id },
271 };
272 const root = {
273 ...v2ThreadShell,
274 latestRunId: RunId.make("root-run"),
275 status: "completed" as const,
276 pendingBackgroundTasks: [
277 { taskId: "review", kind: "subagent" as const, childThreadId: child.id },
278 ],
279 };
280 const approval = {
281 id: RuntimeRequestId.make("cat-file"),
282 kind: "command" as const,
283 createdAt: v2Now,
284 };
285 const parent = threads.threadShellAtom({ environmentId, threadId: root.id });
286 const inputs = threads.childThreadInputsAtom({ environmentId, threadId: root.id });
287 const disposeParent = registry.mount(parent);
288 const disposeInputs = registry.mount(inputs);
289 registry.set(snapshotAtom(environmentId), {
290 ...v2ShellSnapshot,
291 threads: [root, child, grandchild],
292 });
293 expect(isThreadWorking(registry.get(parent)!)).toBe(true);
294
295 registry.set(snapshotAtom(environmentId), {
296 ...v2ShellSnapshot,
297 threads: [root, child, { ...grandchild, pendingRuntimeRequest: approval }],
298 });
299 expect(registry.get(parent)?.pendingSubagentRequest).toBe("approval");
300 expect(isThreadWorking(registry.get(parent)!)).toBe(false);
301 const asking = registry.get(inputs);
302 expect(asking.map((thread) => thread.id)).toEqual([grandchild.id]);
303 expect(childThreadInputsTitle(asking)).toBe("Subagent needs approval");
304
305 registry.set(snapshotAtom(environmentId), {
306 ...v2ShellSnapshot,
307 threads: [root, child, grandchild],
308 });
309 expect(registry.get(parent)?.pendingSubagentRequest).toBeNull();
310 expect(isThreadWorking(registry.get(parent)!)).toBe(true);
311 disposeInputs();
312 disposeParent();
313 registry.dispose();
314 });
315
316 it("lists a task a linked environment runs for a listed thread under that thread", () => {
317 const { registry, threads, snapshotAtom, catalogValueAtom } = makeHarness([
318 environmentId,
319 remoteEnvironmentId,
320 ]);
321 const parent = v2ThreadShell;
322 const task = {
323 ...v2ThreadShell,
324 id: ThreadId.make("remote-task"),
325 creationSource: "mcp" as const,
326 status: "running" as const,
327 activityRunStatus: "running" as const,
328 delegatedFrom: { environmentId, threadId: parent.id, title: parent.title },
329 };
330 const helper = {
331 ...task,
332 id: ThreadId.make("remote-helper"),
333 delegatedFrom: undefined,
334 lineage: {
335 ...task.lineage,
336 parentThreadId: task.id,
337 relationshipToParent: "subagent" as const,
338 },
339 };
340 const approval = {
341 id: RuntimeRequestId.make("cat-file"),
342 kind: "command" as const,
343 createdAt: v2Now,
344 };
345 registry.set(snapshotAtom(environmentId), { ...v2ShellSnapshot, threads: [parent] });
346 registry.set(snapshotAtom(remoteEnvironmentId), {
347 ...v2ShellSnapshot,
348 threads: [task, helper],
349 });
350 const parentRef = { environmentId, threadId: parent.id };
351 const inputs = threads.childThreadInputsAtom(parentRef);
352 const disposeNavigation = registry.mount(threads.navigationThreadShellsAtom);
353 const disposeParent = registry.mount(threads.threadShellAtom(parentRef));
354 const disposeInputs = registry.mount(inputs);
355 const listed = () =>
356 registry
357 .get(threads.navigationThreadShellsAtom)
358 .map((thread) => `${thread.environmentId}/${thread.id}`);
359 expect(listed()).toEqual([`${environmentId}/${parent.id}`]);
360
361 // The helper the task started there asks for approval: the parent here hears of it.
362 registry.set(snapshotAtom(remoteEnvironmentId), {
363 ...v2ShellSnapshot,
364 threads: [task, { ...helper, pendingRuntimeRequest: approval }],
365 });
366 expect(registry.get(threads.threadShellAtom(parentRef))?.pendingSubagentRequest).toBe(
367 "approval",
368 );
369 expect(registry.get(inputs).map((thread) => `${thread.environmentId}/${thread.id}`)).toEqual([
370 `${remoteEnvironmentId}/${helper.id}`,
371 ]);
372
373 registry.set(snapshotAtom(remoteEnvironmentId), {
374 ...v2ShellSnapshot,
375 threads: [
376 task,
377 { ...helper, pendingRuntimeRequest: { ...approval, kind: "user_input" as const } },
378 ],
379 });
380 expect(registry.get(threads.threadShellAtom(parentRef))?.pendingSubagentRequest).toBeNull();
381 expect(registry.get(inputs).map((thread) => thread.id)).toEqual([helper.id]);
382
383 // A task whose parent is gone there is listed on its own, as is one whose
384 // parent's environment is switched off.
385 registry.set(snapshotAtom(environmentId), { ...v2ShellSnapshot, threads: [] });
386 expect(listed()).toEqual([`${remoteEnvironmentId}/${task.id}`]);
387 // Archiving the parent here leaves the task there unarchived, so it is listed on its own.
388 registry.set(snapshotAtom(environmentId), {
389 ...v2ShellSnapshot,
390 threads: [{ ...parent, archivedAt: v2Now }],
391 });
392 expect(listed()).toEqual([`${remoteEnvironmentId}/${task.id}`]);
393 registry.set(snapshotAtom(environmentId), { ...v2ShellSnapshot, threads: [parent] });
394 expect(listed()).toEqual([`${environmentId}/${parent.id}`]);
395 const catalog = registry.get(catalogValueAtom);
396 const entries = new Map(catalog.entries);
397 entries.set(environmentId, { ...entries.get(environmentId)!, enabled: false });
398 registry.set(catalogValueAtom, { ...catalog, entries });
399 expect(listed()).toEqual([`${remoteEnvironmentId}/${task.id}`]);
400 disposeInputs();
401 disposeParent();
402 disposeNavigation();
403 registry.dispose();
404 });
405
406 it("brings an approval up a chain of tasks across three environments to its root", () => {
407 const thirdEnvironmentId = EnvironmentId.make("third-environment-v2");
408 const { registry, threads, snapshotAtom } = makeHarness([
409 environmentId,
410 remoteEnvironmentId,
411 thirdEnvironmentId,
412 ]);
413 const root = v2ThreadShell;
414 const running = {
415 creationSource: "mcp" as const,
416 status: "running" as const,
417 activityRunStatus: "running" as const,
418 };
419 const task = {
420 ...v2ThreadShell,
421 ...running,
422 id: ThreadId.make("remote-task"),
423 delegatedFrom: { environmentId, threadId: root.id, title: root.title },
424 };
425 const nestedTask = {
426 ...v2ThreadShell,
427 ...running,
428 id: ThreadId.make("third-task"),
429 delegatedFrom: { environmentId: remoteEnvironmentId, threadId: task.id, title: task.title },
430 };
431 // A task the middle one delegates back to the root's own environment.
432 const returningTask = {
433 ...nestedTask,
434 id: ThreadId.make("returning-task"),
435 };
436 const approval = {
437 id: RuntimeRequestId.make("cat-file"),
438 kind: "command" as const,
439 createdAt: v2Now,
440 };
441 registry.set(snapshotAtom(environmentId), { ...v2ShellSnapshot, threads: [root] });
442 registry.set(snapshotAtom(remoteEnvironmentId), { ...v2ShellSnapshot, threads: [task] });
443 registry.set(snapshotAtom(thirdEnvironmentId), {
444 ...v2ShellSnapshot,
445 threads: [{ ...nestedTask, pendingRuntimeRequest: approval }],
446 });
447 const rootRef = { environmentId, threadId: root.id };
448 const taskRef = { environmentId: remoteEnvironmentId, threadId: task.id };
449 const disposeRoot = registry.mount(threads.threadShellAtom(rootRef));
450 const disposeTask = registry.mount(threads.threadShellAtom(taskRef));
451 expect(registry.get(threads.threadShellAtom(taskRef))?.pendingSubagentRequest).toBe("approval");
452 expect(registry.get(threads.threadShellAtom(rootRef))?.pendingSubagentRequest).toBe("approval");
453
454 // Once it is answered, neither stays flagged.
455 registry.set(snapshotAtom(thirdEnvironmentId), {
456 ...v2ShellSnapshot,
457 threads: [nestedTask],
458 });
459 expect(registry.get(threads.threadShellAtom(rootRef))?.pendingSubagentRequest).toBeNull();
460
461 // Environments that delegate to each other still settle on the root's flag.
462 registry.set(snapshotAtom(environmentId), {
463 ...v2ShellSnapshot,
464 threads: [root, { ...returningTask, pendingRuntimeRequest: approval }],
465 });
466 expect(registry.get(threads.threadShellAtom(rootRef))?.pendingSubagentRequest).toBe("approval");
467 disposeTask();
468 disposeRoot();
469 registry.dispose();
470 });
471
472 it.each([
473 { reason: "switched-off", state: { enabled: false } },
474 {
475 reason: "unsupported",
476 state: { enabled: true, unsupportedReason: "Use a compatible client." },
477 },
478 ])("leaves $reason environments out of navigation until restored", ({ state }) => {
479 const { registry, threads, catalogValueAtom } = makeHarness([
480 environmentId,
481 remoteEnvironmentId,
482 ]);
483 const dispose = registry.mount(threads.navigationThreadShellsAtom);
484 const environmentsOf = () =>
485 new Set(registry.get(threads.navigationThreadShellsAtom).map((t) => t.environmentId));
486 expect(environmentsOf()).toEqual(new Set([environmentId, remoteEnvironmentId]));
487
488 const setRemotePaused = (paused: boolean) => {
489 const catalog = registry.get(catalogValueAtom);
490 const entries = new Map(catalog.entries);
491 entries.set(remoteEnvironmentId, {
492 ...entries.get(remoteEnvironmentId)!,
493 ...(paused ? state : { enabled: true, unsupportedReason: undefined }),
494 });
495 registry.set(catalogValueAtom, { ...catalog, entries });
496 };
497 setRemotePaused(true);
498 expect(environmentsOf()).toEqual(new Set([environmentId]));
499 setRemotePaused(false);
500 expect(environmentsOf()).toEqual(new Set([environmentId, remoteEnvironmentId]));
501 dispose();
502 registry.dispose();
503 });
504
505 it("preserves project memberships on updates and keeps the same thread separate per environment", () => {
506 const harness = makeHarness([environmentId, remoteEnvironmentId]);
507 const otherThread = {
508 ...v2ThreadShell,
509 id: ThreadId.make("other-thread"),
510 projectId: otherProjectId,
511 };
512 const snapshot = { ...v2ShellSnapshot, threads: [v2ThreadShell, otherThread] };
513 harness.registry.set(harness.snapshotAtom(environmentId), snapshot);
514 const membershipAtom = harness.threads.environmentThreadRefsByProjectAtom(environmentId);
515 const listAtom = harness.threads.threadShellsForProjectRefsAtom([
516 { environmentId: remoteEnvironmentId, projectId: v2ThreadShell.projectId },
517 { environmentId, projectId: v2ThreadShell.projectId },
518 ]);
519 const disposeList = harness.registry.mount(listAtom);
520 try {
521 const membership = harness.registry.get(membershipAtom);
522 const before = harness.registry.get(listAtom);
523 expect(before).toHaveLength(2);
524 expect(before[0]).not.toBe(before[1]);
525 expect(before.map((thread) => thread.environmentId)).toEqual([
526 remoteEnvironmentId,
527 environmentId,
528 ]);
529
530 harness.registry.set(harness.snapshotAtom(environmentId), {
531 ...snapshot,
532 threads: [v2ThreadShell, { ...otherThread, title: "Changed elsewhere" }],
533 });
534 expect(harness.registry.get(membershipAtom)).toBe(membership);
535 expect(harness.registry.get(listAtom)).toBe(before);
536
537 harness.registry.set(harness.snapshotAtom(environmentId), {
538 ...snapshot,
539 threads: [{ ...v2ThreadShell, projectId: otherProjectId }, otherThread],
540 });
541 expect(harness.registry.get(listAtom)).toEqual([before[0]]);
542 harness.registry.set(harness.snapshotAtom(remoteEnvironmentId), {
543 ...v2ShellSnapshot,
544 threads: [],
545 });
546 expect(harness.registry.get(listAtom)).toEqual([]);
547 } finally {
548 disposeList();
549 harness.registry.dispose();
550 }
551 });
552
553 it("keeps the quiet shell through streaming ticks and follows every change once the run stops", () => {
554 const harness = makeHarness();
555 const runId = RunId.make("streaming-run");
556 const running: OrchestrationV2ThreadShell = {
557 ...v2ThreadShell,
558 latestRunId: runId,
559 activeRunId: runId,
560 status: "running",
561 itemCount: 1,
562 visibleItemCount: 1,
563 };
564 const setThread = (thread: OrchestrationV2ThreadShell, sequence: number) =>
565 harness.registry.set(harness.snapshotAtom(environmentId), {
566 ...v2ShellSnapshot,
567 snapshotSequence: sequence,
568 threads: [thread],
569 });
570 const at = (seconds: number) => DateTime.add(v2Now, { seconds });
571 setThread(running, 1);
572 const ref = { environmentId, threadId: v2ThreadShell.id };
573 const quietAtom = harness.threads.quietThreadShellAtom(ref);
574 const dispose = harness.registry.mount(quietAtom);
575 try {
576 const first = harness.registry.get(quietAtom);
577 setThread({ ...running, updatedAt: at(1), itemCount: 4, visibleItemCount: 3 }, 2);
578 expect(harness.registry.get(quietAtom)).toBe(first);
579 expect(harness.registry.get(harness.threads.threadShellAtom(ref))?.itemCount).toBe(4);
580
581 setThread({ ...running, updatedAt: at(2), itemCount: 5, title: "Renamed" }, 3);
582 const renamed = harness.registry.get(quietAtom);
583 expect(renamed?.title).toBe("Renamed");
584
585 const stopped: OrchestrationV2ThreadShell = {
586 ...running,
587 activeRunId: null,
588 status: "completed",
589 };
590 setThread({ ...stopped, title: "Renamed", updatedAt: at(3) }, 4);
591 const completed = harness.registry.get(quietAtom);
592 expect(completed).not.toBe(renamed);
593 setThread({ ...stopped, title: "Renamed", updatedAt: at(4) }, 5);
594 expect(harness.registry.get(quietAtom)?.updatedAt).toBe(DateTime.formatIso(at(4)));
595 expect(harness.registry.get(quietAtom)).not.toBe(completed);
596 } finally {
597 dispose();
598 harness.registry.dispose();
599 }
600 });
601
602 it("keeps the quiet shell list through streaming ticks", () => {
603 const harness = makeHarness();
604 const runId = RunId.make("streaming-list-run");
605 const running: OrchestrationV2ThreadShell = {
606 ...v2ThreadShell,
607 latestRunId: runId,
608 activeRunId: runId,
609 status: "running",
610 itemCount: 1,
611 visibleItemCount: 1,
612 };
613 const setThread = (thread: OrchestrationV2ThreadShell, sequence: number) =>
614 harness.registry.set(harness.snapshotAtom(environmentId), {
615 ...v2ShellSnapshot,
616 snapshotSequence: sequence,
617 threads: [thread],
618 });
619 setThread(running, 1);
620 const listAtom = harness.threads.quietThreadShellsAtom;
621 const dispose = harness.registry.mount(listAtom);
622 try {
623 const first = harness.registry.get(listAtom);
624 expect(first).toHaveLength(1);
625 setThread({ ...running, updatedAt: DateTime.add(v2Now, { seconds: 1 }), itemCount: 4 }, 2);
626 expect(harness.registry.get(listAtom)).toBe(first);
627
628 setThread({ ...running, title: "Renamed" }, 3);
629 expect(harness.registry.get(listAtom)).not.toBe(first);
630 expect(harness.registry.get(listAtom)[0]?.title).toBe("Renamed");
631 } finally {
632 dispose();
633 harness.registry.dispose();
634 }
635 });
636});