Files
orca/src/main/ipc/worktree-base-directory-poller.test.ts
T
NeilandOrca 414bdc9f86 perf(main): gate idle git-common polling + coarse-FS add/remove fix (re-open of #9882) (#10074)
* perf(main): gate idle git-common polling

Co-authored-by: Orca <help@stably.ai>

* fix(main): enumerate git-common worktrees every tick to catch coarse-FS add/remove

The worktrees-dir readdir was gated on the dir's mtime:ctime:ino:size signature, so on
a coarse-mtime/FAT filesystem a same-granule add+remove (all four fields collide) went
undetected until the ~30s index backstop. A single readdir of a small dir is negligible
next to the per-entry structural stats that already run every tick, so always enumerate —
the listing is the authoritative add/remove signal. The expensive per-entry index read
stays gated on each entry's own dir signature; onFullScan now reflects the ungated
index-metadata backstop fan-out (the real periodic cost) rather than the readdir.

Co-authored-by: Orca <help@stably.ai>

* fix(main): don't fabricate worktree deletions on a transient git-common readdir failure

Follow-up to always-enumerating the worktrees dir: the readdir catch-all treated ANY
error as an authoritative empty listing, so a transient failure (EIO/ESTALE/EMFILE,
network/SSH hiccup) emitted a false delete for every linked worktree (and a false create
next tick) — and enumerating every tick widened that exposure. Only ENOENT (dir truly
absent) now yields an empty listing; other errors retain the known entries so per-entry
stats still run and a real removal surfaces as that entry's own stat miss. Adds a
regression test (readdir → ENOTDIR) asserting no false delete.

Co-authored-by: Orca <help@stably.ai>

---------

Co-authored-by: Orca <help@stably.ai>
2026-07-22 20:41:30 -07:00

631 lines
23 KiB
TypeScript

import { afterEach, describe, expect, it, vi } from 'vitest'
import { mkdtemp, mkdir, realpath, rm, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import {
createWorktreePollerWindowVisibility,
startWorktreeBaseDirectoryPoller,
type WorktreeBasePollEvent,
type WorktreePollerWindowVisibility
} from './worktree-base-directory-poller'
import type {
WorktreeBaseRepoWatchConfig,
WorktreeBaseWatchTarget
} from './worktree-base-directory-event-filter'
const POLL_MS = 25
type VisibilityHarness = {
source: WorktreePollerWindowVisibility
hide: () => void
show: () => void
}
function createVisibilityHarness(initiallyVisible = true): VisibilityHarness {
let visible = initiallyVisible
let listener: (() => void) | null = null
return {
source: {
isWindowVisible: () => visible,
onWindowBecameVisible: (nextListener) => {
listener = nextListener
return () => {
if (listener === nextListener) {
listener = null
}
}
}
},
hide: () => {
visible = false
},
show: () => {
visible = true
listener?.()
}
}
}
function makeTarget(
kind: 'base' | 'git-common',
path: string,
config: Partial<WorktreeBaseRepoWatchConfig> = {}
): WorktreeBaseWatchTarget {
const repoConfig: WorktreeBaseRepoWatchConfig = {
repoId: 'repo-1',
repoName: 'project',
nestWorkspaces: false,
...config
}
return {
key: `${kind}:local:${path}`,
kind,
path,
repos: new Map([[repoConfig.repoId, repoConfig]])
}
}
async function waitForEvents(
events: WorktreeBasePollEvent[][],
predicate: (flat: WorktreeBasePollEvent[]) => boolean
): Promise<WorktreeBasePollEvent[]> {
await vi.waitFor(
() => {
if (!predicate(events.flat())) {
throw new Error('expected poll events not observed yet')
}
},
{ timeout: 5_000, interval: 20 }
)
return events.flat()
}
describe('worktree base directory poller', () => {
const cleanups: (() => Promise<void>)[] = []
afterEach(async () => {
await Promise.all(cleanups.splice(0).map((cleanup) => cleanup()))
})
async function makeRoot(): Promise<string> {
const root = await mkdtemp(join(tmpdir(), 'orca-base-poller-'))
cleanups.push(() => rm(root, { recursive: true, force: true }))
// Why: macOS tmpdir lives behind the /var -> /private/var symlink and
// native watcher events report resolved paths; production targets are
// realpath'd the same way (canonicalizeExistingPath).
return realpath(root)
}
it('emits a .git marker create and a worktree delete for flat layouts', async () => {
const root = await makeRoot()
const received: WorktreeBasePollEvent[][] = []
const target = makeTarget('base', root)
const poller = await startWorktreeBaseDirectoryPoller(
target,
() => target.repos,
(events) => received.push(events),
{ pollIntervalMs: POLL_MS }
)
cleanups.push(() => poller.unsubscribe())
const worktree = join(root, 'external-1')
await mkdir(worktree)
await writeFile(join(worktree, '.git'), 'gitdir: elsewhere')
const afterCreate = await waitForEvents(received, (flat) =>
flat.some((event) => event.type === 'create' && event.path === join(worktree, '.git'))
)
expect(
afterCreate.filter((event) => event.type === 'create' && event.path.endsWith('.git'))
).toHaveLength(1)
await rm(worktree, { recursive: true })
await waitForEvents(received, (flat) =>
flat.some((event) => event.type === 'delete' && event.path === worktree)
)
})
it('emits the marker only after it appears for slow checkouts', async () => {
const root = await makeRoot()
const received: WorktreeBasePollEvent[][] = []
const target = makeTarget('base', root)
const poller = await startWorktreeBaseDirectoryPoller(
target,
() => target.repos,
(events) => received.push(events),
{ pollIntervalMs: POLL_MS }
)
cleanups.push(() => poller.unsubscribe())
const worktree = join(root, 'external-2')
await mkdir(worktree)
// Give the poller time to observe the marker-less dir first.
await new Promise((resolve) => setTimeout(resolve, POLL_MS * 3))
expect(received.flat()).toHaveLength(0)
await writeFile(join(worktree, '.git'), 'gitdir: elsewhere')
await waitForEvents(received, (flat) =>
flat.some((event) => event.type === 'create' && event.path === join(worktree, '.git'))
)
})
it('scans nested repo containers for nested layouts', async () => {
const root = await makeRoot()
const received: WorktreeBasePollEvent[][] = []
const target = makeTarget('base', root, { nestWorkspaces: true })
const poller = await startWorktreeBaseDirectoryPoller(
target,
() => target.repos,
(events) => received.push(events),
{ pollIntervalMs: POLL_MS }
)
cleanups.push(() => poller.unsubscribe())
const worktree = join(root, 'project', 'external-3')
await mkdir(worktree, { recursive: true })
await writeFile(join(worktree, '.git'), 'gitdir: elsewhere')
await waitForEvents(received, (flat) =>
flat.some((event) => event.type === 'create' && event.path === join(worktree, '.git'))
)
})
it('skips full scans while the gate dirs are untouched', async () => {
const root = await makeRoot()
const worktree = join(root, 'external-idle')
await mkdir(worktree)
await writeFile(join(worktree, '.git'), 'gitdir: elsewhere')
const received: WorktreeBasePollEvent[][] = []
const fullScans: number[] = []
const target = makeTarget('base', root)
const poller = await startWorktreeBaseDirectoryPoller(
target,
() => target.repos,
(events) => received.push(events),
{ pollIntervalMs: POLL_MS, onFullScan: () => fullScans.push(Date.now()) }
)
cleanups.push(() => poller.unsubscribe())
// ~8 idle ticks: under the backstop cadence, so the gate should skip
// every full scan (each tick is just gate-dir stats).
await new Promise((resolve) => setTimeout(resolve, POLL_MS * 8))
expect(fullScans).toHaveLength(0)
expect(received.flat()).toHaveLength(0)
// Touching the root (new entry) flips the gate and triggers a scan.
await mkdir(join(root, 'external-new'))
await writeFile(join(root, 'external-new', '.git'), 'gitdir: elsewhere')
await waitForEvents(received, (flat) =>
flat.some(
(event) => event.type === 'create' && event.path === join(root, 'external-new', '.git')
)
)
expect(fullScans.length).toBeGreaterThan(0)
})
it('parks base scans while hidden and losslessly detects changes on resume', async () => {
const root = await makeRoot()
const visibility = createVisibilityHarness()
const received: WorktreeBasePollEvent[][] = []
const fullScans: number[] = []
const target = makeTarget('base', root)
const poller = await startWorktreeBaseDirectoryPoller(
target,
() => target.repos,
(events) => received.push(events),
{
pollIntervalMs: POLL_MS,
visibility: visibility.source,
onFullScan: () => fullScans.push(Date.now())
}
)
cleanups.push(() => poller.unsubscribe())
visibility.hide()
await new Promise((resolve) => setTimeout(resolve, POLL_MS * 2))
const worktree = join(root, 'added-while-hidden')
await mkdir(worktree)
await writeFile(join(worktree, '.git'), 'gitdir: elsewhere')
await new Promise((resolve) => setTimeout(resolve, POLL_MS * 2))
expect(received.flat()).toHaveLength(0)
expect(fullScans).toHaveLength(0)
visibility.show()
expect(fullScans).toHaveLength(1)
await waitForEvents(received, (flat) =>
flat.some((event) => event.type === 'create' && event.path === join(worktree, '.git'))
)
})
it('keeps polling without a main window', async () => {
const root = await makeRoot()
const received: WorktreeBasePollEvent[][] = []
const target = makeTarget('base', root)
const visibility = createWorktreePollerWindowVisibility(() => null)
const poller = await startWorktreeBaseDirectoryPoller(
target,
() => target.repos,
(events) => received.push(events),
{ pollIntervalMs: POLL_MS, visibility }
)
cleanups.push(() => poller.unsubscribe())
const worktree = join(root, 'headless-add')
await mkdir(worktree)
await writeFile(join(worktree, '.git'), 'gitdir: elsewhere')
await waitForEvents(received, (flat) =>
flat.some((event) => event.type === 'create' && event.path === join(worktree, '.git'))
)
expect(visibility.isWindowVisible()).toBe(true)
})
it('treats a destroyed window as absent instead of parking forever', () => {
const visibility = createWorktreePollerWindowVisibility(() => ({ isDestroyed: () => true }))
expect(visibility.isWindowVisible()).toBe(true)
})
it('keeps polling a live window that has never been shown (E2E headless)', () => {
// ORCA_E2E_HEADLESS keeps a live BrowserWindow that is never shown; no show/restore
// signal is coming to resume a parked poller, so a never-shown window must keep polling.
const visibility = createWorktreePollerWindowVisibility(() => ({
isDestroyed: () => false,
isVisible: () => false,
isMinimized: () => false
}))
expect(visibility.isWindowVisible()).toBe(true)
expect(visibility.isWindowVisible()).toBe(true)
})
it('parks only after the window has been shown at least once', () => {
let visible = true
const visibility = createWorktreePollerWindowVisibility(() => ({
isDestroyed: () => false,
isVisible: () => visible,
isMinimized: () => false
}))
// Shown at least once: a later reveal will fire the visibility signal to resume.
expect(visibility.isWindowVisible()).toBe(true)
// Now hidden — a previously-shown window parks (its show/restore will resume it).
visible = false
expect(visibility.isWindowVisible()).toBe(false)
})
it('reports git-common entry creates, allowlisted leaf updates, and removals via polling', async () => {
const commonDir = await makeRoot()
const received: WorktreeBasePollEvent[][] = []
const target = makeTarget('git-common', commonDir)
const poller = await startWorktreeBaseDirectoryPoller(
target,
() => target.repos,
(events) => received.push(events),
// Force the non-darwin poll path so this test is deterministic on all CI.
{ pollIntervalMs: POLL_MS, platform: 'linux' }
)
cleanups.push(() => poller.unsubscribe())
const entry = join(commonDir, 'worktrees', 'external-4')
await mkdir(entry, { recursive: true })
await waitForEvents(received, (flat) =>
flat.some((event) => event.type === 'create' && event.path === entry)
)
await new Promise((resolve) => setTimeout(resolve, 10))
await writeFile(join(entry, 'HEAD'), 'ref: refs/heads/main')
await waitForEvents(received, (flat) =>
flat.some((event) => event.type === 'create' && event.path === join(entry, 'HEAD'))
)
await new Promise((resolve) => setTimeout(resolve, 10))
await writeFile(join(entry, 'index'), 'status')
await waitForEvents(received, (flat) =>
flat.some((event) => event.type === 'create' && event.path === join(entry, 'index'))
)
await rm(entry, { recursive: true })
await waitForEvents(received, (flat) =>
flat.some((event) => event.type === 'delete' && event.path === entry)
)
})
it('detects an in-place linked HEAD rewrite that leaves the entry directory signature unchanged', async () => {
const commonDir = await makeRoot()
const entry = join(commonDir, 'worktrees', 'external-head')
await mkdir(entry, { recursive: true })
await writeFile(join(entry, 'HEAD'), 'ref: refs/heads/main')
const received: WorktreeBasePollEvent[][] = []
const target = makeTarget('git-common', commonDir)
const poller = await startWorktreeBaseDirectoryPoller(
target,
() => target.repos,
(events) => received.push(events),
{ pollIntervalMs: POLL_MS, platform: 'linux' }
)
cleanups.push(() => poller.unsubscribe())
// Why: rewriting an existing HEAD in place changes only the file's own metadata, never the
// parent entry directory's mtime/ctime/ino/size — so HEAD (like the other structural leaves)
// must be re-stat'd every tick, not gated behind the entry-dir signature.
await new Promise((resolve) => setTimeout(resolve, 10))
await writeFile(join(entry, 'HEAD'), 'ref: refs/heads/next')
await waitForEvents(received, (flat) =>
flat.some((event) => event.type === 'update' && event.path === join(entry, 'HEAD'))
)
})
it('reports linked HEAD reflog appends that bump no watched leaf or entry dir', async () => {
const commonDir = await makeRoot()
const entry = join(commonDir, 'worktrees', 'external-amend')
await mkdir(join(entry, 'logs'), { recursive: true })
await writeFile(join(entry, 'logs', 'HEAD'), 'baseline\n')
const received: WorktreeBasePollEvent[][] = []
const target = makeTarget('git-common', commonDir)
const poller = await startWorktreeBaseDirectoryPoller(
target,
() => target.repos,
(events) => received.push(events),
{ pollIntervalMs: POLL_MS, platform: 'linux' }
)
cleanups.push(() => poller.unsubscribe())
// commit --amend appends to the reflog inside logs/ — the entry dir mtime
// never moves, so the leaf must be stat'd directly.
await new Promise((resolve) => setTimeout(resolve, 10))
await writeFile(join(entry, 'logs', 'HEAD'), 'baseline\namended\n')
await waitForEvents(received, (flat) =>
flat.some((event) => event.type === 'update' && event.path === join(entry, 'logs', 'HEAD'))
)
})
it('surfaces in-place index rewrites through the backstop re-stat', async () => {
const commonDir = await makeRoot()
const entry = join(commonDir, 'worktrees', 'external-inplace')
await mkdir(entry, { recursive: true })
await writeFile(join(entry, 'index'), 'index-v1')
const received: WorktreeBasePollEvent[][] = []
const target = makeTarget('git-common', commonDir)
const poller = await startWorktreeBaseDirectoryPoller(
target,
() => target.repos,
(events) => received.push(events),
{ pollIntervalMs: POLL_MS, platform: 'linux' }
)
cleanups.push(() => poller.unsubscribe())
// An in-place rewrite leaves the entry-dir signature untouched, so only
// the periodic ungated re-stat can observe it.
await new Promise((resolve) => setTimeout(resolve, 10))
await writeFile(join(entry, 'index'), 'index-v2-longer')
await waitForEvents(received, (flat) =>
flat.some((event) => event.type === 'update' && event.path === join(entry, 'index'))
)
})
it('reports primary-checkout HEAD reflog appends via polling', async () => {
const commonDir = await makeRoot()
await mkdir(join(commonDir, 'logs'), { recursive: true })
await writeFile(join(commonDir, 'logs', 'HEAD'), 'baseline\n')
const received: WorktreeBasePollEvent[][] = []
const target = makeTarget('git-common', commonDir)
const poller = await startWorktreeBaseDirectoryPoller(
target,
() => target.repos,
(events) => received.push(events),
{ pollIntervalMs: POLL_MS, platform: 'linux' }
)
cleanups.push(() => poller.unsubscribe())
await new Promise((resolve) => setTimeout(resolve, 10))
await writeFile(join(commonDir, 'logs', 'HEAD'), 'baseline\namended\n')
await waitForEvents(received, (flat) =>
flat.some(
(event) => event.type === 'update' && event.path === join(commonDir, 'logs', 'HEAD')
)
)
})
it('reports primary-checkout HEAD changes via polling', async () => {
const commonDir = await makeRoot()
const received: WorktreeBasePollEvent[][] = []
const target = makeTarget('git-common', commonDir)
const poller = await startWorktreeBaseDirectoryPoller(
target,
() => target.repos,
(events) => received.push(events),
// Force the non-darwin poll path so this test is deterministic on all CI.
{ pollIntervalMs: POLL_MS, platform: 'linux' }
)
cleanups.push(() => poller.unsubscribe())
const headFile = join(commonDir, 'HEAD')
await writeFile(headFile, 'ref: refs/heads/main')
await waitForEvents(received, (flat) =>
flat.some((event) => event.type === 'create' && event.path === headFile)
)
// A branch switch rewrites HEAD in place; the mtime diff must surface it.
await new Promise((resolve) => setTimeout(resolve, 10))
await writeFile(headFile, 'ref: refs/heads/feature')
await waitForEvents(received, (flat) =>
flat.some((event) => event.type === 'update' && event.path === headFile)
)
})
it('detects a primary HEAD move immediately after resuming', async () => {
const commonDir = await makeRoot()
const headFile = join(commonDir, 'HEAD')
await writeFile(headFile, 'ref: refs/heads/main')
const visibility = createVisibilityHarness()
const received: WorktreeBasePollEvent[][] = []
const fullScans: number[] = []
const target = makeTarget('git-common', commonDir)
const poller = await startWorktreeBaseDirectoryPoller(
target,
() => target.repos,
(events) => received.push(events),
{
pollIntervalMs: POLL_MS,
platform: 'linux',
visibility: visibility.source,
onFullScan: () => fullScans.push(Date.now())
}
)
cleanups.push(() => poller.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(received.flat()).toHaveLength(0)
expect(fullScans).toHaveLength(0)
visibility.show()
await waitForEvents(received, (flat) =>
flat.some((event) => event.type === 'update' && event.path === headFile)
)
expect(fullScans).toHaveLength(1)
})
it('emits deletes for all known worktrees when the root vanishes', async () => {
const root = await makeRoot()
const worktree = join(root, 'external-5')
await mkdir(worktree)
await writeFile(join(worktree, '.git'), 'gitdir: elsewhere')
const received: WorktreeBasePollEvent[][] = []
const target = makeTarget('base', root)
const poller = await startWorktreeBaseDirectoryPoller(
target,
() => target.repos,
(events) => received.push(events),
{ pollIntervalMs: POLL_MS }
)
cleanups.push(() => poller.unsubscribe())
await rm(root, { recursive: true, force: true })
await waitForEvents(received, (flat) =>
flat.some((event) => event.type === 'delete' && event.path === worktree)
)
})
describe.runIf(process.platform === 'darwin')('macOS narrow git-common stream', () => {
it('delivers instant add/update/remove without polling', async () => {
const commonDir = await makeRoot()
await mkdir(join(commonDir, 'worktrees'))
const received: WorktreeBasePollEvent[][] = []
const target = makeTarget('git-common', commonDir)
const poller = await startWorktreeBaseDirectoryPoller(
target,
() => target.repos,
(events) => received.push(events),
{ pollIntervalMs: POLL_MS, platform: 'darwin' }
)
cleanups.push(() => poller.unsubscribe())
const entry = join(commonDir, 'worktrees', 'wt-a')
await mkdir(entry)
await waitForEvents(received, (flat) => flat.some((event) => event.path === entry))
await writeFile(join(entry, 'HEAD'), 'ref: refs/heads/main')
await waitForEvents(received, (flat) =>
flat.some((event) => event.path === join(entry, 'HEAD'))
)
await rm(entry, { recursive: true })
await waitForEvents(received, (flat) =>
flat.some((event) => event.type === 'delete' && event.path === entry)
)
})
it('covers primary-checkout metadata alongside the narrow stream', async () => {
const commonDir = await makeRoot()
await mkdir(join(commonDir, 'worktrees'))
const received: WorktreeBasePollEvent[][] = []
const target = makeTarget('git-common', commonDir)
const poller = await startWorktreeBaseDirectoryPoller(
target,
() => target.repos,
(events) => received.push(events),
{ pollIntervalMs: POLL_MS, platform: 'darwin' }
)
cleanups.push(() => poller.unsubscribe())
// The narrow stream is rooted at worktrees/, so top-level HEAD writes
// must arrive through the companion metadata poll.
const headFile = join(commonDir, 'HEAD')
await writeFile(headFile, 'ref: refs/heads/main')
await waitForEvents(received, (flat) =>
flat.some((event) => event.type === 'create' && event.path === headFile)
)
await new Promise((resolve) => setTimeout(resolve, 10))
await writeFile(headFile, 'ref: refs/heads/feature')
await waitForEvents(received, (flat) =>
flat.some((event) => event.type === 'update' && event.path === headFile)
)
})
it('arms via existence polling when the worktrees dir appears later', async () => {
const commonDir = await makeRoot()
const received: WorktreeBasePollEvent[][] = []
const target = makeTarget('git-common', commonDir)
const poller = await startWorktreeBaseDirectoryPoller(
target,
() => target.repos,
(events) => received.push(events),
{ pollIntervalMs: POLL_MS, platform: 'darwin' }
)
cleanups.push(() => poller.unsubscribe())
const worktreesDir = join(commonDir, 'worktrees')
await mkdir(worktreesDir)
// The dir appearing is itself surfaced (a first worktree was added).
await waitForEvents(received, (flat) =>
flat.some((event) => event.type === 'create' && event.path === worktreesDir)
)
// And the narrow stream is live afterwards: entry adds are seen.
const entry = join(worktreesDir, 'wt-b')
await mkdir(entry)
await waitForEvents(received, (flat) => flat.some((event) => event.path === entry))
})
it('keeps watching across worktrees dir delete and recreate', async () => {
const commonDir = await makeRoot()
await mkdir(join(commonDir, 'worktrees'))
const received: WorktreeBasePollEvent[][] = []
const target = makeTarget('git-common', commonDir)
const poller = await startWorktreeBaseDirectoryPoller(
target,
() => target.repos,
(events) => received.push(events),
{ pollIntervalMs: POLL_MS, platform: 'darwin' }
)
cleanups.push(() => poller.unsubscribe())
// Simulate `git worktree prune` removing the empty dir, then a new add
// recreating it. The stream re-arms via the existence poll; the repo
// gets notified on recreate, and subsequent entries are seen live.
const worktreesDir = join(commonDir, 'worktrees')
await rm(worktreesDir, { recursive: true })
await new Promise((resolve) => setTimeout(resolve, 100))
await mkdir(join(worktreesDir, 'wt-c'), { recursive: true })
await waitForEvents(received, (flat) =>
flat.some((event) => event.type === 'create' && event.path === worktreesDir)
)
const laterEntry = join(worktreesDir, 'wt-d')
await mkdir(laterEntry)
await waitForEvents(received, (flat) => flat.some((event) => event.path === laterEntry))
})
})
})