diff --git a/src/main/runtime/orca-runtime-files-watch.test.ts b/src/main/runtime/orca-runtime-files-watch.test.ts new file mode 100644 index 00000000000..92994bd9445 --- /dev/null +++ b/src/main/runtime/orca-runtime-files-watch.test.ts @@ -0,0 +1,151 @@ +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import type * as Fs from 'fs' +import type * as FsPromises from 'fs/promises' +import type * as FilesystemAuth from '../ipc/filesystem-auth' + +const { resolveAuthorizedPathMock, statMock, subscribeParcelWatcherMock, watchMock } = vi.hoisted( + () => ({ + resolveAuthorizedPathMock: vi.fn(), + statMock: vi.fn(), + subscribeParcelWatcherMock: vi.fn(), + watchMock: vi.fn() + }) +) + +vi.mock('fs', async () => { + const actual = await vi.importActual('fs') + return { + ...actual, + watch: watchMock + } +}) + +vi.mock('fs/promises', async () => { + const actual = await vi.importActual('fs/promises') + return { + ...actual, + stat: statMock + } +}) + +vi.mock('@parcel/watcher', () => ({ + subscribe: subscribeParcelWatcherMock +})) + +vi.mock('../ipc/filesystem-auth', async () => { + const actual = await vi.importActual('../ipc/filesystem-auth') + return { + ...actual, + resolveAuthorizedPath: resolveAuthorizedPathMock + } +}) + +import { awaitRuntimeFileWatcherUnsubscribes, RuntimeFileCommands } from './orca-runtime-files' + +function createRuntimeFileCommands(rootPath: string) { + const store = { getRepo: vi.fn(() => undefined) } + const commands = new RuntimeFileCommands({ + getRuntimeId: () => 'runtime-1', + requireStore: () => store, + resolveWorktreeSelector: vi.fn(async () => ({ + id: 'wt-1', + repoId: 'repo-1', + path: rootPath + })), + resolveRuntimeGitTarget: vi.fn(), + openFile: vi.fn() + } as never) + return { commands, store } +} + +describe('RuntimeFileCommands file watching', () => { + const originalPlatform = process.platform + + beforeEach(() => { + vi.useFakeTimers() + resolveAuthorizedPathMock.mockReset() + statMock.mockReset() + subscribeParcelWatcherMock.mockReset() + watchMock.mockReset() + Object.defineProperty(process, 'platform', { + configurable: true, + value: originalPlatform + }) + }) + + afterEach(async () => { + await awaitRuntimeFileWatcherUnsubscribes() + Object.defineProperty(process, 'platform', { + configurable: true, + value: originalPlatform + }) + vi.useRealTimers() + }) + + it('uses a conservative Node watcher for Windows runtime file watches', async () => { + Object.defineProperty(process, 'platform', { + configurable: true, + value: 'win32' + }) + + const close = vi.fn() + const on = vi.fn() + let listener: (() => void) | null = null + watchMock.mockImplementation((_rootPath, _options, callback) => { + listener = callback + return { close, on } + }) + resolveAuthorizedPathMock.mockResolvedValue('C:\\repo') + statMock.mockResolvedValue({ isDirectory: () => true }) + const { commands } = createRuntimeFileCommands('C:\\repo') + const onEvents = vi.fn() + + const unsubscribe = await commands.watchFileExplorer('id:wt-1', onEvents) + + expect(watchMock).toHaveBeenCalledWith('C:\\repo', { recursive: true }, expect.any(Function)) + const emit = listener as (() => void) | null + expect(emit).not.toBeNull() + + emit?.() + emit?.() + await vi.advanceTimersByTimeAsync(149) + expect(onEvents).not.toHaveBeenCalled() + + await vi.advanceTimersByTimeAsync(1) + expect(onEvents).toHaveBeenCalledTimes(1) + expect(onEvents).toHaveBeenCalledWith([{ kind: 'overflow', absolutePath: 'C:\\repo' }]) + + unsubscribe() + expect(close).toHaveBeenCalledTimes(1) + }) + + it('tracks native Parcel watcher unsubscribe work so shutdown can await it', async () => { + resolveAuthorizedPathMock.mockResolvedValue('/repo') + statMock.mockResolvedValue({ isDirectory: () => true }) + let resolveUnsubscribe: () => void = () => {} + const unsubscribeMock = vi.fn( + () => + new Promise((resolve) => { + resolveUnsubscribe = resolve + }) + ) + subscribeParcelWatcherMock.mockResolvedValue({ unsubscribe: unsubscribeMock }) + const { commands } = createRuntimeFileCommands('/repo') + + const unsubscribe = await commands.watchFileExplorer('id:wt-1', vi.fn()) + unsubscribe() + + let drained = false + const drainPromise = awaitRuntimeFileWatcherUnsubscribes().then(() => { + drained = true + }) + await Promise.resolve() + + expect(unsubscribeMock).toHaveBeenCalledTimes(1) + expect(drained).toBe(false) + + resolveUnsubscribe() + await drainPromise + expect(drained).toBe(true) + }) +}) diff --git a/src/main/runtime/orca-runtime-files.test.ts b/src/main/runtime/orca-runtime-files.test.ts index 3e6b7a06c0e..1ae10717221 100644 --- a/src/main/runtime/orca-runtime-files.test.ts +++ b/src/main/runtime/orca-runtime-files.test.ts @@ -239,95 +239,4 @@ describe('RuntimeFileCommands', () => { ) expect(renameMock).not.toHaveBeenCalled() }) - - it('uses a conservative Node watcher for Windows runtime file watches', async () => { - Object.defineProperty(process, 'platform', { - configurable: true, - value: 'win32' - }) - - const store = { getRepo: vi.fn(() => undefined) } - const close = vi.fn() - const on = vi.fn() - let listener: (() => void) | null = null - watchMock.mockImplementation((_rootPath, _options, callback) => { - listener = callback - return { close, on } - }) - resolveAuthorizedPathMock.mockResolvedValue('C:\\repo') - statMock.mockResolvedValue({ isDirectory: () => true }) - - const commands = new RuntimeFileCommands({ - getRuntimeId: () => 'runtime-1', - requireStore: () => store, - resolveWorktreeSelector: vi.fn(async () => ({ - id: 'wt-1', - repoId: 'repo-1', - path: 'C:\\repo' - })), - resolveRuntimeGitTarget: vi.fn(), - openFile: vi.fn() - } as never) - const onEvents = vi.fn() - - const unsubscribe = await commands.watchFileExplorer('id:wt-1', onEvents) - - expect(watchMock).toHaveBeenCalledWith('C:\\repo', { recursive: true }, expect.any(Function)) - const emit = listener as (() => void) | null - expect(emit).not.toBeNull() - - emit?.() - emit?.() - await vi.advanceTimersByTimeAsync(149) - expect(onEvents).not.toHaveBeenCalled() - - await vi.advanceTimersByTimeAsync(1) - expect(onEvents).toHaveBeenCalledTimes(1) - expect(onEvents).toHaveBeenCalledWith([{ kind: 'overflow', absolutePath: 'C:\\repo' }]) - - unsubscribe() - expect(close).toHaveBeenCalledTimes(1) - }) - - it('tracks native Parcel watcher unsubscribe work so shutdown can await it', async () => { - const store = { getRepo: vi.fn(() => undefined) } - resolveAuthorizedPathMock.mockResolvedValue('/repo') - statMock.mockResolvedValue({ isDirectory: () => true }) - let resolveUnsubscribe: () => void = () => {} - const unsubscribeMock = vi.fn( - () => - new Promise((resolve) => { - resolveUnsubscribe = resolve - }) - ) - subscribeParcelWatcherMock.mockResolvedValue({ unsubscribe: unsubscribeMock }) - - const commands = new RuntimeFileCommands({ - getRuntimeId: () => 'runtime-1', - requireStore: () => store, - resolveWorktreeSelector: vi.fn(async () => ({ - id: 'wt-1', - repoId: 'repo-1', - path: '/repo' - })), - resolveRuntimeGitTarget: vi.fn(), - openFile: vi.fn() - } as never) - - const unsubscribe = await commands.watchFileExplorer('id:wt-1', vi.fn()) - unsubscribe() - - let drained = false - const drainPromise = awaitRuntimeFileWatcherUnsubscribes().then(() => { - drained = true - }) - await Promise.resolve() - - expect(unsubscribeMock).toHaveBeenCalledTimes(1) - expect(drained).toBe(false) - - resolveUnsubscribe() - await drainPromise - expect(drained).toBe(true) - }) })