1import type { ServerConfig } from "@t3tools/contracts";
2import { EnvironmentId } from "@t3tools/contracts";
3import { describe, expect, it } from "@effect/vitest";
4import * as Option from "effect/Option";
5import * as DateTime from "effect/DateTime";
6import { Atom, AtomRegistry } from "effect/reactivity";
7
8import { PrimaryConnectionTarget } from "../connection/model.ts";
9import type { EnvironmentShellState } from "./shell.ts";
10import { createEnvironmentServerConfigsAtom, createEnvironmentShellSummaryAtom } from "./shell.ts";
11import { v2ThreadShell } from "./orchestrationV2TestFixtures.ts";
12
13const ENVIRONMENT_ID = EnvironmentId.make("environment-1");
14const OTHER_ENVIRONMENT_ID = EnvironmentId.make("environment-2");
15
16function environmentEntry(environmentId: EnvironmentId, label: string) {
17 return {
18 target: new PrimaryConnectionTarget({
19 environmentId,
20 label,
21 httpBaseUrl: `https://${environmentId}.example.test`,
22 wsBaseUrl: `wss://${environmentId}.example.test`,
23 }),
24 profile: Option.none(),
25 enabled: true,
26 };
27}
28
29function shellState(input: {
30 readonly status: EnvironmentShellState["status"];
31 readonly updatedAt?: string;
32 readonly error?: string;
33 readonly snapshotSequence?: number;
34}): EnvironmentShellState {
35 return {
36 snapshot:
37 input.updatedAt === undefined
38 ? Option.none()
39 : Option.some({
40 schemaVersion: 1,
41 snapshotSequence: input.snapshotSequence ?? 1,
42 projects: [],
43 threads: [
44 {
45 ...v2ThreadShell,
46 updatedAt: DateTime.makeUnsafe(input.updatedAt),
47 },
48 ],
49 archivedThreads: [],
50 }),
51 status: input.status,
52 error: input.error === undefined ? Option.none() : Option.some(input.error),
53 };
54}
55
56function makeHarness() {
57 const shellStateAtoms = Atom.family((environmentId: EnvironmentId) =>
58 Atom.make<EnvironmentShellState>(
59 environmentId === ENVIRONMENT_ID
60 ? shellState({
61 status: "cached",
62 updatedAt: "2026-06-01T00:00:00.000Z",
63 })
64 : shellState({
65 status: "synchronizing",
66 updatedAt: "2026-06-02T00:00:00.000Z",
67 error: "Retrying.",
68 }),
69 ),
70 );
71 const configAtoms = Atom.family((_environmentId: EnvironmentId) =>
72 Atom.make<ServerConfig | null>(null),
73 );
74 const catalogValueAtom = Atom.make({
75 isReady: true,
76 entries: new Map([
77 [ENVIRONMENT_ID, environmentEntry(ENVIRONMENT_ID, "Environment")],
78 [OTHER_ENVIRONMENT_ID, environmentEntry(OTHER_ENVIRONMENT_ID, "Other environment")],
79 ]),
80 });
81 const summaryAtom = createEnvironmentShellSummaryAtom({
82 catalogValueAtom,
83 shellStateValueAtom: shellStateAtoms,
84 });
85 const serverConfigsAtom = createEnvironmentServerConfigsAtom({
86 catalogValueAtom,
87 serverConfigValueAtom: configAtoms,
88 });
89
90 return {
91 catalogValueAtom,
92 registry: AtomRegistry.make(),
93 shellStateAtom: shellStateAtoms,
94 configAtom: configAtoms,
95 summaryAtom,
96 serverConfigsAtom,
97 };
98}
99
100describe("environment shell projections", () => {
101 it("summarizes shell state and preserves identity when only irrelevant snapshot data changes", () => {
102 const harness = makeHarness();
103 const summary = harness.registry.get(harness.summaryAtom);
104
105 expect(summary).toEqual({
106 hasSnapshot: true,
107 hasSynchronizingShell: true,
108 hasCachedShell: true,
109 hasLiveShell: false,
110 firstError: "Retrying.",
111 });
112
113 harness.registry.set(
114 harness.shellStateAtom(ENVIRONMENT_ID),
115 shellState({
116 status: "cached",
117 updatedAt: "2026-06-01T00:00:00.000Z",
118 snapshotSequence: 2,
119 }),
120 );
121
122 expect(harness.registry.get(harness.summaryAtom)).toBe(summary);
123 });
124
125 it("does not notify summary subscribers when thread timestamps advance", () => {
126 const harness = makeHarness();
127 const initial = harness.registry.get(harness.summaryAtom);
128 let changes = 0;
129 const unsubscribe = harness.registry.subscribe(harness.summaryAtom, () => changes++);
130 try {
131 for (let index = 0; index < 20; index++) {
132 harness.registry.set(
133 harness.shellStateAtom(ENVIRONMENT_ID),
134 shellState({
135 status: "cached",
136 updatedAt: `2026-07-01T00:00:${String(index).padStart(2, "0")}.000Z`,
137 snapshotSequence: index + 2,
138 }),
139 );
140 expect(harness.registry.get(harness.summaryAtom)).toBe(initial);
141 }
142 expect(changes).toBe(0);
143 } finally {
144 unsubscribe();
145 harness.registry.dispose();
146 }
147 });
148
149 it("notifies for shell availability, status, errors, and enabled environment changes", () => {
150 const harness = makeHarness();
151 let changes = 0;
152 const unsubscribe = harness.registry.subscribe(harness.summaryAtom, () => changes++);
153 const setCatalog = (entries: ReturnType<typeof environmentEntry>[]) => {
154 harness.registry.set(harness.catalogValueAtom, {
155 isReady: true,
156 entries: new Map(entries.map((entry) => [entry.target.environmentId, entry])),
157 });
158 return harness.registry.get(harness.summaryAtom);
159 };
160 try {
161 expect(setCatalog([])).toEqual({
162 hasSnapshot: false,
163 hasSynchronizingShell: false,
164 hasCachedShell: false,
165 hasLiveShell: false,
166 firstError: null,
167 });
168 expect(setCatalog([environmentEntry(ENVIRONMENT_ID, "Environment")])).toMatchObject({
169 hasSnapshot: true,
170 hasCachedShell: true,
171 });
172 harness.registry.set(harness.shellStateAtom(ENVIRONMENT_ID), shellState({ status: "empty" }));
173 expect(harness.registry.get(harness.summaryAtom)).toMatchObject({
174 hasSnapshot: false,
175 hasCachedShell: false,
176 });
177 harness.registry.set(
178 harness.shellStateAtom(ENVIRONMENT_ID),
179 shellState({ status: "synchronizing", error: "Retrying." }),
180 );
181 expect(harness.registry.get(harness.summaryAtom)).toMatchObject({
182 hasSnapshot: false,
183 hasSynchronizingShell: true,
184 firstError: "Retrying.",
185 });
186 harness.registry.set(
187 harness.shellStateAtom(ENVIRONMENT_ID),
188 shellState({ status: "live", updatedAt: "2026-07-01T00:00:00.000Z" }),
189 );
190 expect(harness.registry.get(harness.summaryAtom)).toMatchObject({
191 hasSnapshot: true,
192 hasSynchronizingShell: false,
193 hasLiveShell: true,
194 firstError: null,
195 });
196 harness.registry.set(
197 harness.shellStateAtom(ENVIRONMENT_ID),
198 shellState({
199 status: "live",
200 updatedAt: "2026-07-01T00:00:00.000Z",
201 error: "Disconnected.",
202 }),
203 );
204 expect(harness.registry.get(harness.summaryAtom).firstError).toBe("Disconnected.");
205 harness.registry.set(
206 harness.shellStateAtom(ENVIRONMENT_ID),
207 shellState({ status: "live", updatedAt: "2026-07-01T00:00:00.000Z" }),
208 );
209 expect(harness.registry.get(harness.summaryAtom).firstError).toBeNull();
210 expect(
211 setCatalog([{ ...environmentEntry(ENVIRONMENT_ID, "Environment"), enabled: false }]),
212 ).toMatchObject({ hasSnapshot: false, hasLiveShell: false });
213 expect(changes).toBe(8);
214 } finally {
215 unsubscribe();
216 harness.registry.dispose();
217 }
218 });
219
220 it("preserves server-config map identity until a config reference changes", () => {
221 const harness = makeHarness();
222 const empty = harness.registry.get(harness.serverConfigsAtom);
223 const config = { cwd: "/repo" } as ServerConfig;
224
225 harness.registry.set(harness.configAtom(ENVIRONMENT_ID), config);
226 const withConfig = harness.registry.get(harness.serverConfigsAtom);
227
228 expect(withConfig).not.toBe(empty);
229 expect(withConfig.get(ENVIRONMENT_ID)).toBe(config);
230
231 harness.registry.set(harness.configAtom(ENVIRONMENT_ID), config);
232 expect(harness.registry.get(harness.serverConfigsAtom)).toBe(withConfig);
233 });
234});