Skip to content
Source
Sign inSign up
Open History to see the last commit

DesktopWslEnvironment.test.ts

461 lines18.0 KB

1// @effect-diagnostics nodeBuiltinImport:off - the executed suite runs the generated install script through a real POSIX shell.
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 { afterAll, expect } from "vite-plus/test";
8import * as NodeChildProcess from "node:child_process";
9​
10import { ManagedPolicyFileError } from "@t3tools/shared/managedPolicyFile";
11​
12import * as DesktopManagedPolicy from "../app/DesktopManagedPolicy.ts";
13import {
14 buildWslRuntimeInstallScript,
15 makeIsAvailable,
16 buildWslRuntimeInvalidateScript,
17 buildWslRuntimePruneScript,
18 buildWslRuntimeProbeScript,
19 parseResolvedPath,
20 parseWslRuntimeRoot,
21} from "./DesktopWslEnvironment.ts";
22​
23// The install script only fails the way this file cares about when a real shell
24// runs it, so find one that has the tools it needs: bash directly on Linux, and
25// the WSL distro on a Windows dev box, where Git Bash ships no flock. Anywhere
26// else the executed suite skips and the generated-text assertions stand alone.
27const REQUIRED_SHELL_TOOLS = ["flock", "sha256sum", "tar", "mktemp"] as const;
28​
29const posixShellRunner = (() => {
30 const candidates = [
31 { file: "bash", args: [] as ReadonlyArray<string> },
32 { file: "wsl.exe", args: ["-e", "bash"] as ReadonlyArray<string> },
33 ];
34 const probe = [
35 "[ -d /proc/1 ] || exit 1",
36 ...REQUIRED_SHELL_TOOLS.map((tool) => `command -v ${tool} >/dev/null || exit 1`),
37 ].join("\n");
38 return (
39 candidates.find((candidate) => {
40 const result = NodeChildProcess.spawnSync(candidate.file, [...candidate.args, "-c", probe], {
41 encoding: "utf8",
42 });
43 return result.status === 0;
44 }) ?? null
45 );
46})();
47​
48const runShell = (script: string) => {
49 if (posixShellRunner === null) throw new Error("no POSIX shell runner available");
50 // Pass the script on stdin, as production does, to avoid matching its own /proc scan.
51 const result = NodeChildProcess.spawnSync(
52 posixShellRunner.file,
53 [...posixShellRunner.args, "-s"],
54 { input: script, encoding: "utf8" },
55 );
56 return { status: result.status, stdout: result.stdout ?? "", stderr: result.stderr ?? "" };
57};
58​
59const sh = (value: string) => `'${value.replaceAll("'", "'\\''")}'`;
60​
61const awaitHolder = (condition: string) =>
62 `i=0; until ${condition}; do i=$((i + 1)); [ "$i" -lt 500 ] || exit 1; sleep 0.01; done`;
63​
64// Use runtime-entry argv[0] for /proc detection; an open pipe keeps it alive only while this script runs.
65const holdRuntimeBusy = (entry: string) =>
66 [
67 `exec 7> >(exec -a ${entry} cat >/dev/null 2>&1)`,
68 awaitHolder(`grep -qF -- ${entry} /proc/[0-9]*/cmdline 2>/dev/null`),
69 ].join("\n");
70​
71const holdInstallLock = (lock: string) =>
72 [
73 `exec 6> >(exec 9> ${lock}; flock -x 9; cat >/dev/null 2>&1)`,
74 awaitHolder(`! flock -n ${lock} true`),
75 ].join("\n");
76​
77const readField = (stdout: string, field: string) => {
78 const line = stdout.split("\n").find((candidate) => candidate.startsWith(`${field}:`));
79 if (line === undefined) throw new Error(`missing ${field} in fixture output: ${stdout}`);
80 return line.slice(field.length + 1).trim();
81};
82​
83const SERVER_ENTRY_SOURCE = '#!/bin/sh\necho "t3code wsl runtime test server 0.0.0"\n';
84​
85describe("WSL runtime cache", () => {
86 it.each([
87 [
88 "install",
89 (id: string) => buildWslRuntimeInstallScript("/runtime.tar.gz", id, "b".repeat(64)),
90 ],
91 ["prune", buildWslRuntimePruneScript],
92 ["invalidate", buildWslRuntimeInvalidateScript],
93 ] as const)("sanitizes cache ids in the %s script", (_, buildScript) => {
94 const runtimeId = "1.2.3/x64; touch /tmp/nope";
95 const script = buildScript(runtimeId);
96 expect(script).toContain("/1.2.3_x64__touch__tmp_nope");
97 expect(script).not.toContain(runtimeId);
98 });
99});
100​
101describe.skipIf(posixShellRunner === null)("WSL runtime install script (executed)", () => {
102 const fixtures: Array<string> = [];
103​
104 afterAll(() => {
105 for (const work of fixtures) runShell(`set -eu\nrm -rf ${sh(work)}`);
106 fixtures.length = 0;
107 });
108​
109 const createFixture = (serverEntry = SERVER_ENTRY_SOURCE) => {
110 const result = runShell(
111 [
112 "set -eu",
113 "work=$(mktemp -d)",
114 'stage="$work/stage/t3-0.0.0-linux-x64"',
115 'mkdir -p "$stage/node_modules/node-pty/build/Release" "$work/home"',
116 `printf '%s' ${sh(serverEntry)} > "$stage/t3"`,
117 'chmod +x "$stage/t3"',
118 `printf '%s' 'pty-native-payload' > "$stage/node_modules/node-pty/build/Release/pty.node"`,
119 `tar -czf "$work/wsl-runtime.tar.gz" -C "$work/stage" t3-0.0.0-linux-x64`,
120 `printf 'work:%s\\n' "$work"`,
121 `printf 'archiveSha:%s\\n' "$(sha256sum "$work/wsl-runtime.tar.gz" | cut -d ' ' -f 1)"`,
122 ].join("\n"),
123 );
124 expect(result.status, result.stderr).toBe(0);
125​
126 const work = readField(result.stdout, "work");
127 fixtures.push(work);
128 const archivePath = `${work}/wsl-runtime.tar.gz`;
129 const archiveSha = readField(result.stdout, "archiveSha");
130 const runtimeId = `sha256-${archiveSha}`;
131 // WSL does not inherit HOME; embed the fixture's override in the script.
132 const installScript = (archive = archivePath, sha = archiveSha) =>
133 [
134 `HOME=${sh(`${work}/home`)}`,
135 "export HOME",
136 buildWslRuntimeInstallScript(archive, runtimeId, sha),
137 ].join("\n");
138 return {
139 work,
140 archivePath,
141 archiveSha,
142 runtimeId,
143 runtimeParent: `${work}/home/.t3/wsl-runtime`,
144 runtimeRoot: `${work}/home/.t3/wsl-runtime/${runtimeId}`,
145 serverEntry: `${work}/home/.t3/wsl-runtime/${runtimeId}/t3`,
146 installScript,
147 install: (archive?: string, sha?: string) => runShell(installScript(archive, sha)),
148 };
149 };
150​
151 const probeFixture = (fixture: ReturnType<typeof createFixture>) =>
152 runShell(
153 [
154 `export HOME=${sh(`${fixture.work}/home`)}`,
155 'export NVM_DIR="$HOME/.nvm" FNM_DIR="$HOME/.fnm" VOLTA_HOME="$HOME/.volta"',
156 "bash() { (",
157 " command() {",
158 ' case "$*" in',
159 ' "-v node"|"-v mise"|"-v fnm"|"-v nodenv") return 1 ;;',
160 ' *) builtin command "$@" ;;',
161 " esac",
162 " }",
163 ' eval "$2"',
164 "); }",
165 buildWslRuntimeProbeScript(fixture.runtimeRoot),
166 ].join("\n"),
167 );
168​
169 it("discovers version-managed Node for providers with a standalone runtime", () => {
170 const fixture = createFixture();
171 expect(fixture.install().status).toBe(0);
172 const nodeBin = `${fixture.work}/home/.nvm/versions/node/v24.15.0/bin`;
173 const setup = runShell(
174 [
175 "set -eu",
176 `mkdir -p ${sh(nodeBin)}`,
177 `printf '%s' ${sh('#!/bin/sh\nprintf "linux-node-provider\\n"\n')} > ${sh(`${nodeBin}/node`)}`,
178 `chmod +x ${sh(`${nodeBin}/node`)}`,
179 ].join("\n"),
180 );
181 expect(setup.status, setup.stderr).toBe(0);
182​
183 const probe = probeFixture(fixture);
184​
185 expect(probe.status, probe.stderr).toBe(0);
186 const resolvedPath = parseResolvedPath(probe.stdout);
187 expect(resolvedPath?.split(":")).toContain(nodeBin);
188 const provider = runShell(`export PATH=${sh(resolvedPath ?? "")}\nnode provider.js`);
189 expect(provider.status, provider.stderr).toBe(0);
190 expect(provider.stdout).toBe("linux-node-provider\n");
191 });
192​
193 it("keeps the inherited PATH when bash is unavailable", () => {
194 const fixture = createFixture();
195 expect(fixture.install().status).toBe(0);
196 const probe = runShell(
197 [
198 "bash() { return 127; }",
199 'export PATH="/fixture/bin:/usr/bin:/bin"',
200 buildWslRuntimeProbeScript(fixture.runtimeRoot),
201 ].join("\n"),
202 );
203​
204 expect(probe.status, probe.stderr).toBe(0);
205 expect(parseResolvedPath(probe.stdout)).toBe("/fixture/bin:/usr/bin:/bin");
206 });
207​
208 it("reuses a warm cache without touching the archive", () => {
209 const fixture = createFixture();
210 expect(fixture.install().status).toBe(0);
211 expect(runShell(`set -eu\nrm ${sh(fixture.archivePath)}`).status).toBe(0);
212​
213 const warm = fixture.install();
214​
215 expect(warm.status, warm.stderr).toBe(0);
216 expect(parseWslRuntimeRoot(warm.stdout)).toBe(fixture.runtimeRoot);
217 });
218​
219 it("retains the loader error when a bundled executable needs a missing system library", () => {
220 const fixture = createFixture(
221 '#!/bin/sh\nprintf "error while loading shared libraries: libatomic.so.1: cannot open shared object file\\n" >&2\nexit 127\n',
222 );
223​
224 const result = fixture.install();
225​
226 expect(result.status).not.toBe(0);
227 expect(result.stderr).toContain("libatomic.so.1");
228 expect(parseWslRuntimeRoot(result.stdout)).toBeNull();
229 });
230​
231 it("reinstalls a cache whose executable was truncated", () => {
232 const fixture = createFixture();
233 expect(fixture.install().status).toBe(0);
234 expect(runShell(`set -eu\n: > ${sh(fixture.serverEntry)}`).status).toBe(0);
235​
236 const repaired = fixture.install();
237​
238 expect(repaired.status, repaired.stderr).toBe(0);
239 expect(parseWslRuntimeRoot(repaired.stdout)).toBe(fixture.runtimeRoot);
240 const restored = runShell(`set -eu\ncat ${sh(fixture.serverEntry)}`);
241 expect(restored.stdout).toBe(SERVER_ENTRY_SOURCE);
242 });
243​
244 it("reinstalls an executable that fails --version despite a matching recorded digest", () => {
245 const fixture = createFixture();
246 expect(fixture.install().status).toBe(0);
247 const brokenEntry = "#!/bin/sh\nexit 42\n";
248 const corrupt = runShell(
249 [
250 "set -eu",
251 `printf '%s' ${sh(brokenEntry)} > ${sh(fixture.serverEntry)}`,
252 `chmod +x ${sh(fixture.serverEntry)}`,
253 `sha256sum ${sh(fixture.serverEntry)} | cut -d ' ' -f 1 > ${sh(`${fixture.runtimeRoot}/.t3code-wsl-runtime-ready`)}`,
254 ].join("\n"),
255 );
256 expect(corrupt.status, corrupt.stderr).toBe(0);
257 expect(runShell(`${sh(fixture.serverEntry)} --version`).status).toBe(42);
258​
259 const repaired = fixture.install();
260 expect(repaired.status, repaired.stderr).toBe(0);
261 expect(parseWslRuntimeRoot(repaired.stdout)).toBe(fixture.runtimeRoot);
262 expect(runShell(`cat ${sh(fixture.serverEntry)}`).stdout).toBe(SERVER_ENTRY_SOURCE);
263 expect(runShell(`${sh(fixture.serverEntry)} --version`).status).toBe(0);
264 });
265​
266 it("falls back instead of launching a corrupted cache it cannot reinstall", () => {
267 const fixture = createFixture();
268 expect(fixture.install().status).toBe(0);
269 expect(runShell(`set -eu\n: > ${sh(fixture.serverEntry)}`).status).toBe(0);
270 expect(runShell(`set -eu\nrm ${sh(fixture.archivePath)}`).status).toBe(0);
271​
272 const broken = fixture.install();
273​
274 expect(broken.status).not.toBe(0);
275 expect(parseWslRuntimeRoot(broken.stdout)).toBeNull();
276 });
277​
278 it("extracts once when two installs race for the same cache", () => {
279 const fixture = createFixture();
280 const raced = runShell(
281 [
282 "set -eu",
283 `work=${sh(fixture.work)}`,
284 'mkdir -p "$work/bin"',
285 "real_tar=$(command -v tar)",
286 `printf '#!/bin/sh\\nprintf x >> "%s/tar-calls"\\nsleep 1\\nexec %s "$@"\\n' "$work" "$real_tar" > "$work/bin/tar"`,
287 'chmod +x "$work/bin/tar"',
288 ': > "$work/tar-calls"',
289 'PATH="$work/bin:$PATH"',
290 "export PATH",
291 `cat > "$work/install.sh" <<'T3CODE_INSTALL_SCRIPT'`,
292 fixture.installScript(),
293 "T3CODE_INSTALL_SCRIPT",
294 // Keep runtime roots out of both racers' argv so their /proc scans cannot match themselves.
295 'sh "$work/install.sh" > "$work/first.out" 2>&1 &',
296 "first=$!",
297 'sh "$work/install.sh" > "$work/second.out" 2>&1 &',
298 "second=$!",
299 "if wait $first; then first_status=0; else first_status=$?; fi",
300 "if wait $second; then second_status=0; else second_status=$?; fi",
301 `printf 'firstStatus:%s\\n' "$first_status"`,
302 `printf 'secondStatus:%s\\n' "$second_status"`,
303 `printf 'extractions:%s\\n' "$(wc -c < "$work/tar-calls" | tr -d ' ')"`,
304 `printf 'firstRoot:%s\\n' "$(sed -n 's/^runtimeRoot://p' "$work/first.out")"`,
305 `printf 'secondRoot:%s\\n' "$(sed -n 's/^runtimeRoot://p' "$work/second.out")"`,
306 ].join("\n"),
307 );
308​
309 expect(raced.status, raced.stderr).toBe(0);
310 expect(readField(raced.stdout, "firstStatus")).toBe("0");
311 expect(readField(raced.stdout, "secondStatus")).toBe("0");
312 expect(readField(raced.stdout, "extractions")).toBe("1");
313 expect(readField(raced.stdout, "firstRoot")).toBe(fixture.runtimeRoot);
314 expect(readField(raced.stdout, "secondRoot")).toBe(fixture.runtimeRoot);
315 });
316​
317 it("leaves no half-built cache when extraction fails", () => {
318 const fixture = createFixture();
319 const truncated = runShell(
320 [
321 "set -eu",
322 `work=${sh(fixture.work)}`,
323 'size=$(wc -c < "$work/wsl-runtime.tar.gz")',
324 'head -c $((size / 2)) "$work/wsl-runtime.tar.gz" > "$work/truncated.tar.gz"',
325 `printf 'sha:%s\\n' "$(sha256sum "$work/truncated.tar.gz" | cut -d ' ' -f 1)"`,
326 ].join("\n"),
327 );
328 expect(truncated.status, truncated.stderr).toBe(0);
329​
330 const failed = fixture.install(
331 `${fixture.work}/truncated.tar.gz`,
332 readField(truncated.stdout, "sha"),
333 );
334​
335 expect(failed.status).not.toBe(0);
336 expect(parseWslRuntimeRoot(failed.stdout)).toBeNull();
337 const leftovers = runShell(
338 `set -eu\nfind ${sh(fixture.runtimeParent)} -mindepth 1 -maxdepth 1 -type d`,
339 );
340 expect(leftovers.stdout.trim()).toBe("");
341 });
342​
343 it("refuses an archive whose bytes do not match the digest recorded for it", () => {
344 const fixture = createFixture();
345​
346 const refused = fixture.install(fixture.archivePath, "e".repeat(64));
347​
348 expect(refused.status).not.toBe(0);
349 expect(refused.stderr).toContain("does not match its recorded SHA-256");
350 expect(parseWslRuntimeRoot(refused.stdout)).toBeNull();
351 const leftovers = runShell(
352 `set -eu\nfind ${sh(fixture.runtimeParent)} -mindepth 1 -maxdepth 1 -type d`,
353 );
354 expect(leftovers.stdout.trim()).toBe("");
355 });
356​
357 it("leases a warm cache across the prepare-to-spawn handoff", () => {
358 const fixture = createFixture();
359 expect(fixture.install().status).toBe(0);
360 const selected = runShell(
361 [
362 "set -eu",
363 `runtime_parent=${sh(fixture.runtimeParent)}`,
364 'mkdir -p "$runtime_parent/sha256-current" "$runtime_parent/sha256-previous"',
365 'printf ready > "$runtime_parent/sha256-current/.t3code-wsl-runtime-ready"',
366 'printf ready > "$runtime_parent/sha256-previous/.t3code-wsl-runtime-ready"',
367 `touch -d "10 minutes ago" ${sh(fixture.runtimeRoot)}`,
368 'touch -d "1 minute ago" "$runtime_parent/sha256-previous"',
369 `cat > ${sh(`${fixture.work}/select.sh`)} <<'T3CODE_SELECT_SCRIPT'`,
370 fixture.installScript(),
371 "T3CODE_SELECT_SCRIPT",
372 `sh ${sh(`${fixture.work}/select.sh`)}`,
373 `HOME=${sh(`${fixture.work}/home`)}`,
374 "export HOME",
375 buildWslRuntimePruneScript("sha256-current"),
376 `test -d ${sh(fixture.runtimeRoot)}`,
377 ].join("\n"),
378 );
379​
380 expect(selected.status, `${selected.stdout}\n${selected.stderr}`).toBe(0);
381 });
382​
383 it("prunes old and markerless caches without touching retained, active, locked, or unrelated roots", () => {
384 const result = runShell(
385 [
386 "set -eu",
387 "work=$(mktemp -d)",
388 'home="$work/home"',
389 'runtime_parent="$home/.t3/wsl-runtime"',
390 'mkdir -p "$runtime_parent"',
391 'make_ready() { mkdir -p "$runtime_parent/$1"; printf ready > "$runtime_parent/$1/.t3code-wsl-runtime-ready"; }',
392 "make_ready sha256-current",
393 "make_ready sha256-previous",
394 "make_ready sha256-active",
395 "make_ready sha256-old",
396 "make_ready sha256-locked",
397 'mkdir -p "$runtime_parent/sha256-markerless" "$runtime_parent/versions"',
398 'touch -d "1 minute ago" "$runtime_parent/sha256-previous"',
399 'touch -d "4 minutes ago" "$runtime_parent/sha256-active"',
400 'touch -d "3 minutes ago" "$runtime_parent/sha256-old"',
401 'touch -d "2 minutes ago" "$runtime_parent/sha256-locked"',
402 holdRuntimeBusy('"$runtime_parent/sha256-active/t3"'),
403 holdInstallLock('"$runtime_parent/.sha256-locked.install.lock"'),
404 `HOME="$home"`,
405 "export HOME",
406 buildWslRuntimePruneScript("sha256-current"),
407 'test -d "$runtime_parent/sha256-current"',
408 'test -d "$runtime_parent/sha256-previous"',
409 'test -d "$runtime_parent/sha256-active"',
410 'test -d "$runtime_parent/sha256-locked"',
411 'test -d "$runtime_parent/versions"',
412 'test ! -e "$runtime_parent/sha256-old"',
413 'test ! -e "$runtime_parent/sha256-markerless"',
414 'rm -rf "$work"',
415 ].join("\n"),
416 );
417​
418 expect(result.status, `${result.stdout}\n${result.stderr}`).toBe(0);
419 });
420});
421​
422describe("parseResolvedPath", () => {
423 it("preserves spaces and apostrophes in the resolved login-shell PATH", () => {
424 const resolvedPath = "/home/test user/bin:/opt/test's tools/bin:/usr/bin:/bin";
425 expect(parseResolvedPath(`nodePath:/usr/bin/node\nresolvedPath:${resolvedPath}\n`)).toBe(
426 resolvedPath,
427 );
428 });
429​
430 it("accepts CRLF output without retaining the carriage return", () => {
431 expect(parseResolvedPath("resolvedPath:/usr/local/bin:/usr/bin\r\n")).toBe(
432 "/usr/local/bin:/usr/bin",
433 );
434 });
435});
436​
437// Path.layer joins with POSIX separators here, so the test windir is a POSIX path.
438const WSL_EXE = "/windows/System32/wsl.exe";
439​
440const isAvailable = (policy?: Parameters<typeof DesktopManagedPolicy.layerTest>[0]) =>
441 makeIsAvailable("win32", "/windows").pipe(
442 Effect.provide(
443 Layer.mergeAll(
444 FileSystem.layerNoop({ exists: (path) => Effect.succeed(path === WSL_EXE) }),
445 Path.layer,
446 DesktopManagedPolicy.layerTest(policy),
447 ),
448 ),
449 );
450​
451describe("makeIsAvailable", () => {
452 it.effect("turns WSL off on a machine with a managed policy, even an invalid one", () =>
453 Effect.gen(function* () {
454 assert.isTrue(yield* isAvailable());
455 assert.isFalse(yield* isAvailable({ schemaVersion: 1, organization: "Example Corp" }));
456 assert.isFalse(
457 yield* isAvailable(new ManagedPolicyFileError({ path: "/test", reason: "bad JSON" })),
458 );
459 }),
460 );
461});