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 type { ConnectionCatalogEntry } from "../connection/catalog.ts";
7import { AVAILABLE_CONNECTION_STATE, PrimaryConnectionTarget } from "../connection/model.ts";
8import {
9 createEnvironmentPresentationAtoms,
10 createEnvironmentSummaryAtoms,
11} from "./presentation.ts";
12import type { EnvironmentCatalogState } from "./connections.ts";
13
14const FIRST = EnvironmentId.make("first");
15const SECOND = EnvironmentId.make("second");
16function entry(environmentId: EnvironmentId, label = environmentId as string) {
17 return {
18 target: new PrimaryConnectionTarget({
19 environmentId,
20 label,
21 httpBaseUrl: "https://example.test",
22 wsBaseUrl: "wss://example.test",
23 }),
24 enabled: true,
25 profile: Option.none(),
26 };
27}
28function config(cwd = "/workspace"): ServerConfig {
29 return {
30 cwd,
31 environment: { capabilities: {}, platform: { machine: "desktop" } },
32 } as ServerConfig;
33}
34function harness() {
35 const catalog = Atom.make<EnvironmentCatalogState>({
36 isReady: true,
37 entries: new Map([
38 [FIRST, entry(FIRST)],
39 [SECOND, entry(SECOND)],
40 ]),
41 });
42 const configs = Atom.family((_id: EnvironmentId) => Atom.make<ServerConfig | null>(config()));
43 const state = Atom.make(AsyncResult.success(AVAILABLE_CONNECTION_STATE));
44 const full = createEnvironmentPresentationAtoms({
45 catalogValueAtom: catalog,
46 stateAtom: () => state,
47 serverConfigValueAtom: configs,
48 });
49 const summaries = createEnvironmentSummaryAtoms({
50 catalogValueAtom: catalog,
51 presentationAtom: full.presentationAtom,
52 });
53 return { catalog, configs, state, full, ...summaries, registry: AtomRegistry.make() };
54}
55
56describe("environment summary subscriptions", () => {
57 it("tracks loaded configs across environments, config loss and removal", () => {
58 const h = harness();
59 const stop = h.registry.subscribe(h.pullRequestsSupportedAtom, () => {});
60 try {
61 h.registry.set(h.configs(FIRST), null);
62 h.registry.set(h.configs(SECOND), null);
63 expect(h.registry.get(h.pullRequestsSupportedAtom)).toBe(false);
64 h.registry.set(h.configs(FIRST), config());
65 expect(h.registry.get(h.pullRequestsSupportedAtom)).toBe(true);
66 h.registry.set(h.configs(SECOND), config());
67 h.registry.set(h.configs(FIRST), null);
68 expect(h.registry.get(h.pullRequestsSupportedAtom)).toBe(true);
69 h.registry.set(h.catalog, { isReady: true, entries: new Map([[FIRST, entry(FIRST)]]) });
70 expect(h.registry.get(h.pullRequestsSupportedAtom)).toBe(false);
71 expect(h.registry.get(h.environmentIdsAtom)).toEqual([FIRST]);
72 expect(h.registry.get(h.identitiesAtom)).toEqual([{ environmentId: FIRST, label: "first" }]);
73 h.registry.set(h.catalog, {
74 isReady: true,
75 entries: new Map([
76 [SECOND, entry(SECOND)],
77 [FIRST, entry(FIRST)],
78 ]),
79 });
80 expect(h.registry.get(h.environmentIdsAtom)).toEqual([SECOND, FIRST]);
81 expect(h.registry.get(h.pullRequestsSupportedAtom)).toBe(true);
82 h.registry.set(h.configs(SECOND), null);
83 expect(h.registry.get(h.pullRequestsSupportedAtom)).toBe(false);
84 h.registry.set(h.catalog, { isReady: true, entries: new Map() });
85 expect(h.registry.get(h.environmentIdsAtom)).toEqual([]);
86 expect(h.registry.get(h.identitiesAtom)).toEqual([]);
87 expect(h.registry.get(h.environmentsAtom)).toEqual([]);
88 } finally {
89 stop();
90 h.registry.dispose();
91 }
92 });
93});
94
95describe("presentations with providers", () => {
96 it("leaves out disabled connections until they are re-enabled", () => {
97 const h = harness();
98 const atom = h.full.presentationsWithProvidersAtom;
99 const withSecond = (enabled: boolean): EnvironmentCatalogState => ({
100 isReady: true,
101 entries: new Map([
102 [FIRST, entry(FIRST)],
103 [SECOND, { ...entry(SECOND), enabled }],
104 ]),
105 });
106 const stop = h.registry.subscribe(atom, () => {});
107 try {
108 h.registry.set(h.configs(SECOND), config("/second"));
109 h.registry.set(h.catalog, withSecond(false));
110 expect([...h.registry.get(atom).keys()]).toEqual([FIRST]);
111 expect(h.registry.get(h.full.presentationsAtom).has(SECOND)).toBe(true);
112 h.registry.set(h.catalog, withSecond(true));
113 expect([...h.registry.get(atom).keys()]).toEqual([FIRST, SECOND]);
114 expect(h.registry.get(atom).get(SECOND)?.serverConfig?.cwd).toBe("/second");
115 } finally {
116 stop();
117 h.registry.dispose();
118 }
119 });
120
121 it("leaves out unsupported environments", () => {
122 const h = harness();
123 const atom = h.full.presentationsWithProvidersAtom;
124 const stop = h.registry.subscribe(atom, () => {});
125 try {
126 h.registry.set(h.catalog, {
127 isReady: true,
128 entries: new Map<EnvironmentId, ConnectionCatalogEntry>([
129 [FIRST, entry(FIRST)],
130 [SECOND, { ...entry(SECOND), unsupportedReason: "Server is too old." }],
131 ]),
132 });
133 expect([...h.registry.get(atom).keys()]).toEqual([FIRST]);
134 } finally {
135 stop();
136 h.registry.dispose();
137 }
138 });
139});
140
141describe("connection presentation", () => {
142 it("presents an enabled environment as connecting until its supervisor reports", () => {
143 const h = harness();
144 const phase = () => h.registry.get(h.full.presentationAtom(FIRST))?.connection.phase;
145 const stop = h.registry.subscribe(h.full.presentationAtom(FIRST), () => {});
146 try {
147 expect(phase()).toBe("connecting");
148 h.registry.set(h.catalog, {
149 isReady: true,
150 entries: new Map([[FIRST, { ...entry(FIRST), enabled: false }]]),
151 });
152 expect(phase()).toBe("available");
153 h.registry.set(h.catalog, { isReady: true, entries: new Map([[FIRST, entry(FIRST)]]) });
154 h.registry.set(h.state, AsyncResult.success({ ...AVAILABLE_CONNECTION_STATE }));
155 expect(phase()).toBe("available");
156 } finally {
157 stop();
158 h.registry.dispose();
159 }
160 });
161});