fix(watcher): stabilize daemon readiness and tests

This commit is contained in:
2026-05-19 23:58:18 -04:00
parent 0aa44bedc4
commit b7e421483d
3 changed files with 17 additions and 14 deletions

View File

@@ -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<void> {
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

View File

@@ -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<string, string | undefined>): () => 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.
}

View File

@@ -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();
});