| 1 | import { describe, expect, it } from "@effect/vitest"; |
| 2 | import { EnvironmentId, type ServerConfig } from "@t3tools/contracts"; |
| 3 | import * as Option from "effect/Option"; |
| 4 | import { AsyncResult, Atom, AtomRegistry } from "effect/reactivity"; |
| 5 | |
| 6 | import { AVAILABLE_CONNECTION_STATE, PrimaryConnectionTarget } from "../connection/model.ts"; |
| 7 | import { |
| 8 | createEnvironmentPresentationAtoms, |
| 9 | createEnvironmentSummaryAtoms, |
| 10 | } from "./presentation.ts"; |
| 11 | import type { EnvironmentCatalogState } from "./connections.ts"; |
| 12 | |
| 13 | const FIRST = EnvironmentId.make("first"); |
| 14 | const SECOND = EnvironmentId.make("second"); |
| 15 | function 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 | } |
| 27 | function config(pullRequests = false, cwd = "/workspace"): ServerConfig { |
| 28 | return { |
| 29 | cwd, |
| 30 | environment: { capabilities: { pullRequests }, platform: { machine: "desktop" } }, |
| 31 | } as ServerConfig; |
| 32 | } |
| 33 | function 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 | |
| 55 | describe("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 | }); |