Skip to content
Source
Sign inSign up

DesktopWslBackend.test.ts

194 lines7.3 KB

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 };
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("clears the stored preflight error when a registered WSL backend becomes ready", () => {
88 let registeredSpec: DesktopBackendPool.BackendInstanceSpec | undefined;
89 let registeredWsl: DesktopBackendPool.DesktopBackendInstance | undefined;
90 let startCount = 0;
91 const primary = makeStubInstance({
92 id: DesktopBackendPool.PRIMARY_INSTANCE_ID,
93 label: "Windows",
94 snapshot: primarySnapshot,
95 });
96 const layerPool = Layer.succeed(DesktopBackendPool.DesktopBackendPool, {
97 get: (id) =>
98 Effect.succeed(
99 id === DesktopBackendPool.PRIMARY_INSTANCE_ID
100 ? Option.some(primary)
101 : Option.none<DesktopBackendPool.DesktopBackendInstance>(),
102 ),
103 list: Effect.sync(() => (registeredWsl === undefined ? [primary] : [primary, registeredWsl])),
104 primary: Effect.succeed(primary),
105 register: (spec) =>
106 Effect.sync(() => {
107 registeredSpec = spec;
108 registeredWsl = makeStubInstance({
109 id: spec.id,
110 label: "WSL",
111 snapshot: idleSnapshot,
112 start: Effect.sync(() => {
113 startCount += 1;
114 }),
115 });
116 return registeredWsl;
117 }),
118 unregister: () =>
119 Effect.sync(() => {
120 registeredWsl = undefined;
121 }),
122 } satisfies DesktopBackendPool.DesktopBackendPool["Service"]);
123​
124 return Effect.gen(function* () {
125 const backend = yield* DesktopWslBackend.DesktopWslBackend;
126​
127 yield* backend.reconcile;
128 const spec = registeredSpec;
129 assert.isDefined(spec);
130 if (spec === undefined) {
131 throw new Error("Expected WSL backend registration");
132 }
133 const recordFailure = spec.onPreflightFailed;
134 const recordOwnershipFailure = spec.onStateDirOwned;
135 const clearFailure = spec.onReady;
136 assert.isDefined(recordFailure);
137 assert.isDefined(recordOwnershipFailure);
138 assert.isDefined(clearFailure);
139 if (
140 recordFailure === undefined ||
141 recordOwnershipFailure === undefined ||
142 clearFailure === undefined
143 ) {
144 throw new Error("Expected WSL backend callbacks");
145 }
146​
147 assert.isFalse(yield* recordFailure({ reason: "Node.js not found", fatal: true }));
148 assert.deepEqual(yield* backend.lastPreflightError, Option.some("Node.js not found"));
149​
150 yield* recordOwnershipFailure();
151 const refusal = yield* backend.lastPreflightError;
152 assert.include(Option.getOrThrow(refusal), "Another T3 Code server");
153 yield* backend.reconcile;
154 assert.equal(startCount, 1);
155 assert.deepEqual(yield* backend.lastPreflightError, refusal);
156​
157 yield* clearFailure(new URL("http://127.0.0.1:41773"), 123);
158 assert.deepEqual(yield* backend.lastPreflightError, Option.none());
159 yield* recordOwnershipFailure();
160 const settings = yield* DesktopAppSettings.DesktopAppSettings;
161 yield* settings.setWslDistro("Debian");
162 yield* backend.reconcile;
163 assert.equal(startCount, 2);
164 assert.deepEqual(yield* backend.lastPreflightError, Option.none());
165 const nextSpec = registeredSpec;
166 if (nextSpec?.onStateDirOwned === undefined) throw new Error("Expected ownership callback");
167 yield* nextSpec.onStateDirOwned();
168 yield* settings.setWslBackendEnabled(false);
169 yield* backend.reconcile;
170 yield* settings.setWslBackendEnabled(true);
171 yield* backend.reconcile;
172 assert.equal(startCount, 3);
173 assert.deepEqual(yield* backend.lastPreflightError, Option.none());
174 }).pipe(
175 Effect.provide(
176 DesktopWslBackend.layer.pipe(
177 Layer.provideMerge(layerPool),
178 Layer.provideMerge(layerBackendConfiguration),
179 Layer.provideMerge(layerServerExposure),
180 Layer.provideMerge(layerNet),
181 Layer.provideMerge(
182 DesktopAppSettings.layerTest({
183 ...DesktopAppSettings.DEFAULT_DESKTOP_SETTINGS,
184 wslBackendEnabled: true,
185 wslDistro: "Ubuntu",
186 wslOnly: false,
187 }),
188 ),
189 Layer.provideMerge(DesktopWslEnvironment.layerTest({ isAvailable: true })),
190 ),
191 ),
192 );
193 });
194});