mirror of
https://github.com/stablyai/orca.git
synced 2026-10-30 08:37:49 +00:00
startGitCommonNarrowWatch was the only watch entry point that never received WorktreePollerWindowVisibility, so its `worktrees/` existence poll kept stat'ing every repo without a linked worktree forever in the background (0.5 stat/sec/repo at the 2s default). Its siblings — the primary-metadata snapshot poller and the non-darwin git-common polling — already park. Threads visibility through and matches the snapshot poller's park/re-arm pattern: the poll stops on the first hidden tick, and re-checks immediately on onWindowBecameVisible so a worktrees dir created while hidden still upgrades to the native stream and emits its create event. The visibility listener is dropped in the dispose path. darwin-only: the narrow watch is the `platform === 'darwin'` branch.
584 lines
22 KiB
TypeScript
584 lines
22 KiB
TypeScript
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 { chmodSync } from 'node:fs'
|
|
import { tmpdir } from 'node:os'
|
|
import { join } from 'node:path'
|
|
import { subscribeViaWatcherProcess } from './parcel-watcher-process'
|
|
import type {
|
|
WatcherProcessCallback,
|
|
WatcherProcessHooks
|
|
} from './parcel-watcher-process-subscription'
|
|
import type { WorktreeBaseWatchTarget } from './worktree-base-directory-event-filter'
|
|
import type {
|
|
WorktreeBasePollEvent,
|
|
WorktreePollerWindowVisibility
|
|
} from './worktree-base-directory-poller'
|
|
import { startGitCommonWatch } from './worktree-git-common-watch'
|
|
|
|
vi.mock('./parcel-watcher-process', () => ({
|
|
subscribeViaWatcherProcess: vi.fn()
|
|
}))
|
|
|
|
// Records every stat target so a test can assert which paths a parked poll stopped touching.
|
|
const { statCalls } = vi.hoisted(() => ({ statCalls: [] as string[] }))
|
|
|
|
vi.mock('node:fs/promises', async (importOriginal) => {
|
|
const actual = await importOriginal<typeof NodeFsPromises>()
|
|
return {
|
|
...actual,
|
|
stat: (...args: Parameters<typeof actual.stat>) => {
|
|
statCalls.push(String(args[0]))
|
|
return actual.stat(...args)
|
|
}
|
|
}
|
|
})
|
|
|
|
const POLL_MS = 25
|
|
|
|
const alwaysVisible: WorktreePollerWindowVisibility = {
|
|
isWindowVisible: () => true,
|
|
onWindowBecameVisible: () => () => {}
|
|
}
|
|
|
|
function createVisibilityHarness(): {
|
|
source: WorktreePollerWindowVisibility
|
|
hide: () => void
|
|
show: () => void
|
|
listenerCount: () => number
|
|
} {
|
|
let visible = true
|
|
// A set, not a single slot: the darwin path parks two independent watches.
|
|
const listeners = new Set<() => void>()
|
|
return {
|
|
source: {
|
|
isWindowVisible: () => visible,
|
|
onWindowBecameVisible: (nextListener) => {
|
|
listeners.add(nextListener)
|
|
return () => {
|
|
listeners.delete(nextListener)
|
|
}
|
|
}
|
|
},
|
|
hide: () => {
|
|
visible = false
|
|
},
|
|
show: () => {
|
|
visible = true
|
|
for (const listener of listeners) {
|
|
listener()
|
|
}
|
|
},
|
|
listenerCount: () => listeners.size
|
|
}
|
|
}
|
|
|
|
type ChildSubscription = {
|
|
dir: string
|
|
callback: WatcherProcessCallback
|
|
hooks: WatcherProcessHooks
|
|
unsubscribe: ReturnType<typeof vi.fn>
|
|
}
|
|
|
|
describe('worktree git-common narrow watch (darwin)', () => {
|
|
const cleanups: (() => Promise<void>)[] = []
|
|
const subscribeMock = vi.mocked(subscribeViaWatcherProcess)
|
|
let childSubscriptions: ChildSubscription[] = []
|
|
|
|
afterEach(async () => {
|
|
await Promise.all(cleanups.splice(0).map((cleanup) => cleanup()))
|
|
childSubscriptions = []
|
|
statCalls.length = 0
|
|
subscribeMock.mockReset()
|
|
})
|
|
|
|
function installSubscribeMock(): void {
|
|
subscribeMock.mockImplementation(async (dir, callback, _opts, hooks = {}) => {
|
|
const unsubscribe = vi.fn(async () => {})
|
|
childSubscriptions.push({ dir, callback, hooks, unsubscribe })
|
|
return { unsubscribe }
|
|
})
|
|
}
|
|
|
|
async function makeCommonDir(withWorktrees: boolean): Promise<string> {
|
|
const root = await mkdtemp(join(tmpdir(), 'orca-git-common-watch-'))
|
|
cleanups.push(() => rm(root, { recursive: true, force: true }))
|
|
const commonDir = await realpath(root)
|
|
if (withWorktrees) {
|
|
await mkdir(join(commonDir, 'worktrees'))
|
|
}
|
|
return commonDir
|
|
}
|
|
|
|
function makeTarget(path: string): WorktreeBaseWatchTarget {
|
|
return {
|
|
key: `git-common:local:${path}`,
|
|
kind: 'git-common',
|
|
path,
|
|
repos: new Map([['repo-1', { repoId: 'repo-1', repoName: 'project', nestWorkspaces: false }]])
|
|
}
|
|
}
|
|
|
|
async function startWatch(commonDir: string, received: WorktreeBasePollEvent[][]): Promise<void> {
|
|
const watch = await startGitCommonWatch(
|
|
makeTarget(commonDir),
|
|
(events) => received.push(events),
|
|
POLL_MS,
|
|
'darwin',
|
|
alwaysVisible
|
|
)
|
|
cleanups.push(() => watch.unsubscribe())
|
|
}
|
|
|
|
it('hosts the narrow stream in the watcher child, not in-process', async () => {
|
|
installSubscribeMock()
|
|
const commonDir = await makeCommonDir(true)
|
|
const received: WorktreeBasePollEvent[][] = []
|
|
await startWatch(commonDir, received)
|
|
|
|
expect(subscribeMock).toHaveBeenCalledTimes(1)
|
|
const [dir, , opts] = subscribeMock.mock.calls[0]
|
|
expect(dir).toBe(join(commonDir, 'worktrees'))
|
|
expect(opts).toEqual({})
|
|
|
|
const entryPath = join(commonDir, 'worktrees', 'wt-a')
|
|
childSubscriptions[0].callback(null, [{ type: 'create', path: entryPath }])
|
|
expect(received.flat()).toContainEqual({ type: 'create', path: entryPath })
|
|
})
|
|
|
|
it('tears down and re-arms when the watched root is deleted', async () => {
|
|
installSubscribeMock()
|
|
const commonDir = await makeCommonDir(true)
|
|
const worktreesDir = join(commonDir, 'worktrees')
|
|
const received: WorktreeBasePollEvent[][] = []
|
|
await startWatch(commonDir, received)
|
|
|
|
await rm(worktreesDir, { recursive: true, force: true })
|
|
childSubscriptions[0].callback(null, [{ type: 'delete', path: worktreesDir }])
|
|
await vi.waitFor(() => {
|
|
expect(childSubscriptions[0].unsubscribe).toHaveBeenCalledTimes(1)
|
|
})
|
|
expect(received.flat()).toContainEqual({ type: 'delete', path: worktreesDir })
|
|
|
|
// The existence poll re-subscribes once a new first worktree recreates it.
|
|
await mkdir(worktreesDir)
|
|
await vi.waitFor(() => {
|
|
expect(subscribeMock).toHaveBeenCalledTimes(2)
|
|
})
|
|
expect(received.flat()).toContainEqual({ type: 'create', path: worktreesDir })
|
|
})
|
|
|
|
it('tears down and re-arms on watcher errors', async () => {
|
|
installSubscribeMock()
|
|
const commonDir = await makeCommonDir(true)
|
|
const worktreesDir = join(commonDir, 'worktrees')
|
|
const received: WorktreeBasePollEvent[][] = []
|
|
await startWatch(commonDir, received)
|
|
|
|
childSubscriptions[0].callback(new Error('watcher child reported failure'), [])
|
|
await vi.waitFor(() => {
|
|
expect(childSubscriptions[0].unsubscribe).toHaveBeenCalledTimes(1)
|
|
})
|
|
// The error is surfaced as a structural change so worktrees re-sync.
|
|
expect(received.flat()).toContainEqual({ type: 'update', path: worktreesDir })
|
|
|
|
// The dir still exists, so the existence poll re-subscribes.
|
|
await vi.waitFor(() => {
|
|
expect(subscribeMock).toHaveBeenCalledTimes(2)
|
|
})
|
|
|
|
const receivedAfterRearm = received.length
|
|
childSubscriptions[0].callback(new Error('late error from replaced watcher'), [])
|
|
childSubscriptions[0].callback(null, [
|
|
{ type: 'create', path: join(worktreesDir, 'late-old-event') }
|
|
])
|
|
childSubscriptions[0].hooks.onInterruption?.()
|
|
|
|
// A replaced watch cannot tear down its successor or report stale events.
|
|
expect(received).toHaveLength(receivedAfterRearm)
|
|
expect(childSubscriptions[1].unsubscribe).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('reports a structural change after a watcher-child interruption', async () => {
|
|
installSubscribeMock()
|
|
const commonDir = await makeCommonDir(true)
|
|
const worktreesDir = join(commonDir, 'worktrees')
|
|
const received: WorktreeBasePollEvent[][] = []
|
|
await startWatch(commonDir, received)
|
|
|
|
childSubscriptions[0].hooks.onInterruption?.()
|
|
expect(received.flat()).toContainEqual({ type: 'update', path: worktreesDir })
|
|
// The supervisor resubscribed the same record; no teardown should happen.
|
|
expect(childSubscriptions[0].unsubscribe).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('arms via existence polling when the worktrees dir appears later', async () => {
|
|
installSubscribeMock()
|
|
const commonDir = await makeCommonDir(false)
|
|
const received: WorktreeBasePollEvent[][] = []
|
|
await startWatch(commonDir, received)
|
|
expect(subscribeMock).not.toHaveBeenCalled()
|
|
|
|
await mkdir(join(commonDir, 'worktrees'))
|
|
await vi.waitFor(() => {
|
|
expect(subscribeMock).toHaveBeenCalledTimes(1)
|
|
})
|
|
})
|
|
|
|
async function startHiddenExistencePoll(visibility: {
|
|
source: WorktreePollerWindowVisibility
|
|
hide: () => void
|
|
}): Promise<{ commonDir: string; worktreesDir: string; received: WorktreeBasePollEvent[][] }> {
|
|
const commonDir = await makeCommonDir(false)
|
|
const received: WorktreeBasePollEvent[][] = []
|
|
const watch = await startGitCommonWatch(
|
|
makeTarget(commonDir),
|
|
(events) => received.push(events),
|
|
POLL_MS,
|
|
'darwin',
|
|
visibility.source
|
|
)
|
|
cleanups.push(() => watch.unsubscribe())
|
|
visibility.hide()
|
|
// Let the armed poll observe the hidden window and park itself.
|
|
await new Promise((resolve) => setTimeout(resolve, POLL_MS * 4))
|
|
statCalls.length = 0
|
|
return { commonDir, worktreesDir: join(commonDir, 'worktrees'), received }
|
|
}
|
|
|
|
it('parks the existence poll while the window is hidden', async () => {
|
|
installSubscribeMock()
|
|
const visibility = createVisibilityHarness()
|
|
const { worktreesDir, received } = await startHiddenExistencePoll(visibility)
|
|
|
|
await mkdir(worktreesDir)
|
|
await new Promise((resolve) => setTimeout(resolve, POLL_MS * 4))
|
|
|
|
expect(statCalls.filter((path) => path === worktreesDir)).toHaveLength(0)
|
|
expect(subscribeMock).not.toHaveBeenCalled()
|
|
expect(received.flat()).toHaveLength(0)
|
|
})
|
|
|
|
it('re-checks on show and still reports a worktrees dir created while hidden', async () => {
|
|
installSubscribeMock()
|
|
const visibility = createVisibilityHarness()
|
|
const { worktreesDir, received } = await startHiddenExistencePoll(visibility)
|
|
|
|
await mkdir(worktreesDir)
|
|
await new Promise((resolve) => setTimeout(resolve, POLL_MS * 4))
|
|
expect(subscribeMock).not.toHaveBeenCalled()
|
|
|
|
visibility.show()
|
|
// Promptly: the re-check stats on show, not a poll interval later.
|
|
expect(statCalls.filter((path) => path === worktreesDir)).toHaveLength(1)
|
|
await vi.waitFor(() => {
|
|
expect(subscribeMock).toHaveBeenCalledTimes(1)
|
|
})
|
|
expect(received.flat()).toContainEqual({ type: 'create', path: worktreesDir })
|
|
})
|
|
|
|
it('resumes polling when the dir is still absent on show', async () => {
|
|
installSubscribeMock()
|
|
const visibility = createVisibilityHarness()
|
|
const { worktreesDir } = await startHiddenExistencePoll(visibility)
|
|
|
|
visibility.show()
|
|
await mkdir(worktreesDir)
|
|
await vi.waitFor(() => {
|
|
expect(subscribeMock).toHaveBeenCalledTimes(1)
|
|
})
|
|
})
|
|
|
|
it('keeps polling and reporting while the window stays visible', async () => {
|
|
installSubscribeMock()
|
|
const visibility = createVisibilityHarness()
|
|
const commonDir = await makeCommonDir(false)
|
|
const worktreesDir = join(commonDir, 'worktrees')
|
|
const received: WorktreeBasePollEvent[][] = []
|
|
const watch = await startGitCommonWatch(
|
|
makeTarget(commonDir),
|
|
(events) => received.push(events),
|
|
POLL_MS,
|
|
'darwin',
|
|
visibility.source
|
|
)
|
|
cleanups.push(() => watch.unsubscribe())
|
|
|
|
await mkdir(worktreesDir)
|
|
await vi.waitFor(() => {
|
|
expect(subscribeMock).toHaveBeenCalledTimes(1)
|
|
})
|
|
expect(received.flat()).toContainEqual({ type: 'create', path: worktreesDir })
|
|
})
|
|
|
|
it('drops both visibility subscriptions on dispose', async () => {
|
|
installSubscribeMock()
|
|
const visibility = createVisibilityHarness()
|
|
const commonDir = await makeCommonDir(false)
|
|
const watch = await startGitCommonWatch(
|
|
makeTarget(commonDir),
|
|
() => {},
|
|
POLL_MS,
|
|
'darwin',
|
|
visibility.source
|
|
)
|
|
|
|
// Narrow watch + primary-metadata poll each park on window visibility.
|
|
expect(visibility.listenerCount()).toBe(2)
|
|
await watch.unsubscribe()
|
|
expect(visibility.listenerCount()).toBe(0)
|
|
})
|
|
|
|
it('keeps the native stream live while the primary poll is parked', async () => {
|
|
installSubscribeMock()
|
|
const commonDir = await makeCommonDir(true)
|
|
const headFile = join(commonDir, 'HEAD')
|
|
await writeFile(headFile, 'ref: refs/heads/main')
|
|
const visibility = createVisibilityHarness()
|
|
const received: WorktreeBasePollEvent[][] = []
|
|
const fullScans: number[] = []
|
|
const watch = await startGitCommonWatch(
|
|
makeTarget(commonDir),
|
|
(events) => received.push(events),
|
|
POLL_MS,
|
|
'darwin',
|
|
visibility.source,
|
|
() => fullScans.push(Date.now())
|
|
)
|
|
cleanups.push(() => watch.unsubscribe())
|
|
|
|
visibility.hide()
|
|
await new Promise((resolve) => setTimeout(resolve, POLL_MS * 2))
|
|
await writeFile(headFile, 'ref: refs/heads/feature')
|
|
await new Promise((resolve) => setTimeout(resolve, POLL_MS * 2))
|
|
|
|
expect(fullScans).toHaveLength(0)
|
|
const entryPath = join(commonDir, 'worktrees', 'native-while-hidden')
|
|
childSubscriptions[0].callback(null, [{ type: 'create', path: entryPath }])
|
|
expect(received.flat()).toContainEqual({ type: 'create', path: entryPath })
|
|
expect(childSubscriptions[0].unsubscribe).not.toHaveBeenCalled()
|
|
|
|
visibility.show()
|
|
expect(fullScans).toHaveLength(1)
|
|
await vi.waitFor(() => {
|
|
expect(received.flat()).toContainEqual({ type: 'update', path: headFile })
|
|
})
|
|
})
|
|
|
|
it('stops forwarding events and unsubscribes the child on dispose', async () => {
|
|
installSubscribeMock()
|
|
const commonDir = await makeCommonDir(true)
|
|
const received: WorktreeBasePollEvent[][] = []
|
|
const watch = await startGitCommonWatch(
|
|
makeTarget(commonDir),
|
|
(events) => received.push(events),
|
|
POLL_MS,
|
|
'darwin',
|
|
alwaysVisible
|
|
)
|
|
await watch.unsubscribe()
|
|
expect(childSubscriptions[0].unsubscribe).toHaveBeenCalledTimes(1)
|
|
|
|
received.length = 0
|
|
childSubscriptions[0].callback(null, [
|
|
{ type: 'create', path: join(commonDir, 'worktrees', 'late') }
|
|
])
|
|
expect(received).toHaveLength(0)
|
|
})
|
|
})
|
|
|
|
describe('worktree git-common polling gate (non-darwin)', () => {
|
|
const cleanups: (() => Promise<void>)[] = []
|
|
|
|
afterEach(async () => {
|
|
await Promise.all(cleanups.splice(0).map((cleanup) => cleanup()))
|
|
})
|
|
|
|
async function makePollingCommonDir(): Promise<string> {
|
|
const root = await mkdtemp(join(tmpdir(), 'orca-git-common-polling-'))
|
|
cleanups.push(() => rm(root, { recursive: true, force: true }))
|
|
const commonDir = await realpath(root)
|
|
await mkdir(join(commonDir, 'worktrees'))
|
|
return commonDir
|
|
}
|
|
|
|
function makePollingTarget(path: string): WorktreeBaseWatchTarget {
|
|
return {
|
|
key: `git-common:local:${path}`,
|
|
kind: 'git-common',
|
|
path,
|
|
repos: new Map([['repo-1', { repoId: 'repo-1', repoName: 'project', nestWorkspaces: false }]])
|
|
}
|
|
}
|
|
|
|
async function startPollingWatch(
|
|
commonDir: string,
|
|
received: WorktreeBasePollEvent[][],
|
|
onFullScan?: () => void,
|
|
visibility: WorktreePollerWindowVisibility = alwaysVisible
|
|
): Promise<void> {
|
|
const watch = await startGitCommonWatch(
|
|
makePollingTarget(commonDir),
|
|
(events) => received.push(events),
|
|
POLL_MS,
|
|
'linux',
|
|
visibility,
|
|
onFullScan
|
|
)
|
|
cleanups.push(() => watch.unsubscribe())
|
|
}
|
|
|
|
it('skips the ungated index-metadata backstop on idle ticks', async () => {
|
|
// Why: idle ticks still re-stat structural leaves and list the (small) worktrees dir cheaply, but the
|
|
// heavier ungated per-entry index fan-out (onFullScan) must NOT run until the backstop — and no
|
|
// spurious events are emitted while nothing changes.
|
|
const commonDir = await makePollingCommonDir()
|
|
const entry = join(commonDir, 'worktrees', 'idle')
|
|
await mkdir(join(entry, 'logs'), { recursive: true })
|
|
await writeFile(join(entry, 'HEAD'), 'ref: refs/heads/main')
|
|
await writeFile(join(entry, 'logs', 'HEAD'), 'baseline\n')
|
|
const received: WorktreeBasePollEvent[][] = []
|
|
const fullScans = vi.fn()
|
|
|
|
await startPollingWatch(commonDir, received, fullScans)
|
|
await new Promise((resolve) => setTimeout(resolve, POLL_MS * 6))
|
|
|
|
expect(fullScans).not.toHaveBeenCalled()
|
|
expect(received.flat()).toHaveLength(0)
|
|
})
|
|
|
|
it('detects linked worktree add and remove from the every-tick readdir', async () => {
|
|
// Why: the worktrees-dir listing runs every tick (not gated on its stat signature), so an add/remove
|
|
// surfaces within one poll interval even on a coarse-mtime filesystem whose dir signature would not
|
|
// move — without waiting on the index backstop (onFullScan).
|
|
const commonDir = await makePollingCommonDir()
|
|
const received: WorktreeBasePollEvent[][] = []
|
|
const fullScans = vi.fn()
|
|
await startPollingWatch(commonDir, received, fullScans)
|
|
|
|
const entry = join(commonDir, 'worktrees', 'added')
|
|
await mkdir(entry)
|
|
await vi.waitFor(() => {
|
|
expect(received.flat()).toContainEqual({ type: 'create', path: entry })
|
|
})
|
|
// The add is caught by the every-tick listing, NOT the 15-tick index backstop: detection lands well
|
|
// before a backstop could fire, so onFullScan must not have run. (On the old gated impl a coarse-FS
|
|
// signature collision would have deferred this to the backstop.)
|
|
expect(fullScans).not.toHaveBeenCalled()
|
|
|
|
await rm(entry, { recursive: true })
|
|
await vi.waitFor(() => {
|
|
expect(received.flat()).toContainEqual({ type: 'delete', path: entry })
|
|
})
|
|
})
|
|
|
|
// Why runIf: chmod 0 cannot revoke directory listing on Windows or for root, so the EACCES injection is inert there.
|
|
it.runIf(process.platform !== 'win32' && process.getuid?.() !== 0)(
|
|
'does not fabricate worktree deletions when the readdir fails non-ENOENT (transient)',
|
|
async () => {
|
|
// Why: a transient readdir failure (EIO/ESTALE/EMFILE/EACCES, network/SSH hiccup) must not be read
|
|
// as "every linked worktree removed". Revoke dir permissions so readdir throws EACCES; the known
|
|
// entry must NOT be reported deleted. On the old catch-all (entryPaths = []) this emitted a false
|
|
// delete for every entry. chmod (not a dir->file swap) because it is one atomic syscall: an
|
|
// in-flight tick's threadpool readdir sees success or EACCES, never a transient ENOENT window
|
|
// that would legitimately emit a delete and flake this assertion.
|
|
const commonDir = await makePollingCommonDir()
|
|
const entry = join(commonDir, 'worktrees', 'keep')
|
|
await mkdir(entry)
|
|
await writeFile(join(entry, 'HEAD'), 'ref: refs/heads/main')
|
|
const received: WorktreeBasePollEvent[][] = []
|
|
await startPollingWatch(commonDir, received)
|
|
|
|
await new Promise((resolve) => setTimeout(resolve, POLL_MS * 2))
|
|
const worktreesDir = join(commonDir, 'worktrees')
|
|
chmodSync(worktreesDir, 0o000)
|
|
try {
|
|
await new Promise((resolve) => setTimeout(resolve, POLL_MS * 4))
|
|
} finally {
|
|
// Why: restore before cleanup so the afterEach recursive rm can traverse the dir.
|
|
chmodSync(worktreesDir, 0o755)
|
|
}
|
|
|
|
expect(received.flat()).not.toContainEqual({ type: 'delete', path: entry })
|
|
}
|
|
)
|
|
|
|
it('detects an in-place structural (HEAD) write on a known entry every tick, without the index backstop', async () => {
|
|
// Why: a raw HEAD/gitdir/config.worktree rewrite does not bump the entry-dir mtime, so the
|
|
// structural leaves are re-stat'd every tick (never gated) — the change surfaces within one tick
|
|
// and does NOT require the ungated index-metadata backstop (onFullScan).
|
|
const commonDir = await makePollingCommonDir()
|
|
const entry = join(commonDir, 'worktrees', 'structural')
|
|
await mkdir(entry)
|
|
await writeFile(join(entry, 'HEAD'), 'ref: refs/heads/main')
|
|
const received: WorktreeBasePollEvent[][] = []
|
|
const fullScans = vi.fn()
|
|
await startPollingWatch(commonDir, received, fullScans)
|
|
|
|
const headPath = join(entry, 'HEAD')
|
|
// In-place rewrite: same file, different contents — no entry-dir mtime change.
|
|
await writeFile(headPath, 'ref: refs/heads/feature')
|
|
await vi.waitFor(() => {
|
|
expect(received.flat()).toContainEqual({ type: 'update', path: headPath })
|
|
})
|
|
expect(fullScans).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('polls linked logs/HEAD on every idle tick', async () => {
|
|
const commonDir = await makePollingCommonDir()
|
|
const entry = join(commonDir, 'worktrees', 'reflog')
|
|
await mkdir(join(entry, 'logs'), { recursive: true })
|
|
const headLogPath = join(entry, 'logs', 'HEAD')
|
|
await writeFile(headLogPath, 'baseline\n')
|
|
const received: WorktreeBasePollEvent[][] = []
|
|
const fullScans = vi.fn()
|
|
await startPollingWatch(commonDir, received, fullScans)
|
|
|
|
await appendFile(headLogPath, 'next\n')
|
|
await vi.waitFor(() => {
|
|
expect(received.flat()).toContainEqual({ type: 'update', path: headLogPath })
|
|
})
|
|
expect(fullScans).not.toHaveBeenCalled()
|
|
})
|
|
|
|
it('forces a full scan on the 15-tick backstop', async () => {
|
|
const commonDir = await makePollingCommonDir()
|
|
const entry = join(commonDir, 'worktrees', 'backstop')
|
|
await mkdir(entry)
|
|
await writeFile(join(entry, 'index'), 'baseline')
|
|
const received: WorktreeBasePollEvent[][] = []
|
|
const fullScans = vi.fn()
|
|
await startPollingWatch(commonDir, received, fullScans)
|
|
|
|
await vi.waitFor(() => {
|
|
expect(fullScans).toHaveBeenCalledTimes(1)
|
|
})
|
|
expect(received.flat()).toHaveLength(0)
|
|
})
|
|
|
|
it('forces a full fan-out when resuming after hidden', async () => {
|
|
const commonDir = await makePollingCommonDir()
|
|
const entry = join(commonDir, 'worktrees', 'resume')
|
|
await mkdir(entry)
|
|
const indexPath = join(entry, 'index')
|
|
await writeFile(indexPath, 'before')
|
|
const received: WorktreeBasePollEvent[][] = []
|
|
const fullScans = vi.fn()
|
|
const visibility = createVisibilityHarness()
|
|
await startPollingWatch(commonDir, received, fullScans, visibility.source)
|
|
|
|
visibility.hide()
|
|
await new Promise((resolve) => setTimeout(resolve, POLL_MS * 2))
|
|
await writeFile(indexPath, 'after-longer')
|
|
await new Promise((resolve) => setTimeout(resolve, POLL_MS * 2))
|
|
expect(fullScans).not.toHaveBeenCalled()
|
|
expect(received.flat()).toHaveLength(0)
|
|
|
|
visibility.show()
|
|
await vi.waitFor(() => {
|
|
expect(received.flat()).toContainEqual({ type: 'update', path: indexPath })
|
|
})
|
|
expect(fullScans).toHaveBeenCalledTimes(1)
|
|
})
|
|
})
|