diff --git a/src/main/ipc/filesystem-watcher-local-events.test.ts b/src/main/ipc/filesystem-watcher-local-events.test.ts index be289de2145..b5ec3eab939 100644 --- a/src/main/ipc/filesystem-watcher-local-events.test.ts +++ b/src/main/ipc/filesystem-watcher-local-events.test.ts @@ -16,6 +16,11 @@ vi.mock('./parcel-watcher-process', () => ({ subscribeViaWatcherProcess: subscri import { createLocalWatcher } from './filesystem-watcher-local-events' import { cancelLocalBatchFlush } from './filesystem-watcher-batch-control' +import { + subscribeLocalWatcher, + unsubscribeLocalWatcher +} from './filesystem-watcher-local-subscription' +import { watcherLifecycleState } from './filesystem-watcher-lifecycle-state' function deferred(): { promise: Promise; resolve: (value: T) => void } { let resolve!: (value: T) => void @@ -326,4 +331,26 @@ describe('local filesystem watcher flush serialization', () => { { kind: 'update', absolutePath: otherPath, isDirectory: false } ]) }) + + it('re-arms the debounce window after a re-subscribe inside the teardown grace period', async () => { + // Why real timers: fake-timers' refresh() revives a cleared handle, but Node's is a no-op — the bug only shows on real Timeouts. + vi.useRealTimers() + statMock.mockResolvedValue({ isDirectory: () => true }) + const listener = { ...sender, id: 7, once: vi.fn() } + try { + await subscribeLocalWatcher('/repo', listener as never) + watcherCallback?.(null, [{ type: 'delete', path: '/repo/file.ts' }]) + unsubscribeLocalWatcher('/repo', listener.id) + await subscribeLocalWatcher('/repo', listener as never) + watcherCallback?.(null, [{ type: 'delete', path: '/repo/file.ts' }]) + await new Promise((resolve) => setTimeout(resolve, WATCH_BATCH_TRAILING_MS + 50)) + expect(sender.send).toHaveBeenCalledTimes(1) + } finally { + for (const teardown of watcherLifecycleState.pendingTeardowns.values()) { + clearTimeout(teardown) + } + watcherLifecycleState.pendingTeardowns.clear() + watcherLifecycleState.watchedRoots.clear() + } + }) }) diff --git a/src/main/ipc/filesystem-watcher-local-subscription.ts b/src/main/ipc/filesystem-watcher-local-subscription.ts index 933d02bdb37..e888893dbf4 100644 --- a/src/main/ipc/filesystem-watcher-local-subscription.ts +++ b/src/main/ipc/filesystem-watcher-local-subscription.ts @@ -199,6 +199,8 @@ export function unsubscribeLocalWatcher(worktreePath: string, senderId: number): if (root.listeners.size === 0) { if (root.batch.timer) { clearTimeout(root.batch.timer) + // Why: a cleared handle can't be refresh()ed; null it so a grace-window re-subscribe arms a fresh window. + root.batch.timer = null } // Why: duplicate unwatch calls for a root would leak overwritten grace timers; keep just one. if (watcherLifecycleState.pendingTeardowns.has(rootKey)) {