import { describe, expect, it } from "vitest"; import { chmodSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import path from "node:path"; import { detectPlatform, managedBinDir } from "./platform.js"; import { feasibleOn, install } from "./download.js"; describe("detectPlatform", () => { it("detects Termux from the android platform", () => { const info = detectPlatform({}, "android", "arm64"); expect(info.platform).toBe("android"); expect(info.isTermux).toBe(true); expect(info.assetPlatform).toBe("linux"); expect(info.assetArch).toBe("aarch64"); expect(info.supported).toBe(true); }); it("detects Termux from PREFIX+ANDROID_ROOT on old linux-reporting builds", () => { const info = detectPlatform( { PREFIX: "/data/data/com.termux/files/usr", ANDROID_ROOT: "/system" }, "linux", "arm64", ); expect(info.isTermux).toBe(true); expect(info.platform).toBe("android"); }); it("detects macOS and Windows asset naming", () => { expect(detectPlatform({}, "darwin", "arm64").assetArch).toBe("aarch64"); const win = detectPlatform({}, "win32", "x64"); expect(win.isWindows).toBe(true); expect(win.assetPlatform).toBe("windows"); expect(win.assetArch).toBe("x86_64"); }); it("marks 32-bit architectures as unsupported", () => { const ia32 = detectPlatform({}, "linux", "ia32"); expect(ia32.supported).toBe(false); expect(ia32.assetArch).toBeUndefined(); const arm32 = detectPlatform({}, "linux", "arm"); expect(arm32.supported).toBe(false); expect(detectPlatform({}, "darwin", "arm64").supported).toBe(true); }); }); const NPM_PLAN = { kind: "npm" as const, package: "typescript-language-server", bin: "typescript-language-server" }; const GO_PLAN = { kind: "go-install" as const, package: "golang.org/x/tools/gopls@latest", bin: "gopls", requires: ["go"] as const }; const KOTLIN_PLAN = { kind: "github-release" as const, owner: "Kotlin", repo: "kotlin-lsp", bin: "kotlin-lsp", requires: ["java"] as const }; function termuxEnv(extra: Record = {}): NodeJS.ProcessEnv { // Default PATH is a path that does NOT exist, so resolveBinary() never // finds anything in it. The GitHub Actions ubuntu-latest runner ships // with `go` and `java` pre-installed in /usr/bin, which would falsely // make the "not feasible" tests (lines 72, 83) pass. fakeTool(...) in // `extra` still wins because the spread comes after. return { PATH: "/__pi_lsp_no_binaries__", ...extra }; } function fakeTool(tool: string): Record { const dir = mkdtempSync(path.join(tmpdir(), "pi-lsp-tool-")); const file = path.join(dir, tool); writeFileSync(file, "#!/bin/sh\nexit 0\n"); chmodSync(file, 0o755); return { PATH: dir }; } function cleanupTool(tool: Record) { rmSync(tool.PATH!, { recursive: true, force: true }); } describe("feasibleOn", () => { it("npm plans are feasible on android-arm64", async () => { expect(await feasibleOn(NPM_PLAN, detectPlatform({}, "android", "arm64"), termuxEnv())).toBe(true); }); it("go-install plans need go on PATH", async () => { expect(await feasibleOn(GO_PLAN, detectPlatform({}, "linux", "x64"), termuxEnv())).toBe(false); const go = fakeTool("go"); try { expect(await feasibleOn(GO_PLAN, detectPlatform({}, "linux", "x64"), termuxEnv(go))).toBe(true); } finally { cleanupTool(go); } }); it("github-release plans need their required toolchain and are refused on android", async () => { const info = detectPlatform({}, "linux", "x64"); expect(await feasibleOn(KOTLIN_PLAN, info, termuxEnv())).toBe(false); const java = fakeTool("java"); try { expect(await feasibleOn(KOTLIN_PLAN, info, termuxEnv(java))).toBe(true); } finally { cleanupTool(java); } const termux = detectPlatform({}, "android", "arm64"); expect(await feasibleOn(KOTLIN_PLAN, termux, termuxEnv(fakeTool("java")))).toBe(false); cleanupTool({ PATH: termuxEnv(fakeTool("java")).PATH! }); }); it("refuses installs on unsupported (32-bit) architectures", async () => { const ia32 = detectPlatform({}, "linux", "ia32"); expect(await feasibleOn(NPM_PLAN, ia32, termuxEnv())).toBe(false); }); }); describe("install (npm plan)", () => { it("runs npm install into binDir and returns the binary path", async () => { const binDir = mkdtempSync(path.join(tmpdir(), "pi-lsp-install-")); const fakeNpmDir = mkdtempSync(path.join(tmpdir(), "pi-lsp-npm-")); try { writeFileSync( path.join(fakeNpmDir, "npm"), "#!/bin/sh\nmkdir -p \"$2/node_modules/.bin\"\nprintf '#!/bin/sh\\nexit 0\\n' > \"$2/node_modules/.bin/typescript-language-server\"\nchmod +x \"$2/node_modules/.bin/typescript-language-server\"\nexit 0\n", ); chmodSync(path.join(fakeNpmDir, "npm"), 0o755); const env = { PATH: `${fakeNpmDir}:${process.env.PATH ?? ""}` }; const result = await install(NPM_PLAN, detectPlatform({}, "linux", "x64"), { binDir, env }); expect(result).toBe(`${binDir}/node_modules/.bin/typescript-language-server`); } finally { rmSync(fakeNpmDir, { recursive: true, force: true }); rmSync(binDir, { recursive: true, force: true }); } }); it("refuses to install on unsupported architectures", async () => { const binDir = mkdtempSync(path.join(tmpdir(), "pi-lsp-install-")); try { const result = await install(NPM_PLAN, detectPlatform({}, "linux", "ia32"), { binDir }); expect(result).toBeUndefined(); } finally { rmSync(binDir, { recursive: true, force: true }); } }); }); describe("managedBinDir", () => { it("prefers XDG_CACHE_HOME", () => { expect(managedBinDir({ XDG_CACHE_HOME: "/x/cache", HOME: "/h" })).toBe("/x/cache/pi-lsp/bin"); }); it("falls back to HOME/.cache", () => { expect(managedBinDir({ HOME: "/h" })).toBe("/h/.cache/pi-lsp/bin"); }); });