1import {
2 type DesktopSshEnvironmentTarget,
3 EnvironmentId,
4 type OrchestrationV2ShellSnapshot,
5 ORCHESTRATION_PROTOCOL_VERSION,
6 type ExecutionEnvironmentDescriptor,
7 type ServerConfig,
8} from "@t3tools/contracts";
9import { describe, expect, it } from "@effect/vitest";
10import * as Context from "effect/Context";
11import * as Deferred from "effect/Deferred";
12import * as Effect from "effect/Effect";
13import * as Exit from "effect/Exit";
14import * as Fiber from "effect/Fiber";
15import * as Layer from "effect/Layer";
16import * as Option from "effect/Option";
17import * as Ref from "effect/Ref";
18import * as Result from "effect/Result";
19import * as Scheduler from "effect/Scheduler";
20import * as Scope from "effect/Scope";
21import * as Stream from "effect/Stream";
22import * as SubscriptionRef from "effect/SubscriptionRef";
23
24import * as ClientCapabilities from "../platform/capabilities.ts";
25import * as TokenStore from "../authorization/tokenStore.ts";
26import {
27 BearerConnectionCredential,
28 BearerConnectionProfile,
29 BearerConnectionRegistration,
30 type ConnectionCatalogEntry,
31 type ConnectionRegistration,
32 type ConnectionRoute,
33 PrimaryConnectionRegistration,
34 RelayConnectionRegistration,
35 SshConnectionProfile,
36 SshConnectionRegistration,
37 type ConnectionCredential,
38 type ConnectionProfile,
39} from "./catalog.ts";
40import * as Connectivity from "./connectivity.ts";
41import * as ConnectionCredentialStore from "./credentialStore.ts";
42import * as ConnectionDriver from "./driver.ts";
43import type { RouteCheck } from "./driver.ts";
44import { connectionRouteId, connectionRouteKind } from "./routes.ts";
45import {
46 ConnectionTransientError,
47 ConnectionBlockedError,
48 BearerConnectionTarget,
49 PrimaryConnectionTarget,
50 RelayConnectionTarget,
51 SshConnectionTarget,
52 type ConnectionTarget,
53 type PreparedConnection,
54 type SupervisorConnectionState,
55} from "./model.ts";
56import * as Persistence from "../platform/persistence.ts";
57import * as ConnectionProfileStore from "./profileStore.ts";
58import * as EnvironmentRegistry from "./registry.ts";
59import {
60 GitHubRoutingPermissions,
61 type StoredGitHubRoutingPermission,
62 makeGitHubRoutingPermissions,
63} from "./githubRoutingPermissions.ts";
64import * as RpcSession from "../rpc/session.ts";
65import * as EnvironmentSupervisor from "./supervisor.ts";
66import * as ConnectionWakeups from "./wakeups.ts";
67import { watchDiscoveredCompatibility } from "./layer.ts";
68import * as RelayEnvironmentDiscovery from "../relay/discovery.ts";
69import type { RelayEnvironmentStatusResponse } from "@t3tools/contracts/relay";
70import { runDesktopCommitWithReconnectObserver } from "../state/server.ts";
71import { v2ShellSnapshot } from "../state/orchestrationV2TestFixtures.ts";
72
73const TARGET = new PrimaryConnectionTarget({
74 environmentId: EnvironmentId.make("environment-1"),
75 label: "Test environment",
76 httpBaseUrl: "https://environment.example.test",
77 wsBaseUrl: "wss://environment.example.test",
78});
79const SECOND_TARGET = new PrimaryConnectionTarget({
80 environmentId: EnvironmentId.make("environment-2"),
81 label: "Second environment",
82 httpBaseUrl: "https://environment-2.example.test",
83 wsBaseUrl: "wss://environment-2.example.test",
84});
85
86const PREPARED: PreparedConnection = {
87 environmentId: TARGET.environmentId,
88 label: TARGET.label,
89 httpBaseUrl: TARGET.httpBaseUrl,
90 socketUrl: "wss://environment.example.test/ws",
91 httpAuthorization: null,
92 target: TARGET,
93};
94
95const RELAY_TARGET = new RelayConnectionTarget({
96 environmentId: EnvironmentId.make("environment-relay"),
97 label: "Relay environment",
98});
99const SECOND_RELAY_TARGET = new RelayConnectionTarget({
100 environmentId: EnvironmentId.make("environment-relay-2"),
101 label: "Second relay environment",
102});
103
104const BEARER_TARGET = new BearerConnectionTarget({
105 environmentId: EnvironmentId.make("environment-bearer"),
106 label: "Bearer environment",
107 connectionId: "bearer-connection",
108});
109const BEARER_PROFILE = new BearerConnectionProfile({
110 connectionId: BEARER_TARGET.connectionId,
111 environmentId: BEARER_TARGET.environmentId,
112 label: BEARER_TARGET.label,
113 httpBaseUrl: "https://bearer.example.test",
114 wsBaseUrl: "wss://bearer.example.test",
115});
116const BEARER_CREDENTIAL = new BearerConnectionCredential({
117 token: "bearer-token",
118});
119
120const SSH_TARGET: DesktopSshEnvironmentTarget = {
121 alias: "test",
122 hostname: "test.example.test",
123 username: "developer",
124 port: 22,
125};
126const SSH_CONNECTION = new SshConnectionTarget({
127 environmentId: EnvironmentId.make("environment-ssh"),
128 label: "SSH environment",
129 connectionId: "ssh-connection",
130});
131const SSH_PROFILE = new SshConnectionProfile({
132 connectionId: SSH_CONNECTION.connectionId,
133 environmentId: SSH_CONNECTION.environmentId,
134 label: SSH_CONNECTION.label,
135 target: SSH_TARGET,
136});
137
138const CACHED_SNAPSHOT: OrchestrationV2ShellSnapshot = {
139 ...v2ShellSnapshot,
140 snapshotSequence: 1,
141};
142
143interface SessionControl {
144 readonly closed: Deferred.Deferred<never, ConnectionTransientError>;
145}
146
147const makeHarness = Effect.fn("TestEnvironmentRegistry.makeHarness")(function* (
148 initialTargets: ReadonlyArray<ConnectionTarget>,
149 initialProfiles: ReadonlyArray<ConnectionProfile> = [],
150 initialCredentials: ReadonlyArray<readonly [string, ConnectionCredential]> = [],
151 options?: {
152 readonly prepareError?: ConnectionBlockedError;
153 readonly beforeSessionConnect?: (environmentId: EnvironmentId) => Effect.Effect<void>;
154 readonly beforeRegistrationRegister?: (
155 registration: ConnectionRegistration,
156 ) => Effect.Effect<void, Persistence.ConnectionPersistenceError>;
157 readonly beforeRegistrationRemove?: (
158 environmentId: EnvironmentId,
159 ) => Effect.Effect<void, Persistence.ConnectionPersistenceError>;
160 readonly checkRoute?: (route: ConnectionRoute) => RouteCheck;
161 readonly prepareRoute?: (target: ConnectionTarget) => ConnectionBlockedError | undefined;
162 /** Runs before preparing a route; may suspend to hold an attempt in flight. */
163 readonly beforePrepareRoute?: (
164 target: ConnectionTarget,
165 ) => Effect.Effect<ConnectionBlockedError | undefined>;
166 readonly initialDisabled?: ReadonlyArray<EnvironmentId>;
167 /** Direct addresses the connected server reports. */
168 readonly directEndpoints?: ServerConfig["directEndpoints"];
169 },
170) {
171 const storedTargets = yield* Ref.make<ReadonlyArray<ConnectionTarget>>(initialTargets);
172 const shellCache = yield* Ref.make(new Map([[TARGET.environmentId, CACHED_SNAPSHOT]]));
173 const cacheClears = yield* Ref.make<ReadonlyArray<EnvironmentId>>([]);
174 const ownedDataClears = yield* Ref.make<ReadonlyArray<EnvironmentId>>([]);
175 const sessions = yield* Ref.make<ReadonlyArray<SessionControl>>([]);
176 const releasedSessions = yield* Ref.make(0);
177 const storedProfiles = yield* Ref.make(
178 new Map(initialProfiles.map((profile) => [profile.connectionId, profile])),
179 );
180 const profileReadCount = yield* Ref.make(0);
181 const storedCredentials = yield* Ref.make(new Map(initialCredentials));
182 const storedRemoteTokens = yield* Ref.make(
183 new Map([
184 [
185 SSH_CONNECTION.environmentId,
186 new TokenStore.RemoteDpopAccessToken({
187 environmentId: SSH_CONNECTION.environmentId,
188 label: SSH_CONNECTION.label,
189 endpoint: {
190 httpBaseUrl: "https://ssh.example.test",
191 wsBaseUrl: "wss://ssh.example.test",
192 providerKind: "cloudflare_tunnel",
193 },
194 accessToken: "cached-token",
195 expiresAtEpochMs: Number.MAX_SAFE_INTEGER,
196 dpopThumbprint: "thumbprint",
197 }),
198 ],
199 ]),
200 );
201 const disconnectedSshTargets = yield* Ref.make<ReadonlyArray<DesktopSshEnvironmentTarget>>([]);
202
203 const storedDisabled = yield* Ref.make<ReadonlySet<EnvironmentId>>(
204 new Set(options?.initialDisabled ?? []),
205 );
206 const targetStore = Persistence.ConnectionTargetStore.of({
207 list: Ref.get(storedTargets),
208 listDisabled: Ref.get(storedDisabled).pipe(Effect.map((ids) => [...ids])),
209 });
210 const registrationStore = Persistence.ConnectionRegistrationStore.of({
211 register: (registration, routes) =>
212 Effect.gen(function* () {
213 yield* options?.beforeRegistrationRegister?.(registration) ?? Effect.void;
214 yield* Ref.update(storedTargets, (current) =>
215 replaceRoutes(current, registration.target.environmentId, routes),
216 );
217 switch (registration._tag) {
218 case "RelayConnectionRegistration":
219 return;
220 case "BearerConnectionRegistration":
221 yield* Ref.update(storedProfiles, (current) => {
222 const next = new Map(current);
223 next.set(registration.profile.connectionId, registration.profile);
224 return next;
225 });
226 yield* Ref.update(storedCredentials, (current) => {
227 const next = new Map(current);
228 next.set(registration.target.connectionId, registration.credential);
229 return next;
230 });
231 return;
232 case "SshConnectionRegistration":
233 yield* Ref.update(storedProfiles, (current) => {
234 const next = new Map(current);
235 next.set(registration.profile.connectionId, registration.profile);
236 return next;
237 });
238 }
239 }),
240 setRoutes: (environmentId, routes) =>
241 Ref.update(storedTargets, (current) => replaceRoutes(current, environmentId, routes)),
242 remove: (environmentId) =>
243 Effect.gen(function* () {
244 yield* options?.beforeRegistrationRemove?.(environmentId) ?? Effect.void;
245 const removed = (yield* Ref.get(storedTargets)).filter(
246 (target) => target.environmentId === environmentId,
247 );
248 yield* Ref.update(storedTargets, (current) =>
249 current.filter((target) => target.environmentId !== environmentId),
250 );
251 for (const target of removed) {
252 if (target._tag === "BearerConnectionTarget" || target._tag === "SshConnectionTarget") {
253 yield* Ref.update(storedProfiles, (current) => {
254 const next = new Map(current);
255 next.delete(target.connectionId);
256 return next;
257 });
258 yield* Ref.update(storedCredentials, (current) => {
259 const next = new Map(current);
260 next.delete(target.connectionId);
261 return next;
262 });
263 }
264 }
265 yield* Ref.update(storedRemoteTokens, (current) => {
266 const next = new Map(current);
267 next.delete(environmentId);
268 return next;
269 });
270 }),
271 setEnabled: (environmentId, enabled) =>
272 Ref.update(storedDisabled, (current) => {
273 const next = new Set(current);
274 if (enabled) {
275 next.delete(environmentId);
276 } else {
277 next.add(environmentId);
278 }
279 return next;
280 }),
281 });
282 const cacheStore = Persistence.EnvironmentCacheStore.of({
283 loadShell: (environmentId) =>
284 Ref.get(shellCache).pipe(
285 Effect.map((cache) => Option.fromUndefinedOr(cache.get(environmentId))),
286 ),
287 saveShell: (environmentId, snapshot) =>
288 Ref.update(shellCache, (current) => {
289 const next = new Map(current);
290 next.set(environmentId, snapshot);
291 return next;
292 }),
293 loadThread: (_environmentId, _threadId) => Effect.succeedNone,
294 saveThread: (_environmentId, _thread) => Effect.void,
295 removeThread: (_environmentId, _threadId) => Effect.void,
296 loadServerConfig: () => Effect.succeedNone,
297 saveServerConfig: () => Effect.void,
298 loadVcsRefs: () => Effect.succeedNone,
299 saveVcsRefs: () => Effect.void,
300 removeVcsRefs: () => Effect.void,
301 clearVcsRefs: () => Effect.void,
302 clear: (environmentId) =>
303 Ref.update(shellCache, (current) => {
304 const next = new Map(current);
305 next.delete(environmentId);
306 return next;
307 }).pipe(
308 Effect.andThen(
309 Ref.update(cacheClears, (environmentIds) => [...environmentIds, environmentId]),
310 ),
311 ),
312 });
313 const ownedDataCleanup = Persistence.EnvironmentOwnedDataCleanup.of({
314 clear: (environmentId) =>
315 Ref.update(ownedDataClears, (environmentIds) => [...environmentIds, environmentId]),
316 });
317 const networkStatus = yield* SubscriptionRef.make<"unknown" | "offline" | "online">("online");
318 const connectivity = Connectivity.Connectivity.of({
319 status: SubscriptionRef.get(networkStatus),
320 changes: SubscriptionRef.changes(networkStatus),
321 });
322 const profileStore = ConnectionProfileStore.ConnectionProfileStore.of({
323 get: (connectionId) =>
324 Ref.update(profileReadCount, (count) => count + 1).pipe(
325 Effect.andThen(Ref.get(storedProfiles)),
326 Effect.map((current) => Option.fromUndefinedOr(current.get(connectionId))),
327 ),
328 put: (profile) =>
329 Ref.update(storedProfiles, (current) => {
330 const next = new Map(current);
331 next.set(profile.connectionId, profile);
332 return next;
333 }),
334 remove: (connectionId) =>
335 Ref.update(storedProfiles, (current) => {
336 const next = new Map(current);
337 next.delete(connectionId);
338 return next;
339 }),
340 });
341 const credentialStore = ConnectionCredentialStore.ConnectionCredentialStore.of({
342 get: (connectionId) =>
343 Ref.get(storedCredentials).pipe(
344 Effect.map((current) => Option.fromUndefinedOr(current.get(connectionId))),
345 ),
346 put: (connectionId, credential) =>
347 Ref.update(storedCredentials, (current) => {
348 const next = new Map(current);
349 next.set(connectionId, credential);
350 return next;
351 }),
352 remove: (connectionId) =>
353 Ref.update(storedCredentials, (current) => {
354 const next = new Map(current);
355 next.delete(connectionId);
356 return next;
357 }),
358 });
359 const tokenStore = TokenStore.RemoteDpopAccessTokenStore.of({
360 get: (environmentId) =>
361 Ref.get(storedRemoteTokens).pipe(
362 Effect.map((current) => Option.fromUndefinedOr(current.get(environmentId))),
363 ),
364 put: (token) =>
365 Ref.update(storedRemoteTokens, (current) => {
366 const next = new Map(current);
367 next.set(token.environmentId, token);
368 return next;
369 }),
370 remove: (environmentId) =>
371 Ref.update(storedRemoteTokens, (current) => {
372 const next = new Map(current);
373 next.delete(environmentId);
374 return next;
375 }),
376 });
377 const sshGateway = ClientCapabilities.SshEnvironmentGateway.of({
378 provision: () => Effect.die(new Error("SSH provisioning is not used.")),
379 prepare: () => Effect.die(new Error("SSH preparation is not used.")),
380 disconnect: (target) => Ref.update(disconnectedSshTargets, (current) => [...current, target]),
381 });
382 const connectedRoutes = yield* Ref.make<ReadonlyArray<string>>([]);
383 const checkRoute = (route: ConnectionRoute) =>
384 Effect.succeed(options?.checkRoute?.(route) ?? "unchecked");
385 const connectRoute = (entry: ConnectionCatalogEntry, route: ConnectionRoute) =>
386 Effect.gen(function* () {
387 const target = route.target;
388 const credential =
389 target._tag === "BearerConnectionTarget"
390 ? yield* credentialStore.get(target.connectionId)
391 : Option.none();
392 const prepared: PreparedConnection = {
393 ...PREPARED,
394 environmentId: target.environmentId,
395 label: target.label,
396 httpAuthorization:
397 Option.isSome(credential) && credential.value._tag === "BearerConnectionCredential"
398 ? { _tag: "Bearer", token: credential.value.token }
399 : null,
400 target,
401 };
402 if (options?.prepareError) return yield* options.prepareError;
403 const heldError = options?.beforePrepareRoute
404 ? yield* options.beforePrepareRoute(target)
405 : undefined;
406 if (heldError !== undefined) return yield* heldError;
407 const routeError = options?.prepareRoute?.(target);
408 if (routeError !== undefined) return yield* routeError;
409 yield* Ref.update(connectedRoutes, (current) => [...current, connectionRouteId(target)]);
410 yield* options?.beforeSessionConnect?.(target.environmentId) ?? Effect.void;
411 const closed = yield* Deferred.make<never, ConnectionTransientError>();
412 yield* Ref.update(sessions, (current) => [...current, { closed }]);
413 const session = yield* Effect.acquireRelease(
414 Effect.succeed({
415 client: {} as RpcSession.RpcSession["client"],
416 initialConfig:
417 options?.directEndpoints === undefined
418 ? Effect.die(new Error("Config is not used by registry tests."))
419 : Effect.succeed({ directEndpoints: options.directEndpoints } as ServerConfig),
420 subscribeServerConfig: () =>
421 Stream.die(new Error("Config is not used by registry tests.")),
422 ready: Effect.void,
423 probe: Effect.void,
424 closed: Deferred.await(closed),
425 } satisfies RpcSession.RpcSession),
426 () => Ref.update(releasedSessions, (count) => count + 1),
427 );
428 yield* session.ready;
429 return { prepared, session };
430 });
431 const driver = ConnectionDriver.ConnectionDriver.of({
432 connect: (entry, reportProgress) =>
433 reportProgress({ stage: "preparing" }).pipe(
434 Effect.andThen(
435 ConnectionDriver.connectOverRoutes(entry, checkRoute, (route) =>
436 connectRoute(entry, route),
437 ),
438 ),
439 ),
440 checkRoute: (_entry, route) => checkRoute(route),
441 preflight: (_entry, route) =>
442 checkRoute(route).pipe(Effect.map((check) => check === "answered")),
443 });
444
445 const layerCache = Layer.succeed(Persistence.EnvironmentCacheStore, cacheStore);
446 const layer = EnvironmentRegistry.layer.pipe(
447 Layer.provide(
448 Layer.mergeAll(
449 Layer.succeed(Persistence.ConnectionTargetStore, targetStore),
450 Layer.succeed(Persistence.ConnectionRegistrationStore, registrationStore),
451 Layer.succeed(ConnectionProfileStore.ConnectionProfileStore, profileStore),
452 Layer.succeed(ConnectionCredentialStore.ConnectionCredentialStore, credentialStore),
453 Layer.succeed(TokenStore.RemoteDpopAccessTokenStore, tokenStore),
454 Layer.succeed(ClientCapabilities.SshEnvironmentGateway, sshGateway),
455 Layer.succeed(Connectivity.Connectivity, connectivity),
456 Layer.succeed(
457 ConnectionWakeups.ConnectionWakeups,
458 ConnectionWakeups.ConnectionWakeups.of({ changes: Stream.never }),
459 ),
460 Layer.succeed(ConnectionDriver.ConnectionDriver, driver),
461 layerCache,
462 Layer.succeed(Persistence.EnvironmentOwnedDataCleanup, ownedDataCleanup),
463 ),
464 ),
465 );
466
467 return {
468 layer,
469 storedTargets,
470 shellCache,
471 cacheClears,
472 ownedDataClears,
473 sessions,
474 releasedSessions,
475 storedProfiles,
476 profileReadCount,
477 storedCredentials,
478 storedRemoteTokens,
479 storedDisabled,
480 disconnectedSshTargets,
481 networkStatus,
482 connectedRoutes,
483 };
484});
485
486function routesOf(targets: ReadonlyArray<ConnectionTarget>, environmentId: EnvironmentId) {
487 return targets.filter((target) => target.environmentId === environmentId);
488}
489
490function hasRoutes(targets: ReadonlyArray<ConnectionTarget>, environmentId: EnvironmentId) {
491 return routesOf(targets, environmentId).length > 0;
492}
493
494function replaceRoutes(
495 current: ReadonlyArray<ConnectionTarget>,
496 environmentId: EnvironmentId,
497 routes: ReadonlyArray<ConnectionTarget>,
498): ReadonlyArray<ConnectionTarget> {
499 const firstIndex = current.findIndex((target) => target.environmentId === environmentId);
500 const others = current.filter((target) => target.environmentId !== environmentId);
501 const insertAt =
502 firstIndex === -1
503 ? others.length
504 : current.slice(0, firstIndex).filter((target) => target.environmentId !== environmentId)
505 .length;
506 return [...others.slice(0, insertAt), ...routes, ...others.slice(insertAt)];
507}
508
509function awaitConnectionState(
510 registry: EnvironmentRegistry.EnvironmentRegistry["Service"],
511 environmentId: EnvironmentId,
512 predicate: (state: SupervisorConnectionState) => boolean,
513) {
514 return Effect.gen(function* () {
515 const current = yield* registry.state(environmentId);
516 if (predicate(current)) {
517 return current;
518 }
519 return yield* registry
520 .stateChanges(environmentId)
521 .pipe(Stream.filter(predicate), Stream.runHead, Effect.map(Option.getOrThrow));
522 });
523}
524
525describe("EnvironmentRegistry", () => {
526 it.effect("replays connected state when arming a desktop commit observer", () =>
527 Effect.gen(function* () {
528 const harness = yield* makeHarness([TARGET]);
529
530 yield* Effect.gen(function* () {
531 const registry = yield* EnvironmentRegistry.EnvironmentRegistry;
532 yield* registry.start;
533 yield* awaitConnectionState(
534 registry,
535 TARGET.environmentId,
536 (state) => state.phase === "connected",
537 );
538
539 const commits = yield* Ref.make(0);
540 const result = yield* runDesktopCommitWithReconnectObserver(
541 registry.stateChanges(TARGET.environmentId),
542 Ref.update(commits, (count) => count + 1).pipe(
543 Effect.andThen(Effect.fail("commit refused")),
544 ),
545 ).pipe(Effect.flip, Effect.timeout("1 second"));
546
547 expect(result).toBe("commit refused");
548 expect(yield* Ref.get(commits)).toBe(1);
549 }).pipe(Effect.provide(harness.layer), Effect.scoped);
550 }),
551 );
552
553 it.effect("does not acquire a session after the registry scope has already closed", () =>
554 Effect.gen(function* () {
555 const harness = yield* makeHarness([TARGET]);
556 const registryScope = yield* Scope.make();
557 const context = yield* Layer.build(harness.layer).pipe(Scope.provide(registryScope));
558 const registry = Context.get(context, EnvironmentRegistry.EnvironmentRegistry);
559 const dispatcher = new Scheduler.MixedScheduler("sync", () => () => {}).makeDispatcher();
560 const scheduler: Scheduler.Scheduler = {
561 executionMode: "sync",
562 shouldYield: () => false,
563 makeDispatcher: () => dispatcher,
564 };
565
566 yield* Scope.close(registryScope, Exit.void);
567 yield* registry.start.pipe(Effect.provideService(Scheduler.Scheduler, scheduler));
568 dispatcher.flush();
569
570 expect(yield* Ref.get(harness.sessions)).toHaveLength(0);
571 expect(yield* Ref.get(harness.releasedSessions)).toBe(0);
572 }),
573 );
574
575 it.effect("hydrates connection profiles into catalog entries", () =>
576 Effect.gen(function* () {
577 const harness = yield* makeHarness([SSH_CONNECTION], [SSH_PROFILE]);
578
579 yield* Effect.gen(function* () {
580 const registry = yield* EnvironmentRegistry.EnvironmentRegistry;
581 const entry = (yield* SubscriptionRef.get(registry.entries)).get(
582 SSH_CONNECTION.environmentId,
583 );
584
585 expect(entry?.target).toEqual(SSH_CONNECTION);
586 expect(Option.getOrThrow(entry?.profile ?? Option.none())).toEqual(SSH_PROFILE);
587 }).pipe(Effect.provide(harness.layer), Effect.scoped);
588 }),
589 );
590
591 it.effect("publishes network status changes independently of connection state", () =>
592 Effect.gen(function* () {
593 const harness = yield* makeHarness([]);
594
595 yield* Effect.gen(function* () {
596 const registry = yield* EnvironmentRegistry.EnvironmentRegistry;
597 const offline = yield* Effect.forkChild(
598 SubscriptionRef.changes(registry.networkStatus).pipe(
599 Stream.filter((status) => status === "offline"),
600 Stream.runHead,
601 Effect.map(Option.getOrThrow),
602 ),
603 );
604
605 yield* SubscriptionRef.set(harness.networkStatus, "offline");
606
607 expect(yield* Fiber.join(offline)).toBe("offline");
608 expect(yield* SubscriptionRef.get(registry.networkStatus)).toBe("offline");
609 }).pipe(Effect.provide(harness.layer), Effect.scoped);
610 }),
611 );
612
613 it.effect("starts persisted environments independently", () =>
614 Effect.gen(function* () {
615 const bothLoadsStarted = yield* Deferred.make<void>();
616 const releaseLoads = yield* Deferred.make<void>();
617 const loadCount = yield* Ref.make(0);
618 const harness = yield* makeHarness([TARGET, SECOND_TARGET], [], [], {
619 beforeSessionConnect: () =>
620 Ref.updateAndGet(loadCount, (count) => count + 1).pipe(
621 Effect.tap((count) =>
622 count === 2 ? Deferred.succeed(bothLoadsStarted, undefined) : Effect.void,
623 ),
624 Effect.andThen(Deferred.await(releaseLoads)),
625 ),
626 });
627
628 yield* Effect.gen(function* () {
629 const registry = yield* EnvironmentRegistry.EnvironmentRegistry;
630 const start = yield* Effect.forkChild(registry.start);
631
632 yield* Deferred.await(bothLoadsStarted).pipe(Effect.timeout("1 second"));
633 yield* Deferred.succeed(releaseLoads, undefined);
634 yield* Fiber.join(start);
635
636 expect(yield* Ref.get(loadCount)).toBe(2);
637 }).pipe(Effect.provide(harness.layer), Effect.scoped);
638 }),
639 );
640
641 it.effect("exposes the current RPC generation to late query subscribers", () =>
642 Effect.gen(function* () {
643 const harness = yield* makeHarness([TARGET]);
644 yield* Effect.gen(function* () {
645 const registry = yield* EnvironmentRegistry.EnvironmentRegistry;
646 yield* registry.start;
647 yield* awaitConnectionState(
648 registry,
649 TARGET.environmentId,
650 (state) => state.phase === "connected",
651 );
652
653 const generation = yield* registry
654 .runStream(
655 TARGET.environmentId,
656 Stream.unwrap(
657 EnvironmentSupervisor.EnvironmentSupervisor.pipe(
658 Effect.map((supervisor) =>
659 Stream.concat(
660 Stream.fromEffect(SubscriptionRef.get(supervisor.state)),
661 SubscriptionRef.changes(supervisor.state),
662 ).pipe(
663 Stream.filterMap((state) =>
664 state.phase === "connected"
665 ? Result.succeed(state.generation)
666 : Result.failVoid,
667 ),
668 Stream.changes,
669 ),
670 ),
671 ),
672 ),
673 )
674 .pipe(Stream.runHead, Effect.map(Option.getOrThrow));
675
676 expect(generation).toBe(1);
677 }).pipe(Effect.provide(harness.layer), Effect.scoped);
678 }),
679 );
680
681 it.effect("preserves cached data on connection failure and clears it on explicit removal", () =>
682 Effect.gen(function* () {
683 const harness = yield* makeHarness([TARGET]);
684 yield* Effect.gen(function* () {
685 const registry = yield* EnvironmentRegistry.EnvironmentRegistry;
686 yield* registry.start;
687 yield* awaitConnectionState(
688 registry,
689 TARGET.environmentId,
690 (state) => state.phase === "connected",
691 );
692 const controls = yield* Ref.get(harness.sessions);
693 expect(controls).toHaveLength(1);
694 const active = controls[0];
695 expect(active).toBeDefined();
696 expect((yield* Ref.get(harness.shellCache)).get(TARGET.environmentId)).toEqual(
697 CACHED_SNAPSHOT,
698 );
699
700 const retryFiber = yield* Effect.forkChild(
701 awaitConnectionState(
702 registry,
703 TARGET.environmentId,
704 (state) => state.phase === "backoff",
705 ),
706 );
707 yield* Effect.yieldNow;
708 yield* Deferred.fail(
709 active!.closed,
710 new ConnectionTransientError({
711 reason: "transport",
712 detail: "Disconnected.",
713 }),
714 );
715 yield* Fiber.join(retryFiber);
716 expect((yield* Ref.get(harness.shellCache)).get(TARGET.environmentId)).toEqual(
717 CACHED_SNAPSHOT,
718 );
719
720 yield* registry.remove(TARGET.environmentId);
721 expect(hasRoutes(yield* Ref.get(harness.storedTargets), TARGET.environmentId)).toBe(false);
722 expect((yield* Ref.get(harness.shellCache)).has(TARGET.environmentId)).toBe(false);
723 expect(yield* Ref.get(harness.cacheClears)).toEqual([TARGET.environmentId]);
724 expect((yield* SubscriptionRef.get(registry.entries)).has(TARGET.environmentId)).toBe(
725 false,
726 );
727 }).pipe(Effect.provide(harness.layer));
728 }),
729 );
730
731 it.effect("persists and starts a newly registered environment", () =>
732 Effect.gen(function* () {
733 const harness = yield* makeHarness([]);
734
735 yield* Effect.gen(function* () {
736 const registry = yield* EnvironmentRegistry.EnvironmentRegistry;
737 yield* registry.register(new RelayConnectionRegistration({ target: RELAY_TARGET }));
738 yield* awaitConnectionState(
739 registry,
740 RELAY_TARGET.environmentId,
741 (state) => state.phase === "connected",
742 );
743
744 expect(routesOf(yield* Ref.get(harness.storedTargets), RELAY_TARGET.environmentId)).toEqual(
745 [RELAY_TARGET],
746 );
747 expect(yield* Ref.get(harness.sessions)).toHaveLength(1);
748 }).pipe(Effect.provide(harness.layer));
749 }),
750 );
751
752 it.effect("only a fresh health check for the rejected environment unlocks it", () =>
753 Effect.gen(function* () {
754 const harness = yield* makeHarness([RELAY_TARGET, SECOND_RELAY_TARGET], [], [], {
755 initialDisabled: [RELAY_TARGET.environmentId],
756 });
757 const descriptor = (environmentId: EnvironmentId): ExecutionEnvironmentDescriptor => ({
758 environmentId,
759 label: "Server",
760 platform: { os: "linux", arch: "x64" },
761 serverVersion: "1.0.0",
762 orchestrationProtocolVersion: ORCHESTRATION_PROTOCOL_VERSION,
763 capabilities: { repositoryIdentity: true },
764 });
765 const discovered = (
766 value: ExecutionEnvironmentDescriptor,
767 checkedAt = "2026-09-15T00:00:00Z",
768 ) => {
769 const environment = {
770 environmentId: value.environmentId,
771 label: value.label,
772 endpoint: {
773 httpBaseUrl: "https://relay.example.test",
774 wsBaseUrl: "wss://relay.example.test",
775 providerKind: "manual" as const,
776 },
777 linkedAt: "2026-09-15T00:00:00Z",
778 };
779 const status: RelayEnvironmentStatusResponse = {
780 environmentId: value.environmentId,
781 endpoint: environment.endpoint,
782 status: "online",
783 checkedAt,
784 descriptor: value,
785 };
786 return {
787 environment,
788 availability: "online" as const,
789 status: Option.some(status),
790 error: Option.none(),
791 };
792 };
793 const original = discovered(descriptor(RELAY_TARGET.environmentId));
794 const discoveryState =
795 yield* SubscriptionRef.make<RelayEnvironmentDiscovery.RelayEnvironmentDiscoveryState>({
796 ...RelayEnvironmentDiscovery.EMPTY_RELAY_ENVIRONMENT_DISCOVERY_STATE,
797 environments: new Map([[RELAY_TARGET.environmentId, original]]),
798 });
799 const initial = yield* Deferred.make<void>();
800 const unrelated = yield* Deferred.make<void>();
801 const replayed = yield* Deferred.make<void>();
802 const refreshed = yield* Deferred.make<void>();
803 let firstEnvironmentCalls = 0;
804 let secondEnvironmentCalls = 0;
805 yield* Effect.gen(function* () {
806 const registry = yield* EnvironmentRegistry.EnvironmentRegistry;
807 yield* watchDiscoveredCompatibility().pipe(
808 Effect.provideService(EnvironmentRegistry.EnvironmentRegistry, {
809 ...registry,
810 setCompatibility: (environmentId, error) =>
811 registry.setCompatibility(environmentId, error).pipe(
812 Effect.andThen(
813 Effect.gen(function* () {
814 if (environmentId === RELAY_TARGET.environmentId) {
815 firstEnvironmentCalls += 1;
816 yield* Deferred.succeed(
817 firstEnvironmentCalls === 1 ? initial : refreshed,
818 undefined,
819 );
820 } else {
821 secondEnvironmentCalls += 1;
822 yield* Deferred.succeed(
823 secondEnvironmentCalls === 1 ? unrelated : replayed,
824 undefined,
825 );
826 }
827 }),
828 ),
829 ),
830 }),
831 Effect.provideService(
832 RelayEnvironmentDiscovery.RelayEnvironmentDiscovery,
833 RelayEnvironmentDiscovery.RelayEnvironmentDiscovery.of({
834 state: discoveryState,
835 refresh: Effect.void,
836 }),
837 ),
838 Effect.forkScoped,
839 );
840 yield* Deferred.await(initial);
841 const error = new ConnectionBlockedError({
842 reason: "unsupported",
843 detail: "Socket discovered a newer protocol.",
844 });
845 yield* registry.setCompatibility(RELAY_TARGET.environmentId, error);
846 yield* SubscriptionRef.update(discoveryState, (state) => ({
847 ...state,
848 environments: new Map(state.environments).set(
849 SECOND_RELAY_TARGET.environmentId,
850 discovered(descriptor(SECOND_RELAY_TARGET.environmentId)),
851 ),
852 }));
853 yield* Deferred.await(unrelated);
854 expect(
855 (yield* SubscriptionRef.get(registry.entries)).get(RELAY_TARGET.environmentId)
856 ?.unsupportedReason,
857 ).toBe(error.message);
858 yield* SubscriptionRef.update(discoveryState, (state) => ({
859 ...state,
860 refreshing: true,
861 environments: new Map(),
862 }));
863 yield* SubscriptionRef.update(discoveryState, (state) => ({
864 ...state,
865 refreshing: false,
866 environments: new Map([
867 [RELAY_TARGET.environmentId, discovered(descriptor(RELAY_TARGET.environmentId))],
868 [
869 SECOND_RELAY_TARGET.environmentId,
870 discovered(descriptor(SECOND_RELAY_TARGET.environmentId), "2026-09-15T00:01:00Z"),
871 ],
872 ]),
873 }));
874 yield* Deferred.await(replayed);
875 expect(
876 (yield* SubscriptionRef.get(registry.entries)).get(RELAY_TARGET.environmentId)
877 ?.unsupportedReason,
878 ).toBe(error.message);
879 yield* SubscriptionRef.update(discoveryState, (state) => ({
880 ...state,
881 environments: new Map(state.environments).set(
882 RELAY_TARGET.environmentId,
883 discovered(descriptor(RELAY_TARGET.environmentId), "2026-09-15T00:02:00Z"),
884 ),
885 }));
886 yield* Deferred.await(refreshed);
887 expect(
888 (yield* SubscriptionRef.get(registry.entries)).get(RELAY_TARGET.environmentId),
889 ).toMatchObject({ enabled: false });
890 expect(
891 (yield* SubscriptionRef.get(registry.entries)).get(RELAY_TARGET.environmentId)
892 ?.unsupportedReason,
893 ).toBeUndefined();
894 }).pipe(Effect.provide(harness.layer), Effect.scoped);
895 }),
896 );
897
898 it.effect("discovery leaves direct connections that share an environment id alone", () =>
899 Effect.gen(function* () {
900 const harness = yield* makeHarness([TARGET, RELAY_TARGET]);
901 const discovered = (environmentId: EnvironmentId) => {
902 const endpoint = {
903 httpBaseUrl: "https://relay.example.test",
904 wsBaseUrl: "wss://relay.example.test",
905 providerKind: "manual" as const,
906 };
907 return {
908 environment: {
909 environmentId,
910 label: "Preview server",
911 endpoint,
912 linkedAt: "2026-09-25T00:00:00Z",
913 },
914 availability: "online" as const,
915 status: Option.some<RelayEnvironmentStatusResponse>({
916 environmentId,
917 endpoint,
918 status: "online",
919 checkedAt: "2026-09-25T00:00:00Z",
920 descriptor: {
921 environmentId,
922 label: "Preview server",
923 platform: { os: "darwin", arch: "arm64" },
924 serverVersion: "2.0.0",
925 orchestrationProtocolVersion: ORCHESTRATION_PROTOCOL_VERSION + 1,
926 capabilities: { repositoryIdentity: true },
927 },
928 }),
929 error: Option.none(),
930 };
931 };
932 yield* Effect.gen(function* () {
933 const registry = yield* EnvironmentRegistry.EnvironmentRegistry;
934 yield* registry.start;
935 yield* awaitConnectionState(
936 registry,
937 TARGET.environmentId,
938 (state) => state.phase === "connected",
939 );
940 const relayChecked = yield* Deferred.make<void>();
941 yield* watchDiscoveredCompatibility().pipe(
942 Effect.provideService(EnvironmentRegistry.EnvironmentRegistry, {
943 ...registry,
944 setCompatibility: (environmentId, error) =>
945 registry
946 .setCompatibility(environmentId, error)
947 .pipe(
948 Effect.andThen(
949 environmentId === RELAY_TARGET.environmentId
950 ? Deferred.succeed(relayChecked, undefined)
951 : Effect.void,
952 ),
953 ),
954 }),
955 Effect.provideService(
956 RelayEnvironmentDiscovery.RelayEnvironmentDiscovery,
957 RelayEnvironmentDiscovery.RelayEnvironmentDiscovery.of({
958 state:
959 yield* SubscriptionRef.make<RelayEnvironmentDiscovery.RelayEnvironmentDiscoveryState>(
960 {
961 ...RelayEnvironmentDiscovery.EMPTY_RELAY_ENVIRONMENT_DISCOVERY_STATE,
962 environments: new Map([
963 [TARGET.environmentId, discovered(TARGET.environmentId)],
964 [RELAY_TARGET.environmentId, discovered(RELAY_TARGET.environmentId)],
965 ]),
966 },
967 ),
968 refresh: Effect.void,
969 }),
970 ),
971 Effect.forkScoped,
972 );
973 yield* Deferred.await(relayChecked);
974
975 const entries = yield* SubscriptionRef.get(registry.entries);
976 expect(entries.get(RELAY_TARGET.environmentId)).toMatchObject({ enabled: false });
977 expect(entries.get(TARGET.environmentId)).toMatchObject({ enabled: true });
978 expect(entries.get(TARGET.environmentId)?.unsupportedReason).toBeUndefined();
979 expect((yield* registry.state(TARGET.environmentId)).phase).toBe("connected");
980 }).pipe(Effect.provide(harness.layer), Effect.scoped);
981 }),
982 );
983
984 it.effect("discovery keeps unsupported environments off until compatibility changes", () =>
985 Effect.gen(function* () {
986 const harness = yield* makeHarness([RELAY_TARGET], [], [], {
987 initialDisabled: [RELAY_TARGET.environmentId],
988 });
989 yield* Effect.gen(function* () {
990 const registry = yield* EnvironmentRegistry.EnvironmentRegistry;
991 yield* registry.start;
992 const error = new ConnectionBlockedError({
993 reason: "unsupported",
994 detail: "Use a compatible client.",
995 });
996 yield* registry.setCompatibility(RELAY_TARGET.environmentId, error);
997 const entry = (yield* SubscriptionRef.get(registry.entries)).get(
998 RELAY_TARGET.environmentId,
999 );
1000 expect(entry).toMatchObject({ enabled: false, unsupportedReason: error.message });
1001 expect(
1002 yield* Effect.flip(registry.setEnabled(RELAY_TARGET.environmentId, true)),
1003 ).toMatchObject({ reason: "unsupported" });
1004 expect(yield* Ref.get(harness.sessions)).toHaveLength(0);
1005 yield* registry.setCompatibility(RELAY_TARGET.environmentId, null);
1006 expect(
1007 (yield* SubscriptionRef.get(registry.entries)).get(RELAY_TARGET.environmentId)?.enabled,
1008 ).toBe(false);
1009 yield* registry.setEnabled(RELAY_TARGET.environmentId, true);
1010 yield* awaitConnectionState(
1011 registry,
1012 RELAY_TARGET.environmentId,
1013 (state) => state.phase === "connected",
1014 );
1015 }).pipe(Effect.provide(harness.layer));
1016 }),
1017 );
1018
1019 it.effect("a socket preflight rejection persists the connection as switched off", () =>
1020 Effect.gen(function* () {
1021 const error = new ConnectionBlockedError({
1022 reason: "unsupported",
1023 detail: "Use a compatible client.",
1024 });
1025 const harness = yield* makeHarness([RELAY_TARGET], [], [], { prepareError: error });
1026 yield* Effect.gen(function* () {
1027 const registry = yield* EnvironmentRegistry.EnvironmentRegistry;
1028 yield* registry.start;
1029 yield* SubscriptionRef.changes(registry.entries).pipe(
1030 Stream.filter((entries) => entries.get(RELAY_TARGET.environmentId)?.enabled === false),
1031 Stream.take(1),
1032 Stream.runDrain,
1033 );
1034 expect((yield* Ref.get(harness.storedDisabled)).has(RELAY_TARGET.environmentId)).toBe(true);
1035 expect(
1036 (yield* SubscriptionRef.get(registry.entries)).get(RELAY_TARGET.environmentId)
1037 ?.unsupportedReason,
1038 ).toBe(error.message);
1039 expect(yield* Ref.get(harness.sessions)).toHaveLength(0);
1040 }).pipe(Effect.provide(harness.layer));
1041 }),
1042 );
1043
1044 it.effect("switching an environment off disconnects it and persists the flag", () =>
1045 Effect.gen(function* () {
1046 const harness = yield* makeHarness([RELAY_TARGET]);
1047
1048 yield* Effect.gen(function* () {
1049 const registry = yield* EnvironmentRegistry.EnvironmentRegistry;
1050 yield* registry.start;
1051 yield* awaitConnectionState(
1052 registry,
1053 RELAY_TARGET.environmentId,
1054 (state) => state.phase === "connected",
1055 );
1056
1057 yield* registry.setEnabled(RELAY_TARGET.environmentId, false);
1058 yield* awaitConnectionState(
1059 registry,
1060 RELAY_TARGET.environmentId,
1061 (state) => state.phase === "available",
1062 );
1063
1064 const entry = (yield* SubscriptionRef.get(registry.entries)).get(
1065 RELAY_TARGET.environmentId,
1066 );
1067 expect(entry?.enabled).toBe(false);
1068 expect((yield* Ref.get(harness.storedDisabled)).has(RELAY_TARGET.environmentId)).toBe(true);
1069 expect(hasRoutes(yield* Ref.get(harness.storedTargets), RELAY_TARGET.environmentId)).toBe(
1070 true,
1071 );
1072 expect(yield* Ref.get(harness.releasedSessions)).toBe(1);
1073
1074 yield* registry.setEnabled(RELAY_TARGET.environmentId, true);
1075 yield* awaitConnectionState(
1076 registry,
1077 RELAY_TARGET.environmentId,
1078 (state) => state.phase === "connected",
1079 );
1080 expect((yield* Ref.get(harness.storedDisabled)).has(RELAY_TARGET.environmentId)).toBe(
1081 false,
1082 );
1083 }).pipe(Effect.provide(harness.layer));
1084 }),
1085 );
1086
1087 it.effect("re-registering a switched-off environment keeps it off", () =>
1088 Effect.gen(function* () {
1089 const harness = yield* makeHarness([RELAY_TARGET], [], [], {
1090 initialDisabled: [RELAY_TARGET.environmentId],
1091 });
1092
1093 yield* Effect.gen(function* () {
1094 const registry = yield* EnvironmentRegistry.EnvironmentRegistry;
1095 yield* registry.start;
1096 yield* registry.register(
1097 new RelayConnectionRegistration({
1098 target: new RelayConnectionTarget({ ...RELAY_TARGET, label: "Renamed" }),
1099 }),
1100 );
1101 yield* Effect.yieldNow;
1102
1103 const entry = (yield* SubscriptionRef.get(registry.entries)).get(
1104 RELAY_TARGET.environmentId,
1105 );
1106 expect(entry?.target.label).toBe("Renamed");
1107 expect(entry?.enabled).toBe(false);
1108 expect(yield* Ref.get(harness.sessions)).toHaveLength(0);
1109 }).pipe(Effect.provide(harness.layer));
1110 }),
1111 );
1112
1113 it.effect("switching an SSH environment off tears down its managed backend", () =>
1114 Effect.gen(function* () {
1115 const harness = yield* makeHarness([SSH_CONNECTION], [SSH_PROFILE]);
1116
1117 yield* Effect.gen(function* () {
1118 const registry = yield* EnvironmentRegistry.EnvironmentRegistry;
1119 yield* registry.start;
1120 yield* awaitConnectionState(
1121 registry,
1122 SSH_CONNECTION.environmentId,
1123 (state) => state.phase === "connected",
1124 );
1125
1126 yield* registry.setEnabled(SSH_CONNECTION.environmentId, false);
1127 yield* awaitConnectionState(
1128 registry,
1129 SSH_CONNECTION.environmentId,
1130 (state) => state.phase === "available",
1131 );
1132
1133 expect(yield* Ref.get(harness.disconnectedSshTargets)).toEqual([SSH_TARGET]);
1134 expect(hasRoutes(yield* Ref.get(harness.storedTargets), SSH_CONNECTION.environmentId)).toBe(
1135 true,
1136 );
1137 }).pipe(Effect.provide(harness.layer));
1138 }),
1139 );
1140
1141 it.effect("does not connect a persisted environment that was switched off", () =>
1142 Effect.gen(function* () {
1143 const harness = yield* makeHarness([RELAY_TARGET], [], [], {
1144 initialDisabled: [RELAY_TARGET.environmentId],
1145 });
1146
1147 yield* Effect.gen(function* () {
1148 const registry = yield* EnvironmentRegistry.EnvironmentRegistry;
1149 yield* registry.start;
1150 yield* Effect.yieldNow;
1151
1152 const entry = (yield* SubscriptionRef.get(registry.entries)).get(
1153 RELAY_TARGET.environmentId,
1154 );
1155 expect(entry?.enabled).toBe(false);
1156 expect((yield* registry.state(RELAY_TARGET.environmentId)).phase).toBe("available");
1157 expect(yield* Ref.get(harness.sessions)).toHaveLength(0);
1158 }).pipe(Effect.provide(harness.layer));
1159 }),
1160 );
1161
1162 it.effect("moves durable streams to a replacement supervisor", () =>
1163 Effect.gen(function* () {
1164 const replacement = new RelayConnectionTarget({
1165 environmentId: RELAY_TARGET.environmentId,
1166 label: "Replacement relay environment",
1167 });
1168 const harness = yield* makeHarness([RELAY_TARGET]);
1169
1170 yield* Effect.gen(function* () {
1171 const registry = yield* EnvironmentRegistry.EnvironmentRegistry;
1172 const firstObserved = yield* Deferred.make<void>();
1173 const secondObserved = yield* Deferred.make<void>();
1174 const labels = yield* Ref.make<ReadonlyArray<string>>([]);
1175 yield* registry.start;
1176 yield* awaitConnectionState(
1177 registry,
1178 RELAY_TARGET.environmentId,
1179 (state) => state.phase === "connected",
1180 );
1181
1182 const subscription = yield* Effect.forkChild(
1183 registry
1184 .followStream(
1185 RELAY_TARGET.environmentId,
1186 Stream.unwrap(
1187 EnvironmentSupervisor.EnvironmentSupervisor.pipe(
1188 Effect.map((supervisor) =>
1189 Stream.concat(Stream.succeed(supervisor.target.label), Stream.never),
1190 ),
1191 ),
1192 ),
1193 )
1194 .pipe(
1195 Stream.tap((label) =>
1196 Ref.updateAndGet(labels, (current) => [...current, label]).pipe(
1197 Effect.flatMap((current) =>
1198 current.length === 1
1199 ? Deferred.succeed(firstObserved, undefined)
1200 : Deferred.succeed(secondObserved, undefined),
1201 ),
1202 ),
1203 ),
1204 Stream.runDrain,
1205 ),
1206 );
1207
1208 yield* Deferred.await(firstObserved).pipe(Effect.timeout("1 second"));
1209 yield* registry.register(new RelayConnectionRegistration({ target: replacement }));
1210 yield* Deferred.await(secondObserved).pipe(Effect.timeout("1 second"));
1211 yield* Fiber.interrupt(subscription);
1212
1213 expect(yield* Ref.get(labels)).toEqual([RELAY_TARGET.label, replacement.label]);
1214 }).pipe(Effect.provide(harness.layer), Effect.scoped);
1215 }),
1216 );
1217
1218 it.effect("moves prepared credential observers when re-pairing an unchanged environment", () =>
1219 Effect.gen(function* () {
1220 const replacementCredential = new BearerConnectionCredential({ token: "replacement-token" });
1221 const harness = yield* makeHarness(
1222 [BEARER_TARGET],
1223 [BEARER_PROFILE],
1224 [[BEARER_TARGET.connectionId, BEARER_CREDENTIAL]],
1225 );
1226
1227 yield* Effect.gen(function* () {
1228 const registry = yield* EnvironmentRegistry.EnvironmentRegistry;
1229 const initialObserved = yield* Deferred.make<void>();
1230 const replacementObserved = yield* Deferred.make<void>();
1231 const tokens = yield* Ref.make<ReadonlyArray<string>>([]);
1232 const followedSupervisors = yield* Ref.make(0);
1233 yield* registry.start;
1234
1235 const subscription = yield* Effect.forkChild(
1236 registry
1237 .followStream(
1238 BEARER_TARGET.environmentId,
1239 Stream.unwrap(
1240 Effect.gen(function* () {
1241 const supervisor = yield* EnvironmentSupervisor.EnvironmentSupervisor;
1242 yield* Ref.update(followedSupervisors, (count) => count + 1);
1243 return SubscriptionRef.changes(supervisor.prepared);
1244 }),
1245 ),
1246 )
1247 .pipe(
1248 Stream.filterMap((prepared) =>
1249 Option.isSome(prepared) && prepared.value.httpAuthorization?._tag === "Bearer"
1250 ? Result.succeed(prepared.value.httpAuthorization.token)
1251 : Result.failVoid,
1252 ),
1253 Stream.changes,
1254 Stream.runForEach((token) =>
1255 Effect.gen(function* () {
1256 yield* Ref.update(tokens, (current) => [...current, token]);
1257 yield* Deferred.succeed(
1258 token === replacementCredential.token ? replacementObserved : initialObserved,
1259 undefined,
1260 );
1261 }),
1262 ),
1263 ),
1264 );
1265
1266 yield* Deferred.await(initialObserved);
1267 yield* registry.register(new RelayConnectionRegistration({ target: RELAY_TARGET }));
1268 yield* awaitConnectionState(
1269 registry,
1270 RELAY_TARGET.environmentId,
1271 (state) => state.phase === "connected",
1272 );
1273 yield* registry.register(
1274 new BearerConnectionRegistration({
1275 target: new BearerConnectionTarget({ ...BEARER_TARGET }),
1276 profile: new BearerConnectionProfile({ ...BEARER_PROFILE }),
1277 credential: replacementCredential,
1278 }),
1279 );
1280 yield* Deferred.await(replacementObserved);
1281 yield* Fiber.interrupt(subscription);
1282
1283 expect(yield* Ref.get(tokens)).toEqual([
1284 BEARER_CREDENTIAL.token,
1285 replacementCredential.token,
1286 ]);
1287 expect(yield* Ref.get(followedSupervisors)).toBe(2);
1288 }).pipe(Effect.provide(harness.layer), Effect.scoped);
1289 }),
1290 );
1291
1292 it.effect("ignores retry signals for environments that are no longer registered", () =>
1293 Effect.gen(function* () {
1294 const harness = yield* makeHarness([]);
1295
1296 yield* Effect.gen(function* () {
1297 const registry = yield* EnvironmentRegistry.EnvironmentRegistry;
1298 yield* registry.retryNow(EnvironmentId.make("removed-environment"));
1299 }).pipe(Effect.provide(harness.layer), Effect.scoped);
1300 }),
1301 );
1302
1303 it.effect("removes all relay-owned data without touching non-cloud connections", () =>
1304 Effect.gen(function* () {
1305 const harness = yield* makeHarness(
1306 [RELAY_TARGET, SECOND_RELAY_TARGET, BEARER_TARGET],
1307 [BEARER_PROFILE],
1308 [[BEARER_TARGET.connectionId, BEARER_CREDENTIAL]],
1309 );
1310
1311 yield* Effect.gen(function* () {
1312 const registry = yield* EnvironmentRegistry.EnvironmentRegistry;
1313 yield* registry.removeRelayEnvironments();
1314
1315 const targets = yield* Ref.get(harness.storedTargets);
1316 expect(hasRoutes(targets, RELAY_TARGET.environmentId)).toBe(false);
1317 expect(hasRoutes(targets, SECOND_RELAY_TARGET.environmentId)).toBe(false);
1318 expect(routesOf(targets, BEARER_TARGET.environmentId)).toEqual([BEARER_TARGET]);
1319 expect(yield* Ref.get(harness.cacheClears)).toEqual(
1320 expect.arrayContaining([RELAY_TARGET.environmentId, SECOND_RELAY_TARGET.environmentId]),
1321 );
1322 expect(yield* Ref.get(harness.ownedDataClears)).toEqual(
1323 expect.arrayContaining([RELAY_TARGET.environmentId, SECOND_RELAY_TARGET.environmentId]),
1324 );
1325 expect(
1326 (yield* SubscriptionRef.get(registry.entries)).has(BEARER_TARGET.environmentId),
1327 ).toBe(true);
1328 }).pipe(Effect.provide(harness.layer), Effect.scoped);
1329 }),
1330 );
1331
1332 it.effect("keeps the runtime registered when durable removal fails", () =>
1333 Effect.gen(function* () {
1334 const harness = yield* makeHarness([RELAY_TARGET], [], [], {
1335 beforeRegistrationRemove: () =>
1336 Effect.fail(
1337 new Persistence.ConnectionPersistenceError({
1338 operation: "remove-connection",
1339 message: "Storage is unavailable.",
1340 }),
1341 ),
1342 });
1343
1344 yield* Effect.gen(function* () {
1345 const registry = yield* EnvironmentRegistry.EnvironmentRegistry;
1346 yield* registry.start;
1347 yield* awaitConnectionState(
1348 registry,
1349 RELAY_TARGET.environmentId,
1350 (state) => state.phase === "connected",
1351 );
1352
1353 const error = yield* Effect.flip(registry.removeRelayEnvironments());
1354
1355 expect(error._tag).toBe("ConnectionPersistenceError");
1356 expect(yield* Ref.get(harness.releasedSessions)).toBe(0);
1357 expect((yield* SubscriptionRef.get(registry.entries)).has(RELAY_TARGET.environmentId)).toBe(
1358 true,
1359 );
1360 expect(hasRoutes(yield* Ref.get(harness.storedTargets), RELAY_TARGET.environmentId)).toBe(
1361 true,
1362 );
1363 expect(yield* Ref.get(harness.cacheClears)).toEqual([]);
1364 expect(yield* Ref.get(harness.ownedDataClears)).toEqual([]);
1365 }).pipe(Effect.provide(harness.layer), Effect.scoped);
1366 }),
1367 );
1368
1369 it.effect("a replaced platform bearer wakes a supervisor blocked on the old one", () =>
1370 Effect.gen(function* () {
1371 let storedCredentials: Ref.Ref<Map<string, ConnectionCredential>> | undefined;
1372 const harness = yield* makeHarness([], [], [], {
1373 // Authentication fails while the old bearer is stored, as the server
1374 // would answer once that session was revoked.
1375 beforePrepareRoute: (target) =>
1376 target._tag !== "BearerConnectionTarget" || storedCredentials === undefined
1377 ? Effect.succeed(undefined)
1378 : Ref.get(storedCredentials).pipe(
1379 Effect.map((current) => {
1380 const credential = current.get(target.connectionId);
1381 return credential?._tag === "BearerConnectionCredential" &&
1382 credential.token === "old-bearer"
1383 ? new ConnectionBlockedError({ reason: "authentication", detail: "revoked" })
1384 : undefined;
1385 }),
1386 ),
1387 });
1388 storedCredentials = harness.storedCredentials;
1389
1390 yield* Effect.gen(function* () {
1391 const registry = yield* EnvironmentRegistry.EnvironmentRegistry;
1392 const registration = (token: string) =>
1393 new BearerConnectionRegistration({
1394 target: BEARER_TARGET,
1395 profile: BEARER_PROFILE,
1396 credential: new BearerConnectionCredential({ token }),
1397 });
1398 yield* registry.reconcilePlatform([registration("old-bearer")]);
1399 yield* awaitConnectionState(
1400 registry,
1401 BEARER_TARGET.environmentId,
1402 (state) => state.phase === "blocked",
1403 );
1404
1405 yield* registry.reconcilePlatform([registration("new-bearer")]);
1406
1407 yield* awaitConnectionState(
1408 registry,
1409 BEARER_TARGET.environmentId,
1410 (state) => state.phase === "connected",
1411 );
1412 }).pipe(Effect.provide(harness.layer), Effect.scoped);
1413 }),
1414 );
1415
1416 // The desktop rotates the bootstrap token, so the renderer exchanges it again
1417 // and the server revokes the old bearer while the socket stays open. HTTP
1418 // reads such as the session's scopes must move to the new bearer (#13324).
1419 it.effect("a replaced platform bearer reaches a connected session's HTTP requests", () =>
1420 Effect.gen(function* () {
1421 const harness = yield* makeHarness([]);
1422
1423 yield* Effect.gen(function* () {
1424 const registry = yield* EnvironmentRegistry.EnvironmentRegistry;
1425 const registration = (token: string) =>
1426 new BearerConnectionRegistration({
1427 target: BEARER_TARGET,
1428 profile: BEARER_PROFILE,
1429 credential: new BearerConnectionCredential({ token }),
1430 });
1431 const preparedToken = registry.run(
1432 BEARER_TARGET.environmentId,
1433 EnvironmentSupervisor.EnvironmentSupervisor.pipe(
1434 Effect.flatMap((supervisor) => SubscriptionRef.get(supervisor.prepared)),
1435 Effect.map((prepared) =>
1436 Option.isSome(prepared) && prepared.value.httpAuthorization?._tag === "Bearer"
1437 ? prepared.value.httpAuthorization.token
1438 : null,
1439 ),
1440 ),
1441 );
1442 yield* registry.reconcilePlatform([registration("old-bearer")]);
1443 yield* awaitConnectionState(
1444 registry,
1445 BEARER_TARGET.environmentId,
1446 (state) => state.phase === "connected",
1447 );
1448 expect(yield* preparedToken).toBe("old-bearer");
1449
1450 yield* registry.reconcilePlatform([registration("new-bearer")]);
1451
1452 expect(yield* preparedToken).toBe("new-bearer");
1453 expect(yield* Ref.get(harness.sessions)).toHaveLength(1);
1454 }).pipe(Effect.provide(harness.layer), Effect.scoped);
1455 }),
1456 );
1457
1458 it.effect("a bearer replaced while an attempt is in flight retries with the new one", () =>
1459 Effect.gen(function* () {
1460 const attemptStarted = yield* Deferred.make<void>();
1461 const releaseFirstAttempt = yield* Deferred.make<void>();
1462 const attempts = yield* Ref.make(0);
1463 const harness = yield* makeHarness([], [], [], {
1464 // The first attempt read the old bearer; it stalls, then fails
1465 // authentication after the bearer has already been replaced.
1466 beforePrepareRoute: (target) =>
1467 target._tag !== "BearerConnectionTarget"
1468 ? Effect.succeed(undefined)
1469 : Ref.updateAndGet(attempts, (count) => count + 1).pipe(
1470 Effect.flatMap((count) =>
1471 count === 1
1472 ? Deferred.succeed(attemptStarted, undefined).pipe(
1473 Effect.andThen(Deferred.await(releaseFirstAttempt)),
1474 Effect.as(
1475 new ConnectionBlockedError({
1476 reason: "authentication",
1477 detail: "revoked",
1478 }),
1479 ),
1480 )
1481 : Effect.succeed(undefined),
1482 ),
1483 ),
1484 });
1485
1486 yield* Effect.gen(function* () {
1487 const registry = yield* EnvironmentRegistry.EnvironmentRegistry;
1488 const registration = (token: string) =>
1489 new BearerConnectionRegistration({
1490 target: BEARER_TARGET,
1491 profile: BEARER_PROFILE,
1492 credential: new BearerConnectionCredential({ token }),
1493 });
1494 yield* registry.reconcilePlatform([registration("old-bearer")]);
1495 yield* Deferred.await(attemptStarted).pipe(Effect.timeout("1 second"));
1496
1497 // A live state subscription, as the UI holds, keeps the runtime in place
1498 // so the stalled attempt's failure lands on the same supervisor.
1499 yield* registry
1500 .stateChanges(BEARER_TARGET.environmentId)
1501 .pipe(Stream.runDrain, Effect.forkScoped({ startImmediately: true }));
1502 yield* Effect.yieldNow;
1503 yield* registry.reconcilePlatform([registration("new-bearer")]);
1504 yield* Deferred.succeed(releaseFirstAttempt, undefined);
1505
1506 yield* awaitConnectionState(
1507 registry,
1508 BEARER_TARGET.environmentId,
1509 (state) => state.phase === "connected",
1510 );
1511 expect(yield* Ref.get(attempts)).toBeGreaterThanOrEqual(2);
1512 }).pipe(Effect.provide(harness.layer), Effect.scoped);
1513 }),
1514 );
1515
1516 it.effect("starts a newly paired bearer environment without re-reading its profile", () =>
1517 Effect.gen(function* () {
1518 const harness = yield* makeHarness([]);
1519
1520 yield* Effect.gen(function* () {
1521 const registry = yield* EnvironmentRegistry.EnvironmentRegistry;
1522 yield* registry.register(
1523 new BearerConnectionRegistration({
1524 target: BEARER_TARGET,
1525 profile: BEARER_PROFILE,
1526 credential: BEARER_CREDENTIAL,
1527 }),
1528 );
1529 yield* awaitConnectionState(
1530 registry,
1531 BEARER_TARGET.environmentId,
1532 (state) => state.phase === "connected",
1533 );
1534
1535 expect(yield* Ref.get(harness.profileReadCount)).toBe(0);
1536 expect(
1537 Option.getOrThrow(
1538 (yield* SubscriptionRef.get(registry.entries)).get(BEARER_TARGET.environmentId)
1539 ?.profile ?? Option.none(),
1540 ),
1541 ).toEqual(BEARER_PROFILE);
1542 }).pipe(Effect.provide(harness.layer), Effect.scoped);
1543 }),
1544 );
1545
1546 it.effect("starts platform environments without persisting or removing them", () =>
1547 Effect.gen(function* () {
1548 const harness = yield* makeHarness([]);
1549
1550 yield* Effect.gen(function* () {
1551 const registry = yield* EnvironmentRegistry.EnvironmentRegistry;
1552 yield* registry.registerPlatform(new PrimaryConnectionRegistration({ target: TARGET }));
1553 yield* awaitConnectionState(
1554 registry,
1555 TARGET.environmentId,
1556 (state) => state.phase === "connected",
1557 );
1558
1559 expect(hasRoutes(yield* Ref.get(harness.storedTargets), TARGET.environmentId)).toBe(false);
1560 expect(
1561 (yield* SubscriptionRef.get(registry.entries)).get(TARGET.environmentId)?.target,
1562 ).toEqual(TARGET);
1563
1564 const error = yield* Effect.flip(registry.remove(TARGET.environmentId));
1565 expect(error._tag).toBe("PlatformEnvironmentRemovalError");
1566 expect(
1567 (yield* SubscriptionRef.get(registry.entries)).get(TARGET.environmentId)?.target,
1568 ).toEqual(TARGET);
1569 }).pipe(Effect.provide(harness.layer));
1570 }),
1571 );
1572
1573 it.effect("gives a primary platform registration precedence over persisted registrations", () =>
1574 Effect.gen(function* () {
1575 const shadowedTarget = new RelayConnectionTarget({
1576 environmentId: TARGET.environmentId,
1577 label: "Shadowed relay environment",
1578 });
1579 const harness = yield* makeHarness([shadowedTarget]);
1580 const permissions = yield* makeGitHubRoutingPermissions({
1581 read: Effect.succeed([]),
1582 write: () => Effect.void,
1583 });
1584 const shadowedEntry = { target: shadowedTarget, profile: Option.none(), enabled: true };
1585 yield* permissions.set(shadowedEntry, "read-write");
1586
1587 yield* Effect.gen(function* () {
1588 const registry = yield* EnvironmentRegistry.EnvironmentRegistry;
1589 yield* registry.registerPlatform(new PrimaryConnectionRegistration({ target: TARGET }));
1590 expect(yield* permissions.get(shadowedEntry)).toBe("off");
1591
1592 expect(
1593 (yield* SubscriptionRef.get(registry.entries)).get(TARGET.environmentId)?.target,
1594 ).toEqual(TARGET);
1595 expect(hasRoutes(yield* Ref.get(harness.storedTargets), TARGET.environmentId)).toBe(false);
1596
1597 yield* registry.register(new RelayConnectionRegistration({ target: shadowedTarget }));
1598
1599 expect(
1600 (yield* SubscriptionRef.get(registry.entries)).get(TARGET.environmentId)?.target,
1601 ).toEqual(TARGET);
1602 expect(hasRoutes(yield* Ref.get(harness.storedTargets), TARGET.environmentId)).toBe(false);
1603 }).pipe(
1604 Effect.provide(harness.layer),
1605 Effect.provideService(GitHubRoutingPermissions, permissions),
1606 Effect.scoped,
1607 );
1608 }),
1609 );
1610
1611 it.effect("rechecks platform ownership after waiting for the environment lease", () =>
1612 Effect.gen(function* () {
1613 const registrationStarted = yield* Deferred.make<void>();
1614 const continueRegistration = yield* Deferred.make<void>();
1615 const shadowedTarget = new RelayConnectionTarget({
1616 environmentId: TARGET.environmentId,
1617 label: "Shadowed relay environment",
1618 });
1619 const harness = yield* makeHarness([], [], [], {
1620 beforeRegistrationRegister: () =>
1621 Deferred.succeed(registrationStarted, undefined).pipe(
1622 Effect.andThen(Deferred.await(continueRegistration)),
1623 ),
1624 });
1625
1626 yield* Effect.gen(function* () {
1627 const registry = yield* EnvironmentRegistry.EnvironmentRegistry;
1628 const persistedRegistration = yield* registry
1629 .register(new RelayConnectionRegistration({ target: shadowedTarget }))
1630 .pipe(Effect.forkChild({ startImmediately: true }));
1631 yield* Deferred.await(registrationStarted);
1632
1633 const platformRegistration = yield* registry
1634 .registerPlatform(new PrimaryConnectionRegistration({ target: TARGET }))
1635 .pipe(Effect.forkChild({ startImmediately: true }));
1636 yield* Effect.yieldNow;
1637 const removal = yield* Effect.flip(registry.remove(TARGET.environmentId)).pipe(
1638 Effect.forkChild({ startImmediately: true }),
1639 );
1640
1641 yield* Deferred.succeed(continueRegistration, undefined);
1642 yield* Fiber.join(persistedRegistration);
1643 yield* Fiber.join(platformRegistration);
1644 const error = yield* Fiber.join(removal);
1645
1646 expect(error._tag).toBe("PlatformEnvironmentRemovalError");
1647 expect(
1648 (yield* SubscriptionRef.get(registry.entries)).get(TARGET.environmentId)?.target,
1649 ).toEqual(TARGET);
1650 }).pipe(Effect.provide(harness.layer), Effect.scoped);
1651 }),
1652 );
1653
1654 it.effect("does not reacquire a runtime while its registration is being removed", () =>
1655 Effect.gen(function* () {
1656 const removalStarted = yield* Deferred.make<void>();
1657 const continueRemoval = yield* Deferred.make<void>();
1658 const harness = yield* makeHarness([TARGET], [], [], {
1659 beforeRegistrationRemove: () =>
1660 Deferred.succeed(removalStarted, undefined).pipe(
1661 Effect.andThen(Deferred.await(continueRemoval)),
1662 ),
1663 });
1664
1665 yield* Effect.gen(function* () {
1666 const registry = yield* EnvironmentRegistry.EnvironmentRegistry;
1667 yield* registry.start;
1668 yield* awaitConnectionState(
1669 registry,
1670 TARGET.environmentId,
1671 (state) => state.phase === "connected",
1672 );
1673
1674 const removal = yield* Effect.forkChild(registry.remove(TARGET.environmentId));
1675 yield* Deferred.await(removalStarted);
1676
1677 const stateLookup = yield* Effect.forkChild(
1678 Effect.flip(registry.state(TARGET.environmentId)),
1679 );
1680 yield* Effect.yieldNow;
1681 expect(yield* Ref.get(harness.sessions)).toHaveLength(1);
1682
1683 yield* Deferred.succeed(continueRemoval, undefined);
1684 yield* Fiber.join(removal);
1685 const error = yield* Fiber.join(stateLookup);
1686 expect(error._tag).toBe("EnvironmentNotRegisteredError");
1687 }).pipe(Effect.provide(harness.layer), Effect.scoped);
1688 }),
1689 );
1690
1691 it.effect("platform refreshes preserve unsupported state for the same endpoint", () =>
1692 Effect.gen(function* () {
1693 const harness = yield* makeHarness([]);
1694 yield* Effect.gen(function* () {
1695 const registry = yield* EnvironmentRegistry.EnvironmentRegistry;
1696 const registration = new PrimaryConnectionRegistration({ target: TARGET });
1697 yield* registry.registerPlatform(registration);
1698 yield* awaitConnectionState(
1699 registry,
1700 TARGET.environmentId,
1701 (state) => state.phase === "connected",
1702 );
1703 const error = new ConnectionBlockedError({
1704 reason: "unsupported",
1705 detail: "Use a compatible client.",
1706 });
1707 yield* registry.setCompatibility(TARGET.environmentId, error);
1708 yield* awaitConnectionState(
1709 registry,
1710 TARGET.environmentId,
1711 (state) => state.phase === "available",
1712 );
1713 yield* registry.registerPlatform(registration);
1714 yield* registry.reconcilePlatform([registration]);
1715 expect(
1716 (yield* SubscriptionRef.get(registry.entries)).get(TARGET.environmentId),
1717 ).toMatchObject({ enabled: false, unsupportedReason: error.message });
1718 expect(yield* Ref.get(harness.sessions)).toHaveLength(1);
1719 yield* registry.registerPlatform(
1720 new PrimaryConnectionRegistration({
1721 target: new PrimaryConnectionTarget({
1722 ...TARGET,
1723 httpBaseUrl: "https://changed.example.test",
1724 }),
1725 }),
1726 );
1727 yield* awaitConnectionState(
1728 registry,
1729 TARGET.environmentId,
1730 (state) => state.phase === "connected",
1731 );
1732 expect(
1733 (yield* SubscriptionRef.get(registry.entries)).get(TARGET.environmentId)
1734 ?.unsupportedReason,
1735 ).toBeUndefined();
1736 }).pipe(Effect.provide(harness.layer));
1737 }),
1738 );
1739
1740 it.effect("keeps one session per environment across concurrent registrations and retries", () =>
1741 Effect.gen(function* () {
1742 const harness = yield* makeHarness([]);
1743
1744 yield* Effect.gen(function* () {
1745 const registry = yield* EnvironmentRegistry.EnvironmentRegistry;
1746 const registration = new PrimaryConnectionRegistration({ target: TARGET });
1747 yield* Effect.all(
1748 Array.from({ length: 5 }, () => registry.registerPlatform(registration)),
1749 { concurrency: "unbounded", discard: true },
1750 );
1751 yield* awaitConnectionState(
1752 registry,
1753 TARGET.environmentId,
1754 (state) => state.phase === "connected",
1755 );
1756
1757 // Platform polls and explicit retries reach a healthy connection at once.
1758 yield* Effect.all(
1759 [
1760 ...Array.from({ length: 5 }, () => registry.registerPlatform(registration)),
1761 ...Array.from({ length: 5 }, () => registry.retryNow(TARGET.environmentId)),
1762 registry.reconcilePlatform([registration]),
1763 ],
1764 { concurrency: "unbounded", discard: true },
1765 );
1766 for (let attempt = 0; attempt < 100; attempt += 1) {
1767 yield* Effect.yieldNow;
1768 }
1769
1770 expect(yield* Ref.get(harness.sessions)).toHaveLength(1);
1771 expect(yield* Ref.get(harness.releasedSessions)).toBe(0);
1772 expect(yield* registry.state(TARGET.environmentId)).toMatchObject({
1773 phase: "connected",
1774 generation: 1,
1775 });
1776 }).pipe(Effect.provide(harness.layer), Effect.scoped);
1777 }),
1778 );
1779
1780 it.effect("retains a healthy runtime when the platform repeats an identical registration", () =>
1781 Effect.gen(function* () {
1782 const harness = yield* makeHarness([]);
1783 let failWrites = false;
1784 const permissions = yield* makeGitHubRoutingPermissions({
1785 read: Effect.succeed([]),
1786 write: () =>
1787 failWrites
1788 ? Effect.fail(
1789 new ConnectionTransientError({
1790 reason: "remote-unavailable",
1791 detail: "Storage unavailable",
1792 }),
1793 )
1794 : Effect.void,
1795 });
1796 const entry = { target: TARGET, profile: Option.none(), enabled: true };
1797
1798 yield* Effect.gen(function* () {
1799 const registry = yield* EnvironmentRegistry.EnvironmentRegistry;
1800 const registration = new PrimaryConnectionRegistration({ target: TARGET });
1801 yield* registry.registerPlatform(registration);
1802 yield* awaitConnectionState(
1803 registry,
1804 TARGET.environmentId,
1805 (state) => state.phase === "connected",
1806 );
1807 yield* permissions.set(entry, "read-write");
1808
1809 yield* registry.registerPlatform(registration);
1810
1811 expect(yield* Ref.get(harness.sessions)).toHaveLength(1);
1812 expect(yield* permissions.get(entry)).toBe("read-write");
1813
1814 const changedTarget = new PrimaryConnectionTarget({
1815 ...TARGET,
1816 httpBaseUrl: "https://changed.example.test",
1817 });
1818 failWrites = true;
1819 yield* registry.registerPlatform(
1820 new PrimaryConnectionRegistration({ target: changedTarget }),
1821 );
1822 expect(
1823 (yield* SubscriptionRef.get(registry.entries)).get(TARGET.environmentId)?.target,
1824 ).toEqual(TARGET);
1825 yield* registry.reconcilePlatform([]);
1826 expect((yield* SubscriptionRef.get(registry.entries)).has(TARGET.environmentId)).toBe(true);
1827
1828 failWrites = false;
1829 yield* registry.registerPlatform(
1830 new PrimaryConnectionRegistration({ target: changedTarget }),
1831 );
1832 expect(yield* permissions.get(entry)).toBe("off");
1833 yield* registry.registerPlatform(registration);
1834 expect(yield* permissions.get(entry)).toBe("off");
1835 yield* permissions.set(entry, "read-write");
1836 yield* registry.reconcilePlatform([]);
1837 yield* registry.registerPlatform(registration);
1838 expect(yield* permissions.get(entry)).toBe("off");
1839 }).pipe(
1840 Effect.provide(harness.layer),
1841 Effect.provideService(GitHubRoutingPermissions, permissions),
1842 Effect.scoped,
1843 );
1844 }),
1845 );
1846
1847 it.effect("removes all owned SSH state only on explicit removal", () =>
1848 Effect.gen(function* () {
1849 const harness = yield* makeHarness(
1850 [SSH_CONNECTION],
1851 [SSH_PROFILE],
1852 [
1853 [
1854 SSH_CONNECTION.connectionId,
1855 new BearerConnectionCredential({ token: "temporary-token" }),
1856 ],
1857 ],
1858 );
1859
1860 yield* Effect.gen(function* () {
1861 const registry = yield* EnvironmentRegistry.EnvironmentRegistry;
1862 yield* registry.start;
1863 yield* registry.remove(SSH_CONNECTION.environmentId);
1864
1865 expect((yield* Ref.get(harness.storedProfiles)).has(SSH_CONNECTION.connectionId)).toBe(
1866 false,
1867 );
1868 expect((yield* Ref.get(harness.storedCredentials)).has(SSH_CONNECTION.connectionId)).toBe(
1869 false,
1870 );
1871 expect((yield* Ref.get(harness.storedRemoteTokens)).has(SSH_CONNECTION.environmentId)).toBe(
1872 false,
1873 );
1874 expect(yield* Ref.get(harness.disconnectedSshTargets)).toEqual([SSH_TARGET]);
1875 }).pipe(Effect.provide(harness.layer));
1876 }),
1877 );
1878});
1879
1880describe("EnvironmentRegistry routes", () => {
1881 // The relay environment, reachable on the LAN as well.
1882 const LAN_TARGET = new BearerConnectionTarget({
1883 environmentId: RELAY_TARGET.environmentId,
1884 label: RELAY_TARGET.label,
1885 connectionId: "bearer:lan",
1886 });
1887 const LAN_PROFILE = new BearerConnectionProfile({
1888 connectionId: LAN_TARGET.connectionId,
1889 environmentId: LAN_TARGET.environmentId,
1890 label: LAN_TARGET.label,
1891 httpBaseUrl: "http://192.168.1.10:3773/",
1892 wsBaseUrl: "ws://192.168.1.10:3773/",
1893 });
1894 const lanRegistration = (target = LAN_TARGET, profile = LAN_PROFILE) =>
1895 new BearerConnectionRegistration({ target, profile, credential: BEARER_CREDENTIAL });
1896
1897 it.effect("adds a paired LAN route ahead of T3 Connect instead of replacing it", () =>
1898 Effect.gen(function* () {
1899 const harness = yield* makeHarness([RELAY_TARGET]);
1900 yield* Effect.gen(function* () {
1901 const registry = yield* EnvironmentRegistry.EnvironmentRegistry;
1902 yield* registry.register(lanRegistration());
1903
1904 expect(routesOf(yield* Ref.get(harness.storedTargets), LAN_TARGET.environmentId)).toEqual([
1905 LAN_TARGET,
1906 RELAY_TARGET,
1907 ]);
1908 const entry = (yield* SubscriptionRef.get(registry.entries)).get(LAN_TARGET.environmentId);
1909 expect(entry?.target).toEqual(LAN_TARGET);
1910 expect(entry?.alternateRoutes?.map((route) => route.target)).toEqual([RELAY_TARGET]);
1911 }).pipe(Effect.provide(harness.layer), Effect.scoped);
1912 }),
1913 );
1914
1915 it.effect("re-pairing the same address replaces that route rather than adding another", () =>
1916 Effect.gen(function* () {
1917 const harness = yield* makeHarness(
1918 [LAN_TARGET, RELAY_TARGET],
1919 [LAN_PROFILE],
1920 [[LAN_TARGET.connectionId, BEARER_CREDENTIAL]],
1921 );
1922 yield* Effect.gen(function* () {
1923 const registry = yield* EnvironmentRegistry.EnvironmentRegistry;
1924 const repaired = new BearerConnectionTarget({
1925 ...LAN_TARGET,
1926 connectionId: "bearer:lan-again",
1927 });
1928 yield* registry.register(
1929 lanRegistration(
1930 repaired,
1931 new BearerConnectionProfile({ ...LAN_PROFILE, connectionId: repaired.connectionId }),
1932 ),
1933 );
1934
1935 expect(routesOf(yield* Ref.get(harness.storedTargets), LAN_TARGET.environmentId)).toEqual([
1936 repaired,
1937 RELAY_TARGET,
1938 ]);
1939 }).pipe(Effect.provide(harness.layer), Effect.scoped);
1940 }),
1941 );
1942
1943 it.effect("a second SSH host for the same machine adds a route beside the first", () =>
1944 Effect.gen(function* () {
1945 const harness = yield* makeHarness([SSH_CONNECTION], [SSH_PROFILE]);
1946 yield* Effect.gen(function* () {
1947 const registry = yield* EnvironmentRegistry.EnvironmentRegistry;
1948 const other = new SshConnectionTarget({
1949 ...SSH_CONNECTION,
1950 connectionId: "ssh-connection-other-host",
1951 });
1952 yield* registry.register(
1953 new SshConnectionRegistration({
1954 target: other,
1955 profile: new SshConnectionProfile({
1956 ...SSH_PROFILE,
1957 connectionId: other.connectionId,
1958 target: { ...SSH_TARGET, alias: "other", hostname: "other.example.test" },
1959 }),
1960 }),
1961 );
1962
1963 expect(
1964 routesOf(yield* Ref.get(harness.storedTargets), SSH_CONNECTION.environmentId),
1965 ).toEqual([SSH_CONNECTION, other]);
1966 }).pipe(Effect.provide(harness.layer), Effect.scoped);
1967 }),
1968 );
1969
1970 it.effect(
1971 "adding a saved SSH host again replaces its route, whatever id it was saved under",
1972 () =>
1973 Effect.gen(function* () {
1974 const harness = yield* makeHarness([SSH_CONNECTION], [SSH_PROFILE]);
1975 yield* Effect.gen(function* () {
1976 const registry = yield* EnvironmentRegistry.EnvironmentRegistry;
1977 const again = new SshConnectionTarget({
1978 ...SSH_CONNECTION,
1979 connectionId: "ssh-connection-new-id",
1980 });
1981 yield* registry.register(
1982 new SshConnectionRegistration({
1983 target: again,
1984 profile: new SshConnectionProfile({
1985 ...SSH_PROFILE,
1986 connectionId: again.connectionId,
1987 }),
1988 }),
1989 );
1990
1991 expect(
1992 routesOf(yield* Ref.get(harness.storedTargets), SSH_CONNECTION.environmentId),
1993 ).toEqual([again]);
1994 }).pipe(Effect.provide(harness.layer), Effect.scoped);
1995 }),
1996 );
1997
1998 it.effect("signing out of T3 Connect keeps an environment that still has a LAN route", () =>
1999 Effect.gen(function* () {
2000 const harness = yield* makeHarness(
2001 [LAN_TARGET, RELAY_TARGET, SECOND_RELAY_TARGET],
2002 [LAN_PROFILE],
2003 [[LAN_TARGET.connectionId, BEARER_CREDENTIAL]],
2004 );
2005 yield* Effect.gen(function* () {
2006 const registry = yield* EnvironmentRegistry.EnvironmentRegistry;
2007 yield* registry.removeRelayEnvironments();
2008
2009 const targets = yield* Ref.get(harness.storedTargets);
2010 expect(routesOf(targets, LAN_TARGET.environmentId)).toEqual([LAN_TARGET]);
2011 expect(hasRoutes(targets, SECOND_RELAY_TARGET.environmentId)).toBe(false);
2012 // Only the environment that lost its last route is forgotten.
2013 expect(yield* Ref.get(harness.cacheClears)).toEqual([SECOND_RELAY_TARGET.environmentId]);
2014 expect(
2015 (yield* SubscriptionRef.get(registry.entries)).get(LAN_TARGET.environmentId)?.target,
2016 ).toEqual(LAN_TARGET);
2017 }).pipe(Effect.provide(harness.layer), Effect.scoped);
2018 }),
2019 );
2020
2021 it.effect("reorders routes and rejects an order that drops one", () =>
2022 Effect.gen(function* () {
2023 const harness = yield* makeHarness(
2024 [LAN_TARGET, RELAY_TARGET],
2025 [LAN_PROFILE],
2026 [[LAN_TARGET.connectionId, BEARER_CREDENTIAL]],
2027 );
2028 yield* Effect.gen(function* () {
2029 const registry = yield* EnvironmentRegistry.EnvironmentRegistry;
2030 yield* registry.reorderRoutes(LAN_TARGET.environmentId, ["relay", LAN_TARGET.connectionId]);
2031 expect(routesOf(yield* Ref.get(harness.storedTargets), LAN_TARGET.environmentId)).toEqual([
2032 RELAY_TARGET,
2033 LAN_TARGET,
2034 ]);
2035
2036 const error = yield* registry
2037 .reorderRoutes(LAN_TARGET.environmentId, ["relay"])
2038 .pipe(Effect.flip);
2039 expect(error._tag).toBe("ConnectionBlockedError");
2040 }).pipe(Effect.provide(harness.layer), Effect.scoped);
2041 }),
2042 );
2043
2044 it.effect("removing the last route forgets the environment", () =>
2045 Effect.gen(function* () {
2046 const harness = yield* makeHarness(
2047 [LAN_TARGET, RELAY_TARGET],
2048 [LAN_PROFILE],
2049 [[LAN_TARGET.connectionId, BEARER_CREDENTIAL]],
2050 );
2051 yield* Effect.gen(function* () {
2052 const registry = yield* EnvironmentRegistry.EnvironmentRegistry;
2053 yield* registry.removeRoute(LAN_TARGET.environmentId, LAN_TARGET.connectionId);
2054 expect(routesOf(yield* Ref.get(harness.storedTargets), LAN_TARGET.environmentId)).toEqual([
2055 RELAY_TARGET,
2056 ]);
2057 expect(yield* Ref.get(harness.cacheClears)).toEqual([]);
2058
2059 yield* registry.removeRoute(LAN_TARGET.environmentId, "relay");
2060 expect(hasRoutes(yield* Ref.get(harness.storedTargets), LAN_TARGET.environmentId)).toBe(
2061 false,
2062 );
2063 expect(yield* Ref.get(harness.cacheClears)).toEqual([LAN_TARGET.environmentId]);
2064 }).pipe(Effect.provide(harness.layer), Effect.scoped);
2065 }),
2066 );
2067
2068 it.effect("revokes GitHub routing trust when a route is added but keeps it on reorder", () =>
2069 Effect.gen(function* () {
2070 const harness = yield* makeHarness([RELAY_TARGET]);
2071 let stored: ReadonlyArray<StoredGitHubRoutingPermission> = [];
2072 const permissions = yield* makeGitHubRoutingPermissions({
2073 read: Effect.sync(() => stored),
2074 write: (next) => Effect.sync(() => void (stored = next)),
2075 });
2076 yield* Effect.gen(function* () {
2077 const registry = yield* EnvironmentRegistry.EnvironmentRegistry;
2078 const current = () =>
2079 SubscriptionRef.get(registry.entries).pipe(
2080 Effect.map((entries) => entries.get(RELAY_TARGET.environmentId)!),
2081 );
2082 yield* permissions.set(yield* current(), "read");
2083 yield* registry.register(lanRegistration());
2084 expect(yield* permissions.get(yield* current())).toBe("off");
2085
2086 yield* permissions.set(yield* current(), "read");
2087 yield* registry.reorderRoutes(LAN_TARGET.environmentId, ["relay", LAN_TARGET.connectionId]);
2088 expect(yield* permissions.get(yield* current())).toBe("read");
2089 }).pipe(
2090 Effect.provide(harness.layer),
2091 Effect.provideService(GitHubRoutingPermissions, permissions),
2092 Effect.scoped,
2093 );
2094 }),
2095 );
2096
2097 it.effect("loads saved routes grouped by environment, in saved order", () =>
2098 Effect.gen(function* () {
2099 const harness = yield* makeHarness(
2100 [LAN_TARGET, BEARER_TARGET, RELAY_TARGET],
2101 [LAN_PROFILE, BEARER_PROFILE],
2102 [
2103 [LAN_TARGET.connectionId, BEARER_CREDENTIAL],
2104 [BEARER_TARGET.connectionId, BEARER_CREDENTIAL],
2105 ],
2106 );
2107 yield* Effect.gen(function* () {
2108 const registry = yield* EnvironmentRegistry.EnvironmentRegistry;
2109 const entries = yield* SubscriptionRef.get(registry.entries);
2110 expect([...entries.keys()]).toEqual([
2111 LAN_TARGET.environmentId,
2112 BEARER_TARGET.environmentId,
2113 ]);
2114 expect(entries.get(LAN_TARGET.environmentId)?.alternateRoutes?.[0]?.target).toEqual(
2115 RELAY_TARGET,
2116 );
2117 }).pipe(Effect.provide(harness.layer), Effect.scoped);
2118 }),
2119 );
2120
2121 it.effect("saves the Tailscale mark when the server reports a learned address as tailnet", () =>
2122 Effect.gen(function* () {
2123 const httpBaseUrl = "http://100.101.102.103:3773/";
2124 const learnedId = `learned:${LAN_TARGET.environmentId}:http://100.101.102.103:3773@${LAN_TARGET.connectionId}`;
2125 const learned = new BearerConnectionTarget({ ...LAN_TARGET, connectionId: learnedId });
2126 // Learned by an earlier build, which saved no network.
2127 const learnedProfile = new BearerConnectionProfile({
2128 connectionId: learnedId,
2129 environmentId: LAN_TARGET.environmentId,
2130 label: LAN_TARGET.label,
2131 httpBaseUrl,
2132 wsBaseUrl: "ws://100.101.102.103:3773/",
2133 learned: true,
2134 });
2135 const harness = yield* makeHarness(
2136 [LAN_TARGET, learned],
2137 [LAN_PROFILE, learnedProfile],
2138 [[LAN_TARGET.connectionId, BEARER_CREDENTIAL]],
2139 {
2140 directEndpoints: [
2141 { kind: "lan", httpBaseUrl: LAN_PROFILE.httpBaseUrl },
2142 { kind: "tailnet", httpBaseUrl },
2143 ],
2144 },
2145 );
2146 yield* Effect.gen(function* () {
2147 const registry = yield* EnvironmentRegistry.EnvironmentRegistry;
2148 yield* registry.start;
2149 yield* SubscriptionRef.changes(registry.entries).pipe(
2150 Stream.map((entries) => entries.get(LAN_TARGET.environmentId)?.alternateRoutes?.[0]),
2151 Stream.filter((route) => route !== undefined && connectionRouteKind(route) === "tailnet"),
2152 Stream.runHead,
2153 );
2154 expect((yield* Ref.get(harness.storedProfiles)).get(learnedId)).toMatchObject({
2155 network: "tailscale",
2156 learned: true,
2157 httpBaseUrl,
2158 });
2159 }).pipe(Effect.provide(harness.layer), Effect.scoped);
2160 }),
2161 );
2162
2163 it.effect("drops a learned route whose profile was not saved instead of duplicating it", () =>
2164 Effect.gen(function* () {
2165 const learnedId = `learned:${LAN_TARGET.environmentId}:100.64.0.9:3773@${LAN_TARGET.connectionId}`;
2166 const learned = new BearerConnectionTarget({ ...LAN_TARGET, connectionId: learnedId });
2167 // An earlier build saved the learned target twice and never its profile.
2168 const harness = yield* makeHarness(
2169 [LAN_TARGET, learned, learned],
2170 [LAN_PROFILE],
2171 [[LAN_TARGET.connectionId, BEARER_CREDENTIAL]],
2172 );
2173 yield* Effect.gen(function* () {
2174 const registry = yield* EnvironmentRegistry.EnvironmentRegistry;
2175 const entry = (yield* SubscriptionRef.get(registry.entries)).get(LAN_TARGET.environmentId);
2176 expect(entry?.target).toEqual(LAN_TARGET);
2177 expect(entry?.alternateRoutes ?? []).toEqual([]);
2178 }).pipe(Effect.provide(harness.layer), Effect.scoped);
2179 }),
2180 );
2181});