import { describe, expect, it } from "vitest"; import { chmodSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import path from "node:path"; import { resolveCommand } from "./binary.js"; function makeBin(): { dir: string; cleanup: () => void } { const dir = mkdtempSync(path.join(tmpdir(), "pi-lsp-bin-")); return { dir, cleanup: () => rmSync(dir, { recursive: true, force: true }) }; } function makeExecutable(file: string) { writeFileSync(file, "#!/bin/sh\nexit 0\n", { mode: 0o755 }); chmodSync(file, 0o755); } const TS_SERVER = { command: ["typescript-language-server", "--stdio"], extensions: [".ts"] }; describe("resolveCommand", () => { it("finds a binary on PATH", async () => { const p = makeBin(); try { makeExecutable(path.join(p.dir, "typescript-language-server")); const result = await resolveCommand(TS_SERVER, p.dir, { PATH: p.dir }); expect(result).not.toBeUndefined(); expect(result!.command).toBe(path.join(p.dir, "typescript-language-server")); expect(result!.args).toEqual(["--stdio"]); } finally { p.cleanup(); } }); it("honors env.PATH over the process PATH", async () => { const p = makeBin(); const other = makeBin(); try { makeExecutable(path.join(p.dir, "gopls")); makeExecutable(path.join(other.dir, "gopls")); const result = await resolveCommand( { command: ["gopls"] }, p.dir, { PATH: other.dir }, ); expect(result!.command).toBe(path.join(other.dir, "gopls")); } finally { p.cleanup(); other.cleanup(); } }); it("finds a project-local node_modules/.bin binary by walking up", async () => { const p = makeBin(); try { const nested = path.join(p.dir, "src", "deep"); mkdirSync(nested, { recursive: true }); const binDir = path.join(p.dir, "node_modules", ".bin"); mkdirSync(binDir, { recursive: true }); makeExecutable(path.join(binDir, "typescript-language-server")); const result = await resolveCommand(TS_SERVER, nested, { PATH: "/nonexistent" }); expect(result!.command).toBe( path.join(p.dir, "node_modules", ".bin", "typescript-language-server"), ); } finally { p.cleanup(); } }); it("returns undefined when the binary is missing everywhere", async () => { const p = makeBin(); try { const result = await resolveCommand(TS_SERVER, p.dir, { PATH: "/nonexistent" }); expect(result).toBeUndefined(); } finally { p.cleanup(); } }); it("uses an absolute command path directly", async () => { const p = makeBin(); try { makeExecutable(path.join(p.dir, "clangd")); const absolute = path.join(p.dir, "clangd"); const result = await resolveCommand( { command: [absolute, "--background-index"] }, p.dir, { PATH: "/nonexistent" }, ); expect(result!.command).toBe(absolute); } finally { p.cleanup(); } }); });