From 85dd8b2a159cddeac88d91dd46b3f6ca45ca6b93 Mon Sep 17 00:00:00 2001 From: Colby Mchenry Date: Tue, 29 Sep 2026 18:28:40 +0000 Subject: [PATCH] test(watcher): wait for a live FSEvents stream before the #770 restart edit (#2104) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The symlink retarget/restart test flaked inside the full suite with "waitFor timed out". A traced failing run showed the edit made right after unwatch()+watch() was never reported: fs.watch() returns before macOS registers the stream (libuv serves a process's watches from one FSEvents stream, rebuilt on its own thread after each watch/close), and an edit made in that gap is dropped, not delayed — so no timeout would have helped. Poke a probe file under the link until the watcher reports it, then make the edit under test once. Applied after the restart (the observed failure) and after the retarget (the same race with a 100-300ms gap). Under a simulated 300ms registration delay the old test fails 3/3 and the new one passes 3/3; a dead post-restart stream still fails it. Co-authored-by: Claude Opus 5.5 --- __tests__/watcher.test.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/__tests__/watcher.test.ts b/__tests__/watcher.test.ts index 05349c430..e0bb14723 100644 --- a/__tests__/watcher.test.ts +++ b/__tests__/watcher.test.ts @@ -1180,6 +1180,21 @@ describe('FileWatcher', () => { // FSEvents can coalesce writes made while a new stream is being registered. await new Promise(resolve => setTimeout(resolve, 700)); }; + // fs.watch() returns before macOS registers the stream: libuv serves every + // watch in a process from one FSEvents stream, rebuilt on its own thread + // after each watch/close, and an edit made before that lands is never + // reported. Under full-suite load that dropped the edit made right after a + // restart. Poke a probe under `dir` until the watcher reports it, so the + // edit under test is made once, against a stream known to be live. + const untilLive = (dir: string) => { + const probe = path.join(dir, 'watch-probe.ts'); + const rel = path.relative(testDir, probe).replace(/\\/g, '/'); + return waitFor(() => { + if (cg!.getPendingFiles().some(p => p.path === rel)) return true; + fs.writeFileSync(probe, `// ${Date.now()}\n`); + return false; + }, 8000); + }; afterEach(() => { cg = undefined; @@ -1234,10 +1249,12 @@ describe('FileWatcher', () => { fs.unlinkSync(alias); link(b, alias); await waitFor(() => has('secondTarget') && !has('firstTarget'), 8000); + await untilLive(alias); fs.writeFileSync(path.join(b, 'b.ts'), 'export function retargetEdit() {}'); await waitFor(() => has('retargetEdit'), 8000); cg!.unwatch(); expect(cg!.watch({ debounceMs: 100 })).toBe(true); + await untilLive(alias); fs.writeFileSync(path.join(b, 'b.ts'), 'export function restartedEdit() {}'); await waitFor(() => has('restartedEdit'), 8000); fs.unlinkSync(alias);