261 lines · 9.1 KB · TypeScript
RawDownload
import { describe, it } from "@effect/vitest";import { expect } from "vite-plus/test";import * as Duration from "effect/Duration";import * as Effect from "effect/Effect";import * as Fiber from "effect/Fiber";import * as Layer from "effect/Layer";import * as Sink from "effect/Sink";import * as Stream from "effect/Stream";import * as TestClock from "effect/testing/TestClock";import { ChildProcessSpawner } from "effect/unstable/process"; import {  buildWslNodeEnvPreamble,  DesktopWslDistroListError,  formatMissingToolsReason,  formatNodePtyProbeFailureReason,  formatWslShellTransportFailureReason,  parseNodePath,  parseNodeVersion,  parseResolvedPath,  parseToolchainReport,  probeWslDistros,} from "./DesktopWslEnvironment.ts"; const encoder = new TextEncoder(); const makeDistroListSpawner = (result: { readonly stdout?: string; readonly exitCode?: number }) =>  ChildProcessSpawner.make(() =>    Effect.succeed(      ChildProcessSpawner.makeHandle({        pid: ChildProcessSpawner.ProcessId(1),        exitCode:          result.exitCode === undefined            ? Effect.never            : Effect.succeed(ChildProcessSpawner.ExitCode(result.exitCode)),        isRunning: Effect.succeed(result.exitCode === undefined),        kill: () => Effect.void,        unref: Effect.succeed(Effect.void),        stdin: Sink.drain,        stdout: Stream.make(encoder.encode(result.stdout ?? "")),        stderr: Stream.empty,        all: Stream.empty,        getInputFd: () => Sink.drain,        getOutputFd: () => Stream.empty,      }),    ),  ); describe("probeWslDistros", () => {  it.effect("preserves a successful empty distro list", () =>    Effect.gen(function* () {      const distros = yield* probeWslDistros;      expect(distros).toEqual([]);    }).pipe(      Effect.provideService(        ChildProcessSpawner.ChildProcessSpawner,        makeDistroListSpawner({ stdout: "", exitCode: 0 }),      ),    ),  );   it.effect("fails when the distro-list command exits unsuccessfully", () =>    Effect.gen(function* () {      const error = yield* probeWslDistros.pipe(Effect.flip);      expect(error).toBeInstanceOf(DesktopWslDistroListError);      expect(error.message).toContain("exited with code 1");    }).pipe(      Effect.provideService(        ChildProcessSpawner.ChildProcessSpawner,        makeDistroListSpawner({ exitCode: 1 }),      ),    ),  );   it.effect("fails when the distro-list command times out", () => {    const layer = Layer.merge(      TestClock.layer(),      Layer.succeed(ChildProcessSpawner.ChildProcessSpawner, makeDistroListSpawner({})),    );    return Effect.gen(function* () {      const fiber = yield* probeWslDistros.pipe(Effect.flip, Effect.forkScoped);      yield* Effect.yieldNow;      yield* TestClock.adjust(Duration.seconds(8));      const error = yield* Fiber.join(fiber);      expect(error).toBeInstanceOf(DesktopWslDistroListError);      expect(error.message).toContain("timed out");    }).pipe(Effect.provide(layer));  });}); describe("formatNodePtyProbeFailureReason", () => {  it("identifies a packaged build that omitted the Linux node-pty prebuild", () => {    const reason = formatNodePtyProbeFailureReason(4);     expect(reason).toContain("packaged Linux node-pty binary was not included");    expect(reason).toContain("--wsl-prebuild");  });   it("leaves other node-pty load failures to the compatibility diagnostic", () => {    expect(formatNodePtyProbeFailureReason(1)).toBeNull();  });}); describe("formatWslShellTransportFailureReason", () => {  it("distinguishes timeouts and spawn failures from normal shell exit codes", () => {    expect(formatWslShellTransportFailureReason("timeout")).toContain("timed out");    expect(formatWslShellTransportFailureReason("spawn")).toContain("could not start wsl.exe");    expect(formatWslShellTransportFailureReason("process")).toContain("lost communication");    expect(formatWslShellTransportFailureReason(null)).toBeNull();  });}); describe("buildWslNodeEnvPreamble", () => {  it("passes the required Node engine range into the shared resolver", () => {    const preamble = buildWslNodeEnvPreamble("^22.16 || ^23.11 || >=24.10");     expect(preamble).toContain("T3_NODE_ENGINE_RANGE='^22.16 || ^23.11 || >=24.10'");    expect(preamble.indexOf("T3_NODE_ENGINE_RANGE=")).toBeLessThan(      preamble.lastIndexOf("ensure_remote_node_path || true"),    );  });   it("keeps the shared resolver permissive when no Node engine range is provided", () => {    expect(buildWslNodeEnvPreamble()).toContain("T3_NODE_ENGINE_RANGE=''");  });}); describe("parseToolchainReport", () => {  it("returns no missing tools and no node version on empty output", () => {    expect(parseToolchainReport("")).toEqual({ missingTools: [], nodeVersion: null });  });   it("collects all missing: lines", () => {    const stdout = ["missing:make", "missing:g++", "nodeVersion:24.10.0"].join("\n");    expect(parseToolchainReport(stdout)).toEqual({      missingTools: ["make", "g++"],      nodeVersion: "24.10.0",    });  });   it("ignores blank lines and trims whitespace", () => {    const stdout = ["  missing:python3  ", "", "  nodeVersion:v22.16.0  "].join("\n");    expect(parseToolchainReport(stdout)).toEqual({      missingTools: ["python3"],      nodeVersion: "v22.16.0",    });  });   it("returns null node version when value after prefix is empty", () => {    expect(parseToolchainReport("nodeVersion:")).toEqual({      missingTools: [],      nodeVersion: null,    });  });}); describe("parseNodePath", () => {  it("extracts the absolute node path from a nodePath: line", () => {    const stdout = "nodePath:/home/josh/.nvm/versions/node/v22.16.0/bin/node";    expect(parseNodePath(stdout)).toBe("/home/josh/.nvm/versions/node/v22.16.0/bin/node");  });   it("returns null when node was not found (empty value after prefix)", () => {    expect(parseNodePath("nodePath:")).toBeNull();  });   it("returns null when there is no nodePath line at all", () => {    expect(parseNodePath("missing:node\nnodeVersion:")).toBeNull();  });   it("ignores surrounding noise and trims whitespace", () => {    const stdout = ["some preamble noise", "  nodePath:/usr/bin/node  ", "trailing"].join("\n");    expect(parseNodePath(stdout)).toBe("/usr/bin/node");  });}); describe("parseNodeVersion", () => {  it("extracts the node version from a nodeVersion: line", () => {    expect(parseNodeVersion("nodeVersion:24.10.0")).toBe("24.10.0");  });   it("returns null when the version value is empty", () => {    expect(parseNodeVersion("nodeVersion:")).toBeNull();  });   it("returns null when there is no nodeVersion line at all", () => {    expect(parseNodeVersion("nodePath:/usr/bin/node\nresolvedPath:/usr/bin")).toBeNull();  });   it("ignores surrounding noise and trims whitespace", () => {    const stdout = [      "some preamble noise",      "  nodeVersion:22.16.0  ",      "nodePath:/usr/bin/node",    ].join("\n");    expect(parseNodeVersion(stdout)).toBe("22.16.0");  });}); describe("parseResolvedPath", () => {  it("preserves spaces and apostrophes in the resolved login-shell PATH", () => {    const resolvedPath = "/home/test user/bin:/opt/test's tools/bin:/usr/bin:/bin";    expect(parseResolvedPath(`nodePath:/usr/bin/node\nresolvedPath:${resolvedPath}\n`)).toBe(      resolvedPath,    );  });   it("accepts CRLF output without retaining the carriage return", () => {    expect(parseResolvedPath("resolvedPath:/usr/local/bin:/usr/bin\r\n")).toBe(      "/usr/local/bin:/usr/bin",    );  });   it("returns null when the resolved PATH is absent or empty", () => {    expect(parseResolvedPath("nodePath:/usr/bin/node\n")).toBeNull();    expect(parseResolvedPath("resolvedPath:\n")).toBeNull();  });}); describe("formatMissingToolsReason", () => {  it("returns null when everything is present and node is in range", () => {    expect(      formatMissingToolsReason({ missingTools: [], nodeVersion: "24.10.0" }, "^24.10"),    ).toBeNull();  });   it("returns null when range is not specified and tools are present", () => {    expect(formatMissingToolsReason({ missingTools: [], nodeVersion: "18.0.0" }, null)).toBeNull();  });   it("flags missing node first", () => {    const reason = formatMissingToolsReason(      { missingTools: ["node", "make"], nodeVersion: null },      "^24.10",    );    expect(reason).toContain("node");    expect(reason).toContain("^24.10");    expect(reason).toContain("make");    expect(reason).toContain("nvm");  });   it("flags an out-of-range node version with the actual version surfaced", () => {    const reason = formatMissingToolsReason(      { missingTools: [], nodeVersion: "20.0.0" },      "^24.10 || ^22.16",    );    expect(reason).toContain("node 20.0.0");    expect(reason).toContain("requires ^24.10 || ^22.16");  });   it("flags missing build tools without node when node is fine", () => {    const reason = formatMissingToolsReason(      { missingTools: ["g++", "python3"], nodeVersion: "24.10.0" },      "^24.10",    );    expect(reason).toContain("g++");    expect(reason).toContain("python3");    expect(reason).toContain("build-essential");    expect(reason).not.toContain("nvm");  });});