1import { EnvironmentId } from "@t3tools/contracts";
2import { describe, expect, it } from "@effect/vitest";
3import * as Option from "effect/Option";
4
5import {
6 BearerConnectionProfile,
7 type ConnectionCatalogEntry,
8 type ConnectionRoute,
9} from "./catalog.ts";
10import {
11 BearerConnectionTarget,
12 ConnectionBlockedError,
13 ConnectionTransientError,
14 RelayConnectionTarget,
15 type SupervisorConnectionState,
16} from "./model.ts";
17import {
18 connectionCatalogDisplayUrl,
19 environmentMcpUrl,
20 connectionStatusText,
21 connectionStatusTitle,
22 presentEnvironmentConnection,
23 presentConnectionState,
24} from "./presentation.ts";
25
26const TARGET = new BearerConnectionTarget({
27 environmentId: EnvironmentId.make("environment-1"),
28 label: "Remote environment",
29 connectionId: "connection-1",
30});
31
32const ENTRY: ConnectionCatalogEntry = {
33 target: TARGET,
34 profile: Option.some(
35 new BearerConnectionProfile({
36 connectionId: TARGET.connectionId,
37 environmentId: TARGET.environmentId,
38 label: TARGET.label,
39 httpBaseUrl: "https://environment.example.test",
40 wsBaseUrl: "wss://environment.example.test",
41 }),
42 ),
43 enabled: true,
44};
45
46function supervisorState(overrides: Partial<SupervisorConnectionState>): SupervisorConnectionState {
47 return {
48 desired: true,
49 network: "online",
50 phase: "connecting",
51 stage: "preparing",
52 attempt: 1,
53 generation: 0,
54 lastFailure: null,
55 retryAt: null,
56 ...overrides,
57 };
58}
59
60describe("connection presentation", () => {
61 it("labels a blocked protocol as unsupported", () => {
62 const connection = presentConnectionState(
63 supervisorState({
64 phase: "blocked",
65 lastFailure: new ConnectionBlockedError({
66 reason: "unsupported",
67 detail: "Update your app.",
68 }),
69 }),
70 );
71 expect(connection.phase).toBe("unsupported");
72 expect(connection.error).toBe("Update your app.");
73 expect(connectionStatusText(connection)).toBe("Client not supported");
74 });
75
76 it("preserves profile display information without exposing credentials", () => {
77 expect(connectionCatalogDisplayUrl(ENTRY)).toBe("https://environment.example.test");
78 });
79
80 it("copies the MCP address of the route this device is connected over", () => {
81 const route = (connectionId: string, httpBaseUrl: string): ConnectionRoute => {
82 const target = new BearerConnectionTarget({ ...TARGET, connectionId });
83 return {
84 target,
85 profile: Option.some(
86 new BearerConnectionProfile({
87 connectionId,
88 environmentId: TARGET.environmentId,
89 label: TARGET.label,
90 httpBaseUrl,
91 wsBaseUrl: httpBaseUrl.replace(/^http/, "ws"),
92 }),
93 ),
94 };
95 };
96 const lan = route("lan", "http://192.168.4.53:3773/");
97 const tailnet = route("tailnet", "http://100.115.1.44:3773/");
98 const serve = route("serve", "https://machine.tailnet.ts.net/");
99 const entry: ConnectionCatalogEntry = {
100 ...ENTRY,
101 target: lan.target,
102 profile: lan.profile,
103 alternateRoutes: [tailnet, serve],
104 };
105
106 expect(environmentMcpUrl({ entry, connectedTarget: tailnet.target })).toBe(
107 "http://100.115.1.44:3773/mcp",
108 );
109 // Not connected: the preferred route, plain http or not.
110 expect(environmentMcpUrl({ entry })).toBe("http://192.168.4.53:3773/mcp");
111 });
112
113 it("passes over routes without an address of their own", () => {
114 const relay = new RelayConnectionTarget({
115 environmentId: TARGET.environmentId,
116 label: TARGET.label,
117 });
118 const entry: ConnectionCatalogEntry = {
119 ...ENTRY,
120 target: relay,
121 profile: Option.none(),
122 alternateRoutes: [{ target: ENTRY.target, profile: ENTRY.profile }],
123 };
124
125 // Relay discovery has not reported the tunnel address yet.
126 expect(environmentMcpUrl({ entry, connectedTarget: relay })).toBe(
127 "https://environment.example.test/mcp",
128 );
129 expect(
130 environmentMcpUrl({
131 entry,
132 connectedTarget: relay,
133 relayHttpBaseUrl: "https://tunnel.example.test",
134 }),
135 ).toBe("https://tunnel.example.test/mcp");
136 });
137
138 it("keeps the selected environment prefix in MCP addresses", () => {
139 const entry = {
140 ...ENTRY,
141 profile: Option.some(
142 new BearerConnectionProfile({
143 connectionId: TARGET.connectionId,
144 environmentId: TARGET.environmentId,
145 label: TARGET.label,
146 httpBaseUrl: "https://hub.example/prefix/environments/one/",
147 wsBaseUrl: "wss://hub.example/prefix/environments/one/",
148 }),
149 ),
150 };
151 expect(environmentMcpUrl({ entry })).toBe("https://hub.example/prefix/environments/one/mcp");
152 });
153
154 it("distinguishes initial connection, reconnect, and retry errors", () => {
155 expect(presentConnectionState(supervisorState({ phase: "connecting", attempt: 1 }))).toEqual({
156 phase: "connecting",
157 error: null,
158 traceId: null,
159 });
160 expect(
161 presentConnectionState(
162 supervisorState({
163 phase: "connecting",
164 attempt: 2,
165 lastFailure: new ConnectionTransientError({
166 reason: "transport",
167 detail: "Socket closed.",
168 traceId: "trace-previous",
169 }),
170 }),
171 ),
172 ).toEqual({
173 phase: "reconnecting",
174 error: "Socket closed.",
175 traceId: "trace-previous",
176 });
177 expect(
178 presentConnectionState(
179 supervisorState({
180 phase: "backoff",
181 attempt: 2,
182 retryAt: 1,
183 lastFailure: new ConnectionTransientError({
184 reason: "transport",
185 detail: "Disconnected.",
186 traceId: "trace-1",
187 }),
188 }),
189 ),
190 ).toEqual({
191 phase: "reconnecting",
192 error: "Disconnected.",
193 traceId: "trace-1",
194 });
195 });
196
197 it("preserves the latest failure while the next attempt is active", () => {
198 expect(
199 presentEnvironmentConnection(
200 supervisorState({
201 phase: "connecting",
202 stage: "opening",
203 attempt: 2,
204 lastFailure: new ConnectionTransientError({
205 reason: "transport",
206 detail: "Relay connection timed out.",
207 traceId: "trace-retry",
208 }),
209 }),
210 ),
211 ).toEqual({
212 phase: "reconnecting",
213 error: "Relay connection timed out.",
214 traceId: "trace-retry",
215 });
216 });
217
218 it("combines reconnect progress with the latest failure", () => {
219 const connection = {
220 phase: "reconnecting",
221 error: "Relay request timed out.",
222 traceId: "trace-retry",
223 } as const;
224 expect(connectionStatusText(connection)).toBe(
225 "Failed to connect. Reconnecting... Reason: Relay request timed out.",
226 );
227 expect(connectionStatusTitle(connection)).toBe("Failed to connect. Reconnecting...");
228 });
229
230 it("presents the supervisor's offline state without consulting shell state", () => {
231 expect(
232 presentEnvironmentConnection(
233 supervisorState({
234 network: "offline",
235 phase: "offline",
236 stage: null,
237 }),
238 ),
239 ).toEqual({
240 phase: "offline",
241 error: null,
242 traceId: null,
243 });
244 });
245
246 it("presents a connected supervisor snapshot as connected", () => {
247 expect(
248 presentEnvironmentConnection(
249 supervisorState({
250 phase: "connected",
251 stage: null,
252 generation: 1,
253 }),
254 ),
255 ).toEqual({
256 phase: "connected",
257 error: null,
258 traceId: null,
259 });
260 });
261
262 it("preserves an explicitly available environment while offline", () => {
263 expect(
264 presentEnvironmentConnection(
265 supervisorState({
266 desired: false,
267 network: "offline",
268 phase: "available",
269 stage: null,
270 attempt: 0,
271 }),
272 ),
273 ).toEqual({
274 phase: "available",
275 error: null,
276 traceId: null,
277 });
278 });
279});