import { dirname, join, resolve } from 'node:path' import { statSync } from 'node:fs' import type * as Fs from 'node:fs' import { mkdir, realpath, rename, rm, stat, symlink, writeFile } from 'node:fs/promises' import type * as FsPromises from 'node:fs/promises' import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' import { subscribeViaWatcherProcess } from '../ipc/filesystem/watcher-process/parcel-watcher-process' import type { WatcherProcessCallback, WatcherProcessHooks } from '../ipc/filesystem/watcher-process/parcel-watcher-process-subscription' import type { WatcherProcessSubscribeOptions } from '../ipc/filesystem/watcher-process/parcel-watcher-process-protocol' import { createAliasedWatcherRoot as createNativeAliasedWatcherRoot, removeAliasedWatcherRoot, type AliasedWatcherRoot } from '../ipc/watcher-aliased-root-fixture' import { PluginDevWatcher } from './plugin-dev-watcher' import { PluginServiceHousekeeping } from './plugin-service-housekeeping' vi.mock('../ipc/filesystem/watcher-process/parcel-watcher-process', () => ({ subscribeViaWatcherProcess: vi.fn() })) vi.mock('node:fs', async () => { const actual = await vi.importActual('node:fs') return { ...actual, statSync: vi.fn(actual.statSync) } }) vi.mock('node:fs/promises', async () => { const actual = await vi.importActual('node:fs/promises') return { ...actual, realpath: vi.fn(actual.realpath), stat: vi.fn(actual.stat) } }) type PluginWatchSubscription = { path: string callback: WatcherProcessCallback options: WatcherProcessSubscribeOptions hooks: WatcherProcessHooks unsubscribe: ReturnType Promise>> } const devPath = resolve('plugins', 'demo') /** The catch-up refresh a live subscription schedules; later counts start after it. */ async function flushCatchUpRefresh(refresh: ReturnType): Promise { await vi.advanceTimersByTimeAsync(300) expect(refresh).toHaveBeenCalledOnce() refresh.mockClear() } const originalPlatform = process.platform const subscribeMock = vi.mocked(subscribeViaWatcherProcess) let subscriptions: PluginWatchSubscription[] = [] let devWatchers: PluginDevWatcher[] = [] let housekeeping: PluginServiceHousekeeping | null = null let aliasedRoot: AliasedWatcherRoot | null = null beforeEach(() => { Object.defineProperty(process, 'platform', { configurable: true, value: 'linux' }) subscribeMock.mockReset() vi.mocked(statSync).mockReset() vi.mocked(stat).mockReset() subscribeMock.mockImplementation(async (path, callback, options, hooks = {}) => { const unsubscribe = vi.fn().mockResolvedValue(undefined) subscriptions.push({ path, callback, options, hooks, unsubscribe }) return { unsubscribe } }) }) afterEach(async () => { housekeeping?.dispose() housekeeping = null for (const watcher of devWatchers) { watcher.dispose() } devWatchers = [] subscriptions = [] Object.defineProperty(process, 'platform', { configurable: true, value: originalPlatform }) await removeAliasedWatcherRoot(aliasedRoot) aliasedRoot = null vi.useRealTimers() vi.restoreAllMocks() }) async function createAliasedWatcherRoot(prefix: string): Promise { const watcherPlatform = process.platform Object.defineProperty(process, 'platform', { configurable: true, value: originalPlatform }) try { return await createNativeAliasedWatcherRoot(prefix) } finally { Object.defineProperty(process, 'platform', { configurable: true, value: watcherPlatform }) } } function startDevWatcher( refresh = vi.fn(), onWatcherError = vi.fn(), paths = [devPath] ): { watcher: PluginDevWatcher refresh: ReturnType onWatcherError: ReturnType } { const watcher = new PluginDevWatcher() devWatchers.push(watcher) watcher.start(paths, refresh, onWatcherError) return { watcher, refresh, onWatcherError } } function pluginRootSubscription(): PluginWatchSubscription { const subscription = subscriptions.find((item) => item.options.mode !== 'shallow') if (!subscription) { throw new Error('Plugin root subscription missing') } return subscription } function pluginParentSubscription(): PluginWatchSubscription { const subscription = subscriptions.find((item) => item.options.mode === 'shallow') if (!subscription) { throw new Error('Plugin parent subscription missing') } return subscription } describe('PluginDevWatcher', () => { it('contains asynchronous watcher errors and requests a retrying refresh', async () => { vi.useFakeTimers() const { watcher, refresh, onWatcherError } = startDevWatcher() await Promise.resolve() const subscription = pluginRootSubscription() expect(() => subscription.callback(new Error('watch failed'), [])).not.toThrow() await vi.waitFor(() => expect(subscription.unsubscribe).toHaveBeenCalledOnce()) expect(onWatcherError).toHaveBeenCalledOnce() vi.advanceTimersByTime(300) expect(refresh).toHaveBeenCalledOnce() watcher.dispose() }) it('unsubscribes every subscription that resolves after disposal', async () => { const pending: (() => void)[] = [] subscribeMock.mockImplementation((path, callback, options, hooks = {}) => { const unsubscribe = vi.fn().mockResolvedValue(undefined) subscriptions.push({ path, callback, options, hooks, unsubscribe }) return new Promise((settle) => pending.push(() => settle({ unsubscribe }))) }) const { watcher } = startDevWatcher() watcher.dispose() for (const settle of pending) { settle() } await vi.waitFor(() => { for (const subscription of subscriptions) { expect(subscription.unsubscribe).toHaveBeenCalledOnce() } }) }) it('releases a subscription that resolves after reporting a setup failure', async () => { vi.useFakeTimers() subscribeMock.mockImplementation(async (path, callback, options, hooks = {}) => { const unsubscribe = vi.fn().mockResolvedValue(undefined) subscriptions.push({ path, callback, options, hooks, unsubscribe }) if (options.mode !== 'shallow') { callback(new Error('setup failed'), []) } return { unsubscribe } }) const { refresh, onWatcherError } = startDevWatcher() await vi.waitFor(() => expect(pluginRootSubscription().unsubscribe).toHaveBeenCalledOnce()) expect(onWatcherError).toHaveBeenCalledOnce() pluginRootSubscription().hooks.onInterruption?.() vi.advanceTimersByTime(300) expect(refresh).toHaveBeenCalledOnce() expect(onWatcherError).toHaveBeenCalledOnce() }) it('contains unsubscribe rejections so disposal cannot fail the host process', async () => { const { watcher } = startDevWatcher() await Promise.resolve() for (const subscription of subscriptions) { subscription.unsubscribe.mockRejectedValue(new Error('Root already deleted')) } expect(() => watcher.dispose()).not.toThrow() await vi.waitFor(() => { for (const subscription of subscriptions) { expect(subscription.unsubscribe).toHaveBeenCalledOnce() } }) }) it('keeps a missing root idle until its parent reports recreation', async () => { vi.useFakeTimers() const subscribeNormally = subscribeMock.getMockImplementation() if (!subscribeNormally) { throw new Error('Subscription mock missing') } subscribeMock.mockImplementation((path, callback, options, hooks) => path === devPath ? Promise.reject(new Error('missing path')) : subscribeNormally(path, callback, options, hooks) ) const { refresh, onWatcherError } = startDevWatcher() await vi.waitFor(() => expect(onWatcherError).toHaveBeenCalledOnce()) vi.advanceTimersByTime(10_000) expect(refresh).not.toHaveBeenCalled() pluginParentSubscription().callback(null, [{ type: 'update', path: devPath }]) vi.advanceTimersByTime(300) expect(onWatcherError).toHaveBeenCalledTimes(2) expect(refresh).toHaveBeenCalledOnce() }) it('invalidates failed parent registration without starting a refresh loop', async () => { vi.useFakeTimers() subscribeMock.mockRejectedValue(new Error('parent unavailable')) const { refresh, onWatcherError } = startDevWatcher() await vi.waitFor(() => expect(onWatcherError).toHaveBeenCalledTimes(2)) vi.advanceTimersByTime(10_000) expect(refresh).not.toHaveBeenCalled() expect(subscribeMock).toHaveBeenCalledTimes(2) }) it('releases a root deleted without an error and refreshes again when recreated', async () => { vi.useFakeTimers() const { refresh, onWatcherError } = startDevWatcher() await Promise.resolve() const root = pluginRootSubscription() root.callback(null, [{ type: 'delete', path: devPath }]) expect(root.unsubscribe).toHaveBeenCalledOnce() expect(onWatcherError).toHaveBeenCalledOnce() vi.advanceTimersByTime(300) expect(refresh).toHaveBeenCalledOnce() pluginParentSubscription().callback(null, [{ type: 'update', path: devPath }]) vi.advanceTimersByTime(300) expect(refresh).toHaveBeenCalledTimes(2) root.callback(null, [{ type: 'create', path: resolve(devPath, 'stale.html') }]) vi.advanceTimersByTime(300) expect(refresh).toHaveBeenCalledTimes(2) }) it('catches up once when a dev root goes live after an edit it could not report', async () => { vi.useFakeTimers() const pending: (() => void)[] = [] subscribeMock.mockImplementation((path, callback, options, hooks = {}) => { const unsubscribe = vi.fn().mockResolvedValue(undefined) subscriptions.push({ path, callback, options, hooks, unsubscribe }) return new Promise((resolve) => pending.push(() => resolve({ unsubscribe }))) }) const { refresh } = startDevWatcher(vi.fn(), vi.fn(), [devPath]) // The edit lands while the native subscription is still being set up, so no event reports it. await vi.advanceTimersByTimeAsync(1_000) expect(refresh).not.toHaveBeenCalled() for (const goLive of pending) { goLive() } await vi.advanceTimersByTimeAsync(300) expect(refresh).toHaveBeenCalledOnce() await vi.advanceTimersByTimeAsync(60_000) expect(refresh).toHaveBeenCalledOnce() }) it('refreshes the whole plugin projection after an overflow', () => { vi.useFakeTimers() const { refresh, onWatcherError } = startDevWatcher() const overflow = pluginRootSubscription().hooks.onOverflow expect(overflow).toBeTypeOf('function') overflow?.() vi.advanceTimersByTime(300) expect(onWatcherError).toHaveBeenCalledOnce() expect(refresh).toHaveBeenCalledOnce() }) it('shares a shallow parent subscription between neighboring dev plugins', () => { const sibling = resolve('plugins', 'other') startDevWatcher(undefined, undefined, [devPath, sibling]) expect(subscribeMock).toHaveBeenCalledTimes(3) expect(pluginParentSubscription().path).toBe(dirname(devPath)) expect(pluginParentSubscription().options).toEqual({ mode: 'shallow', include: ['demo', 'other'] }) }) it.skipIf(process.platform === 'win32')( 'preserves a backslash in a POSIX plugin basename', () => { const backslashPath = resolve('plugins', 'demo\\name') startDevWatcher(undefined, undefined, [backslashPath]) expect(pluginParentSubscription().options).toEqual({ mode: 'shallow', include: ['demo\\name'] }) } ) it('watches physical and aliased root names without duplicating their parent', async () => { aliasedRoot = await createAliasedWatcherRoot('orca-plugin-dev-alias-') startDevWatcher(undefined, undefined, [aliasedRoot.aliasRoot]) expect(pluginRootSubscription().path).toBe(aliasedRoot.aliasRoot) expect(pluginParentSubscription().path).toBe(aliasedRoot.base) expect(pluginParentSubscription().options).toEqual({ mode: 'shallow', include: ['alias', 'real'] }) expect(subscribeMock).toHaveBeenCalledTimes(2) }) it('keeps the physical root name while a configured alias is dangling', async () => { aliasedRoot = await createAliasedWatcherRoot('orca-plugin-dev-missing-alias-') const { watcher } = startDevWatcher(undefined, undefined, [aliasedRoot.aliasRoot]) await rm(aliasedRoot.realRoot, { recursive: true }) subscriptions = [] watcher.start([aliasedRoot.aliasRoot], vi.fn()) expect(pluginParentSubscription().options).toEqual({ mode: 'shallow', include: ['alias', 'real'] }) }) it('rewatches the new physical root when an ancestor alias is retargeted', async () => { aliasedRoot = await createAliasedWatcherRoot('orca-plugin-dev-ancestor-alias-') const nextRoot = join(aliasedRoot.base, 'next') await mkdir(join(aliasedRoot.realRoot, 'demo')) await mkdir(join(nextRoot, 'demo'), { recursive: true }) const path = join(aliasedRoot.aliasRoot, 'demo') Object.defineProperty(process, 'platform', { configurable: true, value: 'darwin' }) const watcher = new PluginDevWatcher() devWatchers.push(watcher) const refresh = vi.fn(() => watcher.start([path], refresh, onWatcherError)) const onWatcherError = vi.fn() watcher.start([path], refresh, onWatcherError) await Promise.resolve() const oldSubscriptions = [...subscriptions] expect(subscribeMock).toHaveBeenCalledOnce() expect(subscriptions.every((item) => item.options.mode !== 'shallow')).toBe(true) await rm(aliasedRoot.aliasRoot, { recursive: true }) await symlink( nextRoot, aliasedRoot.aliasRoot, originalPlatform === 'win32' ? 'junction' : 'dir' ) expect(await watcher.checkRootBindings()).toBe(true) refresh() expect(refresh).toHaveBeenCalledOnce() expect(onWatcherError).not.toHaveBeenCalled() expect(await watcher.checkRootBindings()).toBe(false) expect(subscribeMock).toHaveBeenCalledTimes(2) for (const subscription of oldSubscriptions) { expect(subscription.unsubscribe).toHaveBeenCalledOnce() } }) it('detects changes through chained aliases without adding broad parent guards', async () => { aliasedRoot = await createAliasedWatcherRoot('orca-plugin-dev-nested-alias-') const nestedRoot = join(aliasedRoot.base, 'nested') await mkdir(join(nestedRoot, 'demo'), { recursive: true }) await mkdir(join(nestedRoot, 'other')) const intermediate = join(aliasedRoot.base, 'intermediate') const inner = join(aliasedRoot.realRoot, 'inner') const linkType = originalPlatform === 'win32' ? 'junction' : 'dir' await symlink(nestedRoot, intermediate, linkType) await symlink(intermediate, inner, linkType) Object.defineProperty(process, 'platform', { configurable: true, value: 'darwin' }) const { watcher, refresh } = startDevWatcher(undefined, undefined, [ join(aliasedRoot.aliasRoot, 'inner', 'demo'), join(aliasedRoot.aliasRoot, 'inner', 'other') ]) expect(subscriptions.every((item) => item.options.mode !== 'shallow')).toBe(true) expect(subscribeMock).toHaveBeenCalledTimes(2) expect(await watcher.checkRootBindings()).toBe(false) const nextRoot = join(aliasedRoot.base, 'next') await mkdir(join(nextRoot, 'demo'), { recursive: true }) await mkdir(join(nextRoot, 'other')) await rm(intermediate, { recursive: true }) await symlink(nextRoot, intermediate, linkType) expect(await watcher.checkRootBindings()).toBe(true) expect(await watcher.checkRootBindings()).toBe(false) expect(refresh).not.toHaveBeenCalled() }) it.skipIf(originalPlatform !== 'darwin')( 'detects alias retargeting with different casing', async () => { aliasedRoot = await createAliasedWatcherRoot('orca-plugin-dev-case-alias-') Object.defineProperty(process, 'platform', { configurable: true, value: 'darwin' }) const configuredRoot = join(aliasedRoot.base, 'ALIAS') const { watcher } = startDevWatcher(undefined, undefined, [configuredRoot]) expect(await watcher.checkRootBindings()).toBe(false) const nextRoot = join(aliasedRoot.base, 'next') await mkdir(nextRoot) await rm(aliasedRoot.aliasRoot, { recursive: true }) await symlink(nextRoot, aliasedRoot.aliasRoot, 'dir') expect(await watcher.checkRootBindings()).toBe(true) expect(subscriptions.every((item) => item.options.mode !== 'shallow')).toBe(true) } ) it('detects a replacement inode at the same canonical root', async () => { aliasedRoot = await createAliasedWatcherRoot('orca-plugin-dev-inode-') const { watcher } = startDevWatcher(undefined, undefined, [aliasedRoot.realRoot]) expect(await watcher.checkRootBindings()).toBe(false) await rename(aliasedRoot.realRoot, join(aliasedRoot.base, 'old')) await mkdir(aliasedRoot.realRoot) expect(await watcher.checkRootBindings()).toBe(true) expect(await watcher.checkRootBindings()).toBe(false) }) it.each(['dev', 'ino'] as const)( 'detects adjacent 64-bit %s values without losing precision', async (field) => { aliasedRoot = await createAliasedWatcherRoot('orca-plugin-dev-large-identity-') Object.defineProperty(process, 'platform', { configurable: true, value: 'darwin' }) const path = aliasedRoot.realRoot const previousId = 9_007_199_254_740_992n const nextId = previousId + 1n expect(Number(previousId)).toBe(Number(nextId)) const previousIdentity = { dev: previousId, ino: previousId } const nextIdentity = { ...previousIdentity, [field]: nextId } const previousNumeric = Object.assign(statSync(path), { dev: Number(previousIdentity.dev), ino: Number(previousIdentity.ino) }) const previousBigInt = Object.assign(statSync(path, { bigint: true }), previousIdentity) const nextNumeric = Object.assign(statSync(path), { dev: Number(nextIdentity.dev), ino: Number(nextIdentity.ino) }) const nextBigInt = Object.assign(statSync(path, { bigint: true }), nextIdentity) vi.mocked(statSync).mockClear() vi.mocked(stat).mockClear() vi.mocked(statSync).mockImplementationOnce((_path, options) => options?.bigint ? previousBigInt : previousNumeric ) vi.mocked(stat) .mockImplementationOnce(async (_path, options) => options?.bigint ? previousBigInt : previousNumeric ) .mockImplementationOnce(async (_path, options) => options?.bigint ? nextBigInt : nextNumeric ) .mockImplementationOnce(async (_path, options) => options?.bigint ? nextBigInt : nextNumeric ) const { watcher } = startDevWatcher(undefined, undefined, [path]) expect(await watcher.checkRootBindings()).toBe(false) expect(await watcher.checkRootBindings()).toBe(true) expect(await watcher.checkRootBindings()).toBe(false) expect(statSync).toHaveBeenCalledExactlyOnceWith(path, { bigint: true }) expect(stat).toHaveBeenCalledTimes(3) for (const args of vi.mocked(stat).mock.calls) { expect(args).toEqual([path, { bigint: true }]) } } ) it('detects inode-less replacement using full-precision birth time', async () => { aliasedRoot = await createAliasedWatcherRoot('orca-plugin-dev-inode-less-') Object.defineProperty(process, 'platform', { configurable: true, value: 'darwin' }) const path = aliasedRoot.realRoot const birthtimeNs = 9_007_199_254_740_992n const initial = Object.assign(statSync(path, { bigint: true }), { dev: 1n, ino: 0n, birthtimeNs }) const replacement = Object.assign(statSync(path, { bigint: true }), { dev: 1n, ino: 0n, birthtimeNs: birthtimeNs + 1n }) vi.mocked(statSync).mockReturnValueOnce(initial) vi.mocked(stat) .mockResolvedValueOnce(initial) .mockResolvedValueOnce(replacement) .mockResolvedValueOnce(replacement) const { watcher } = startDevWatcher(undefined, undefined, [path]) expect(await watcher.checkRootBindings()).toBe(false) expect(await watcher.checkRootBindings()).toBe(true) expect(await watcher.checkRootBindings()).toBe(false) }) it('distinguishes an inode ID from the same birth time on an inode-less replacement', async () => { aliasedRoot = await createAliasedWatcherRoot('orca-plugin-dev-identity-domain-') Object.defineProperty(process, 'platform', { configurable: true, value: 'darwin' }) const path = aliasedRoot.realRoot const initial = Object.assign(statSync(path, { bigint: true }), { dev: 1n, ino: 77n, birthtimeNs: 1n }) const replacement = Object.assign(statSync(path, { bigint: true }), { dev: 1n, ino: 0n, birthtimeNs: 77n }) vi.mocked(statSync).mockReturnValueOnce(initial) vi.mocked(stat).mockResolvedValue(replacement) const { watcher } = startDevWatcher(undefined, undefined, [path]) expect(await watcher.checkRootBindings()).toBe(true) expect(await watcher.checkRootBindings()).toBe(false) }) it.each([0n, 77n])( 'keeps timestamp edits from refreshing a healthy root with inode %s', async (ino) => { aliasedRoot = await createAliasedWatcherRoot('orca-plugin-dev-edited-binding-') Object.defineProperty(process, 'platform', { configurable: true, value: 'darwin' }) const path = aliasedRoot.realRoot const initial = Object.assign(statSync(path, { bigint: true }), { dev: 1n, ino, birthtimeNs: 1n }) const edited = Object.assign(statSync(path, { bigint: true }), { dev: 1n, ino, birthtimeNs: ino === 0n ? 1n : 2n, mtimeNs: initial.mtimeNs + 1n, ctimeNs: initial.ctimeNs + 1n }) vi.mocked(statSync).mockReturnValueOnce(initial) vi.mocked(stat).mockResolvedValue(edited) vi.useFakeTimers() const lifecycle = new PluginServiceHousekeeping() housekeeping = lifecycle const checkBindings = vi.spyOn(PluginDevWatcher.prototype, 'checkRootBindings') const refresh = vi.fn(() => lifecycle.sync(options)) const options = { enabled: true, devPaths: [path], refresh, reapIdle: vi.fn() } lifecycle.sync(options) await Promise.resolve() await flushCatchUpRefresh(refresh) for (let tick = 0; tick < 3; tick += 1) { await vi.advanceTimersByTimeAsync(60_000) expect(checkBindings).toHaveBeenCalledTimes(tick + 1) await expect(checkBindings.mock.results[tick]?.value).resolves.toBe(false) } expect(refresh).not.toHaveBeenCalled() expect(subscribeMock).toHaveBeenCalledOnce() expect(pluginRootSubscription().unsubscribe).not.toHaveBeenCalled() expect(vi.getTimerCount()).toBe(1) } ) it('keeps a missing root unavailable while a file occupies its path', async () => { aliasedRoot = await createAliasedWatcherRoot('orca-plugin-dev-file-binding-') const path = join(aliasedRoot.realRoot, 'demo') const { watcher } = startDevWatcher(undefined, undefined, [path]) await writeFile(path, '') expect(await watcher.checkRootBindings()).toBe(false) await rm(path) await mkdir(path) expect(await watcher.checkRootBindings()).toBe(true) }) it('detects missing and restored roots once per transition', async () => { aliasedRoot = await createAliasedWatcherRoot('orca-plugin-dev-missing-binding-') const missingRoot = join(aliasedRoot.realRoot, 'demo') const { watcher } = startDevWatcher(undefined, undefined, [missingRoot]) expect(await watcher.checkRootBindings()).toBe(false) await mkdir(missingRoot) expect(await watcher.checkRootBindings()).toBe(true) expect(await watcher.checkRootBindings()).toBe(false) await rm(missingRoot, { recursive: true }) expect(await watcher.checkRootBindings()).toBe(true) expect(await watcher.checkRootBindings()).toBe(false) }) it('uses constant metadata checks without refreshing healthy roots', async () => { aliasedRoot = await createAliasedWatcherRoot('orca-plugin-dev-stable-binding-') Object.defineProperty(process, 'platform', { configurable: true, value: 'darwin' }) const { watcher, refresh } = startDevWatcher(undefined, undefined, [aliasedRoot.realRoot]) vi.mocked(realpath).mockClear() vi.mocked(stat).mockClear() for (let tick = 0; tick < 3; tick += 1) { expect(await watcher.checkRootBindings()).toBe(false) } expect(realpath).toHaveBeenCalledTimes(3) expect(stat).toHaveBeenCalledTimes(3) expect(subscribeMock).toHaveBeenCalledOnce() expect(refresh).not.toHaveBeenCalled() }) it('refreshes Windows child creates and deletes without restarting the healthy root', async () => { aliasedRoot = await createAliasedWatcherRoot('orca-plugin-dev-windows-edits-') Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' }) vi.useFakeTimers() const lifecycle = new PluginServiceHousekeeping() housekeeping = lifecycle const checkBindings = vi.spyOn(PluginDevWatcher.prototype, 'checkRootBindings') const path = aliasedRoot.realRoot const refresh = vi.fn(() => lifecycle.sync(options)) const options = { enabled: true, devPaths: [path], refresh, reapIdle: vi.fn() } lifecycle.sync(options) await Promise.resolve() await flushCatchUpRefresh(refresh) const root = pluginRootSubscription() for (const type of ['create', 'delete'] as const) { root.callback(null, [{ type, path: join(path, 'nested', 'manifest.json') }]) // Windows also reports the child directory name to its parent's fs.watch. for (const parent of subscriptions.filter((item) => item.options.mode === 'shallow')) { parent.callback(null, [{ type: 'update', path }]) } await vi.advanceTimersByTimeAsync(300) } expect(refresh).toHaveBeenCalledTimes(2) expect(root.options).toEqual({ backend: 'windows' }) expect(root.unsubscribe).not.toHaveBeenCalled() expect(subscribeMock).toHaveBeenCalledOnce() await vi.advanceTimersByTimeAsync(60_000) expect(checkBindings).toHaveBeenCalledOnce() await expect(checkBindings.mock.results[0]?.value).resolves.toBe(false) expect(refresh).toHaveBeenCalledTimes(2) expect(subscribeMock).toHaveBeenCalledOnce() expect(vi.getTimerCount()).toBe(1) }) it('recovers a silent Windows root replacement on the existing maintenance tick', async () => { aliasedRoot = await createAliasedWatcherRoot('orca-plugin-dev-windows-replacement-') Object.defineProperty(process, 'platform', { configurable: true, value: 'win32' }) vi.useFakeTimers() const lifecycle = new PluginServiceHousekeeping() housekeeping = lifecycle const path = aliasedRoot.realRoot const refresh = vi.fn(() => lifecycle.sync(options)) const options = { enabled: true, devPaths: [path], refresh, reapIdle: vi.fn() } lifecycle.sync(options) await Promise.resolve() await flushCatchUpRefresh(refresh) const root = pluginRootSubscription() await rename(path, join(aliasedRoot.base, 'old')) await mkdir(path) await vi.advanceTimersByTimeAsync(60_000) await vi.waitFor(() => expect(subscribeMock).toHaveBeenCalledTimes(2)) await vi.advanceTimersByTimeAsync(300) // The rebind refresh, then the replacement root's one catch-up. expect(refresh).toHaveBeenCalledTimes(2) expect(root.unsubscribe).toHaveBeenCalledOnce() expect(subscriptions.map((item) => ({ path: item.path, options: item.options }))).toEqual([ { path, options: { backend: 'windows' } }, { path, options: { backend: 'windows' } } ]) subscriptions.at(-1)?.callback(null, [{ type: 'create', path: join(path, 'manifest.json') }]) await vi.advanceTimersByTimeAsync(300) expect(refresh).toHaveBeenCalledTimes(3) await vi.advanceTimersByTimeAsync(60_000) expect(refresh).toHaveBeenCalledTimes(3) expect(subscribeMock).toHaveBeenCalledTimes(2) lifecycle.dispose() expect(vi.getTimerCount()).toBe(0) }) it('retries setup failures only for roots that are present', async () => { aliasedRoot = await createAliasedWatcherRoot('orca-plugin-dev-retry-availability-') Object.defineProperty(process, 'platform', { configurable: true, value: 'darwin' }) subscribeMock.mockRejectedValue(new Error('native setup failed')) const existing = startDevWatcher(undefined, undefined, [aliasedRoot.realRoot]) await vi.waitFor(() => expect(existing.onWatcherError).toHaveBeenCalledExactlyOnceWith(true)) const missing = startDevWatcher(undefined, undefined, [join(aliasedRoot.realRoot, 'missing')]) await vi.waitFor(() => expect(missing.onWatcherError).toHaveBeenCalledExactlyOnceWith(false)) expect(await missing.watcher.checkRootBindings()).toBe(false) expect(missing.refresh).not.toHaveBeenCalled() }) it('fences a pending binding check after disposal or restart', async () => { aliasedRoot = await createAliasedWatcherRoot('orca-plugin-dev-late-binding-') const { watcher } = startDevWatcher(undefined, undefined, [aliasedRoot.realRoot]) const { promise, resolve: resolveBinding } = Promise.withResolvers() vi.mocked(realpath).mockReturnValueOnce(promise) const check = watcher.checkRootBindings() watcher.start([devPath], vi.fn()) resolveBinding(aliasedRoot.realRoot) expect(await check).toBe(null) watcher.dispose() expect(await watcher.checkRootBindings()).toBe(false) }) it('releases previous subscriptions before starting a new generation', async () => { const { watcher } = startDevWatcher() await Promise.resolve() const oldSubscriptions = [...subscriptions] watcher.start([resolve('plugins', 'replacement')], vi.fn()) for (const subscription of oldSubscriptions) { expect(subscription.unsubscribe).toHaveBeenCalledOnce() } }) it('fences events, errors, interruptions and overflows after disposal', async () => { vi.useFakeTimers() const { watcher, refresh, onWatcherError } = startDevWatcher() await Promise.resolve() watcher.dispose() for (const subscription of subscriptions) { subscription.callback(null, [{ type: 'update', path: subscription.path }]) subscription.callback(new Error('late failure'), []) subscription.hooks.onInterruption?.() subscription.hooks.onOverflow?.() subscription.hooks.onTerminalError?.(new Error('late terminal failure')) } vi.advanceTimersByTime(1_000) expect(refresh).not.toHaveBeenCalled() expect(onWatcherError).not.toHaveBeenCalled() }) })