1import type {
2 EnvironmentPresentation,
3 PreparedConnection,
4} from "@t3tools/client-runtime/connection";
5import { PrimaryConnectionTarget } from "@t3tools/client-runtime/connection";
6import type { ServerConfig } from "@t3tools/contracts";
7import { EnvironmentId } from "@t3tools/contracts";
8import { describe, expect, it } from "@effect/vitest";
9import * as Option from "effect/Option";
10import { Atom, AtomRegistry } from "effect/reactivity";
11
12import { createRemoteEnvironmentProjectionAtoms } from "./remote-environment-projections";
13
14const ENVIRONMENT_ID = EnvironmentId.make("environment-1");
15const OTHER_ENVIRONMENT_ID = EnvironmentId.make("environment-2");
16
17function target(environmentId: EnvironmentId, endpoint: string = environmentId) {
18 return new PrimaryConnectionTarget({
19 environmentId,
20 label: `Environment ${environmentId}`,
21 httpBaseUrl: `https://${endpoint}.example.test`,
22 wsBaseUrl: `wss://${endpoint}.example.test`,
23 });
24}
25
26function presentation(
27 environmentId: EnvironmentId,
28 endpoint: string = environmentId,
29 serverConfig: ServerConfig | null = null,
30): EnvironmentPresentation {
31 return {
32 entry: { target: target(environmentId, endpoint), profile: Option.none(), enabled: true },
33 connection: { phase: "connected", error: null, traceId: null },
34 serverConfig,
35 };
36}
37
38function prepared(
39 environmentId: EnvironmentId,
40 endpoint: string,
41 token: string,
42): PreparedConnection {
43 return {
44 environmentId,
45 label: `Environment ${environmentId}`,
46 httpBaseUrl: `https://${endpoint}.example.test`,
47 socketUrl: `wss://${endpoint}.example.test/ws?token=redacted`,
48 httpAuthorization: { _tag: "Bearer", token },
49 target: target(environmentId, endpoint),
50 };
51}
52
53function makeHarness() {
54 const presentationAtoms = Atom.family((environmentId: EnvironmentId) =>
55 Atom.make<EnvironmentPresentation | null>(presentation(environmentId)),
56 );
57 const preparedConnectionAtoms = Atom.family((_environmentId: EnvironmentId) =>
58 Atom.make<Option.Option<PreparedConnection>>(Option.none()),
59 );
60 const serverConfigAtoms = Atom.family((_environmentId: EnvironmentId) =>
61 Atom.make<ServerConfig | null>(null),
62 );
63 const projections = createRemoteEnvironmentProjectionAtoms({
64 presentationAtom: presentationAtoms,
65 preparedConnectionAtom: preparedConnectionAtoms,
66 serverConfigAtom: serverConfigAtoms,
67 });
68
69 return {
70 registry: AtomRegistry.make(),
71 presentationAtom: presentationAtoms,
72 preparedConnectionAtom: preparedConnectionAtoms,
73 serverConfigAtom: serverConfigAtoms,
74 projections,
75 };
76}
77
78describe("remote environment projections", () => {
79 it("shares each environment projection and invalidates only changed inputs", () => {
80 const harness = makeHarness();
81 const firstConsumer = Atom.make((get) =>
82 get(harness.projections.savedConnectionAtom(ENVIRONMENT_ID)),
83 );
84 const secondConsumer = Atom.make((get) =>
85 get(harness.projections.savedConnectionAtom(ENVIRONMENT_ID)),
86 );
87 const otherConsumer = Atom.make((get) =>
88 get(harness.projections.savedConnectionAtom(OTHER_ENVIRONMENT_ID)),
89 );
90 const initial = harness.registry.get(firstConsumer);
91 const otherInitial = harness.registry.get(otherConsumer);
92
93 expect(harness.registry.get(secondConsumer)).toBe(initial);
94 expect(initial).toMatchObject({
95 environmentLabel: "Environment environment-1",
96 pairingUrl: "https://environment-1.example.test",
97 displayUrl: "https://environment-1.example.test",
98 httpBaseUrl: "https://environment-1.example.test",
99 wsBaseUrl: "wss://environment-1.example.test",
100 bearerToken: null,
101 });
102
103 harness.registry.set(
104 harness.preparedConnectionAtom(ENVIRONMENT_ID),
105 Option.some(prepared(ENVIRONMENT_ID, "rotated", "rotated-token")),
106 );
107 const rotated = harness.registry.get(firstConsumer);
108
109 expect(rotated).not.toBe(initial);
110 expect(rotated).toMatchObject({
111 httpBaseUrl: "https://rotated.example.test",
112 wsBaseUrl: "wss://rotated.example.test",
113 bearerToken: "rotated-token",
114 });
115 expect(harness.registry.get(secondConsumer)).toBe(rotated);
116 expect(harness.registry.get(otherConsumer)).toBe(otherInitial);
117
118 harness.registry.set(harness.preparedConnectionAtom(ENVIRONMENT_ID), Option.none());
119 harness.registry.set(
120 harness.presentationAtom(ENVIRONMENT_ID),
121 presentation(ENVIRONMENT_ID, "catalog-updated"),
122 );
123
124 expect(harness.registry.get(firstConsumer)).toMatchObject({
125 displayUrl: "https://catalog-updated.example.test",
126 httpBaseUrl: "https://catalog-updated.example.test",
127 wsBaseUrl: "wss://catalog-updated.example.test",
128 bearerToken: null,
129 });
130 });
131
132 it("preserves saved identity across config-only updates and refreshes runtime state", () => {
133 const harness = makeHarness();
134 const savedAtom = harness.projections.savedConnectionAtom(ENVIRONMENT_ID);
135 const runtimeAtom = harness.projections.runtimeStateAtom(ENVIRONMENT_ID);
136 const savedInitial = harness.registry.get(savedAtom);
137 const runtimeInitial = harness.registry.get(runtimeAtom);
138 const config = { cwd: "/repo" } as ServerConfig;
139 const initialPresentation = harness.registry.get(harness.presentationAtom(ENVIRONMENT_ID));
140
141 harness.registry.set(
142 harness.presentationAtom(ENVIRONMENT_ID),
143 initialPresentation === null ? null : { ...initialPresentation, serverConfig: config },
144 );
145 harness.registry.set(harness.serverConfigAtom(ENVIRONMENT_ID), config);
146
147 expect(harness.registry.get(savedAtom)).toBe(savedInitial);
148 expect(harness.registry.get(runtimeAtom)).not.toBe(runtimeInitial);
149 expect(harness.registry.get(runtimeAtom)?.serverConfig).toBe(config);
150 });
151
152 it("keeps missing environments null", () => {
153 const harness = makeHarness();
154 harness.registry.set(harness.presentationAtom(ENVIRONMENT_ID), null);
155
156 expect(
157 harness.registry.get(harness.projections.savedConnectionAtom(ENVIRONMENT_ID)),
158 ).toBeNull();
159 expect(harness.registry.get(harness.projections.runtimeStateAtom(ENVIRONMENT_ID))).toBeNull();
160 });
161});