1import { describe, expect, it } from "@effect/vitest";
2import { EnvironmentId, type ServerConfig } from "@t3tools/contracts";
3import * as Option from "effect/Option";
4import { AsyncResult, Atom, AtomRegistry } from "effect/reactivity";
5
6import { AVAILABLE_CONNECTION_STATE, PrimaryConnectionTarget } from "../connection/model.ts";
7import {
8 createEnvironmentPresentationAtoms,
9 createEnvironmentSummaryAtoms,
10} from "./presentation.ts";
11import type { EnvironmentCatalogState } from "./connections.ts";
12
13const FIRST = EnvironmentId.make("first");
14const SECOND = EnvironmentId.make("second");
15function entry(environmentId: EnvironmentId, label = environmentId as string) {
16 return {
17 target: new PrimaryConnectionTarget({
18 environmentId,
19 label,
20 httpBaseUrl: "https://example.test",
21 wsBaseUrl: "wss://example.test",
22 }),
23 enabled: true,
24 profile: Option.none(),
25 };
26}
27function config(pullRequests = false, cwd = "/workspace"): ServerConfig {
28 return {
29 cwd,
30 environment: { capabilities: { pullRequests }, platform: { machine: "desktop" } },
31 } as ServerConfig;
32}
33function harness() {
34 const catalog = Atom.make<EnvironmentCatalogState>({
35 isReady: true,
36 entries: new Map([
37 [FIRST, entry(FIRST)],
38 [SECOND, entry(SECOND)],
39 ]),
40 });
41 const configs = Atom.family((_id: EnvironmentId) => Atom.make<ServerConfig | null>(config()));
42 const state = Atom.make(AsyncResult.success(AVAILABLE_CONNECTION_STATE));
43 const full = createEnvironmentPresentationAtoms({
44 catalogValueAtom: catalog,
45 stateAtom: () => state,
46 serverConfigValueAtom: configs,
47 });
48 const summaries = createEnvironmentSummaryAtoms({
49 catalogValueAtom: catalog,
50 presentationAtom: full.presentationAtom,
51 });
52 return { catalog, configs, state, full, ...summaries, registry: AtomRegistry.make() };
53}
54
55describe("environment summary subscriptions", () => {
56 it("publishes full config updates without notifying membership, labels, connections or capability consumers", () => {
57 const h = harness();
58 h.registry.get(h.full.presentationsAtom);
59 h.registry.get(h.environmentIdsAtom);
60 h.registry.get(h.identitiesAtom);
61 h.registry.get(h.environmentsAtom);
62 h.registry.get(h.pullRequestsSupportedAtom);
63 h.registry.get(h.machineByIdAtom);
64 const counts = { full: 0, ids: 0, labels: 0, connections: 0, capability: 0, machines: 0 };
65 const stops = [
66 h.registry.subscribe(h.machineByIdAtom, () => counts.machines++),
67 h.registry.subscribe(h.full.presentationsAtom, () => counts.full++),
68 h.registry.subscribe(h.environmentIdsAtom, () => counts.ids++),
69 h.registry.subscribe(h.identitiesAtom, () => counts.labels++),
70 h.registry.subscribe(h.environmentsAtom, () => counts.connections++),
71 h.registry.subscribe(h.pullRequestsSupportedAtom, () => counts.capability++),
72 ];
73 const initialIds = h.registry.get(h.environmentIdsAtom);
74 const initialLabels = h.registry.get(h.identitiesAtom);
75 try {
76 for (let i = 0; i < 20; i++) {
77 const next = config(false, `/workspace-${i}`);
78 h.registry.set(h.configs(FIRST), next);
79 expect(h.registry.get(h.full.presentationsAtom).get(FIRST)?.serverConfig).toBe(next);
80 expect(h.registry.get(h.environmentIdsAtom)).toBe(initialIds);
81 expect(h.registry.get(h.identitiesAtom)).toBe(initialLabels);
82 h.registry.get(h.environmentsAtom);
83 expect(h.registry.get(h.machineByIdAtom).get(FIRST)).toBe("desktop");
84 expect(h.registry.get(h.pullRequestsSupportedAtom)).toBe(false);
85 }
86 expect(counts).toEqual({
87 full: 20,
88 ids: 0,
89 labels: 0,
90 connections: 0,
91 capability: 0,
92 machines: 0,
93 });
94 h.registry.set(h.catalog, {
95 isReady: true,
96 entries: new Map([
97 [FIRST, entry(FIRST, "Renamed")],
98 [SECOND, entry(SECOND)],
99 ]),
100 });
101 expect(h.registry.get(h.identitiesAtom)[0]?.label).toBe("Renamed");
102 expect(h.registry.get(h.environmentIdsAtom)).toBe(initialIds);
103 expect(h.registry.get(h.environmentsAtom)[0]?.environmentLabel).toBe("Renamed");
104 expect(counts.labels).toBe(1);
105 expect(counts.ids).toBe(0);
106 } finally {
107 stops.forEach((stop) => stop());
108 h.registry.dispose();
109 }
110 });
111
112 it("keeps connected search targets stable through config refreshes and updates them on disconnect", () => {
113 const h = harness();
114 let changes = 0;
115 const stop = h.registry.subscribe(h.connectedEnvironmentIdsAtom, () => changes++);
116 try {
117 expect(h.registry.get(h.connectedEnvironmentIdsAtom)).toEqual([]);
118 h.registry.set(
119 h.state,
120 AsyncResult.success({
121 ...AVAILABLE_CONNECTION_STATE,
122 phase: "connected",
123 generation: 1,
124 }),
125 );
126 const connected = h.registry.get(h.connectedEnvironmentIdsAtom);
127 expect(connected).toEqual([FIRST, SECOND]);
128 const connectionChanges = changes;
129 expect(connectionChanges).toBeGreaterThan(0);
130 for (let index = 0; index < 20; index++) {
131 h.registry.set(h.configs(FIRST), config(false, `/workspace-${index}`));
132 expect(h.registry.get(h.connectedEnvironmentIdsAtom)).toBe(connected);
133 }
134 expect(changes).toBe(connectionChanges);
135 h.registry.set(h.state, AsyncResult.success(AVAILABLE_CONNECTION_STATE));
136 expect(h.registry.get(h.connectedEnvironmentIdsAtom)).toEqual([]);
137 expect(changes).toBeGreaterThan(connectionChanges);
138 } finally {
139 stop();
140 h.registry.dispose();
141 }
142 });
143
144 it("updates machine icons and preserves cached icons for disabled environments", () => {
145 const h = harness();
146 try {
147 expect(h.registry.get(h.machineByIdAtom).get(FIRST)).toBe("desktop");
148 h.registry.set(h.configs(FIRST), {
149 ...config(),
150 settings: { environmentIcon: "laptop" },
151 } as ServerConfig);
152 expect(h.registry.get(h.machineByIdAtom).get(FIRST)).toBe("laptop");
153 h.registry.set(h.catalog, {
154 isReady: true,
155 entries: new Map([[FIRST, { ...entry(FIRST), enabled: false }]]),
156 });
157 expect(h.registry.get(h.machineByIdAtom)).toEqual(new Map([[FIRST, "laptop"]]));
158 h.registry.set(h.configs(FIRST), null);
159 expect(h.registry.get(h.machineByIdAtom).get(FIRST)).toBe("server");
160 } finally {
161 h.registry.dispose();
162 }
163 });
164
165 it("tracks capabilities across environments, config loss and removal", () => {
166 const h = harness();
167 const stop = h.registry.subscribe(h.pullRequestsSupportedAtom, () => {});
168 try {
169 expect(h.registry.get(h.pullRequestsSupportedAtom)).toBe(false);
170 h.registry.set(h.configs(FIRST), config(true));
171 expect(h.registry.get(h.pullRequestsSupportedAtom)).toBe(true);
172 // The first true result short-circuits: the second must be read when the first stops supporting it.
173 h.registry.set(h.configs(SECOND), config(true));
174 h.registry.set(h.configs(FIRST), null);
175 expect(h.registry.get(h.pullRequestsSupportedAtom)).toBe(true);
176 h.registry.set(h.catalog, { isReady: true, entries: new Map([[FIRST, entry(FIRST)]]) });
177 expect(h.registry.get(h.pullRequestsSupportedAtom)).toBe(false);
178 expect(h.registry.get(h.environmentIdsAtom)).toEqual([FIRST]);
179 expect(h.registry.get(h.identitiesAtom)).toEqual([{ environmentId: FIRST, label: "first" }]);
180 h.registry.set(h.catalog, {
181 isReady: true,
182 entries: new Map([
183 [SECOND, entry(SECOND)],
184 [FIRST, entry(FIRST)],
185 ]),
186 });
187 expect(h.registry.get(h.environmentIdsAtom)).toEqual([SECOND, FIRST]);
188 expect(h.registry.get(h.pullRequestsSupportedAtom)).toBe(true);
189 h.registry.set(h.configs(SECOND), config(false));
190 expect(h.registry.get(h.pullRequestsSupportedAtom)).toBe(false);
191 h.registry.set(h.catalog, { isReady: true, entries: new Map() });
192 expect(h.registry.get(h.environmentIdsAtom)).toEqual([]);
193 expect(h.registry.get(h.identitiesAtom)).toEqual([]);
194 expect(h.registry.get(h.environmentsAtom)).toEqual([]);
195 } finally {
196 stop();
197 h.registry.dispose();
198 }
199 });
200});