1import * as DesktopTerminalHost from "./DesktopTerminalHost.ts";
2import { assert, describe, it } from "@effect/vitest";
3import * as Duration from "effect/Duration";
4import * as Effect from "effect/Effect";
5import * as FileSystem from "effect/FileSystem";
6import * as Layer from "effect/Layer";
7import * as Option from "effect/Option";
8import * as Ref from "effect/Ref";
9import * as Stream from "effect/Stream";
10import { HttpClient } from "effect/http";
11import { ChildProcessSpawner } from "effect/process";
12
13import * as DesktopObservability from "../app/DesktopObservability.ts";
14import * as DesktopAppSettings from "../settings/DesktopAppSettings.ts";
15import * as DesktopTelemetryPublisher from "../telemetry/DesktopTelemetryPublisher.ts";
16import * as DesktopBrowserHost from "../preview/DesktopBrowserHost.ts";
17import * as ElectronDialog from "../electron/ElectronDialog.ts";
18import * as ElectronApp from "../electron/ElectronApp.ts";
19import * as DesktopWindow from "../window/DesktopWindow.ts";
20import * as DesktopWslEnvironment from "../wsl/DesktopWslEnvironment.ts";
21import * as DesktopBackendConfiguration from "./DesktopBackendConfiguration.ts";
22import * as DesktopBackendPool from "./DesktopBackendPool.ts";
23import type { DesktopBackendSnapshot, DesktopBackendStartConfig } from "./DesktopBackendManager.ts";
24
25function makeStubInstance(
26 id: DesktopBackendPool.BackendInstanceId,
27 label: string,
28): DesktopBackendPool.DesktopBackendInstance {
29 const snapshot: DesktopBackendSnapshot = {
30 desiredRunning: false,
31 ready: false,
32 activePid: Option.none(),
33 restartAttempt: 0,
34 restartScheduled: false,
35 };
36 return {
37 id,
38 label: Effect.succeed(label),
39 start: Effect.void,
40 stop: () => Effect.void,
41 currentConfig: Effect.succeed(Option.none<DesktopBackendStartConfig>()),
42 snapshot: Effect.succeed(snapshot),
43 waitForReady: (_timeout: Duration.Duration) => Effect.succeed(false),
44 };
45}
46
47function layerPool(labelRef: Ref.Ref<string>): Layer.Layer<DesktopBackendPool.DesktopBackendPool> {
48 return DesktopBackendPool.layer.pipe(
49 Layer.provideMerge(
50 Layer.mergeAll(
51 FileSystem.layerNoop({}),
52 Layer.succeed(
53 ChildProcessSpawner.ChildProcessSpawner,
54 ChildProcessSpawner.make(() => Effect.die("unexpected child process spawn")),
55 ),
56 Layer.succeed(
57 HttpClient.HttpClient,
58 HttpClient.make(() => Effect.die("unexpected HTTP request")),
59 ),
60 Layer.succeed(DesktopObservability.DesktopBackendOutputLogFactory, {
61 forInstance: () =>
62 Effect.succeed({
63 beginSession: () => Effect.void,
64 writeOutputChunk: () => Effect.void,
65 persistFailureSnapshot: () => Effect.void,
66 persistFailure: () => Effect.void,
67 discardSession: Effect.void,
68 } satisfies DesktopObservability.DesktopBackendOutputLogShape),
69 } satisfies DesktopObservability.DesktopBackendOutputLogFactory["Service"]),
70 Layer.succeed(DesktopTelemetryPublisher.DesktopTelemetryPublisher, {
71 latest: Effect.succeedNone,
72 changes: Stream.empty,
73 encoded: Stream.empty,
74 handleControlForSource: () => Effect.void,
75 removeControlSource: () => Effect.void,
76 publishUpdateReport: () => Effect.void,
77 updateRequests: Stream.empty,
78 updateCommits: Stream.empty,
79 updateCancellations: Stream.empty,
80 }),
81 Layer.succeed(DesktopTerminalHost.DesktopTerminalHost, {
82 requests: Stream.empty,
83 handleLine: () => Effect.void,
84 reset: Effect.void,
85 configure: () => Effect.void,
86 }),
87 DesktopBrowserHost.layer,
88 Layer.succeed(DesktopBackendConfiguration.DesktopBackendConfiguration, {
89 resolvePrimary: Effect.die("unexpected primary config resolve"),
90 resolvePrimaryLabel: Ref.get(labelRef),
91 resolveWsl: () => Effect.die("unexpected WSL config resolve"),
92 currentBootstrapToken: Effect.die("unexpected bootstrap token read"),
93 } satisfies DesktopBackendConfiguration.DesktopBackendConfiguration["Service"]),
94 DesktopAppSettings.layerTest(),
95 DesktopWslEnvironment.layerTest(),
96 ElectronDialog.layer,
97 ElectronApp.layer,
98 Layer.succeed(DesktopWindow.DesktopWindow, {
99 createMain: Effect.die("unexpected window create"),
100 ensureMain: Effect.die("unexpected window ensure"),
101 revealOrCreateMain: Effect.die("unexpected window reveal"),
102 activate: Effect.die("unexpected window activate"),
103 createMainIfBackendReady: Effect.die("unexpected window create"),
104 showConnectingSplash: Effect.void,
105 handleBackendReady: () => Effect.void,
106 handleBackendNotReady: Effect.void,
107 flushMainWindowBounds: Effect.void,
108 prepareCaptureReveal: Effect.void,
109 dispatchMenuAction: () => Effect.die("unexpected menu action"),
110 dispatchSnapShotEvent: () => Effect.void,
111 zoomMain: () => Effect.die("unexpected zoom"),
112 runMainContentsCommand: () => Effect.die("unexpected main contents command"),
113 syncAppearance: Effect.void,
114 } satisfies DesktopWindow.DesktopWindow["Service"]),
115 ),
116 ),
117 );
118}
119
120describe("DesktopBackendPool", () => {
121 it.effect("layerTest exposes registered instances by id", () =>
122 Effect.gen(function* () {
123 const pool = yield* DesktopBackendPool.DesktopBackendPool;
124 const fetchedPrimary = yield* pool.get(DesktopBackendPool.PRIMARY_INSTANCE_ID);
125 const fetchedWsl = yield* pool.get(DesktopBackendPool.BackendInstanceId("wsl:ubuntu"));
126 const fetchedMissing = yield* pool.get(DesktopBackendPool.BackendInstanceId("missing"));
127 const all = yield* pool.list;
128 const resolvedPrimary = yield* pool.primary;
129
130 assert.equal(yield* Option.getOrThrow(fetchedPrimary).label, "Windows");
131 assert.equal(yield* Option.getOrThrow(fetchedWsl).label, "WSL (Ubuntu)");
132 assert.isTrue(Option.isNone(fetchedMissing));
133 assert.lengthOf(all, 2);
134 // First instance becomes primary in layerTest so single-instance
135 // stubs don't have to wire an explicit primary.
136 assert.equal(resolvedPrimary.id, DesktopBackendPool.PRIMARY_INSTANCE_ID);
137 }).pipe(
138 Effect.provide(
139 DesktopBackendPool.layerTest([
140 makeStubInstance(DesktopBackendPool.PRIMARY_INSTANCE_ID, "Windows"),
141 makeStubInstance(DesktopBackendPool.BackendInstanceId("wsl:ubuntu"), "WSL (Ubuntu)"),
142 ]),
143 ),
144 ),
145 );
146
147 it.effect("layerTest dies when no instances are supplied", () =>
148 Effect.exit(
149 DesktopBackendPool.DesktopBackendPool.pipe(Effect.provide(DesktopBackendPool.layerTest([]))),
150 ).pipe(Effect.map((exit) => assert.equal(exit._tag, "Failure"))),
151 );
152
153 it.effect("resolves the primary label lazily after pool layer construction", () =>
154 Effect.scoped(
155 Effect.gen(function* () {
156 const labelRef = yield* Ref.make("Windows");
157 const pool = yield* DesktopBackendPool.DesktopBackendPool.pipe(
158 Effect.provide(layerPool(labelRef)),
159 );
160 const primary = yield* pool.primary;
161
162 yield* Ref.set(labelRef, "WSL (Ubuntu)");
163
164 assert.equal(yield* primary.label, "WSL (Ubuntu)");
165 }),
166 ),
167 );
168});