1import { assert, describe, it } from "@effect/vitest";
2import * as Duration from "effect/Duration";
3import * as Effect from "effect/Effect";
4import * as Layer from "effect/Layer";
5import * as Option from "effect/Option";
6
7import * as NetService from "@t3tools/shared/Net";
8
9import * as DesktopBackendConfiguration from "../backend/DesktopBackendConfiguration.ts";
10import * as DesktopBackendPool from "../backend/DesktopBackendPool.ts";
11import type {
12 DesktopBackendSnapshot,
13 DesktopBackendStartConfig,
14} from "../backend/DesktopBackendManager.ts";
15import * as DesktopServerExposure from "../backend/DesktopServerExposure.ts";
16import * as DesktopAppSettings from "../settings/DesktopAppSettings.ts";
17import * as DesktopWslEnvironment from "./DesktopWslEnvironment.ts";
18import * as DesktopWslBackend from "./DesktopWslBackend.ts";
19
20function makeStubInstance(input: {
21 readonly id: DesktopBackendPool.BackendInstanceId;
22 readonly label: string;
23 readonly snapshot: DesktopBackendSnapshot;
24 readonly start?: Effect.Effect<void>;
25}): DesktopBackendPool.DesktopBackendInstance {
26 return {
27 id: input.id,
28 label: Effect.succeed(input.label),
29 start: input.start ?? Effect.void,
30 stop: () => Effect.void,
31 currentConfig: Effect.succeed(Option.none<DesktopBackendStartConfig>()),
32 snapshot: Effect.succeed(input.snapshot),
33 waitForReady: (_timeout: Duration.Duration) => Effect.succeed(false),
34 };
35}
36
37const idleSnapshot: DesktopBackendSnapshot = {
38 desiredRunning: false,
39 ready: false,
40 activePid: Option.none(),
41 restartAttempt: 5,
42 restartScheduled: false,
43};
44
45const primarySnapshot: DesktopBackendSnapshot = {
46 desiredRunning: true,
47 ready: true,
48 activePid: Option.some(123),
49 restartAttempt: 0,
50 restartScheduled: false,
51};
52
53const layerServerExposure = Layer.succeed(DesktopServerExposure.DesktopServerExposure, {
54 getState: Effect.die("unexpected getState"),
55 backendConfig: Effect.succeed({
56 port: 3773,
57 bindHost: "127.0.0.1",
58 httpBaseUrl: new URL("http://127.0.0.1:3773"),
59 tailscaleServeEnabled: false,
60 tailscaleServePort: 443,
61 }),
62 configureFromSettings: () => Effect.die("unexpected configureFromSettings"),
63 setMode: () => Effect.die("unexpected setMode"),
64 setTailscaleServeEnabled: () => Effect.die("unexpected setTailscaleServeEnabled"),
65 getAdvertisedEndpoints: Effect.succeed([]),
66} satisfies DesktopServerExposure.DesktopServerExposure["Service"]);
67
68const layerBackendConfiguration = Layer.succeed(
69 DesktopBackendConfiguration.DesktopBackendConfiguration,
70 {
71 resolvePrimary: Effect.die("unexpected resolvePrimary"),
72 resolvePrimaryLabel: Effect.succeed("Windows"),
73 resolveWsl: () => Effect.die("unexpected resolveWsl"),
74 currentBootstrapToken: Effect.die("unexpected currentBootstrapToken"),
75 } satisfies DesktopBackendConfiguration.DesktopBackendConfiguration["Service"],
76);
77
78const layerNet = Layer.succeed(NetService.NetService, {
79 canListenOnHost: () => Effect.succeed(true),
80 isPortAvailableOnLoopback: () => Effect.succeed(true),
81 hasListenerOnHost: () => Effect.succeed(false),
82 reserveLoopbackPort: () => Effect.succeed(41773),
83 findAvailablePort: (preferred) => Effect.succeed(preferred),
84} satisfies NetService.NetService["Service"]);
85
86describe("DesktopWslBackend", () => {
87 it.effect("does not discover or start WSL when local execution is disabled", () =>
88 Effect.gen(function* () {
89 const backend = yield* DesktopWslBackend.DesktopWslBackend;
90 yield* backend.reconcile;
91 }).pipe(
92 Effect.provide(
93 DesktopWslBackend.layer.pipe(
94 Layer.provide(Layer.mock(DesktopBackendPool.DesktopBackendPool, {})),
95 Layer.provide(layerBackendConfiguration),
96 Layer.provide(layerServerExposure),
97 Layer.provide(layerNet),
98 Layer.provide(Layer.mock(DesktopWslEnvironment.DesktopWslEnvironment, {})),
99 Layer.provide(
100 DesktopAppSettings.layerTest({
101 ...DesktopAppSettings.DEFAULT_DESKTOP_SETTINGS,
102 localEnvironmentEnabled: false,
103 wslBackendEnabled: true,
104 }),
105 ),
106 ),
107 ),
108 ),
109 );
110 it.effect("clears the stored preflight error when a registered WSL backend becomes ready", () => {
111 let registeredSpec: DesktopBackendPool.BackendInstanceSpec | undefined;
112 let registeredWsl: DesktopBackendPool.DesktopBackendInstance | undefined;
113 let startCount = 0;
114 const primary = makeStubInstance({
115 id: DesktopBackendPool.PRIMARY_INSTANCE_ID,
116 label: "Windows",
117 snapshot: primarySnapshot,
118 });
119 const layerPool = Layer.succeed(DesktopBackendPool.DesktopBackendPool, {
120 get: (id) =>
121 Effect.succeed(
122 id === DesktopBackendPool.PRIMARY_INSTANCE_ID
123 ? Option.some(primary)
124 : Option.none<DesktopBackendPool.DesktopBackendInstance>(),
125 ),
126 list: Effect.sync(() => (registeredWsl === undefined ? [primary] : [primary, registeredWsl])),
127 primary: Effect.succeed(primary),
128 register: (spec) =>
129 Effect.sync(() => {
130 registeredSpec = spec;
131 registeredWsl = makeStubInstance({
132 id: spec.id,
133 label: "WSL",
134 snapshot: idleSnapshot,
135 start: Effect.sync(() => {
136 startCount += 1;
137 }),
138 });
139 return registeredWsl;
140 }),
141 unregister: () =>
142 Effect.sync(() => {
143 registeredWsl = undefined;
144 }),
145 } satisfies DesktopBackendPool.DesktopBackendPool["Service"]);
146
147 return Effect.gen(function* () {
148 const backend = yield* DesktopWslBackend.DesktopWslBackend;
149
150 yield* backend.reconcile;
151 const spec = registeredSpec;
152 assert.isDefined(spec);
153 if (spec === undefined) {
154 throw new Error("Expected WSL backend registration");
155 }
156 const recordFailure = spec.onPreflightFailed;
157 const recordOwnershipFailure = spec.onStateDirOwned;
158 const clearFailure = spec.onReady;
159 assert.isDefined(recordFailure);
160 assert.isDefined(recordOwnershipFailure);
161 assert.isDefined(clearFailure);
162 if (
163 recordFailure === undefined ||
164 recordOwnershipFailure === undefined ||
165 clearFailure === undefined
166 ) {
167 throw new Error("Expected WSL backend callbacks");
168 }
169
170 assert.isFalse(yield* recordFailure({ reason: "Node.js not found", fatal: true }));
171 assert.deepEqual(yield* backend.lastPreflightError, Option.some("Node.js not found"));
172
173 yield* recordOwnershipFailure();
174 const refusal = yield* backend.lastPreflightError;
175 assert.include(Option.getOrThrow(refusal), "Another The Knife server");
176 yield* backend.reconcile;
177 assert.equal(startCount, 1);
178 assert.deepEqual(yield* backend.lastPreflightError, refusal);
179
180 yield* clearFailure(new URL("http://127.0.0.1:41773"));
181 assert.deepEqual(yield* backend.lastPreflightError, Option.none());
182 yield* recordOwnershipFailure();
183 const settings = yield* DesktopAppSettings.DesktopAppSettings;
184 yield* settings.setWslDistro("Debian");
185 yield* backend.reconcile;
186 assert.equal(startCount, 2);
187 assert.deepEqual(yield* backend.lastPreflightError, Option.none());
188 const nextSpec = registeredSpec;
189 if (nextSpec?.onStateDirOwned === undefined) throw new Error("Expected ownership callback");
190 yield* nextSpec.onStateDirOwned();
191 yield* settings.setWslBackendEnabled(false);
192 yield* backend.reconcile;
193 yield* settings.setWslBackendEnabled(true);
194 yield* backend.reconcile;
195 assert.equal(startCount, 3);
196 assert.deepEqual(yield* backend.lastPreflightError, Option.none());
197 }).pipe(
198 Effect.provide(
199 DesktopWslBackend.layer.pipe(
200 Layer.provideMerge(layerPool),
201 Layer.provideMerge(layerBackendConfiguration),
202 Layer.provideMerge(layerServerExposure),
203 Layer.provideMerge(layerNet),
204 Layer.provideMerge(
205 DesktopAppSettings.layerTest({
206 ...DesktopAppSettings.DEFAULT_DESKTOP_SETTINGS,
207 wslBackendEnabled: true,
208 wslDistro: "Ubuntu",
209 wslOnly: false,
210 }),
211 ),
212 Layer.provideMerge(DesktopWslEnvironment.layerTest({ isAvailable: true })),
213 ),
214 ),
215 );
216 });
217
218 it.effect("retries an unchanged WSL instance when it is idle after failed preflight", () => {
219 let startCount = 0;
220 const primary = makeStubInstance({
221 id: DesktopBackendPool.PRIMARY_INSTANCE_ID,
222 label: "Windows",
223 snapshot: primarySnapshot,
224 });
225 const wsl = makeStubInstance({
226 id: DesktopBackendPool.BackendInstanceId("wsl:Ubuntu"),
227 label: "WSL (Ubuntu)",
228 snapshot: idleSnapshot,
229 start: Effect.sync(() => {
230 startCount += 1;
231 }),
232 });
233
234 return Effect.gen(function* () {
235 const backend = yield* DesktopWslBackend.DesktopWslBackend;
236
237 yield* backend.reconcile;
238
239 assert.equal(startCount, 1);
240 }).pipe(
241 Effect.provide(
242 DesktopWslBackend.layer.pipe(
243 Layer.provideMerge(DesktopBackendPool.layerTest([primary, wsl])),
244 Layer.provideMerge(layerBackendConfiguration),
245 Layer.provideMerge(layerServerExposure),
246 Layer.provideMerge(layerNet),
247 Layer.provideMerge(
248 DesktopAppSettings.layerTest({
249 ...DesktopAppSettings.DEFAULT_DESKTOP_SETTINGS,
250 wslBackendEnabled: true,
251 wslDistro: "Ubuntu",
252 wslOnly: false,
253 }),
254 ),
255 Layer.provideMerge(DesktopWslEnvironment.layerTest({ isAvailable: true })),
256 ),
257 ),
258 );
259 });
260});