import { describe, expect, it } from "vitest"; import { mkdtempSync, rmSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import path from "node:path"; import { createClient } from "./client.js"; function makeFile(content: string): { file: string; cleanup: () => void } { const dir = mkdtempSync(path.join(tmpdir(), "pi-lsp-client-")); const file = path.join(dir, "a.ts"); writeFileSync(file, content, "utf8"); return { file, cleanup: () => rmSync(dir, { recursive: true, force: true }) }; } const FAKE = ["node", new URL("../test/fixtures/fake-server.mjs", import.meta.url).pathname]; describe("createClient", () => { it("collects push diagnostics after touchFile", async () => { const f = makeFile("export const x = 1;"); const client = await createClient({ serverID: "typescript", command: FAKE, cwd: path.dirname(f.file), env: { FAKE_ERRORS: "1", FAKE_PUSH_DELAY_MS: "10" }, timeoutMs: 5000, }); try { const version = await client.touchFile(f.file); await client.waitForDiagnostics({ path: f.file, version, mode: "document" }); const diags = client.diagnostics.get(f.file); expect(diags?.some((d) => d.message === "fake error" && d.severity === 1)).toBe(true); expect(diags?.some((d) => d.message === "fake warning" && d.severity === 2)).toBe(true); } finally { await client.shutdown(); f.cleanup(); } }); it("merges pull diagnostics when the server advertises pull support", async () => { const f = makeFile("let x;"); const client = await createClient({ serverID: "typescript", command: FAKE, cwd: path.dirname(f.file), env: { FAKE_PULL: "1", FAKE_PUSH_DELAY_MS: "200" }, timeoutMs: 5000, }); try { const version = await client.touchFile(f.file); await client.waitForDiagnostics({ path: f.file, version, mode: "full" }); const messages = (client.diagnostics.get(f.file) ?? []).map((d) => d.message); expect(messages).toContain("pulled"); } finally { await client.shutdown(); f.cleanup(); } }); it("returns the document version and bumps it on re-touch", async () => { const f = makeFile("let a = 1;"); const client = await createClient({ serverID: "t", command: FAKE, cwd: path.dirname(f.file), timeoutMs: 5000, }); try { const v1 = await client.touchFile(f.file); writeFileSync(f.file, "let a = 2;", "utf8"); const v2 = await client.touchFile(f.file); expect(v2).toBeGreaterThan(v1); } finally { await client.shutdown(); f.cleanup(); } }); it("answers workspace/configuration requests from initialization settings", async () => { const f = makeFile("let x;"); const client = await createClient({ serverID: "t", command: FAKE, cwd: path.dirname(f.file), initialization: { server: { maxErrors: 10 } }, env: { FAKE_CONFIG: "1" }, timeoutMs: 5000, }); try { await client.touchFile(f.file); const value = await client.request("test/configResult"); expect(value).toEqual({ maxErrors: 10 }); } finally { await client.shutdown(); f.cleanup(); } }); it("returns null for requests after shutdown", async () => { const f = makeFile("let x;"); const client = await createClient({ serverID: "t", command: FAKE, cwd: path.dirname(f.file), timeoutMs: 5000, }); await client.touchFile(f.file); await client.shutdown(); await expect(client.request("test/anything", {})).resolves.toBeNull(); }); it("waits past a provisional empty publish for real diagnostics", async () => { const f = makeFile("let x;"); const client = await createClient({ serverID: "typescript", command: FAKE, cwd: path.dirname(f.file), env: { FAKE_EMPTY_FIRST: "1", FAKE_PUSH_DELAY_MS: "10", FAKE_EMPTY_GAP_MS: "150" }, diagnosticsGraceMs: 500, timeoutMs: 5000, }); try { const version = await client.touchFile(f.file); await client.waitForDiagnostics({ path: f.file, version, mode: "document" }); const messages = (client.diagnostics.get(f.file) ?? []).map((d) => d.message); expect(messages).toContain("fake error"); } finally { await client.shutdown(); f.cleanup(); } }); it("settles on empty for clean files within the grace window", async () => { const f = makeFile("let x;"); const client = await createClient({ serverID: "typescript", command: FAKE, cwd: path.dirname(f.file), diagnosticsGraceMs: 200, timeoutMs: 5000, }); try { const version = await client.touchFile(f.file); const started = Date.now(); await client.waitForDiagnostics({ path: f.file, version, mode: "document" }); const elapsed = Date.now() - started; expect((client.diagnostics.get(f.file) ?? []).length).toBe(0); // Settles on the empty publish after the grace window, not the full timeout. expect(elapsed).toBeLessThan(2000); } finally { await client.shutdown(); f.cleanup(); } }); it("full mode waits past the grace window for real diagnostics", async () => { const f = makeFile("let x;"); const client = await createClient({ serverID: "typescript", command: FAKE, cwd: path.dirname(f.file), env: { FAKE_EMPTY_FIRST: "1", FAKE_PUSH_DELAY_MS: "10", FAKE_EMPTY_GAP_MS: "400" }, diagnosticsGraceMs: 100, timeoutMs: 5000, }); try { const version = await client.touchFile(f.file); await client.waitForDiagnostics({ path: f.file, version, mode: "full" }); const messages = (client.diagnostics.get(f.file) ?? []).map((d) => d.message); // Document mode would have settled empty at the 100ms grace; full mode // keeps waiting for a non-empty publication. expect(messages).toContain("fake error"); } finally { await client.shutdown(); f.cleanup(); } }); it("survives garbage frames and malformed URIs without crashing", async () => { const f = makeFile("let x;"); const client = await createClient({ serverID: "typescript", command: FAKE, cwd: path.dirname(f.file), env: { FAKE_GARBAGE: "1", FAKE_PUSH_DELAY_MS: "10" }, timeoutMs: 5000, }); try { await client.touchFile(f.file); // Give the garbage + malformed publish time to arrive, then verify the // client still answers requests (process survived). await new Promise((r) => setTimeout(r, 300)); const value = await client.request("test/configResult"); expect(value).not.toBeUndefined(); } finally { await client.shutdown(); f.cleanup(); } }); });