From 7bdec29519b6fb40a66fe7233f44b4d539e347df Mon Sep 17 00:00:00 2001 From: Neil <4138956+nwparker@users.noreply.github.com> Date: Sun, 23 Aug 2026 19:41:40 -0700 Subject: [PATCH] test(git-watch): use deterministic reconciliation timers --- .../ipc/worktree-git-common-watch.test.ts | 162 ++++++++++-------- 1 file changed, 86 insertions(+), 76 deletions(-) diff --git a/src/main/ipc/worktree-git-common-watch.test.ts b/src/main/ipc/worktree-git-common-watch.test.ts index e5549d82508..dbcfc3ac3e2 100644 --- a/src/main/ipc/worktree-git-common-watch.test.ts +++ b/src/main/ipc/worktree-git-common-watch.test.ts @@ -1,6 +1,7 @@ import { afterEach, describe, expect, it, vi } from 'vitest' import { appendFile, mkdtemp, mkdir, realpath, rm, writeFile } from 'node:fs/promises' import type * as NodeFsPromises from 'node:fs/promises' +import type * as NodePerfHooks from 'node:perf_hooks' import { chmodSync } from 'node:fs' import { tmpdir } from 'node:os' import { join } from 'node:path' @@ -35,12 +36,22 @@ vi.mock('node:fs/promises', async (importOriginal) => { } } }) +vi.mock('node:perf_hooks', async (importOriginal) => { + const actual = await importOriginal() + return { + ...actual, + performance: { + ...actual.performance, + now: () => Date.now() + } + } +}) const POLL_MS = 25 -// Native re-arm is intentionally deferred to the 15-tick reconciliation. Give -// loaded CI shards enough wall-clock slack without changing the exact call count. -const RECONCILIATION_WAIT_TIMEOUT_MS = 30_000 - +// Reconciliation runs every 15 poll ticks. Fake timers keep native re-arm tests +// independent of host load while preserving the production interval. +const RECONCILIATION_TICKS = 15 +// Advance multiple windows to cover the scheduler's staggered phase and async snapshot work. const alwaysVisible: WorktreePollerWindowVisibility = { isWindowVisible: () => true, onWindowBecameVisible: () => () => {} @@ -514,90 +525,89 @@ describe('worktree git-common narrow watch (darwin)', () => { }) it('re-arms the native stream when the root is replaced with the same child names', async () => { - installSubscribeMock() - const commonDir = await makeCommonDir(true) - const worktreesDir = join(commonDir, 'worktrees') - const retainedEntry = join(worktreesDir, 'same-child') - await mkdir(retainedEntry) - const received: WorktreeBasePollEvent[][] = [] - await startWatch(commonDir, received) - const staleSubscription = childSubscriptions[0] + vi.useFakeTimers() + try { + installSubscribeMock() + const commonDir = await makeCommonDir(true) + const worktreesDir = join(commonDir, 'worktrees') + const retainedEntry = join(worktreesDir, 'same-child') + await mkdir(retainedEntry) + const received: WorktreeBasePollEvent[][] = [] + await startWatch(commonDir, received) + const staleSubscription = childSubscriptions[0] - await rm(worktreesDir, { recursive: true }) - await mkdir(retainedEntry, { recursive: true }) - await vi.waitFor( - () => { - expect(subscribeMock).toHaveBeenCalledTimes(2) - }, - { timeout: RECONCILIATION_WAIT_TIMEOUT_MS } - ) - expect(staleSubscription.unsubscribe).toHaveBeenCalledOnce() + await rm(worktreesDir, { recursive: true }) + await mkdir(retainedEntry, { recursive: true }) + await vi.advanceTimersByTimeAsync(POLL_MS * RECONCILIATION_TICKS * 3) + expect(subscribeMock).toHaveBeenCalledTimes(2) + expect(staleSubscription.unsubscribe).toHaveBeenCalledOnce() - const beforeStaleEvent = received.length - staleSubscription.callback(null, [{ type: 'update', path: retainedEntry }]) - expect(received).toHaveLength(beforeStaleEvent) + const beforeStaleEvent = received.length + staleSubscription.callback(null, [{ type: 'update', path: retainedEntry }]) + expect(received).toHaveLength(beforeStaleEvent) - const immediateEntry = join(worktreesDir, 'after-rearm') - childSubscriptions[1].callback(null, [{ type: 'create', path: immediateEntry }]) - expect(received.flat()).toContainEqual({ type: 'create', path: immediateEntry }) + const immediateEntry = join(worktreesDir, 'after-rearm') + childSubscriptions[1].callback(null, [{ type: 'create', path: immediateEntry }]) + expect(received.flat()).toContainEqual({ type: 'create', path: immediateEntry }) + } finally { + await Promise.all(cleanups.splice(0).map((cleanup) => cleanup())) + vi.useRealTimers() + } }) it('disposes an in-flight stale resubscribe and fences its interruption hook', async () => { - const deferredSubscribe = Promise.withResolvers<{ - unsubscribe: () => Promise - }>() - subscribeMock.mockImplementation(async (dir, callback, _opts, hooks = {}) => { - const unsubscribe = vi.fn(async () => {}) - childSubscriptions.push({ dir, callback, hooks, unsubscribe }) - return childSubscriptions.length === 2 ? deferredSubscribe.promise : { unsubscribe } - }) - const commonDir = await makeCommonDir(true) - const worktreesDir = join(commonDir, 'worktrees') - const retainedEntry = join(worktreesDir, 'same-child') - await mkdir(retainedEntry) - const received: WorktreeBasePollEvent[][] = [] - const onWatchError = vi.fn() - await startWatch(commonDir, received, () => [], onWatchError) + vi.useFakeTimers() + try { + const deferredSubscribe = Promise.withResolvers<{ + unsubscribe: () => Promise + }>() + subscribeMock.mockImplementation(async (dir, callback, _opts, hooks = {}) => { + const unsubscribe = vi.fn(async () => {}) + childSubscriptions.push({ dir, callback, hooks, unsubscribe }) + return childSubscriptions.length === 2 ? deferredSubscribe.promise : { unsubscribe } + }) + const commonDir = await makeCommonDir(true) + const worktreesDir = join(commonDir, 'worktrees') + const retainedEntry = join(worktreesDir, 'same-child') + await mkdir(retainedEntry) + const received: WorktreeBasePollEvent[][] = [] + const onWatchError = vi.fn() + await startWatch(commonDir, received, () => [], onWatchError) - await rm(worktreesDir, { recursive: true }) - await mkdir(retainedEntry, { recursive: true }) - await vi.waitFor( - () => { - expect(subscribeMock).toHaveBeenCalledTimes(2) - }, - { timeout: RECONCILIATION_WAIT_TIMEOUT_MS } - ) - const stalePendingSubscription = childSubscriptions[1] + await rm(worktreesDir, { recursive: true }) + await mkdir(retainedEntry, { recursive: true }) + await vi.advanceTimersByTimeAsync(POLL_MS * RECONCILIATION_TICKS * 3) + expect(subscribeMock).toHaveBeenCalledTimes(2) + const stalePendingSubscription = childSubscriptions[1] - await rm(worktreesDir, { recursive: true }) - await mkdir(retainedEntry, { recursive: true }) - await vi.waitFor( - () => { - expect( - received.flat().filter((event) => event.type === 'create' && event.path === worktreesDir) - ).toHaveLength(2) - }, - { timeout: RECONCILIATION_WAIT_TIMEOUT_MS } - ) + await rm(worktreesDir, { recursive: true }) + await mkdir(retainedEntry, { recursive: true }) + await vi.advanceTimersByTimeAsync(POLL_MS * RECONCILIATION_TICKS * 3) + expect( + received.flat().filter((event) => event.type === 'create' && event.path === worktreesDir) + ).toHaveLength(2) - const beforeStaleInterruption = received.length - stalePendingSubscription.hooks.onInterruption?.() - expect(received).toHaveLength(beforeStaleInterruption) - expect(onWatchError).not.toHaveBeenCalled() + const beforeStaleInterruption = received.length + stalePendingSubscription.hooks.onInterruption?.() + expect(received).toHaveLength(beforeStaleInterruption) + expect(onWatchError).not.toHaveBeenCalled() - deferredSubscribe.resolve({ - unsubscribe: async () => { - await stalePendingSubscription.unsubscribe() - } - }) - await vi.waitFor(() => { + deferredSubscribe.resolve({ + unsubscribe: async () => { + await stalePendingSubscription.unsubscribe() + } + }) + await vi.advanceTimersByTimeAsync(POLL_MS) expect(subscribeMock).toHaveBeenCalledTimes(3) - }) - expect(stalePendingSubscription.unsubscribe).toHaveBeenCalledOnce() + expect(stalePendingSubscription.unsubscribe).toHaveBeenCalledOnce() - const immediateEntry = join(worktreesDir, 'current-generation') - childSubscriptions[2].callback(null, [{ type: 'create', path: immediateEntry }]) - expect(received.flat()).toContainEqual({ type: 'create', path: immediateEntry }) + const immediateEntry = join(worktreesDir, 'current-generation') + childSubscriptions[2].callback(null, [{ type: 'create', path: immediateEntry }]) + expect(received.flat()).toContainEqual({ type: 'create', path: immediateEntry }) + } finally { + await Promise.all(cleanups.splice(0).map((cleanup) => cleanup())) + vi.useRealTimers() + } }) it('stops forwarding events and unsubscribes the child on dispose', async () => {