import { afterEach, beforeEach, describe, expect, test } from "bun:test"; import { chmodSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { delimiter, join } from "node:path"; import { Effect, Fiber } from "effect"; import { resolveBinary, runCommandAsync, runCommandAsyncEffect } from "./command"; test("retains only bounded stdout and stderr tails", async () => { const result = await runCommandAsync( process.execPath, [ "-e", 'process.stdout.write("a".repeat(512)+"stdout-tail");process.stderr.write("b".repeat(512)+"stderr-tail")', ], { timeoutMs: 5_000, maxOutputBytes: 32 }, ); expect(Buffer.byteLength(result.stdout)).toBeLessThanOrEqual(32); expect(Buffer.byteLength(result.stderr)).toBeLessThanOrEqual(32); expect(result.stdout.endsWith("stdout-tail")).toBe(true); expect(result.stderr.endsWith("stderr-tail")).toBe(true); }); test("abort waits for the child process to exit", async () => { const controller = new AbortController(); let closed = false; const command = runCommandAsync(process.execPath, ["-e", "setInterval(()=>{},1000)"], { timeoutMs: 60_000, signal: controller.signal, onSpawn: (child): void => { child.once("close", () => { closed = true; }); }, }); controller.abort(); await command; expect(closed).toBe(true); }); test("effect interruption kills and settles the child process", async () => { let closed = false; const fiber = Effect.runFork( runCommandAsyncEffect(process.execPath, ["-e", "setInterval(()=>{},1000)"], { timeoutMs: 60_000, onSpawn: (child): void => { child.once("close", () => { closed = true; }); }, }), ); await Effect.runPromise(Fiber.interrupt(fiber)); expect(closed).toBe(true); }); const executableName = (name: string): string => process.platform === "win32" ? `${name}.exe` : name; const createExecutable = (directory: string, name: string): string => { const filePath = join(directory, executableName(name)); writeFileSync(filePath, ""); chmodSync(filePath, 0o755); return filePath; }; describe("resolveBinary", () => { let temporaryDirectory: string; let savedPath: string | undefined; let savedRuntimeBin: string | undefined; let savedSnap: string | undefined; let savedHome: string | undefined; const restoreEnvironment = (key: string, value: string | undefined): void => { if (value === undefined) delete process.env[key]; else process.env[key] = value; }; beforeEach(() => { temporaryDirectory = mkdtempSync(join(tmpdir(), "resolve-binary-")); savedPath = process.env["PATH"]; savedRuntimeBin = process.env["LOCAL_STUDIO_RUNTIME_BIN"]; savedSnap = process.env["SNAP"]; savedHome = process.env["HOME"]; delete process.env["LOCAL_STUDIO_RUNTIME_BIN"]; delete process.env["SNAP"]; }); afterEach(() => { restoreEnvironment("PATH", savedPath); restoreEnvironment("LOCAL_STUDIO_RUNTIME_BIN", savedRuntimeBin); restoreEnvironment("SNAP", savedSnap); restoreEnvironment("HOME", savedHome); rmSync(temporaryDirectory, { recursive: true, force: true }); }); test("resolves a bare name from PATH using the platform delimiter", () => { const expected = createExecutable(temporaryDirectory, "local-studio-test-tool"); process.env["PATH"] = temporaryDirectory; expect(resolveBinary("local-studio-test-tool")).toBe(expected); }); test("searches every PATH entry, not just the first", () => { const emptyDirectory = join(temporaryDirectory, "empty"); const toolDirectory = join(temporaryDirectory, "tools"); mkdirSync(emptyDirectory); mkdirSync(toolDirectory); const expected = createExecutable(toolDirectory, "local-studio-test-tool"); process.env["PATH"] = [emptyDirectory, toolDirectory].join(delimiter); expect(resolveBinary("local-studio-test-tool")).toBe(expected); }); test("resolves from a PATH entry wrapped in quotes", () => { const expected = createExecutable(temporaryDirectory, "local-studio-test-tool"); process.env["PATH"] = `"${temporaryDirectory}"`; expect(resolveBinary("local-studio-test-tool")).toBe(expected); }); test("returns null when the binary is not on PATH", () => { process.env["PATH"] = temporaryDirectory; expect(resolveBinary("local-studio-missing-tool")).toBeNull(); }); test("returns null for an empty name", () => { expect(resolveBinary("")).toBeNull(); }); test("resolves an explicit path directly", () => { const expected = createExecutable(temporaryDirectory, "local-studio-test-tool"); expect(resolveBinary(expected)).toBe(expected); }); test("returns null for an explicit path that does not exist", () => { expect( resolveBinary(join(temporaryDirectory, executableName("local-studio-missing-tool"))), ).toBeNull(); }); test("prefers LOCAL_STUDIO_RUNTIME_BIN over PATH", () => { const runtimeDirectory = join(temporaryDirectory, "runtime-bin"); const pathDirectory = join(temporaryDirectory, "path-bin"); mkdirSync(runtimeDirectory); mkdirSync(pathDirectory); const expected = createExecutable(runtimeDirectory, "local-studio-test-tool"); createExecutable(pathDirectory, "local-studio-test-tool"); process.env["LOCAL_STUDIO_RUNTIME_BIN"] = runtimeDirectory; process.env["PATH"] = pathDirectory; expect(resolveBinary("local-studio-test-tool")).toBe(expected); }); test("falls back to HOME local bin after PATH", () => { const homeDirectory = join(temporaryDirectory, "home"); const localBin = join(homeDirectory, ".local", "bin"); mkdirSync(localBin, { recursive: true }); const expected = createExecutable(localBin, "local-studio-test-tool"); process.env["HOME"] = homeDirectory; process.env["PATH"] = join(temporaryDirectory, "does-not-exist"); expect(resolveBinary("local-studio-test-tool")).toBe(expected); }); test("returns null for a file without the execute bit", () => { if (process.platform === "win32") return; const filePath = join(temporaryDirectory, "local-studio-test-tool"); writeFileSync(filePath, ""); chmodSync(filePath, 0o644); process.env["PATH"] = temporaryDirectory; expect(resolveBinary("local-studio-test-tool")).toBeNull(); }); });