fix(watcher): cap startup wait

This commit is contained in:
2026-05-20 00:09:07 -04:00
parent b7e421483d
commit 62fc80c70f
6 changed files with 34 additions and 158 deletions

View File

@@ -1,6 +1,3 @@
// Watcher Unit Tests — exercises WorkspaceWatcher against a temp dir with
// real chokidar. We use real FS because mocking it would test the mock,
// not the actual behavior under inotify.
import { describe, it, before, after, beforeEach } from "node:test";
import * as assert from "node:assert/strict";
import * as fs from "node:fs";
@@ -9,9 +6,6 @@ import * as path from "node:path";
import { WorkspaceWatcher, type FileEvent } from "../../src/watcher.ts";
import { pathToUri } from "../../src/root.ts";
// Wait For - Polls a predicate up to timeoutMs. Returns true if it became
// true, false if it timed out. Used to wait on async chokidar events
// without arbitrary sleeps.
async function waitFor(
predicate: () => boolean,
timeoutMs = 2000,
@@ -24,8 +18,6 @@ async function waitFor(
return predicate();
}
// Wait Quiet - Waits for the debounce window to elapse so any pending
// events flush. Slightly longer than DEBOUNCE_MAX_WAIT_MS.
const FLUSH_WAIT_MS = 700;
describe("WorkspaceWatcher", () => {
@@ -47,7 +39,6 @@ describe("WorkspaceWatcher", () => {
watcher = null;
}
received = [];
// Reset Dir - Wipe contents between tests so file lists are predictable.
for (const entry of fs.readdirSync(tmpDir)) {
fs.rmSync(path.join(tmpDir, entry), { recursive: true, force: true });
}
@@ -140,7 +131,6 @@ describe("WorkspaceWatcher", () => {
it("respects WatchKind to filter event types", async () => {
watcher = new WorkspaceWatcher(tmpDir, (evs) => received.push(evs));
// Kind 1 = Create only - delete events should be suppressed.
watcher.setPatterns([{ globPattern: "**/*.ts", kind: 1 }]);
await watcher.ready();
@@ -166,8 +156,6 @@ describe("WorkspaceWatcher", () => {
watcher.setPatterns([{ globPattern: "**/*.ts" }]);
await watcher.ready();
// Write Several Files Quickly - within the 50ms debounce window they
// should all land in one batch, capped at 500ms max wait.
for (let i = 0; i < 5; i++) {
fs.writeFileSync(path.join(tmpDir, `f${i}.ts`), "x");
}
@@ -176,8 +164,6 @@ describe("WorkspaceWatcher", () => {
const all = received.flat();
assert.strictEqual(all.length, 5, `Expected 5 events, got ${all.length}`);
// The batches should be << 5 (debounce coalesces). Allow up to 2 in
// case the loop straddled a flush boundary.
assert.ok(
received.length <= 2,
`Expected <=2 batches, got ${received.length}: ${JSON.stringify(received)}`,
@@ -217,8 +203,6 @@ describe("WorkspaceWatcher", () => {
});
it("matches absolute-path glob patterns (gopls-style)", async () => {
// Regression - gopls registers e.g. `/tmp/.../**/*.{go,mod}` rather
// than a bare relative `**/*.go`. The matcher must accept both.
watcher = new WorkspaceWatcher(tmpDir, (evs) => received.push(evs));
watcher.setPatterns([
{ globPattern: `${tmpDir}/**/*.{go,mod}` },
@@ -240,7 +224,6 @@ describe("WorkspaceWatcher", () => {
watcher.setPatterns([{ globPattern: "**/*.ts" }]);
await watcher.ready();
// Create Then Delete - both within the debounce quiet window.
const file = path.join(tmpDir, "transient.ts");
fs.writeFileSync(file, "x");
fs.unlinkSync(file);
@@ -255,15 +238,12 @@ describe("WorkspaceWatcher", () => {
});
it("coalesces Deleted+Created (replacement) to Changed", async () => {
// Setup - file exists before the watcher starts, so chokidar's initial
// scan registers it (with ignoreInitial: true, no event fires).
const file = path.join(tmpDir, "replace.ts");
fs.writeFileSync(file, "v1");
watcher = new WorkspaceWatcher(tmpDir, (evs) => received.push(evs));
watcher.setPatterns([{ globPattern: "**/*.ts" }]);
await watcher.ready();
// Delete Then Recreate - within one debounce window.
fs.unlinkSync(file);
fs.writeFileSync(file, "v2");
@@ -272,11 +252,6 @@ describe("WorkspaceWatcher", () => {
const all = received.flat();
const events = all.filter((e) => e.uri === pathToUri(file));
assert.ok(events.length > 0, `Expected an event for replaced file`);
// Expected: the coalesced single event is Changed (type=2). Some
// platforms may split the delete+create across debounce windows, in
// which case the server still sees correct state. Accept either:
// - one Changed event (coalesced), or
// - Deleted then Created in separate batches (not coalesced, fine)
const types = events.map((e) => e.type).sort();
const acceptable =
JSON.stringify(types) === JSON.stringify([2]) ||
@@ -293,8 +268,6 @@ describe("WorkspaceWatcher", () => {
await watcher.ready();
fs.writeFileSync(path.join(tmpDir, "pending.ts"), "x");
// Don't wait for the debounce - clear patterns immediately so the
// pending batch should be dropped, not flushed.
await new Promise((r) => setTimeout(r, 10));
watcher.setPatterns([]);