From b7e421483db45cc72ee80a5d49f340ed86126b30 Mon Sep 17 00:00:00 2001 From: Evan Reichard Date: Tue, 19 May 2026 23:58:18 -0400 Subject: [PATCH] fix(watcher): stabilize daemon readiness and tests --- src/daemon.ts | 14 ++++++++------ test/helpers.ts | 9 +++++---- test/integration/cli-daemon.test.ts | 8 ++++---- 3 files changed, 17 insertions(+), 14 deletions(-) diff --git a/src/daemon.ts b/src/daemon.ts index d4f01fa..0167043 100644 --- a/src/daemon.ts +++ b/src/daemon.ts @@ -104,7 +104,7 @@ async function getOrCreateEntry( entries.delete(key); throw err; } - attachWatcher(entry); + await attachWatcher(entry); bumpIdle(entry); return entry; } @@ -113,9 +113,9 @@ async function getOrCreateEntry( // The first non-empty registration lazily creates the WorkspaceWatcher; // subsequent register/unregister calls update its pattern set in place. // Honors PI_LSP_DISABLE_WATCHERS for emergency rollback. -function attachWatcher(entry: ClientEntry): void { +async function attachWatcher(entry: ClientEntry): Promise { if (process.env.PI_LSP_DISABLE_WATCHERS) return; - const sync = () => { + const sync = async () => { const patterns = entry.client.getFileWatchers(); if (patterns.length === 0 && !entry.watcher) return; if (!entry.watcher) { @@ -128,11 +128,13 @@ function attachWatcher(entry: ClientEntry): void { log(`watcher patterns`, entry.server.id, JSON.stringify(patterns)); } entry.watcher.setPatterns(patterns); + if (patterns.length > 0) await entry.watcher.ready(); }; - entry.unsubscribeWatchers = entry.client.onWatchersChanged(sync); + entry.unsubscribeWatchers = entry.client.onWatchersChanged(() => void sync()); // Initial Sync - Server may have already sent registerCapability during - // initialize before we subscribed. - sync(); + // initialize before we subscribed. Wait for chokidar's initial scan so + // externally-created files are not swallowed as ignoreInitial events. + await sync(); } // Forward Events - Sends a batched workspace/didChangeWatchedFiles to the diff --git a/test/helpers.ts b/test/helpers.ts index da923a7..e4ba9aa 100644 --- a/test/helpers.ts +++ b/test/helpers.ts @@ -25,10 +25,11 @@ export const tsx = path.resolve( "cli.mjs", ); -// Unique Test Socket — each test run gets its own Unix socket so we don't -// touch any real session daemon. +// Unique Test Socket — each suite gets its own Unix socket so parallel +// integration tests don't race through the same daemon. export function testSocket(): string { - return path.join(os.tmpdir(), `pi-lsp-test-${process.pid}.sock`); + const dir = fs.mkdtempSync(path.join(os.tmpdir(), "pi-lsp-test-")); + return path.join(dir, "daemon.sock"); } // Set Test Socket — sets PI_LSP_SOCKET_PATH for the current process and @@ -39,7 +40,7 @@ export function setTestSocket(env: Record): () => vo return () => { delete env.PI_LSP_SOCKET_PATH; try { - fs.unlinkSync(sock); + fs.rmSync(path.dirname(sock), { recursive: true, force: true }); } catch { // Socket may not exist — that's fine. } diff --git a/test/integration/cli-daemon.test.ts b/test/integration/cli-daemon.test.ts index 3e419d3..93cbe32 100644 --- a/test/integration/cli-daemon.test.ts +++ b/test/integration/cli-daemon.test.ts @@ -13,15 +13,15 @@ describe("cli daemon lifecycle", () => { const env = { ...process.env }; let cleanup: () => void; - before(() => { + before(async () => { cleanup = setTestSocket(env); // Stop any stale daemon on this socket before tests run. - stopTestDaemon(env); + await stopTestDaemon(env); }); - after(() => { + after(async () => { // Tear down daemon and clean up socket after all tests. - stopTestDaemon(env); + await stopTestDaemon(env); cleanup(); });