1import {
2 EnvironmentId,
3 type OrchestrationV2ArchivedShellSnapshot,
4 type ScopedThreadRef,
5 ThreadId,
6} from "@t3tools/contracts";
7import * as Cause from "effect/Cause";
8import { AsyncResult, Atom, AtomRegistry } from "effect/reactivity";
9import { describe, expect, it } from "vite-plus/test";
10
11import {
12 createArchivedThreadSnapshotsAtomFamily,
13 makeArchivedThreadsEnvironmentKey,
14} from "./archivedThreads.ts";
15import { v2Project, v2ThreadShell } from "./orchestrationV2TestFixtures.ts";
16
17it("does not expose an archived snapshot failure message", () => {
18 const environmentId = EnvironmentId.make("env-sensitive");
19 const snapshotsAtom = createArchivedThreadSnapshotsAtomFamily<Error>({
20 getSnapshotAtom: () =>
21 Atom.make(
22 AsyncResult.failure<OrchestrationV2ArchivedShellSnapshot, Error>(
23 Cause.fail(new Error("credential=secret-value")),
24 ),
25 ),
26 hiddenThreadTagsAtom: () => Atom.make(null),
27 hiddenThreadKeysAtom: Atom.make((): ReadonlySet<string> => new Set()),
28 liveThreadRefsAtom: Atom.make((): ReadonlyArray<ScopedThreadRef> => []),
29 anyEnvironmentHidingAtom: Atom.make(false),
30 labelPrefix: "test:archived-thread-snapshots",
31 });
32 const registry = AtomRegistry.make();
33
34 expect(registry.get(snapshotsAtom(makeArchivedThreadsEnvironmentKey([environmentId])))).toEqual({
35 snapshots: [],
36 error: "Failed to load archived threads.",
37 isLoading: false,
38 });
39
40 registry.dispose();
41});
42
43describe("archived delegated tasks follow their parent on another machine", () => {
44 const parentEnvironmentId = EnvironmentId.make("env-parent");
45 const taskEnvironmentId = EnvironmentId.make("env-task");
46 const parentId = ThreadId.make("thread-parent");
47 const page = (
48 threads: OrchestrationV2ArchivedShellSnapshot["threads"],
49 ): OrchestrationV2ArchivedShellSnapshot => ({
50 schemaVersion: 1,
51 snapshotSequence: 1,
52 projects: [v2Project],
53 threads,
54 });
55 const taskPage = page([
56 {
57 ...v2ThreadShell,
58 id: ThreadId.make("thread-task"),
59 delegatedFrom: { environmentId: parentEnvironmentId, threadId: parentId, title: "p" },
60 },
61 ]);
62 const archivedTaggedParent = page([{ ...v2ThreadShell, id: parentId, tags: ["private"] }]);
63
64 it.each([
65 { name: "archived tagged parent", parentPage: archivedTaggedParent, live: false, tasks: 0 },
66 { name: "live visible parent", parentPage: page([]), live: true, tasks: 1 },
67 { name: "parent page not loaded yet", parentPage: null, live: false, tasks: 0 },
68 { name: "archive scoped to the task's machine", parentPage: undefined, live: false, tasks: 0 },
69 {
70 name: "only a third machine hides tags",
71 parentPage: page([]),
72 live: false,
73 tasks: 0,
74 parentTags: null,
75 },
76 ])("$name", ({ parentPage, live, tasks, parentTags = ["private"] }) => {
77 const snapshotsAtom = createArchivedThreadSnapshotsAtomFamily<Error>({
78 getSnapshotAtom: (environmentId) =>
79 Atom.make(
80 environmentId === taskEnvironmentId
81 ? AsyncResult.success(taskPage)
82 : parentPage === null || parentPage === undefined
83 ? AsyncResult.initial<OrchestrationV2ArchivedShellSnapshot, Error>(true)
84 : AsyncResult.success(parentPage),
85 ),
86 hiddenThreadTagsAtom: (environmentId) =>
87 Atom.make(environmentId === parentEnvironmentId ? parentTags : null),
88 hiddenThreadKeysAtom: Atom.make((): ReadonlySet<string> => new Set()),
89 liveThreadRefsAtom: Atom.make((): ReadonlyArray<ScopedThreadRef> =>
90 live ? [{ environmentId: parentEnvironmentId, threadId: parentId }] : [],
91 ),
92 // The parent's machine hides tags; a scoped archive learns that only from this flag.
93 anyEnvironmentHidingAtom: Atom.make(true),
94 labelPrefix: "test:archived-thread-snapshots",
95 });
96 const registry = AtomRegistry.make();
97 const { snapshots } = registry.get(
98 snapshotsAtom(
99 makeArchivedThreadsEnvironmentKey(
100 parentPage === undefined ? [taskEnvironmentId] : [parentEnvironmentId, taskEnvironmentId],
101 ),
102 ),
103 );
104 const taskSnapshot = snapshots.find((entry) => entry.environmentId === taskEnvironmentId);
105 expect(taskSnapshot?.snapshot.threads.length).toBe(tasks);
106 registry.dispose();
107 });
108});