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
112 lines
3.8 KiB
TypeScript
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");
|