1import { assert, describe, it } from "@effect/vitest";
2import * as NodeServices from "@effect/platform-node/NodeServices";
3import * as HostProcess from "@t3tools/shared/HostProcess";
4import * as FileSystem from "effect/FileSystem";
5import * as Path from "effect/Path";
6import * as Effect from "effect/Effect";
7import * as Layer from "effect/Layer";
8import * as Option from "effect/Option";
9import { vi } from "vite-plus/test";
10
11import type * as Electron from "electron";
12
13const { focusedWebContents, ownerWindow } = vi.hoisted(() => ({
14 focusedWebContents: vi.fn(),
15 ownerWindow: vi.fn(),
16}));
17vi.mock("electron", () => ({
18 webContents: { getFocusedWebContents: focusedWebContents },
19 BrowserWindow: { fromWebContents: ownerWindow },
20}));
21
22import * as DesktopBackendConfiguration from "../../backend/DesktopBackendConfiguration.ts";
23import * as DesktopBackendManager from "../../backend/DesktopBackendManager.ts";
24import * as DesktopBackendPool from "../../backend/DesktopBackendPool.ts";
25import * as ElectronDialog from "../../electron/ElectronDialog.ts";
26import * as ElectronWindow from "../../electron/ElectronWindow.ts";
27import * as DesktopAppSettings from "../../settings/DesktopAppSettings.ts";
28import {
29 getLocalEnvironmentBootstraps,
30 getWindowFullscreenState,
31 pasteAsText,
32 pickProjectFavicon,
33 probeRemoteEditors,
34} from "./window.ts";
35
36const readyWslConfig: DesktopBackendManager.DesktopBackendStartConfig = {
37 executablePath: "wsl.exe",
38 args: ["-d", "Ubuntu", "--", "node", "/app/bin.mjs"],
39 entryPath: "/app/bin.mjs",
40 cwd: "/app",
41 env: {},
42 extendEnv: false,
43 bootstrap: {
44 mode: "desktop",
45 noBrowser: true,
46 port: 3774,
47 host: "0.0.0.0",
48 desktopBootstrapToken: "bootstrap-token",
49 tailscaleServeEnabled: false,
50 tailscaleServePort: 443,
51 },
52 bootstrapDelivery: "stdin",
53 httpBaseUrl: new URL("http://127.0.0.1:3774"),
54 captureOutput: true,
55 preflightFailure: Option.none(),
56 runningDistro: "Ubuntu",
57};
58
59const defaultWslInstance: DesktopBackendManager.DesktopBackendInstance = {
60 id: DesktopBackendManager.BackendInstanceId("wsl:default"),
61 label: Effect.succeed("WSL (default distro)"),
62 start: Effect.void,
63 stop: () => Effect.void,
64 currentConfig: Effect.succeedSome(readyWslConfig),
65 snapshot: Effect.succeed({
66 desiredRunning: true,
67 ready: true,
68 activePid: Option.some(123),
69 restartAttempt: 0,
70 restartScheduled: false,
71 }),
72 waitForReady: () => Effect.succeed(true),
73};
74
75const backendConfigurationLayer = Layer.succeed(
76 DesktopBackendConfiguration.DesktopBackendConfiguration,
77 {
78 resolvePrimary: Effect.die("unexpected resolvePrimary"),
79 resolvePrimaryLabel: Effect.succeed("Windows"),
80 resolveWsl: () => Effect.die("unexpected resolveWsl"),
81 currentBootstrapToken: Effect.succeed("current-window-token"),
82 } satisfies DesktopBackendConfiguration.DesktopBackendConfiguration["Service"],
83);
84
85const bootstrapsLayer = (instances: ReadonlyArray<DesktopBackendManager.DesktopBackendInstance>) =>
86 Layer.merge(DesktopBackendPool.layerTest([...instances]), backendConfigurationLayer);
87
88describe("getLocalEnvironmentBootstraps", () => {
89 it.effect("publishes the concrete running distro without replacing the stable instance id", () =>
90 Effect.gen(function* () {
91 const result = yield* getLocalEnvironmentBootstraps.handler();
92
93 assert.deepEqual(result, [
94 {
95 id: "wsl:default",
96 label: "WSL (Ubuntu)",
97 runningDistro: "Ubuntu",
98 httpBaseUrl: "http://127.0.0.1:3774/",
99 wsBaseUrl: "ws://127.0.0.1:3774/",
100 bootstrapToken: "bootstrap-token",
101 },
102 ]);
103 }).pipe(Effect.provide(bootstrapsLayer([defaultWslInstance]))),
104 );
105
106 it.effect("hands out the current window's token to a backend launched with the secret", () =>
107 Effect.gen(function* () {
108 const result = yield* getLocalEnvironmentBootstraps.handler();
109
110 assert.deepEqual(result, [
111 {
112 id: "wsl:default",
113 label: "WSL (Ubuntu)",
114 runningDistro: "Ubuntu",
115 httpBaseUrl: "http://127.0.0.1:3774/",
116 wsBaseUrl: "ws://127.0.0.1:3774/",
117 bootstrapToken: "current-window-token",
118 },
119 ]);
120 }).pipe(
121 Effect.provide(
122 bootstrapsLayer([
123 {
124 ...defaultWslInstance,
125 currentConfig: Effect.succeedSome({
126 ...readyWslConfig,
127 bootstrap: {
128 ...readyWslConfig.bootstrap,
129 desktopBootstrapSecret: "desktop-secret",
130 },
131 }),
132 },
133 ]),
134 ),
135 ),
136 );
137
138 it.effect("publishes a pending bootstrap only while a transient retry is scheduled", () => {
139 const retryingConfig: DesktopBackendManager.DesktopBackendStartConfig = {
140 ...readyWslConfig,
141 preflightFailure: Option.some({
142 reason: "WSL probe timed out",
143 fatal: false,
144 retryLimit: 12,
145 }),
146 };
147 const retryingInstance: DesktopBackendManager.DesktopBackendInstance = {
148 ...defaultWslInstance,
149 currentConfig: Effect.succeedSome(retryingConfig),
150 snapshot: Effect.succeed({
151 desiredRunning: true,
152 ready: false,
153 activePid: Option.none(),
154 restartAttempt: 2,
155 restartScheduled: true,
156 }),
157 };
158
159 return Effect.gen(function* () {
160 const result = yield* getLocalEnvironmentBootstraps.handler();
161 assert.deepEqual(result, [
162 {
163 id: "wsl:default",
164 label: "WSL (default distro)",
165 runningDistro: null,
166 httpBaseUrl: null,
167 wsBaseUrl: null,
168 },
169 ]);
170 }).pipe(Effect.provide(bootstrapsLayer([retryingInstance])));
171 });
172
173 it.effect("omits a bounded transient bootstrap after retries stop", () => {
174 const stoppedInstance: DesktopBackendManager.DesktopBackendInstance = {
175 ...defaultWslInstance,
176 currentConfig: Effect.succeedSome({
177 ...readyWslConfig,
178 preflightFailure: Option.some({
179 reason: "WSL probe timed out",
180 fatal: false,
181 retryLimit: 12,
182 }),
183 }),
184 snapshot: Effect.succeed({
185 desiredRunning: false,
186 ready: false,
187 activePid: Option.none(),
188 restartAttempt: 12,
189 restartScheduled: false,
190 }),
191 };
192
193 return Effect.gen(function* () {
194 const result = yield* getLocalEnvironmentBootstraps.handler();
195 assert.deepEqual(result, []);
196 }).pipe(Effect.provide(bootstrapsLayer([stoppedInstance])));
197 });
198});
199
200describe("getWindowFullscreenState", () => {
201 it.effect("reads the current native window state", () => {
202 const window = { isFullScreen: () => true } as Electron.BrowserWindow;
203
204 return Effect.gen(function* () {
205 assert.isTrue(yield* getWindowFullscreenState.handler());
206 }).pipe(
207 Effect.provide(
208 Layer.mock(ElectronWindow.ElectronWindow)({
209 currentMainOrFirst: Effect.succeedSome(window),
210 }),
211 ),
212 );
213 });
214});
215
216describe("pasteAsText", () => {
217 it.effect(
218 "pastes into the focused guest only after the main renderer acknowledges the menu action",
219 () => {
220 const paste = vi.fn();
221 const mainPaste = vi.fn();
222 const window = {
223 webContents: { id: 42, paste: mainPaste },
224 isDestroyed: () => false,
225 } as unknown as Electron.BrowserWindow;
226 focusedWebContents.mockReturnValue({ paste, isDestroyed: () => false });
227 ownerWindow.mockReturnValue(window);
228
229 return Effect.gen(function* () {
230 yield* pasteAsText.handler(undefined, { sender: { id: 42 } });
231 assert.equal(paste.mock.calls.length, 1);
232 assert.equal(mainPaste.mock.calls.length, 0);
233
234 yield* pasteAsText.handler(undefined, { sender: { id: 99 } });
235 assert.equal(paste.mock.calls.length, 1);
236 ownerWindow.mockReturnValue({}); // A focused PiP/other BrowserWindow.
237 yield* pasteAsText.handler(undefined, { sender: { id: 42 } });
238 assert.equal(paste.mock.calls.length, 1);
239 ownerWindow.mockReturnValue(null); // Detached contents.
240 yield* pasteAsText.handler(undefined, { sender: { id: 42 } });
241 assert.equal(paste.mock.calls.length, 1);
242 ownerWindow.mockReturnValue(window);
243 focusedWebContents.mockReturnValue({ paste, isDestroyed: () => true });
244 yield* pasteAsText.handler(undefined, { sender: { id: 42 } });
245 assert.equal(paste.mock.calls.length, 1);
246 focusedWebContents.mockReturnValue(null);
247 yield* pasteAsText.handler(undefined, { sender: { id: 42 } });
248 assert.equal(paste.mock.calls.length, 1);
249 }).pipe(
250 Effect.provide(
251 Layer.mock(ElectronWindow.ElectronWindow)({
252 main: Effect.succeedSome(window),
253 }),
254 ),
255 );
256 },
257 );
258});
259
260describe("pickProjectFavicon", () => {
261 const pickerLayer = (
262 pickFiles: () => Effect.Effect<Array<string>>,
263 settings?: DesktopAppSettings.DesktopSettings,
264 ) =>
265 Layer.mergeAll(
266 Layer.mock(ElectronDialog.ElectronDialog)({ pickFiles }),
267 Layer.mock(ElectronWindow.ElectronWindow)({
268 focusedMainOrFirst: Effect.succeedNone,
269 }),
270 DesktopAppSettings.layerTest(settings),
271 );
272
273 it.effect("opens a single-image picker from the project directory", () =>
274 Effect.gen(function* () {
275 const pickFiles = vi.fn(() => Effect.succeed(["/pictures/icon.png"]));
276 const result = yield* pickProjectFavicon
277 .handler("/project")
278 .pipe(Effect.provide(pickerLayer(pickFiles)));
279
280 assert.strictEqual(result, "/pictures/icon.png");
281 assert.deepEqual(pickFiles.mock.calls, [
282 [
283 {
284 owner: Option.none(),
285 defaultPath: Option.some("/project"),
286 multiple: false,
287 filters: [
288 {
289 name: "Images",
290 extensions: ["avif", "gif", "ico", "jpeg", "jpg", "png", "svg", "webp", "icns"],
291 },
292 ],
293 },
294 ],
295 ]);
296 }),
297 );
298
299 it.effect("does not open a picker while the local environment is off", () =>
300 Effect.gen(function* () {
301 const pickFiles = vi.fn(() => Effect.succeed(["/pictures/icon.png"]));
302 const result = yield* pickProjectFavicon.handler("/project").pipe(
303 Effect.provide(
304 pickerLayer(pickFiles, {
305 ...DesktopAppSettings.DEFAULT_DESKTOP_SETTINGS,
306 localEnvironmentEnabled: false,
307 }),
308 ),
309 );
310
311 assert.strictEqual(result, null);
312 assert.strictEqual(pickFiles.mock.calls.length, 0);
313 }),
314 );
315});
316
317it.effect.skipIf(HostProcess.Platform.defaultValue() === "win32")(
318 "finds remote editors installed without PATH launchers",
319 () =>
320 Effect.gen(function* () {
321 const fs = yield* FileSystem.FileSystem;
322 const path = yield* Path.Path;
323 const home = yield* fs.makeTempDirectoryScoped({ prefix: "t3-remote-editors-" });
324 for (const app of ["Cursor", "Visual Studio Code", "WebStorm"]) {
325 const executable = path.join(
326 home,
327 "Applications",
328 `${app}.app`,
329 app === "WebStorm" ? "Contents/MacOS/webstorm" : "Contents/Resources/app/bin/code",
330 );
331 yield* fs.makeDirectory(path.dirname(executable), { recursive: true });
332 yield* fs.writeFileString(executable, "#!/bin/sh\n");
333 yield* fs.chmod(executable, 0o755);
334 }
335 const editors = yield* probeRemoteEditors.handler(undefined).pipe(
336 Effect.provideService(HostProcess.Environment, {
337 HOME: home,
338 PATH: path.join(home, "empty"),
339 }),
340 Effect.provideService(HostProcess.Platform, "darwin"),
341 );
342 assert.include(editors, "cursor");
343 assert.include(editors, "vscode");
344 assert.include(editors, "webstorm");
345 }).pipe(Effect.scoped, Effect.provide(NodeServices.layer)),
346);