// Config Unit Tests — getServersForPath() merge semantics, mtime caching, // and graceful fallback on parse errors. import { describe, it, beforeEach, afterEach } from "node:test"; import * as assert from "node:assert/strict"; import * as fs from "node:fs"; import * as os from "node:os"; import * as path from "node:path"; import { getServersForPath, clearConfigCache } from "../../src/config.ts"; import { servers as builtinServers } from "../../server.ts"; let tmpDir: string; beforeEach(() => { tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-lsp-config-test-")); clearConfigCache(); }); afterEach(() => { fs.rmSync(tmpDir, { recursive: true, force: true }); clearConfigCache(); }); function writeConfig(dir: string, content: unknown): void { fs.writeFileSync(path.join(dir, ".pi-lsp.json"), JSON.stringify(content)); } describe("getServersForPath", () => { it("returns built-in servers when no config file is found", () => { // tmpDir lives under /tmp; no ancestor will have .pi-lsp.json. const result = getServersForPath(tmpDir); assert.deepStrictEqual( result.map((s) => s.id), builtinServers.map((s) => s.id), ); }); it("appends a new server entry from config", () => { writeConfig(tmpDir, { servers: [ { id: "rust-analyzer", match: ["rs"], command: "rust-analyzer", args: [], rootMarkers: ["Cargo.toml"], languageId: "rust", }, ], }); const result = getServersForPath(tmpDir); const rust = result.find((s) => s.id === "rust-analyzer"); assert.ok(rust, "rust-analyzer should be present"); assert.deepStrictEqual(rust!.match, ["rs"]); // Built-ins are still there assert.ok(result.find((s) => s.id === "gopls")); }); it("shallow-merges overrides into existing built-in servers", () => { writeConfig(tmpDir, { servers: [{ id: "gopls", args: ["-vv"] }], }); const result = getServersForPath(tmpDir); const gopls = result.find((s) => s.id === "gopls")!; assert.deepStrictEqual(gopls.args, ["-vv"]); // Other fields preserved from built-in assert.strictEqual(gopls.command, "gopls"); assert.deepStrictEqual(gopls.match, ["go"]); }); it("filters out servers listed in `disable`", () => { writeConfig(tmpDir, { disable: ["oxlint", "gopls"] }); const result = getServersForPath(tmpDir); assert.ok(!result.some((s) => s.id === "oxlint")); assert.ok(!result.some((s) => s.id === "gopls")); assert.ok(result.some((s) => s.id === "pyright")); }); it("walks upward to find config in an ancestor directory", () => { writeConfig(tmpDir, { disable: ["oxlint"] }); const sub = path.join(tmpDir, "a", "b", "c"); fs.mkdirSync(sub, { recursive: true }); const result = getServersForPath(sub); assert.ok(!result.some((s) => s.id === "oxlint")); }); it("accepts a file path and walks from its directory", () => { writeConfig(tmpDir, { disable: ["oxlint"] }); const file = path.join(tmpDir, "src", "main.go"); fs.mkdirSync(path.dirname(file), { recursive: true }); fs.writeFileSync(file, "package main"); const result = getServersForPath(file); assert.ok(!result.some((s) => s.id === "oxlint")); }); it("falls back to built-ins on invalid JSON", () => { fs.writeFileSync(path.join(tmpDir, ".pi-lsp.json"), "{ not valid"); const result = getServersForPath(tmpDir); assert.deepStrictEqual( result.map((s) => s.id), builtinServers.map((s) => s.id), ); }); it("falls back to built-ins when a new server is missing required fields", () => { writeConfig(tmpDir, { servers: [{ id: "incomplete", match: ["foo"] }], }); const result = getServersForPath(tmpDir); assert.deepStrictEqual( result.map((s) => s.id), builtinServers.map((s) => s.id), ); }); it("invalidates cache on mtime change", () => { writeConfig(tmpDir, { disable: ["oxlint"] }); let result = getServersForPath(tmpDir); assert.ok(!result.some((s) => s.id === "oxlint")); // Rewrite With Different Content And Bump mtime const cfgPath = path.join(tmpDir, ".pi-lsp.json"); fs.writeFileSync(cfgPath, JSON.stringify({ disable: ["gopls"] })); const future = new Date(Date.now() + 5000); fs.utimesSync(cfgPath, future, future); result = getServersForPath(tmpDir); assert.ok(result.some((s) => s.id === "oxlint")); assert.ok(!result.some((s) => s.id === "gopls")); }); });