1import { DesktopWslStateSchema } from "@t3tools/contracts";
2import { assert, describe, it } from "@effect/vitest";
3import * as Effect from "effect/Effect";
4import * as Layer from "effect/Layer";
5import * as Schema from "effect/Schema";
6
7import * as DesktopEnvironment from "../../app/DesktopEnvironment.ts";
8import * as DesktopLifecycle from "../../app/DesktopLifecycle.ts";
9import * as DesktopShutdown from "../../app/DesktopShutdown.ts";
10import * as DesktopState from "../../app/DesktopState.ts";
11import * as ElectronApp from "../../electron/ElectronApp.ts";
12import * as ElectronDialog from "../../electron/ElectronDialog.ts";
13import * as ElectronTheme from "../../electron/ElectronTheme.ts";
14import * as ElectronWindow from "../../electron/ElectronWindow.ts";
15import * as DesktopAppSettings from "../../settings/DesktopAppSettings.ts";
16import * as DesktopClientSettings from "../../settings/DesktopClientSettings.ts";
17import * as DesktopWindow from "../../window/DesktopWindow.ts";
18import * as DesktopWslBackend from "../../wsl/DesktopWslBackend.ts";
19import * as DesktopWslEnvironment from "../../wsl/DesktopWslEnvironment.ts";
20import { getWslState, setWslBackendEnabled, setWslDistro, setWslOnly } from "./wsl.ts";
21
22const decodeWslState = Schema.decodeUnknownEffect(DesktopWslStateSchema);
23
24const invokeSetWslBackendEnabled = (enabled: boolean) =>
25 setWslBackendEnabled.handler(enabled).pipe(Effect.flatMap(decodeWslState));
26const invokeSetWslDistro = (distro: string | null) =>
27 setWslDistro.handler(distro).pipe(Effect.flatMap(decodeWslState));
28const invokeSetWslOnly = (enabled: boolean) =>
29 setWslOnly.handler(enabled).pipe(Effect.flatMap(decodeWslState));
30
31function layerWslBackend(input: { readonly onReconcile?: Effect.Effect<void> } = {}) {
32 return Layer.succeed(
33 DesktopWslBackend.DesktopWslBackend,
34 DesktopWslBackend.DesktopWslBackend.of({
35 reconcile: input.onReconcile ?? Effect.void,
36 lastPreflightError: Effect.succeedNone,
37 }),
38 );
39}
40
41function layerLifecycle(relaunchReasons: Array<string>) {
42 return Layer.succeed(
43 DesktopLifecycle.DesktopLifecycle,
44 DesktopLifecycle.DesktopLifecycle.of({
45 relaunch: (reason) =>
46 Effect.sync(() => {
47 relaunchReasons.push(reason);
48 }),
49 register: Effect.void,
50 }),
51 );
52}
53
54const layerUnusedLifecycleRuntime = Layer.mergeAll(
55 DesktopShutdown.layer,
56 DesktopState.layer,
57 Layer.succeed(
58 DesktopEnvironment.DesktopEnvironment,
59 DesktopEnvironment.DesktopEnvironment.of(
60 {} as DesktopEnvironment.DesktopEnvironment["Service"],
61 ),
62 ),
63 Layer.succeed(
64 DesktopWindow.DesktopWindow,
65 DesktopWindow.DesktopWindow.of({} as DesktopWindow.DesktopWindow["Service"]),
66 ),
67 Layer.succeed(
68 ElectronApp.ElectronApp,
69 ElectronApp.ElectronApp.of({} as ElectronApp.ElectronApp["Service"]),
70 ),
71 Layer.succeed(
72 ElectronTheme.ElectronTheme,
73 ElectronTheme.ElectronTheme.of({} as ElectronTheme.ElectronTheme["Service"]),
74 ),
75 Layer.succeed(
76 ElectronDialog.ElectronDialog,
77 ElectronDialog.ElectronDialog.of({} as ElectronDialog.ElectronDialog["Service"]),
78 ),
79 Layer.succeed(
80 ElectronWindow.ElectronWindow,
81 ElectronWindow.ElectronWindow.of({} as ElectronWindow.ElectronWindow["Service"]),
82 ),
83 DesktopClientSettings.layerTest(),
84);
85
86describe("WSL IPC", () => {
87 it.effect("does not probe WSL when local execution is disabled", () =>
88 Effect.gen(function* () {
89 const wsl = yield* DesktopWslEnvironment.DesktopWslEnvironment;
90 const state = yield* getWslState.handler(undefined).pipe(
91 Effect.provideService(DesktopWslEnvironment.DesktopWslEnvironment, {
92 ...wsl,
93 isAvailable: Effect.die("must not probe WSL"),
94 listDistros: Effect.die("must not enumerate distros"),
95 }),
96 Effect.flatMap(decodeWslState),
97 );
98 assert.deepEqual(state, {
99 enabled: true,
100 distro: "Ubuntu",
101 available: false,
102 wslOnly: true,
103 distros: [],
104 preflightError: null,
105 });
106 }).pipe(
107 Effect.provide(
108 Layer.mergeAll(
109 DesktopAppSettings.layerTest({
110 ...DesktopAppSettings.DEFAULT_DESKTOP_SETTINGS,
111 localEnvironmentEnabled: false,
112 wslBackendEnabled: true,
113 wslDistro: "Ubuntu",
114 wslOnly: true,
115 }),
116 DesktopWslEnvironment.layerTest(),
117 layerWslBackend(),
118 ),
119 ),
120 ),
121 );
122
123 it.effect("stages dual-backend preferences before enabling without relaunching", () => {
124 const relaunchReasons: Array<string> = [];
125 const layer = Layer.mergeAll(
126 DesktopAppSettings.layerTest({
127 ...DesktopAppSettings.DEFAULT_DESKTOP_SETTINGS,
128 wslBackendEnabled: false,
129 wslOnly: true,
130 }),
131 DesktopWslEnvironment.layerTest({ isAvailable: true }),
132 layerWslBackend(),
133 layerLifecycle(relaunchReasons),
134 layerUnusedLifecycleRuntime,
135 );
136
137 return Effect.gen(function* () {
138 yield* invokeSetWslOnly(false);
139 yield* invokeSetWslDistro("Debian");
140 const state = yield* invokeSetWslBackendEnabled(true);
141
142 assert.deepEqual(state, {
143 enabled: true,
144 distro: "Debian",
145 available: true,
146 wslOnly: false,
147 distros: [],
148 preflightError: null,
149 });
150 assert.deepEqual(relaunchReasons, []);
151 }).pipe(Effect.provide(layer));
152 });
153
154 it.effect("stages WSL-only preferences and relaunches only after enabling", () => {
155 const relaunchReasons: Array<string> = [];
156 const layer = Layer.mergeAll(
157 DesktopAppSettings.layerTest({
158 ...DesktopAppSettings.DEFAULT_DESKTOP_SETTINGS,
159 wslBackendEnabled: false,
160 wslOnly: false,
161 }),
162 DesktopWslEnvironment.layerTest({ isAvailable: true }),
163 layerWslBackend(),
164 layerLifecycle(relaunchReasons),
165 layerUnusedLifecycleRuntime,
166 );
167
168 return Effect.gen(function* () {
169 const stagedMode = yield* invokeSetWslOnly(true);
170 assert.equal(stagedMode.enabled, false);
171 assert.equal(stagedMode.wslOnly, true);
172 assert.deepEqual(relaunchReasons, []);
173
174 yield* invokeSetWslDistro("Debian");
175 assert.deepEqual(relaunchReasons, []);
176
177 const state = yield* invokeSetWslBackendEnabled(true);
178 assert.deepEqual(state, {
179 enabled: true,
180 distro: "Debian",
181 available: true,
182 wslOnly: true,
183 distros: [],
184 preflightError: null,
185 });
186 assert.deepEqual(relaunchReasons, ["wslBackendEnabled=true"]);
187 }).pipe(Effect.provide(layer));
188 });
189
190 it.effect("relaunches when enabling the WSL backend while wsl-only is already persisted", () => {
191 const relaunchReasons: Array<string> = [];
192 let reconcileCount = 0;
193 const layer = Layer.mergeAll(
194 DesktopAppSettings.layerTest({
195 ...DesktopAppSettings.DEFAULT_DESKTOP_SETTINGS,
196 wslBackendEnabled: false,
197 wslOnly: true,
198 }),
199 DesktopWslEnvironment.layerTest({ isAvailable: true }),
200 layerWslBackend({
201 onReconcile: Effect.sync(() => {
202 reconcileCount += 1;
203 }),
204 }),
205 layerLifecycle(relaunchReasons),
206 layerUnusedLifecycleRuntime,
207 );
208
209 return Effect.gen(function* () {
210 const state = yield* invokeSetWslBackendEnabled(true);
211
212 assert.deepEqual(state, {
213 enabled: true,
214 distro: null,
215 available: true,
216 wslOnly: true,
217 distros: [],
218 preflightError: null,
219 });
220 assert.equal(reconcileCount, 0);
221 assert.deepEqual(relaunchReasons, ["wslBackendEnabled=true"]);
222 }).pipe(Effect.provide(layer));
223 });
224
225 it.effect("reconciles in dual-backend mode without relaunching", () => {
226 const relaunchReasons: Array<string> = [];
227 let reconcileCount = 0;
228 const layer = Layer.mergeAll(
229 DesktopAppSettings.layerTest({
230 ...DesktopAppSettings.DEFAULT_DESKTOP_SETTINGS,
231 wslBackendEnabled: false,
232 wslOnly: false,
233 }),
234 DesktopWslEnvironment.layerTest({ isAvailable: true }),
235 layerWslBackend({
236 onReconcile: Effect.sync(() => {
237 reconcileCount += 1;
238 }),
239 }),
240 layerLifecycle(relaunchReasons),
241 layerUnusedLifecycleRuntime,
242 );
243
244 return Effect.gen(function* () {
245 const state = yield* invokeSetWslBackendEnabled(true);
246
247 assert.equal(state.enabled, true);
248 assert.equal(state.wslOnly, false);
249 assert.equal(reconcileCount, 1);
250 assert.deepEqual(relaunchReasons, []);
251 }).pipe(Effect.provide(layer));
252 });
253
254 it.effect("clears wsl-only before relaunching when disabling a WSL-only backend", () => {
255 const relaunchReasons: Array<string> = [];
256 let reconcileCount = 0;
257 const layer = Layer.mergeAll(
258 DesktopAppSettings.layerTest({
259 ...DesktopAppSettings.DEFAULT_DESKTOP_SETTINGS,
260 wslBackendEnabled: true,
261 wslOnly: true,
262 }),
263 DesktopWslEnvironment.layerTest({ isAvailable: true }),
264 layerWslBackend({
265 onReconcile: Effect.sync(() => {
266 reconcileCount += 1;
267 }),
268 }),
269 layerLifecycle(relaunchReasons),
270 layerUnusedLifecycleRuntime,
271 );
272
273 return Effect.gen(function* () {
274 const state = yield* invokeSetWslBackendEnabled(false);
275 const appSettings = yield* DesktopAppSettings.DesktopAppSettings;
276 const settings = yield* appSettings.get;
277
278 assert.deepEqual(state, {
279 enabled: false,
280 distro: null,
281 available: true,
282 wslOnly: false,
283 distros: [],
284 preflightError: null,
285 });
286 assert.equal(settings.wslBackendEnabled, false);
287 assert.equal(settings.wslOnly, false);
288 assert.equal(reconcileCount, 0);
289 assert.deepEqual(relaunchReasons, ["wslBackendEnabled=false"]);
290 }).pipe(Effect.provide(layer));
291 });
292
293 it.effect("clears dual-backend WSL without relaunching", () => {
294 const relaunchReasons: Array<string> = [];
295 let reconcileCount = 0;
296 const layer = Layer.mergeAll(
297 DesktopAppSettings.layerTest({
298 ...DesktopAppSettings.DEFAULT_DESKTOP_SETTINGS,
299 wslBackendEnabled: true,
300 wslOnly: false,
301 }),
302 DesktopWslEnvironment.layerTest({ isAvailable: true }),
303 layerWslBackend({
304 onReconcile: Effect.sync(() => {
305 reconcileCount += 1;
306 }),
307 }),
308 layerLifecycle(relaunchReasons),
309 layerUnusedLifecycleRuntime,
310 );
311
312 return Effect.gen(function* () {
313 const state = yield* invokeSetWslBackendEnabled(false);
314
315 assert.equal(state.enabled, false);
316 assert.equal(state.wslOnly, false);
317 assert.equal(reconcileCount, 1);
318 assert.deepEqual(relaunchReasons, []);
319 }).pipe(Effect.provide(layer));
320 });
321});