// Watcher Integration Test — proves the FS-watcher → LSP wiring works // end-to-end against gopls: a stale "undefined symbol" diagnostic clears // after the missing file is created externally (no LSP query touches it). // // This is the canonical staleness scenario from `_scratch/plan-fs-watching.md`. import { describe, it, before, after } 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 { setTestSocket, stopTestDaemon, runCliJson, requireServer, } from "../helpers.ts"; const skip = requireServer("gopls"); // Poll Until - Runs `fn` repeatedly until `predicate(result)` is true or // the timeout elapses. We need this because gopls reanalysis after a // workspace/didChangeWatchedFiles is asynchronous; diagnostics arrive on // a `textDocument/publishDiagnostics` push that we then re-fetch. async function pollUntil( fn: () => Promise, predicate: (v: T) => boolean, timeoutMs: number, intervalMs = 250, ): Promise { const deadline = Date.now() + timeoutMs; let last: T = await fn(); while (Date.now() < deadline) { if (predicate(last)) return last; await new Promise((r) => setTimeout(r, intervalMs)); last = await fn(); } return last; } interface DiagResult { [serverId: string]: { diagnostics?: { message: string }[] }; } describe("watcher: gopls picks up externally-created files", { skip: skip ?? undefined }, () => { let tmpDir: string; let mainFile: string; let helperFile: string; const env = { ...process.env }; let cleanup: () => void; before(async () => { cleanup = setTestSocket(env); await stopTestDaemon(env); tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-lsp-gopls-watch-")); // Minimal Go Module - go.mod is gopls's primary root marker. fs.writeFileSync(path.join(tmpDir, "go.mod"), "module example.com/wtest\n\ngo 1.21\n"); mainFile = path.join(tmpDir, "main.go"); helperFile = path.join(tmpDir, "helper.go"); // main.go references Helper() which doesn't exist yet \u2014 should produce // an "undefined: Helper" diagnostic. fs.writeFileSync( mainFile, "package main\n\nfunc main() {\n\tHelper()\n}\n", ); }); after(async () => { await stopTestDaemon(env); cleanup(); fs.rmSync(tmpDir, { recursive: true, force: true }); }); it("initially reports undefined symbol", async () => { // Poll - gopls's workspace load on a fresh tmp dir takes a beat; the // first diagnostics call can return "No active builds contain ..." // before the real analysis lands. Wait up to 15s for it to settle. const result = await pollUntil( async () => (await runCliJson( [mainFile, "diagnostics", '{"timeoutMs":3000}'], env, )) as DiagResult, (r) => { const diags = r["gopls"]?.diagnostics ?? []; return diags.some( (d) => d.message.toLowerCase().includes("undefined") || d.message.includes("Helper"), ); }, 15000, 500, ); const diags = result["gopls"]?.diagnostics ?? []; const hasUndefined = diags.some((d) => d.message.toLowerCase().includes("undefined") || d.message.includes("Helper"), ); assert.ok( hasUndefined, `Expected undefined-symbol diagnostic, got: ${JSON.stringify(diags)}`, ); }); it("clears the diagnostic after helper.go is created externally", async () => { // Create The Missing File Without Touching It Via LSP - This is the // whole point: only chokidar + workspace/didChangeWatchedFiles can tell // gopls about helper.go's existence. fs.writeFileSync( helperFile, "package main\n\nfunc Helper() {}\n", ); // Poll - Allow up to 15s for the watcher to fire, gopls to reanalyze, // and the diagnostic to clear. Re-query the same file (main.go) only. const result = await pollUntil( async () => (await runCliJson( [mainFile, "diagnostics", '{"timeoutMs":3000}'], env, )) as DiagResult, (r) => { const diags = r["gopls"]?.diagnostics ?? []; return !diags.some( (d) => d.message.toLowerCase().includes("undefined") || d.message.includes("Helper"), ); }, 15000, 500, ); const finalDiags = result["gopls"]?.diagnostics ?? []; const stillUndefined = finalDiags.some( (d) => d.message.toLowerCase().includes("undefined") || d.message.includes("Helper"), ); assert.ok( !stillUndefined, `Expected diagnostic to clear after creating helper.go, still got: ${JSON.stringify(finalDiags)}`, ); }); });