mirror of
https://github.com/stablyai/orca.git
synced 2026-10-23 16:03:49 +00:00
test(git-watch): reserve replacement root inode
This commit is contained in:
@@ -67,14 +67,11 @@ export class GitMetadataPollScheduler {
|
||||
private nextId = 1
|
||||
private activePolls = 0
|
||||
private activeFilesystemOperations = 0
|
||||
private resetGeneration = 0
|
||||
private now: () => number
|
||||
|
||||
constructor(
|
||||
private readonly concurrency = GIT_METADATA_FILESYSTEM_CONCURRENCY,
|
||||
now: () => number = () => performance.now()
|
||||
private readonly now: () => number = () => performance.now()
|
||||
) {
|
||||
this.now = now
|
||||
if (!Number.isInteger(concurrency) || concurrency < 1) {
|
||||
throw new Error('Git metadata scheduler concurrency must be a positive integer')
|
||||
}
|
||||
@@ -163,28 +160,6 @@ export class GitMetadataPollScheduler {
|
||||
return promise
|
||||
}
|
||||
|
||||
setNowForTests(now: () => number): void {
|
||||
this.now = now
|
||||
}
|
||||
|
||||
resetForTests(): void {
|
||||
this.resetGeneration++
|
||||
if (this.timer) {
|
||||
clearTimeout(this.timer)
|
||||
this.timer = null
|
||||
}
|
||||
for (const poll of this.polls.values()) {
|
||||
poll.disposed = true
|
||||
poll.unsubscribeVisibility()
|
||||
poll.queued = false
|
||||
}
|
||||
this.polls.clear()
|
||||
this.pollQueue.length = 0
|
||||
this.filesystemQueue.length = 0
|
||||
this.activePolls = 0
|
||||
this.activeFilesystemOperations = 0
|
||||
}
|
||||
|
||||
private unsubscribePoll(poll: ScheduledPoll): void {
|
||||
if (poll.disposed) {
|
||||
return
|
||||
@@ -277,7 +252,6 @@ export class GitMetadataPollScheduler {
|
||||
}
|
||||
const isVisibilityCatchUp = poll.pendingVisibilityCatchUp
|
||||
poll.pendingVisibilityCatchUp = false
|
||||
const resetGeneration = this.resetGeneration
|
||||
poll.running = true
|
||||
this.activePolls++
|
||||
let result: void | Promise<void>
|
||||
@@ -291,9 +265,6 @@ export class GitMetadataPollScheduler {
|
||||
// Each poller owns its transient-error policy; keep the host scheduler alive.
|
||||
})
|
||||
.finally(() => {
|
||||
if (resetGeneration !== this.resetGeneration) {
|
||||
return
|
||||
}
|
||||
poll.running = false
|
||||
this.activePolls--
|
||||
if (!poll.disposed && poll.rerunPending) {
|
||||
@@ -310,12 +281,8 @@ export class GitMetadataPollScheduler {
|
||||
private drainFilesystemQueue(): void {
|
||||
while (this.activeFilesystemOperations < this.concurrency && this.filesystemQueue.length > 0) {
|
||||
const operation = this.filesystemQueue.shift()!
|
||||
const resetGeneration = this.resetGeneration
|
||||
this.activeFilesystemOperations++
|
||||
void operation.execute().finally(() => {
|
||||
if (resetGeneration !== this.resetGeneration) {
|
||||
return
|
||||
}
|
||||
this.activeFilesystemOperations--
|
||||
this.drainFilesystemQueue()
|
||||
})
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
|
||||
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 { performance as nodePerformance } from 'node:perf_hooks'
|
||||
@@ -18,17 +18,10 @@ import type {
|
||||
} from './worktree-base-directory-poller'
|
||||
import { startGitCommonWatch } from './worktree-git-common-watch'
|
||||
import { startGitCommonPolling } from './worktree-git-common-polling'
|
||||
import { gitMetadataPollScheduler } from './git-metadata-poll-scheduler'
|
||||
|
||||
vi.mock('./parcel-watcher-process', () => ({
|
||||
subscribeViaWatcherProcess: vi.fn()
|
||||
}))
|
||||
|
||||
beforeEach(() => {
|
||||
gitMetadataPollScheduler.resetForTests()
|
||||
gitMetadataPollScheduler.setNowForTests(() => Date.now())
|
||||
})
|
||||
|
||||
// Records every stat target so a test can assert which paths a parked poll stopped touching.
|
||||
const { statCalls } = vi.hoisted(() => ({ statCalls: [] as string[] }))
|
||||
|
||||
@@ -54,6 +47,20 @@ const alwaysVisible: WorktreePollerWindowVisibility = {
|
||||
onWindowBecameVisible: () => () => {}
|
||||
}
|
||||
|
||||
async function replaceWorktreesRoot(
|
||||
commonDir: string,
|
||||
worktreesDir: string,
|
||||
retainedEntry: string
|
||||
): Promise<void> {
|
||||
await rm(worktreesDir, { recursive: true })
|
||||
// Linux can reuse a just-deleted directory inode. Reserve that inode with a
|
||||
// sibling before recreating the watched root so reconciliation sees a replacement.
|
||||
const inodeReservation = join(commonDir, 'worktrees-replacement-inode')
|
||||
await mkdir(inodeReservation)
|
||||
await mkdir(retainedEntry, { recursive: true })
|
||||
await rm(inodeReservation, { recursive: true, force: true })
|
||||
}
|
||||
|
||||
function createVisibilityHarness(): {
|
||||
source: WorktreePollerWindowVisibility
|
||||
hide: () => void
|
||||
@@ -100,7 +107,6 @@ describe('worktree git-common narrow watch (darwin)', () => {
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(cleanups.splice(0).map((cleanup) => cleanup()))
|
||||
gitMetadataPollScheduler.resetForTests()
|
||||
childSubscriptions = []
|
||||
statCalls.length = 0
|
||||
subscribeMock.mockReset()
|
||||
@@ -524,7 +530,6 @@ describe('worktree git-common narrow watch (darwin)', () => {
|
||||
|
||||
it('re-arms the native stream when the root is replaced with the same child names', async () => {
|
||||
vi.useFakeTimers()
|
||||
vi.clearAllTimers()
|
||||
const restorePerformanceNow = vi
|
||||
.spyOn(nodePerformance, 'now')
|
||||
.mockImplementation(() => Date.now())
|
||||
@@ -538,18 +543,9 @@ describe('worktree git-common narrow watch (darwin)', () => {
|
||||
await startWatch(commonDir, received)
|
||||
const staleSubscription = childSubscriptions[0]
|
||||
|
||||
await rm(worktreesDir, { recursive: true })
|
||||
await mkdir(retainedEntry, { recursive: true })
|
||||
await replaceWorktreesRoot(commonDir, worktreesDir, retainedEntry)
|
||||
await vi.advanceTimersByTimeAsync(POLL_MS * RECONCILIATION_TICKS * 4)
|
||||
await vi.runOnlyPendingTimersAsync()
|
||||
// Native filesystem promises settle outside the fake timer callback; wait for the
|
||||
// re-subscribe observable rather than relying on timer advancement to flush them.
|
||||
await vi.waitFor(
|
||||
() => {
|
||||
expect(subscribeMock).toHaveBeenCalledTimes(2)
|
||||
},
|
||||
{ timeout: 1_000 }
|
||||
)
|
||||
expect(subscribeMock).toHaveBeenCalledTimes(2)
|
||||
expect(staleSubscription.unsubscribe).toHaveBeenCalledOnce()
|
||||
|
||||
const beforeStaleEvent = received.length
|
||||
@@ -568,7 +564,6 @@ describe('worktree git-common narrow watch (darwin)', () => {
|
||||
|
||||
it('disposes an in-flight stale resubscribe and fences its interruption hook', async () => {
|
||||
vi.useFakeTimers()
|
||||
vi.clearAllTimers()
|
||||
const restorePerformanceNow = vi
|
||||
.spyOn(nodePerformance, 'now')
|
||||
.mockImplementation(() => Date.now())
|
||||
@@ -589,31 +584,16 @@ describe('worktree git-common narrow watch (darwin)', () => {
|
||||
const onWatchError = vi.fn()
|
||||
await startWatch(commonDir, received, () => [], onWatchError)
|
||||
|
||||
await rm(worktreesDir, { recursive: true })
|
||||
await mkdir(retainedEntry, { recursive: true })
|
||||
await replaceWorktreesRoot(commonDir, worktreesDir, retainedEntry)
|
||||
await vi.advanceTimersByTimeAsync(POLL_MS * RECONCILIATION_TICKS * 4)
|
||||
await vi.runOnlyPendingTimersAsync()
|
||||
await vi.waitFor(
|
||||
() => {
|
||||
expect(subscribeMock).toHaveBeenCalledTimes(2)
|
||||
},
|
||||
{ timeout: 1_000 }
|
||||
)
|
||||
expect(subscribeMock).toHaveBeenCalledTimes(2)
|
||||
const stalePendingSubscription = childSubscriptions[1]
|
||||
|
||||
await rm(worktreesDir, { recursive: true })
|
||||
await mkdir(retainedEntry, { recursive: true })
|
||||
await replaceWorktreesRoot(commonDir, worktreesDir, retainedEntry)
|
||||
await vi.advanceTimersByTimeAsync(POLL_MS * RECONCILIATION_TICKS * 4)
|
||||
await vi.waitFor(
|
||||
() => {
|
||||
expect(
|
||||
received
|
||||
.flat()
|
||||
.filter((event) => event.type === 'create' && event.path === worktreesDir)
|
||||
).toHaveLength(2)
|
||||
},
|
||||
{ timeout: 1_000 }
|
||||
)
|
||||
expect(
|
||||
received.flat().filter((event) => event.type === 'create' && event.path === worktreesDir)
|
||||
).toHaveLength(2)
|
||||
|
||||
const beforeStaleInterruption = received.length
|
||||
stalePendingSubscription.hooks.onInterruption?.()
|
||||
@@ -626,12 +606,7 @@ describe('worktree git-common narrow watch (darwin)', () => {
|
||||
}
|
||||
})
|
||||
await vi.advanceTimersByTimeAsync(POLL_MS)
|
||||
await vi.waitFor(
|
||||
() => {
|
||||
expect(subscribeMock).toHaveBeenCalledTimes(3)
|
||||
},
|
||||
{ timeout: 1_000 }
|
||||
)
|
||||
expect(subscribeMock).toHaveBeenCalledTimes(3)
|
||||
expect(stalePendingSubscription.unsubscribe).toHaveBeenCalledOnce()
|
||||
|
||||
const immediateEntry = join(worktreesDir, 'current-generation')
|
||||
@@ -671,7 +646,6 @@ describe('worktree git-common polling gate (non-darwin)', () => {
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(cleanups.splice(0).map((cleanup) => cleanup()))
|
||||
gitMetadataPollScheduler.resetForTests()
|
||||
})
|
||||
|
||||
async function makePollingCommonDir(): Promise<string> {
|
||||
|
||||
Reference in New Issue
Block a user