Skip to content
Source
Sign inSign up

threadShell.test.ts

681 lines25.5 KB

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 };
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("flags the parent while a live subagent waits on a secret card", () => {
164 const { registry, threads, snapshotAtom } = makeHarness();
165 const root = v2ThreadShell;
166 const child = {
167 ...root,
168 id: ThreadId.make("child"),
169 creationSource: "mcp" as const,
170 status: "running" as const,
171 activityRunStatus: "running" as const,
172 lineage: {
173 ...root.lineage,
174 parentThreadId: root.id,
175 relationshipToParent: "subagent" as const,
176 },
177 pendingRuntimeRequest: {
178 id: RuntimeRequestId.make("secret-card"),
179 kind: "user_input" as const,
180 createdAt: v2Now,
181 blocking: true,
182 userOnly: true,
183 },
184 };
185 registry.set(snapshotAtom(environmentId), { ...v2ShellSnapshot, threads: [root, child] });
186 expect(
187 registry.get(threads.threadShellAtom({ environmentId, threadId: root.id }))
188 ?.pendingSubagentRequest,
189 ).toBe("user_input");
190 registry.dispose();
191 });
192​
193 it("rolls a side chat's questions and approvals up to its parent row", () => {
194 const { registry, threads, snapshotAtom } = makeHarness();
195 const root = v2ThreadShell;
196 const sideChat = {
197 ...root,
198 id: ThreadId.make("side-chat"),
199 status: "running" as const,
200 activityRunStatus: "running" as const,
201 latestRunId: RunId.make("side-chat-run"),
202 lineage: {
203 ...root.lineage,
204 parentThreadId: root.id,
205 relationshipToParent: "fork" as const,
206 sideChat: true,
207 },
208 };
209 const request = { id: RuntimeRequestId.make("ask"), createdAt: v2Now };
210 const parentRef = { environmentId, threadId: root.id };
211 const parent = threads.threadShellAtom(parentRef);
212 const inputs = threads.childThreadInputsAtom(parentRef);
213 const disposeParent = registry.mount(parent);
214 const disposeInputs = registry.mount(inputs);
215 const disposeNavigation = registry.mount(threads.navigationThreadShellsAtom);
216 const withRequest = (
217 kind: "user_input" | "command",
218 options: { readonly blocking?: boolean } = {},
219 ) =>
220 registry.set(snapshotAtom(environmentId), {
221 ...v2ShellSnapshot,
222 threads: [
223 root,
224 {
225 ...sideChat,
226 pendingRuntimeRequest: {
227 ...request,
228 kind,
229 ...(options.blocking === undefined ? {} : { blocking: options.blocking }),
230 },
231 },
232 ],
233 });
234​
235 withRequest("user_input");
236 expect(registry.get(parent)?.pendingSubagentRequest).toBe("user_input");
237 expect(childThreadInputsTitle(registry.get(inputs))).toBe("Side chat needs input");
238 expect(registry.get(threads.navigationThreadShellsAtom).map((thread) => thread.id)).toEqual([
239 root.id,
240 ]);
241​
242 withRequest("user_input", { blocking: false });
243 expect(registry.get(parent)?.pendingSubagentRequest).toBe("user_input");
244​
245 withRequest("command");
246 expect(registry.get(parent)?.pendingSubagentRequest).toBe("approval");
247 expect(childThreadInputsTitle(registry.get(inputs))).toBe("Side chat needs approval");
248​
249 const helper = {
250 ...sideChat,
251 id: ThreadId.make("side-chat-helper"),
252 creationSource: "mcp" as const,
253 lineage: {
254 ...sideChat.lineage,
255 parentThreadId: sideChat.id,
256 relationshipToParent: "subagent" as const,
257 sideChat: false,
258 },
259 pendingRuntimeRequest: { ...request, kind: "command" as const },
260 };
261 const sideChats = threads.sideChatsAtom(parentRef);
262 const disposeSideChats = registry.mount(sideChats);
263 registry.set(snapshotAtom(environmentId), {
264 ...v2ShellSnapshot,
265 threads: [root, sideChat, helper],
266 });
267 expect(registry.get(parent)?.pendingSubagentRequest).toBe("approval");
268 expect(
269 registry.get(sideChats).map((thread) => [thread.id, thread.pendingSubagentRequest]),
270 ).toEqual([[sideChat.id, "approval"]]);
271​
272 registry.set(snapshotAtom(environmentId), { ...v2ShellSnapshot, threads: [root, sideChat] });
273 expect(registry.get(parent)?.pendingSubagentRequest).toBeNull();
274 expect(registry.get(sideChats).map((thread) => thread.pendingSubagentRequest)).toEqual([null]);
275 expect(registry.get(inputs)).toEqual([]);
276 disposeSideChats();
277 disposeNavigation();
278 disposeInputs();
279 disposeParent();
280 registry.dispose();
281 });
282​
283 it("brings a parent back from Working while a nested subagent waits for approval", () => {
284 const { registry, threads, snapshotAtom } = makeHarness();
285 const child = {
286 ...v2ThreadShell,
287 id: ThreadId.make("review"),
288 creationSource: "mcp" as const,
289 status: "running" as const,
290 activityRunStatus: "running" as const,
291 lineage: {
292 ...v2ThreadShell.lineage,
293 parentThreadId: v2ThreadShell.id,
294 relationshipToParent: "subagent" as const,
295 },
296 };
297 const grandchild = {
298 ...child,
299 id: ThreadId.make("review-helper"),
300 lineage: { ...child.lineage, parentThreadId: child.id },
301 };
302 const root = {
303 ...v2ThreadShell,
304 latestRunId: RunId.make("root-run"),
305 status: "completed" as const,
306 pendingBackgroundTasks: [
307 { taskId: "review", kind: "subagent" as const, childThreadId: child.id },
308 ],
309 };
310 const approval = {
311 id: RuntimeRequestId.make("cat-file"),
312 kind: "command" as const,
313 createdAt: v2Now,
314 };
315 const parent = threads.threadShellAtom({ environmentId, threadId: root.id });
316 const inputs = threads.childThreadInputsAtom({ environmentId, threadId: root.id });
317 const disposeParent = registry.mount(parent);
318 const disposeInputs = registry.mount(inputs);
319 registry.set(snapshotAtom(environmentId), {
320 ...v2ShellSnapshot,
321 threads: [root, child, grandchild],
322 });
323 expect(isThreadWorking(registry.get(parent)!)).toBe(true);
324​
325 registry.set(snapshotAtom(environmentId), {
326 ...v2ShellSnapshot,
327 threads: [root, child, { ...grandchild, pendingRuntimeRequest: approval }],
328 });
329 expect(registry.get(parent)?.pendingSubagentRequest).toBe("approval");
330 expect(isThreadWorking(registry.get(parent)!)).toBe(false);
331 const asking = registry.get(inputs);
332 expect(asking.map((thread) => thread.id)).toEqual([grandchild.id]);
333 expect(childThreadInputsTitle(asking)).toBe("Subagent needs approval");
334​
335 registry.set(snapshotAtom(environmentId), {
336 ...v2ShellSnapshot,
337 threads: [root, child, grandchild],
338 });
339 expect(registry.get(parent)?.pendingSubagentRequest).toBeNull();
340 expect(isThreadWorking(registry.get(parent)!)).toBe(true);
341 disposeInputs();
342 disposeParent();
343 registry.dispose();
344 });
345​
346 it("lists a task a linked environment runs for a listed thread under that thread", () => {
347 const { registry, threads, snapshotAtom, catalogValueAtom } = makeHarness([
348 environmentId,
349 remoteEnvironmentId,
350 ]);
351 const parent = v2ThreadShell;
352 const task = {
353 ...v2ThreadShell,
354 id: ThreadId.make("remote-task"),
355 creationSource: "mcp" as const,
356 status: "running" as const,
357 activityRunStatus: "running" as const,
358 delegatedFrom: { environmentId, threadId: parent.id, title: parent.title },
359 };
360 const helper = {
361 ...task,
362 id: ThreadId.make("remote-helper"),
363 delegatedFrom: undefined,
364 lineage: {
365 ...task.lineage,
366 parentThreadId: task.id,
367 relationshipToParent: "subagent" as const,
368 },
369 };
370 const approval = {
371 id: RuntimeRequestId.make("cat-file"),
372 kind: "command" as const,
373 createdAt: v2Now,
374 };
375 registry.set(snapshotAtom(environmentId), { ...v2ShellSnapshot, threads: [parent] });
376 registry.set(snapshotAtom(remoteEnvironmentId), {
377 ...v2ShellSnapshot,
378 threads: [task, helper],
379 });
380 const parentRef = { environmentId, threadId: parent.id };
381 const inputs = threads.childThreadInputsAtom(parentRef);
382 const disposeNavigation = registry.mount(threads.navigationThreadShellsAtom);
383 const disposeParent = registry.mount(threads.threadShellAtom(parentRef));
384 const disposeInputs = registry.mount(inputs);
385 const listed = () =>
386 registry
387 .get(threads.navigationThreadShellsAtom)
388 .map((thread) => `${thread.environmentId}/${thread.id}`);
389 expect(listed()).toEqual([`${environmentId}/${parent.id}`]);
390​
391 // The helper the task started there asks for approval: the parent here hears of it.
392 registry.set(snapshotAtom(remoteEnvironmentId), {
393 ...v2ShellSnapshot,
394 threads: [task, { ...helper, pendingRuntimeRequest: approval }],
395 });
396 expect(registry.get(threads.threadShellAtom(parentRef))?.pendingSubagentRequest).toBe(
397 "approval",
398 );
399 expect(registry.get(inputs).map((thread) => `${thread.environmentId}/${thread.id}`)).toEqual([
400 `${remoteEnvironmentId}/${helper.id}`,
401 ]);
402​
403 registry.set(snapshotAtom(remoteEnvironmentId), {
404 ...v2ShellSnapshot,
405 threads: [
406 task,
407 { ...helper, pendingRuntimeRequest: { ...approval, kind: "user_input" as const } },
408 ],
409 });
410 expect(registry.get(threads.threadShellAtom(parentRef))?.pendingSubagentRequest).toBeNull();
411 expect(registry.get(inputs).map((thread) => thread.id)).toEqual([helper.id]);
412​
413 // A secret card needs the user, so it still reaches the parent here.
414 registry.set(snapshotAtom(remoteEnvironmentId), {
415 ...v2ShellSnapshot,
416 threads: [
417 task,
418 {
419 ...helper,
420 pendingRuntimeRequest: { ...approval, kind: "user_input" as const, userOnly: true },
421 },
422 ],
423 });
424 expect(registry.get(threads.threadShellAtom(parentRef))?.pendingSubagentRequest).toBe(
425 "user_input",
426 );
427​
428 // A task whose parent is gone there is listed on its own, as is one whose
429 // parent's environment is switched off.
430 registry.set(snapshotAtom(environmentId), { ...v2ShellSnapshot, threads: [] });
431 expect(listed()).toEqual([`${remoteEnvironmentId}/${task.id}`]);
432 // Archiving the parent here leaves the task there unarchived, so it is listed on its own.
433 registry.set(snapshotAtom(environmentId), {
434 ...v2ShellSnapshot,
435 threads: [{ ...parent, archivedAt: v2Now }],
436 });
437 expect(listed()).toEqual([`${remoteEnvironmentId}/${task.id}`]);
438 registry.set(snapshotAtom(environmentId), { ...v2ShellSnapshot, threads: [parent] });
439 expect(listed()).toEqual([`${environmentId}/${parent.id}`]);
440 const catalog = registry.get(catalogValueAtom);
441 const entries = new Map(catalog.entries);
442 entries.set(environmentId, { ...entries.get(environmentId)!, enabled: false });
443 registry.set(catalogValueAtom, { ...catalog, entries });
444 expect(listed()).toEqual([`${remoteEnvironmentId}/${task.id}`]);
445 disposeInputs();
446 disposeParent();
447 disposeNavigation();
448 registry.dispose();
449 });
450​
451 it("brings an approval up a chain of tasks across three environments to its root", () => {
452 const thirdEnvironmentId = EnvironmentId.make("third-environment-v2");
453 const { registry, threads, snapshotAtom } = makeHarness([
454 environmentId,
455 remoteEnvironmentId,
456 thirdEnvironmentId,
457 ]);
458 const root = v2ThreadShell;
459 const running = {
460 creationSource: "mcp" as const,
461 status: "running" as const,
462 activityRunStatus: "running" as const,
463 };
464 const task = {
465 ...v2ThreadShell,
466 ...running,
467 id: ThreadId.make("remote-task"),
468 delegatedFrom: { environmentId, threadId: root.id, title: root.title },
469 };
470 const nestedTask = {
471 ...v2ThreadShell,
472 ...running,
473 id: ThreadId.make("third-task"),
474 delegatedFrom: { environmentId: remoteEnvironmentId, threadId: task.id, title: task.title },
475 };
476 // A task the middle one delegates back to the root's own environment.
477 const returningTask = {
478 ...nestedTask,
479 id: ThreadId.make("returning-task"),
480 };
481 const approval = {
482 id: RuntimeRequestId.make("cat-file"),
483 kind: "command" as const,
484 createdAt: v2Now,
485 };
486 registry.set(snapshotAtom(environmentId), { ...v2ShellSnapshot, threads: [root] });
487 registry.set(snapshotAtom(remoteEnvironmentId), { ...v2ShellSnapshot, threads: [task] });
488 registry.set(snapshotAtom(thirdEnvironmentId), {
489 ...v2ShellSnapshot,
490 threads: [{ ...nestedTask, pendingRuntimeRequest: approval }],
491 });
492 const rootRef = { environmentId, threadId: root.id };
493 const taskRef = { environmentId: remoteEnvironmentId, threadId: task.id };
494 const disposeRoot = registry.mount(threads.threadShellAtom(rootRef));
495 const disposeTask = registry.mount(threads.threadShellAtom(taskRef));
496 expect(registry.get(threads.threadShellAtom(taskRef))?.pendingSubagentRequest).toBe("approval");
497 expect(registry.get(threads.threadShellAtom(rootRef))?.pendingSubagentRequest).toBe("approval");
498​
499 // Once it is answered, neither stays flagged.
500 registry.set(snapshotAtom(thirdEnvironmentId), {
501 ...v2ShellSnapshot,
502 threads: [nestedTask],
503 });
504 expect(registry.get(threads.threadShellAtom(rootRef))?.pendingSubagentRequest).toBeNull();
505​
506 // Environments that delegate to each other still settle on the root's flag.
507 registry.set(snapshotAtom(environmentId), {
508 ...v2ShellSnapshot,
509 threads: [root, { ...returningTask, pendingRuntimeRequest: approval }],
510 });
511 expect(registry.get(threads.threadShellAtom(rootRef))?.pendingSubagentRequest).toBe("approval");
512 disposeTask();
513 disposeRoot();
514 registry.dispose();
515 });
516​
517 it.each([
518 { reason: "switched-off", state: { enabled: false } },
519 {
520 reason: "unsupported",
521 state: { enabled: true, unsupportedReason: "Use a compatible client." },
522 },
523 ])("leaves $reason environments out of navigation until restored", ({ state }) => {
524 const { registry, threads, catalogValueAtom } = makeHarness([
525 environmentId,
526 remoteEnvironmentId,
527 ]);
528 const dispose = registry.mount(threads.navigationThreadShellsAtom);
529 const environmentsOf = () =>
530 new Set(registry.get(threads.navigationThreadShellsAtom).map((t) => t.environmentId));
531 expect(environmentsOf()).toEqual(new Set([environmentId, remoteEnvironmentId]));
532​
533 const setRemotePaused = (paused: boolean) => {
534 const catalog = registry.get(catalogValueAtom);
535 const entries = new Map(catalog.entries);
536 entries.set(remoteEnvironmentId, {
537 ...entries.get(remoteEnvironmentId)!,
538 ...(paused ? state : { enabled: true, unsupportedReason: undefined }),
539 });
540 registry.set(catalogValueAtom, { ...catalog, entries });
541 };
542 setRemotePaused(true);
543 expect(environmentsOf()).toEqual(new Set([environmentId]));
544 setRemotePaused(false);
545 expect(environmentsOf()).toEqual(new Set([environmentId, remoteEnvironmentId]));
546 dispose();
547 registry.dispose();
548 });
549​
550 it("preserves project memberships on updates and keeps the same thread separate per environment", () => {
551 const harness = makeHarness([environmentId, remoteEnvironmentId]);
552 const otherThread = {
553 ...v2ThreadShell,
554 id: ThreadId.make("other-thread"),
555 projectId: otherProjectId,
556 };
557 const snapshot = { ...v2ShellSnapshot, threads: [v2ThreadShell, otherThread] };
558 harness.registry.set(harness.snapshotAtom(environmentId), snapshot);
559 const membershipAtom = harness.threads.environmentThreadRefsByProjectAtom(environmentId);
560 const listAtom = harness.threads.threadShellsForProjectRefsAtom([
561 { environmentId: remoteEnvironmentId, projectId: v2ThreadShell.projectId },
562 { environmentId, projectId: v2ThreadShell.projectId },
563 ]);
564 const disposeList = harness.registry.mount(listAtom);
565 try {
566 const membership = harness.registry.get(membershipAtom);
567 const before = harness.registry.get(listAtom);
568 expect(before).toHaveLength(2);
569 expect(before[0]).not.toBe(before[1]);
570 expect(before.map((thread) => thread.environmentId)).toEqual([
571 remoteEnvironmentId,
572 environmentId,
573 ]);
574​
575 harness.registry.set(harness.snapshotAtom(environmentId), {
576 ...snapshot,
577 threads: [v2ThreadShell, { ...otherThread, title: "Changed elsewhere" }],
578 });
579 expect(harness.registry.get(membershipAtom)).toBe(membership);
580 expect(harness.registry.get(listAtom)).toBe(before);
581​
582 harness.registry.set(harness.snapshotAtom(environmentId), {
583 ...snapshot,
584 threads: [{ ...v2ThreadShell, projectId: otherProjectId }, otherThread],
585 });
586 expect(harness.registry.get(listAtom)).toEqual([before[0]]);
587 harness.registry.set(harness.snapshotAtom(remoteEnvironmentId), {
588 ...v2ShellSnapshot,
589 threads: [],
590 });
591 expect(harness.registry.get(listAtom)).toEqual([]);
592 } finally {
593 disposeList();
594 harness.registry.dispose();
595 }
596 });
597​
598 it("keeps the quiet shell through streaming ticks and follows every change once the run stops", () => {
599 const harness = makeHarness();
600 const runId = RunId.make("streaming-run");
601 const running: OrchestrationV2ThreadShell = {
602 ...v2ThreadShell,
603 latestRunId: runId,
604 activeRunId: runId,
605 status: "running",
606 itemCount: 1,
607 visibleItemCount: 1,
608 };
609 const setThread = (thread: OrchestrationV2ThreadShell, sequence: number) =>
610 harness.registry.set(harness.snapshotAtom(environmentId), {
611 ...v2ShellSnapshot,
612 snapshotSequence: sequence,
613 threads: [thread],
614 });
615 const at = (seconds: number) => DateTime.add(v2Now, { seconds });
616 setThread(running, 1);
617 const ref = { environmentId, threadId: v2ThreadShell.id };
618 const quietAtom = harness.threads.quietThreadShellAtom(ref);
619 const dispose = harness.registry.mount(quietAtom);
620 try {
621 const first = harness.registry.get(quietAtom);
622 setThread({ ...running, updatedAt: at(1), itemCount: 4, visibleItemCount: 3 }, 2);
623 expect(harness.registry.get(quietAtom)).toBe(first);
624 expect(harness.registry.get(harness.threads.threadShellAtom(ref))?.itemCount).toBe(4);
625​
626 setThread({ ...running, updatedAt: at(2), itemCount: 5, title: "Renamed" }, 3);
627 const renamed = harness.registry.get(quietAtom);
628 expect(renamed?.title).toBe("Renamed");
629​
630 const stopped: OrchestrationV2ThreadShell = {
631 ...running,
632 activeRunId: null,
633 status: "completed",
634 };
635 setThread({ ...stopped, title: "Renamed", updatedAt: at(3) }, 4);
636 const completed = harness.registry.get(quietAtom);
637 expect(completed).not.toBe(renamed);
638 setThread({ ...stopped, title: "Renamed", updatedAt: at(4) }, 5);
639 expect(harness.registry.get(quietAtom)?.updatedAt).toBe(DateTime.formatIso(at(4)));
640 expect(harness.registry.get(quietAtom)).not.toBe(completed);
641 } finally {
642 dispose();
643 harness.registry.dispose();
644 }
645 });
646​
647 it("keeps the quiet shell list through streaming ticks", () => {
648 const harness = makeHarness();
649 const runId = RunId.make("streaming-list-run");
650 const running: OrchestrationV2ThreadShell = {
651 ...v2ThreadShell,
652 latestRunId: runId,
653 activeRunId: runId,
654 status: "running",
655 itemCount: 1,
656 visibleItemCount: 1,
657 };
658 const setThread = (thread: OrchestrationV2ThreadShell, sequence: number) =>
659 harness.registry.set(harness.snapshotAtom(environmentId), {
660 ...v2ShellSnapshot,
661 snapshotSequence: sequence,
662 threads: [thread],
663 });
664 setThread(running, 1);
665 const listAtom = harness.threads.quietThreadShellsAtom;
666 const dispose = harness.registry.mount(listAtom);
667 try {
668 const first = harness.registry.get(listAtom);
669 expect(first).toHaveLength(1);
670 setThread({ ...running, updatedAt: DateTime.add(v2Now, { seconds: 1 }), itemCount: 4 }, 2);
671 expect(harness.registry.get(listAtom)).toBe(first);
672​
673 setThread({ ...running, title: "Renamed" }, 3);
674 expect(harness.registry.get(listAtom)).not.toBe(first);
675 expect(harness.registry.get(listAtom)[0]?.title).toBe("Renamed");
676 } finally {
677 dispose();
678 harness.registry.dispose();
679 }
680 });
681});