summaryrefslogtreecommitdiffhomepage
path: root/packages/lsp/src/extension.test.ts
diff options
context:
space:
mode:
Diffstat (limited to 'packages/lsp/src/extension.test.ts')
-rw-r--r--packages/lsp/src/extension.test.ts89
1 files changed, 89 insertions, 0 deletions
diff --git a/packages/lsp/src/extension.test.ts b/packages/lsp/src/extension.test.ts
new file mode 100644
index 0000000..16b9df7
--- /dev/null
+++ b/packages/lsp/src/extension.test.ts
@@ -0,0 +1,89 @@
+import { EventEmitter } from "node:events";
+import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
+import { tmpdir } from "node:os";
+import { join } from "node:path";
+import { describe, expect, it } from "vitest";
+import type { FsWatcherHandle, WatchFn } from "./extension.js";
+
+// `realFileWatcher` is a module-private function; the exported seam below
+// re-exposes it for testing. Import through the module to exercise the real
+// production path (the error-listener attachment is what we're verifying).
+import { __test__realFileWatcher } from "./extension.js";
+
+describe("realFileWatcher (Bug 2 — unhandled fs.watch 'error' event)", () => {
+ it("swallows a watcher 'error' event instead of crashing (injected watcher)", () => {
+ // A fake fs.watch: returns an EventEmitter we control. Without an
+ // 'error' listener, Node EventEmitter throws an uncaughtException on
+ // emit('error'). The fix attaches a no-op 'error' listener, so emitting
+ // here must NOT throw. The fake wires the watch callback to the
+ // EventEmitter's "change" event (args: eventType, filename), mirroring
+ // how node:fs.watch invokes its callback.
+ const watcher = new EventEmitter() as unknown as FsWatcherHandle & {
+ close: () => void;
+ };
+ (watcher as { close: () => void }).close = () => {
+ watcher.removeAllListeners();
+ };
+ const fakeWatch: WatchFn = (_root, _opts, cb) => {
+ watcher.on("change", (eventType: string, filename: string | null) => cb(eventType, filename));
+ return watcher;
+ };
+
+ const events: { type: string; path: string }[] = [];
+ const handle = __test__realFileWatcher("/project", (e) => events.push(e), fakeWatch);
+
+ // A transient FS error (e.g. bun install deleting a watched dir) — must
+ // be a no-op, NOT an uncaught exception.
+ expect(() => watcher.emit("error", new Error("ENOENT transient"))).not.toThrow();
+
+ // The watcher still forwards normal change events.
+ watcher.emit("change", "change", "src/a.ts");
+ expect(events).toEqual([{ type: "change", path: "/project/src/a.ts" }]);
+
+ handle.close();
+ });
+
+ it("ignores a null filename (no spurious event)", () => {
+ const watcher = new EventEmitter() as unknown as FsWatcherHandle & {
+ close: () => void;
+ };
+ (watcher as { close: () => void }).close = () => {
+ watcher.removeAllListeners();
+ };
+ const fakeWatch: WatchFn = (_root, _opts, cb) => {
+ watcher.on("change", (eventType: string, filename: string | null) => cb(eventType, filename));
+ return watcher;
+ };
+
+ const events: { type: string; path: string }[] = [];
+ const handle = __test__realFileWatcher("/project", (e) => events.push(e), fakeWatch);
+
+ watcher.emit("change", "change", null);
+ expect(events).toHaveLength(0);
+
+ handle.close();
+ });
+
+ it("integration: watches a real temp directory and fires on file change", async () => {
+ // A real-FS smoke test of the production adapter's happy path. Uses the
+ // real node:fs.watch (recursive on a temp dir). Best-effort: some
+ // platforms coalesce events, so we only assert the adapter runs and
+ // closes cleanly without throwing — we do not hard-assert an event
+ // arrived (that would be flaky across inotify/kqueue/Win backends).
+ const dir = mkdtempSync(join(tmpdir(), "lsp-watch-"));
+ try {
+ const events: { type: string; path: string }[] = [];
+ const handle = __test__realFileWatcher(dir, (e) => events.push(e));
+
+ // Touch a file; give the watcher a moment.
+ writeFileSync(join(dir, "hello.txt"), "hi");
+ await new Promise((r) => setTimeout(r, 150));
+
+ handle.close();
+ // No assertion on events.length — the point is no throw + clean close.
+ expect(true).toBe(true);
+ } finally {
+ rmSync(dir, { recursive: true, force: true });
+ }
+ });
+});