Files
pi-lsp/test/helpers.ts
Evan Reichard aa7309b363 test: add unit and integration test suite
Add 34 tests (27 unit, 7 integration) using node:test runner:

Unit tests:
- pickServer(), findRoot(), pathToUri(), uriToPath()
- isLspCommand(), listCommands()
- formatHover(), formatDefinition(), formatReferences(), formatDiagnostics()

Integration tests:
- daemon lifecycle (status/stop) on isolated socket
- CLI --no-daemon queries (hover, documentSymbol, diagnostics)

Supporting changes:
- socketPath() honors PI_LSP_SOCKET_PATH env var for test isolation
- test fixtures for valid and broken TypeScript files
- npm test / test:unit / test:integration scripts
2026-04-30 10:36:54 -04:00

112 lines
3.8 KiB
TypeScript

// 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<string, string | undefined>): () => 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<string, string | undefined>): Promise<void> {
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<string, string | undefined>,
): 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<string, string | undefined>,
): Promise<unknown> {
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");