// Test Helpers — Shared utilities for running CLI commands, managing the // isolated test daemon, and skipping tests when server binaries are missing. import { execFile, execSync } from "node:child_process"; import * as fs from "node:fs"; import * as os from "node:os"; import * as path from "node:path"; import { fileURLToPath } from "node:url"; import { promisify } from "node:util"; const execFileAsync = promisify(execFile); // Project Root — resolved relative to this file. const __dirname = path.dirname(fileURLToPath(import.meta.url)); export const projectRoot = path.resolve(__dirname, ".."); // CLI Path — absolute path to cli.ts for child_process calls. export const cliPath = path.join(projectRoot, "cli.ts"); // Tsx CLI — resolve the tsx binary for running .ts files via child_process. export const tsx = path.resolve( projectRoot, "node_modules", "tsx", "dist", "cli.mjs", ); // Unique Test Socket — each test run gets its own Unix socket so we don't // touch any real session daemon. export function testSocket(): string { return path.join(os.tmpdir(), `pi-lsp-test-${process.pid}.sock`); } // Set Test Socket — sets PI_LSP_SOCKET_PATH for the current process and // returns a cleanup function that deletes the env var and removes the socket. export function setTestSocket(env: Record): () => void { const sock = testSocket(); env.PI_LSP_SOCKET_PATH = sock; return () => { delete env.PI_LSP_SOCKET_PATH; try { fs.unlinkSync(sock); } catch { // Socket may not exist — that's fine. } }; } // Stop Test Daemon — best-effort shutdown of whatever daemon is on the test // socket. Ignores errors (daemon may not be running). export async function stopTestDaemon(env: Record): Promise { try { await execFileAsync("node", [tsx, cliPath, "daemon", "stop"], { env }); } catch { // Already stopped or never started — ignore. } } // Run CLI — spawns `node tsx cli.ts <...args>` and returns stdout as a string. // Uses the provided env (should include PI_LSP_SOCKET_PATH for daemon tests). export async function runCli( args: string[], env: Record, ): Promise<{ stdout: string; stderr: string }> { try { const { stdout, stderr } = await execFileAsync( "node", [tsx, cliPath, ...args], { env, timeout: 30_000, maxBuffer: 1024 * 1024 }, ); return { stdout: stdout.trim(), stderr: stderr.trim() }; } catch (err: unknown) { const child = err as { stdout?: string; stderr?: string; status?: number }; return { stdout: child.stdout?.trim() ?? "", stderr: child.stderr?.trim() ?? String(err), }; } } // Run CLI Expecting JSON — runs the CLI and parses stdout as JSON. Throws // if parsing fails or the process exits with non-zero status. export async function runCliJson( args: string[], env: Record, ): Promise { const { stdout, stderr } = await runCli(args, env); if (!stdout) throw new Error(`CLI produced no stdout: ${stderr}`); try { return JSON.parse(stdout); } catch (err) { throw new Error( `Failed to parse CLI output as JSON: ${(err as Error).message}\nOutput: ${stdout}`, ); } } // Require Server — checks that the given server binary is on PATH. Returns // a skip message string if not found (caller should use `test.skip(msg)`), // or undefined if the server is available. export function requireServer(command: string): string | undefined { try { execSync(`which ${command}`, { stdio: "pipe" }); return undefined; } catch { return `Server "${command}" not found on PATH`; } } // Fixtures Directory — path to the test fixture files. export const fixturesDir = path.join(__dirname, "fixtures");