1import * as NodeServices from "@effect/platform-node/NodeServices";
2import { assert, describe, it } from "@effect/vitest";
3import * as Effect from "effect/Effect";
4import * as FileSystem from "effect/FileSystem";
5import * as Layer from "effect/Layer";
6import * as Path from "effect/Path";
7import * as PlatformError from "effect/PlatformError";
8import * as Ref from "effect/Ref";
9import * as Scope from "effect/Scope";
10
11import * as DesktopConfig from "../app/DesktopConfig.ts";
12import * as DesktopEnvironment from "../app/DesktopEnvironment.ts";
13import * as DesktopWslServerTree from "./DesktopWslServerTree.ts";
14
15// The service reads packaged Windows roots through the (asar-aware, in
16// Electron) fs, so a plain directory named server.asar exercises the full
17// extraction path under plain Node.
18
19const layerEnvironment = (input: {
20 readonly baseDir: string;
21 readonly resourcesPath: string;
22 readonly appVersion?: string;
23 readonly isPackaged?: boolean;
24}) =>
25 DesktopEnvironment.layer({
26 dirname: "/repo/apps/desktop/src",
27 homeDirectory: input.baseDir,
28 platform: "win32",
29 processArch: "x64",
30 appVersion: input.appVersion ?? "1.2.3",
31 appPath: "/repo",
32 isPackaged: input.isPackaged ?? true,
33 resourcesPath: input.resourcesPath,
34 runningUnderArm64Translation: false,
35 }).pipe(
36 Layer.provide(
37 Layer.mergeAll(
38 NodeServices.layer,
39 DesktopConfig.layerTest({
40 T3CODE_HOME: input.baseDir,
41 T3CODE_MODE: "desktop",
42 }),
43 ),
44 ),
45 );
46
47const withTempDir = <A, E, R>(
48 run: (tempDir: string) => Effect.Effect<A, E, R>,
49): Effect.Effect<
50 A,
51 E | PlatformError.PlatformError,
52 FileSystem.FileSystem | Exclude<R, Scope.Scope>
53> =>
54 Effect.gen(function* () {
55 const fileSystem = yield* FileSystem.FileSystem;
56 const tempDir = yield* fileSystem.makeTempDirectoryScoped({
57 prefix: "t3-wsl-server-tree-test-",
58 });
59 return yield* run(tempDir);
60 }).pipe(Effect.scoped);
61
62const ensureWith = (input: {
63 readonly baseDir: string;
64 readonly resourcesPath: string;
65 readonly appVersion?: string;
66 readonly isPackaged?: boolean;
67}) =>
68 Effect.gen(function* () {
69 const tree = yield* DesktopWslServerTree.DesktopWslServerTree;
70 return yield* tree.ensure;
71 }).pipe(
72 Effect.provide(DesktopWslServerTree.layer.pipe(Layer.provideMerge(layerEnvironment(input)))),
73 );
74
75describe("DesktopWslServerTree", () => {
76 it.effect("bounds entry work across an eight-way nested tree", () =>
77 Effect.gen(function* () {
78 const active = yield* Ref.make(0);
79 const maxActive = yield* Ref.make(0);
80 const visited = yield* Ref.make(0);
81
82 yield* DesktopWslServerTree.forEachBoundedTree([{ depth: 0, id: "root" }], (node) =>
83 Effect.acquireUseRelease(
84 Effect.gen(function* () {
85 const current = yield* Ref.updateAndGet(active, (count) => count + 1);
86 yield* Ref.update(maxActive, (maximum) => Math.max(maximum, current));
87 yield* Ref.update(visited, (count) => count + 1);
88 }),
89 () =>
90 Effect.gen(function* () {
91 // Give every task in the current batch a chance to overlap.
92 yield* Effect.yieldNow;
93 if (node.depth === 4) return [];
94 return Array.from({ length: 8 }, (_, index) => ({
95 depth: node.depth + 1,
96 id: `${node.id}.${String(index)}`,
97 }));
98 }),
99 () => Ref.update(active, (count) => count - 1),
100 ),
101 );
102
103 assert.equal(yield* Ref.get(active), 0);
104 assert.equal(yield* Ref.get(maxActive), 8);
105 assert.equal(yield* Ref.get(visited), 4_681);
106 }),
107 );
108
109 it.effect("returns the server root unchanged when it is a plain directory (dev)", () =>
110 withTempDir((tempDir) =>
111 Effect.gen(function* () {
112 const result = yield* ensureWith({
113 baseDir: tempDir,
114 resourcesPath: tempDir,
115 isPackaged: false,
116 });
117 assert.isTrue(result.ok);
118 assert.isFalse(result.ok && result.root.endsWith(".asar"));
119 }),
120 ).pipe(Effect.provide(NodeServices.layer)),
121 );
122
123 it.effect("extracts an archive root into a version-keyed state directory", () =>
124 withTempDir((tempDir) =>
125 Effect.gen(function* () {
126 const fileSystem = yield* FileSystem.FileSystem;
127 const path = yield* Path.Path;
128 const serverRoot = path.join(tempDir, "resources", "server.asar");
129 yield* fileSystem.makeDirectory(path.join(serverRoot, "apps/server/dist"), {
130 recursive: true,
131 });
132 yield* fileSystem.writeFileString(
133 path.join(serverRoot, "apps/server/dist/bin.mjs"),
134 "server-entry",
135 );
136 yield* fileSystem.makeDirectory(path.join(serverRoot, "node_modules/effect"), {
137 recursive: true,
138 });
139 yield* fileSystem.writeFileString(
140 path.join(serverRoot, "node_modules/effect/package.json"),
141 "{}",
142 );
143
144 const result = yield* ensureWith({
145 baseDir: tempDir,
146 resourcesPath: path.join(tempDir, "resources"),
147 });
148
149 assert.isTrue(result.ok);
150 const root = result.ok ? result.root : "";
151 assert.include(root, path.join("wsl-server-tree", "1.2.3"));
152 const entry = yield* fileSystem.readFileString(path.join(root, "apps/server/dist/bin.mjs"));
153 assert.equal(entry, "server-entry");
154 const dep = yield* fileSystem.exists(path.join(root, "node_modules/effect/package.json"));
155 assert.isTrue(dep);
156 const marker = yield* fileSystem.readFileString(
157 path.join(root, "t3code-wsl-server-tree.json"),
158 );
159 assert.include(marker, '"version":"1.2.3"');
160 }),
161 ).pipe(Effect.provide(NodeServices.layer)),
162 );
163
164 it.effect("serializes concurrent extraction callers and publishes one complete tree", () =>
165 withTempDir((tempDir) =>
166 Effect.gen(function* () {
167 const fileSystem = yield* FileSystem.FileSystem;
168 const path = yield* Path.Path;
169 const resourcesPath = path.join(tempDir, "resources");
170 const serverRoot = path.join(resourcesPath, "server.asar");
171 yield* fileSystem.makeDirectory(path.join(serverRoot, "apps/server/dist"), {
172 recursive: true,
173 });
174 yield* fileSystem.writeFileString(
175 path.join(serverRoot, "apps/server/dist/bin.mjs"),
176 "server-entry",
177 );
178
179 const results = yield* Effect.gen(function* () {
180 const tree = yield* DesktopWslServerTree.DesktopWslServerTree;
181 return yield* Effect.all([tree.ensure, tree.ensure], { concurrency: "unbounded" });
182 }).pipe(
183 Effect.provide(
184 DesktopWslServerTree.layer.pipe(
185 Layer.provideMerge(layerEnvironment({ baseDir: tempDir, resourcesPath })),
186 ),
187 ),
188 );
189
190 assert.isTrue(results.every((result) => result.ok));
191 const roots = results.flatMap((result) => (result.ok ? [result.root] : []));
192 assert.lengthOf(new Set(roots), 1);
193 assert.equal(
194 yield* fileSystem.readFileString(path.join(roots[0] ?? "", "apps/server/dist/bin.mjs")),
195 "server-entry",
196 );
197 }),
198 ).pipe(Effect.provide(NodeServices.layer)),
199 );
200
201 it.effect("reuses a completed extraction instead of copying again", () =>
202 withTempDir((tempDir) =>
203 Effect.gen(function* () {
204 const fileSystem = yield* FileSystem.FileSystem;
205 const path = yield* Path.Path;
206 const serverRoot = path.join(tempDir, "resources", "server.asar");
207 yield* fileSystem.makeDirectory(path.join(serverRoot, "apps/server/dist"), {
208 recursive: true,
209 });
210 yield* fileSystem.writeFileString(path.join(serverRoot, "apps/server/dist/bin.mjs"), "v1");
211
212 const first = yield* ensureWith({
213 baseDir: tempDir,
214 resourcesPath: path.join(tempDir, "resources"),
215 });
216 assert.isTrue(first.ok);
217
218 // Mutate the source; a reused tree must keep the first copy.
219 yield* fileSystem.writeFileString(
220 path.join(serverRoot, "apps/server/dist/bin.mjs"),
221 "v2-should-not-appear",
222 );
223 const second = yield* ensureWith({
224 baseDir: tempDir,
225 resourcesPath: path.join(tempDir, "resources"),
226 });
227 assert.isTrue(second.ok);
228 const root = second.ok ? second.root : "";
229 const entry = yield* fileSystem.readFileString(path.join(root, "apps/server/dist/bin.mjs"));
230 assert.equal(entry, "v1");
231 }),
232 ).pipe(Effect.provide(NodeServices.layer)),
233 );
234
235 it.effect("sweeps stale version directories and leftover partials after extraction", () =>
236 withTempDir((tempDir) =>
237 Effect.gen(function* () {
238 const fileSystem = yield* FileSystem.FileSystem;
239 const path = yield* Path.Path;
240 const serverRoot = path.join(tempDir, "resources", "server.asar");
241 yield* fileSystem.makeDirectory(path.join(serverRoot, "apps/server/dist"), {
242 recursive: true,
243 });
244 yield* fileSystem.writeFileString(path.join(serverRoot, "apps/server/dist/bin.mjs"), "x");
245
246 // T3CODE_HOME is set to tempDir, so the desktop state dir resolves to
247 // <tempDir>/userdata (no .t3 segment).
248 const treeRoot = path.join(tempDir, "userdata", "wsl-server-tree");
249 yield* fileSystem.makeDirectory(path.join(treeRoot, "1.0.0"), { recursive: true });
250 yield* fileSystem.makeDirectory(path.join(treeRoot, "1.2.3.partial"), { recursive: true });
251
252 const result = yield* ensureWith({
253 baseDir: tempDir,
254 resourcesPath: path.join(tempDir, "resources"),
255 });
256 assert.isTrue(result.ok);
257 assert.isFalse(yield* fileSystem.exists(path.join(treeRoot, "1.0.0")));
258 assert.isFalse(yield* fileSystem.exists(path.join(treeRoot, "1.2.3.partial")));
259 assert.isTrue(yield* fileSystem.exists(path.join(treeRoot, "1.2.3")));
260 }),
261 ).pipe(Effect.provide(NodeServices.layer)),
262 );
263
264 it.effect("re-extracts when the app version changes", () =>
265 withTempDir((tempDir) =>
266 Effect.gen(function* () {
267 const fileSystem = yield* FileSystem.FileSystem;
268 const path = yield* Path.Path;
269 const serverRoot = path.join(tempDir, "resources", "server.asar");
270 yield* fileSystem.makeDirectory(path.join(serverRoot, "apps/server/dist"), {
271 recursive: true,
272 });
273 yield* fileSystem.writeFileString(path.join(serverRoot, "apps/server/dist/bin.mjs"), "old");
274
275 const first = yield* ensureWith({
276 baseDir: tempDir,
277 resourcesPath: path.join(tempDir, "resources"),
278 appVersion: "1.2.3",
279 });
280 assert.isTrue(first.ok);
281
282 yield* fileSystem.writeFileString(path.join(serverRoot, "apps/server/dist/bin.mjs"), "new");
283 const second = yield* ensureWith({
284 baseDir: tempDir,
285 resourcesPath: path.join(tempDir, "resources"),
286 appVersion: "1.2.4",
287 });
288 assert.isTrue(second.ok);
289 const root = second.ok ? second.root : "";
290 assert.include(root, "1.2.4");
291 const entry = yield* fileSystem.readFileString(path.join(root, "apps/server/dist/bin.mjs"));
292 assert.equal(entry, "new");
293 // The previous version's tree is gone.
294 const treeRoot = path.dirname(root);
295 assert.isFalse(yield* fileSystem.exists(path.join(treeRoot, "1.2.3")));
296 }),
297 ).pipe(Effect.provide(NodeServices.layer)),
298 );
299
300 it.effect("removes the legacy Windows extraction tree without preparing a fallback", () =>
301 withTempDir((tempDir) =>
302 Effect.gen(function* () {
303 const fileSystem = yield* FileSystem.FileSystem;
304 const path = yield* Path.Path;
305 const resourcesPath = path.join(tempDir, "resources");
306 const treeRoot = path.join(tempDir, "userdata", "wsl-server-tree");
307 yield* fileSystem.makeDirectory(path.join(treeRoot, "1.2.3"), { recursive: true });
308 yield* fileSystem.writeFileString(path.join(treeRoot, "1.2.3", "legacy"), "old");
309
310 yield* Effect.gen(function* () {
311 const tree = yield* DesktopWslServerTree.DesktopWslServerTree;
312 yield* tree.cleanupLegacy;
313 }).pipe(
314 Effect.provide(
315 DesktopWslServerTree.layer.pipe(
316 Layer.provideMerge(layerEnvironment({ baseDir: tempDir, resourcesPath })),
317 ),
318 ),
319 );
320
321 assert.isFalse(yield* fileSystem.exists(treeRoot));
322 }),
323 ).pipe(Effect.provide(NodeServices.layer)),
324 );
325
326 it.effect("re-extracts after legacy cleanup partially deletes the completed tree", () =>
327 withTempDir((tempDir) =>
328 Effect.gen(function* () {
329 const fileSystem = yield* FileSystem.FileSystem;
330 const path = yield* Path.Path;
331 const resourcesPath = path.join(tempDir, "resources");
332 const serverRoot = path.join(resourcesPath, "server.asar");
333 const sourceEntryPath = path.join(serverRoot, "apps/server/dist/bin.mjs");
334 yield* fileSystem.makeDirectory(path.dirname(sourceEntryPath), { recursive: true });
335 yield* fileSystem.writeFileString(sourceEntryPath, "fresh-server-entry");
336
337 const initial = yield* ensureWith({ baseDir: tempDir, resourcesPath });
338 assert.isTrue(initial.ok);
339 const versionDir = initial.ok ? initial.root : "";
340 const treeRoot = path.dirname(versionDir);
341 const extractedEntryPath = path.join(versionDir, "apps/server/dist/bin.mjs");
342 let cleanupFailed = false;
343 const layerPartialCleanupFileSystem = Layer.effect(
344 FileSystem.FileSystem,
345 Effect.gen(function* () {
346 const realFileSystem = yield* FileSystem.FileSystem;
347 return {
348 ...realFileSystem,
349 remove: (target, options) =>
350 String(target) === treeRoot && options?.recursive === true && !cleanupFailed
351 ? Effect.gen(function* () {
352 cleanupFailed = true;
353 yield* realFileSystem.remove(extractedEntryPath);
354 return yield* PlatformError.systemError({
355 _tag: "PermissionDenied",
356 module: "FileSystem",
357 method: "remove",
358 pathOrDescriptor: treeRoot,
359 description: "simulated partial legacy cleanup",
360 });
361 })
362 : realFileSystem.remove(target, options),
363 } satisfies FileSystem.FileSystem;
364 }),
365 ).pipe(Layer.provide(NodeServices.layer));
366
367 const result = yield* Effect.gen(function* () {
368 const tree = yield* DesktopWslServerTree.DesktopWslServerTree;
369 yield* tree.cleanupLegacy;
370 return yield* tree.ensure;
371 }).pipe(
372 Effect.provide(
373 DesktopWslServerTree.layer.pipe(
374 Layer.provideMerge(layerEnvironment({ baseDir: tempDir, resourcesPath })),
375 Layer.provideMerge(layerPartialCleanupFileSystem),
376 ),
377 ),
378 );
379
380 assert.isTrue(cleanupFailed);
381 assert.isTrue(result.ok);
382 assert.equal(yield* fileSystem.readFileString(extractedEntryPath), "fresh-server-entry");
383 }),
384 ).pipe(Effect.provide(NodeServices.layer)),
385 );
386
387 it.effect("reports a retryable failure when the archive cannot be read", () =>
388 withTempDir((tempDir) =>
389 Effect.gen(function* () {
390 const fileSystem = yield* FileSystem.FileSystem;
391 const path = yield* Path.Path;
392 const result = yield* ensureWith({
393 baseDir: tempDir,
394 // resources dir exists but server.asar does not
395 resourcesPath: path.join(tempDir, "resources"),
396 });
397 assert.isFalse(result.ok);
398 if (!result.ok) {
399 assert.include(result.reason, "could not be extracted");
400 assert.isFalse(result.fatal);
401 }
402 const treeRoot = path.join(tempDir, "userdata", "wsl-server-tree");
403 const leftovers = yield* fileSystem
404 .readDirectory(treeRoot)
405 .pipe(Effect.orElseSucceed(() => []));
406 assert.deepStrictEqual(leftovers, []);
407 }),
408 ).pipe(Effect.provide(NodeServices.layer)),
409 );
410});