import { randomUUID } from "node:crypto"; import { mkdtemp, writeFile } from "node:fs/promises"; import { tmpdir } from "node:os"; import path from "node:path"; import { afterEach, describe, expect, it } from "vitest"; import { checkToolAvailability, resolveCommand, spawnToText } from "../spawn"; const originalPath = process.env.PATH; const originalPathAlt = process.env.Path; afterEach(() => { if (originalPath === undefined) delete process.env.PATH; else process.env.PATH = originalPath; if (originalPathAlt === undefined) delete process.env.Path; else process.env.Path = originalPathAlt; }); async function makeFakeTool(dir: string, name: string) { if (process.platform === "win32") { const toolPath = path.join(dir, `${name}.cmd`); await writeFile(toolPath, "@echo off\r\nexit /b 0\r\n"); return toolPath; } const toolPath = path.join(dir, name); await writeFile(toolPath, "#!/bin/sh\nexit 0\n", { mode: 0o755 }); return toolPath; } describe("checkToolAvailability", () => { it("finds executables on PATH without requiring Unix which", async () => { const dir = await mkdtemp(path.join(tmpdir(), "pi-web-path-")); const name = `fake-tool-${randomUUID()}`; await makeFakeTool(dir, name); process.env.PATH = dir; await expect(checkToolAvailability(name, "missing")).resolves.toBeNull(); }); it("does not keep stale negative cache entries after PATH changes", async () => { const dir = await mkdtemp(path.join(tmpdir(), "pi-web-path-")); const emptyDir = await mkdtemp(path.join(tmpdir(), "pi-web-empty-path-")); const name = `fake-tool-${randomUUID()}`; process.env.PATH = emptyDir; await expect(checkToolAvailability(name, "missing")).resolves.toBe( "missing", ); await makeFakeTool(dir, name); process.env.PATH = dir; await expect(checkToolAvailability(name, "missing")).resolves.toBeNull(); }); it("resolves explicit command paths but rejects directories", async () => { const dir = await mkdtemp(path.join(tmpdir(), "pi-web-explicit-path-")); const name = `fake-tool-${randomUUID()}`; const executable = await makeFakeTool(dir, name); expect(await resolveCommand(path.join(dir, name))).toBe(executable); expect(await resolveCommand(dir)).toBeNull(); }); it("runs PATH-resolved tools without a shell-specific lookup utility", async () => { const dir = await mkdtemp(path.join(tmpdir(), "pi web path ")); const name = `fake-tool-${randomUUID()}`; await makeFakeTool(dir, name); process.env.PATH = dir; await expect(spawnToText(name, ["arg with spaces"])).resolves.toBe(""); }); });