mirror of
https://github.com/stablyai/orca.git
synced 2026-10-07 16:02:29 +00:00
* fix(memory): bound session and lifecycle registries * fix(memory): bound transient filesystem registries * fix(memory): cap path and locale caches * fix(memory): bound runtime recovery registries * fix(memory): bound host mirror gap verdicts * fix(memory): bound shell startup env cache * fix(memory): bound gitlab host context cache * fix(memory): release removed ssh generations * fix(memory): expire cloud refresh replay guards * fix(memory): release retired plugin generations * fix(memory): bound plugin log key retention * fix(memory): bound automation authority generations * fix(memory): bound native chat enrichment cache * fix(memory): bound web session tracking generations * fix(memory): bound codex credential absence paths * fix(memory): bound WSL canonical path cache * fix(memory): bound sparse checkout cache * fix(memory): bound shared directory cache * fix(memory): bound advertised URL scan snapshots * fix(memory): bound automation manager cache * fix(memory): bound web session reorder intents * fix(memory): bound web session focus intents * fix(memory): bound web session handoffs * fix(memory): bound automation dispatch tokens * fix(memory): bound host mirror waiters * fix(memory): bound retained session activity * fix(memory): bound retained session activity * fix(memory): bound web session close intents * fix(memory): bound cloud session cache * fix(memory): bound WSL home cache * fix(memory): bound SSH capability cache * fix(memory): bound trust grant cooldowns * fix(memory): bound WSL auth drain state * fix(memory): bound Linear workspace credential cache * fix(memory): bound local Git capability cache * fix(memory): bound WSL Git environment cache * fix(memory): bound WSL Git environment cache * fix(memory): bound WSL preflight cache * fix(memory): keep hot cache entries warm * fix(memory): preserve generation fences across eviction * fix(memory): close remaining eviction fences * fix(memory): align evicted upstream generations * fix(memory): trim successful capability probes * fix(auth): retain expired refresh replay evidence --------- Co-authored-by: m4air <m4air@Mac.localdomain>
368 lines
16 KiB
TypeScript
368 lines
16 KiB
TypeScript
import { beforeEach, describe, expect, it, vi } from 'vitest'
|
|
|
|
const { detectSparseCheckoutMock } = vi.hoisted(() => ({
|
|
detectSparseCheckoutMock: vi.fn()
|
|
}))
|
|
|
|
vi.mock('./worktree-sparse-state', () => ({
|
|
detectSparseCheckout: detectSparseCheckoutMock,
|
|
resolveGitCommonDir: vi.fn()
|
|
}))
|
|
|
|
import {
|
|
__getSparseCheckoutStateCacheSizeForTests,
|
|
__resetSparseCheckoutStateCacheForTests,
|
|
clearSparseCheckoutStateCache,
|
|
clearSparseCheckoutStateCacheForRepo,
|
|
detectSparseCheckoutCached,
|
|
invalidateSparseCheckoutState,
|
|
MAX_SPARSE_CHECKOUT_CACHE_ENTRIES,
|
|
onSparseCheckoutStateChanged
|
|
} from './worktree-sparse-checkout-cache'
|
|
|
|
const RECONCILE_WINDOW_MS = 5 * 60_000
|
|
|
|
// A real setTimeout tick, not a faked one, to flush the microtask chain a background
|
|
// stale-while-revalidate detect runs on without needing vi.useFakeTimers() (which would also
|
|
// have to fake Date.now(), the thing these tests drive manually via the Date.now spy below).
|
|
async function flushBackgroundRevalidation(): Promise<void> {
|
|
await new Promise((resolve) => setTimeout(resolve, 0))
|
|
}
|
|
|
|
beforeEach(() => {
|
|
detectSparseCheckoutMock.mockReset()
|
|
__resetSparseCheckoutStateCacheForTests()
|
|
})
|
|
|
|
describe('detectSparseCheckoutCached', () => {
|
|
it('bounds cache growth when worktree paths churn', async () => {
|
|
detectSparseCheckoutMock.mockResolvedValue(true)
|
|
|
|
for (let index = 0; index < MAX_SPARSE_CHECKOUT_CACHE_ENTRIES + 4; index += 1) {
|
|
await detectSparseCheckoutCached('/repo', `/repo/worktree-${index}`)
|
|
}
|
|
|
|
expect(__getSparseCheckoutStateCacheSizeForTests()).toBe(MAX_SPARSE_CHECKOUT_CACHE_ENTRIES)
|
|
})
|
|
|
|
it('caches a detection result across repeated calls for the same repo+path', async () => {
|
|
detectSparseCheckoutMock.mockResolvedValue(true)
|
|
|
|
expect(await detectSparseCheckoutCached('/repo', '/repo/wt-a')).toBe(true)
|
|
expect(await detectSparseCheckoutCached('/repo', '/repo/wt-a')).toBe(true)
|
|
|
|
expect(detectSparseCheckoutMock).toHaveBeenCalledTimes(1)
|
|
})
|
|
|
|
it('detects each distinct worktree path independently', async () => {
|
|
detectSparseCheckoutMock.mockImplementation(
|
|
async (worktreePath: string) => worktreePath === '/repo/wt-sparse'
|
|
)
|
|
|
|
expect(await detectSparseCheckoutCached('/repo', '/repo/wt-sparse')).toBe(true)
|
|
expect(await detectSparseCheckoutCached('/repo', '/repo/wt-full')).toBe(false)
|
|
expect(detectSparseCheckoutMock).toHaveBeenCalledTimes(2)
|
|
})
|
|
|
|
it('scopes the cache by repo, so the same path under two repos is detected independently', async () => {
|
|
detectSparseCheckoutMock.mockResolvedValue(true)
|
|
|
|
expect(await detectSparseCheckoutCached('/repo-a', '/shared-mount/wt')).toBe(true)
|
|
expect(await detectSparseCheckoutCached('/repo-b', '/shared-mount/wt')).toBe(true)
|
|
|
|
expect(detectSparseCheckoutMock).toHaveBeenCalledTimes(2)
|
|
})
|
|
|
|
it('treats an equivalent path spelling (trailing slash, redundant segment) as the same cache entry', async () => {
|
|
detectSparseCheckoutMock.mockResolvedValue(true)
|
|
|
|
expect(await detectSparseCheckoutCached('/repo', '/repo/wt-a')).toBe(true)
|
|
expect(await detectSparseCheckoutCached('/repo', '/repo/./wt-a/')).toBe(true)
|
|
|
|
expect(detectSparseCheckoutMock).toHaveBeenCalledTimes(1)
|
|
})
|
|
|
|
it('caches a false result too, so a worktree that stays non-sparse costs one detect', async () => {
|
|
detectSparseCheckoutMock.mockResolvedValue(false)
|
|
|
|
expect(await detectSparseCheckoutCached('/repo', '/repo/wt-a')).toBe(false)
|
|
expect(await detectSparseCheckoutCached('/repo', '/repo/wt-a')).toBe(false)
|
|
|
|
expect(detectSparseCheckoutMock).toHaveBeenCalledTimes(1)
|
|
})
|
|
|
|
it('serves the stale value immediately past the reconcile window and corrects it in the background', async () => {
|
|
const nowSpy = vi.spyOn(Date, 'now')
|
|
try {
|
|
nowSpy.mockReturnValue(1_000)
|
|
detectSparseCheckoutMock.mockResolvedValueOnce(false)
|
|
expect(await detectSparseCheckoutCached('/repo', '/repo/wt-a')).toBe(false)
|
|
|
|
// Just under the window: still trusts the cached value, no re-detect.
|
|
nowSpy.mockReturnValue(1_000 + RECONCILE_WINDOW_MS - 1)
|
|
expect(await detectSparseCheckoutCached('/repo', '/repo/wt-a')).toBe(false)
|
|
expect(detectSparseCheckoutMock).toHaveBeenCalledTimes(1)
|
|
|
|
// Past the window: both calls return the (stale) cached value, and only one kicks a
|
|
// background re-detect. Read both without awaiting in between — the underlying function
|
|
// never suspends before returning the stale value, so two calls issued back-to-back in the
|
|
// same tick are the only reliable way to observe "both concurrent callers stay stale" without
|
|
// racing the background revalidation's own microtask.
|
|
nowSpy.mockReturnValue(1_000 + RECONCILE_WINDOW_MS + 1)
|
|
detectSparseCheckoutMock.mockResolvedValueOnce(true)
|
|
const firstPastWindow = detectSparseCheckoutCached('/repo', '/repo/wt-a')
|
|
const secondPastWindow = detectSparseCheckoutCached('/repo', '/repo/wt-a')
|
|
expect(await firstPastWindow).toBe(false)
|
|
expect(await secondPastWindow).toBe(false)
|
|
expect(detectSparseCheckoutMock).toHaveBeenCalledTimes(2)
|
|
|
|
await flushBackgroundRevalidation()
|
|
|
|
// The corrected value is now served without needing another window to elapse.
|
|
expect(await detectSparseCheckoutCached('/repo', '/repo/wt-a')).toBe(true)
|
|
expect(detectSparseCheckoutMock).toHaveBeenCalledTimes(2)
|
|
} finally {
|
|
nowSpy.mockRestore()
|
|
}
|
|
})
|
|
|
|
it('does not resurrect an entry that was explicitly invalidated while a background revalidation was in flight', async () => {
|
|
const nowSpy = vi.spyOn(Date, 'now')
|
|
let resolveDetect: (isSparse: boolean) => void = () => {}
|
|
try {
|
|
nowSpy.mockReturnValue(1_000)
|
|
detectSparseCheckoutMock.mockResolvedValueOnce(false)
|
|
await detectSparseCheckoutCached('/repo', '/repo/wt-a')
|
|
|
|
// Past the window: kicks a background re-detect that we hold open.
|
|
nowSpy.mockReturnValue(1_000 + RECONCILE_WINDOW_MS + 1)
|
|
detectSparseCheckoutMock.mockImplementationOnce(
|
|
() => new Promise<boolean>((resolve) => (resolveDetect = resolve))
|
|
)
|
|
await detectSparseCheckoutCached('/repo', '/repo/wt-a')
|
|
|
|
// The worktree is removed (or the repo cache is cleared) while the detect above is in flight.
|
|
invalidateSparseCheckoutState('/repo', '/repo/wt-a')
|
|
expect(__getSparseCheckoutStateCacheSizeForTests()).toBe(0)
|
|
|
|
// The in-flight detect now resolves; it must not write the entry back.
|
|
resolveDetect(true)
|
|
await flushBackgroundRevalidation()
|
|
expect(__getSparseCheckoutStateCacheSizeForTests()).toBe(0)
|
|
} finally {
|
|
nowSpy.mockRestore()
|
|
}
|
|
})
|
|
|
|
it('does not let a stale in-flight revalidation clobber a fresh value written after remove+recreate at the same path', async () => {
|
|
const nowSpy = vi.spyOn(Date, 'now')
|
|
let resolveStaleDetect: (isSparse: boolean) => void = () => {}
|
|
try {
|
|
nowSpy.mockReturnValue(1_000)
|
|
detectSparseCheckoutMock.mockResolvedValueOnce(false)
|
|
await detectSparseCheckoutCached('/repo', '/repo/wt-a')
|
|
|
|
// Past the window: kicks a background re-detect that we hold open (simulates a slow probe
|
|
// racing a worktree removal + recreation at the same path).
|
|
nowSpy.mockReturnValue(1_000 + RECONCILE_WINDOW_MS + 1)
|
|
detectSparseCheckoutMock.mockImplementationOnce(
|
|
() => new Promise<boolean>((resolve) => (resolveStaleDetect = resolve))
|
|
)
|
|
await detectSparseCheckoutCached('/repo', '/repo/wt-a')
|
|
|
|
// The worktree is removed (invalidate) and a new one is recreated at the exact same path,
|
|
// repopulating the key with a fresh, different value via a normal cold read.
|
|
invalidateSparseCheckoutState('/repo', '/repo/wt-a')
|
|
detectSparseCheckoutMock.mockResolvedValueOnce(true)
|
|
expect(await detectSparseCheckoutCached('/repo', '/repo/wt-a')).toBe(true)
|
|
|
|
// The stale in-flight detect from before the remove+recreate now resolves with the old
|
|
// answer. A presence-only guard would let this overwrite the fresh entry above; the fix
|
|
// must compare entry identity and refuse to write back over a value it didn't produce.
|
|
resolveStaleDetect(false)
|
|
await flushBackgroundRevalidation()
|
|
expect(await detectSparseCheckoutCached('/repo', '/repo/wt-a')).toBe(true)
|
|
} finally {
|
|
nowSpy.mockRestore()
|
|
}
|
|
})
|
|
|
|
it('notifies the registered change listener only when a background revalidation flips the answer', async () => {
|
|
const listener = vi.fn()
|
|
onSparseCheckoutStateChanged(listener)
|
|
const nowSpy = vi.spyOn(Date, 'now')
|
|
try {
|
|
nowSpy.mockReturnValue(1_000)
|
|
detectSparseCheckoutMock.mockResolvedValueOnce(false)
|
|
await detectSparseCheckoutCached('/repo', '/repo/wt-a')
|
|
|
|
nowSpy.mockReturnValue(1_000 + RECONCILE_WINDOW_MS + 1)
|
|
detectSparseCheckoutMock.mockResolvedValueOnce(false)
|
|
await detectSparseCheckoutCached('/repo', '/repo/wt-a')
|
|
await flushBackgroundRevalidation()
|
|
expect(listener).not.toHaveBeenCalled()
|
|
|
|
nowSpy.mockReturnValue(1_000 + 2 * RECONCILE_WINDOW_MS + 2)
|
|
detectSparseCheckoutMock.mockResolvedValueOnce(true)
|
|
await detectSparseCheckoutCached('/repo', '/repo/wt-a')
|
|
await flushBackgroundRevalidation()
|
|
expect(listener).toHaveBeenCalledTimes(1)
|
|
expect(listener).toHaveBeenCalledWith('/repo', '/repo/wt-a', true)
|
|
} finally {
|
|
nowSpy.mockRestore()
|
|
onSparseCheckoutStateChanged(undefined)
|
|
}
|
|
})
|
|
})
|
|
|
|
describe('invalidateSparseCheckoutState', () => {
|
|
it('drops only the named repo+path, leaving other cached paths untouched', async () => {
|
|
detectSparseCheckoutMock.mockResolvedValue(true)
|
|
await detectSparseCheckoutCached('/repo', '/repo/wt-a')
|
|
await detectSparseCheckoutCached('/repo', '/repo/wt-b')
|
|
|
|
invalidateSparseCheckoutState('/repo', '/repo/wt-a')
|
|
expect(__getSparseCheckoutStateCacheSizeForTests()).toBe(1)
|
|
|
|
detectSparseCheckoutMock.mockClear()
|
|
await detectSparseCheckoutCached('/repo', '/repo/wt-a')
|
|
await detectSparseCheckoutCached('/repo', '/repo/wt-b')
|
|
expect(detectSparseCheckoutMock).toHaveBeenCalledTimes(1)
|
|
expect(detectSparseCheckoutMock).toHaveBeenCalledWith('/repo/wt-a', {})
|
|
})
|
|
})
|
|
|
|
describe('clearSparseCheckoutStateCacheForRepo', () => {
|
|
it('drops only the named repo`s entries, leaving a sibling repo`s warm cache intact', async () => {
|
|
detectSparseCheckoutMock.mockResolvedValue(true)
|
|
await detectSparseCheckoutCached('/repo-a', '/repo-a/wt-1')
|
|
await detectSparseCheckoutCached('/repo-b', '/repo-b/wt-1')
|
|
expect(__getSparseCheckoutStateCacheSizeForTests()).toBe(2)
|
|
|
|
clearSparseCheckoutStateCacheForRepo('/repo-a')
|
|
expect(__getSparseCheckoutStateCacheSizeForTests()).toBe(1)
|
|
|
|
detectSparseCheckoutMock.mockClear()
|
|
await detectSparseCheckoutCached('/repo-a', '/repo-a/wt-1')
|
|
await detectSparseCheckoutCached('/repo-b', '/repo-b/wt-1')
|
|
expect(detectSparseCheckoutMock).toHaveBeenCalledTimes(1)
|
|
})
|
|
})
|
|
|
|
describe('clearSparseCheckoutStateCache', () => {
|
|
it('drops every cached path across every repo, matching the fallback used when a repo cannot be resolved', async () => {
|
|
detectSparseCheckoutMock.mockResolvedValue(true)
|
|
await detectSparseCheckoutCached('/repo-a', '/repo-a/wt-1')
|
|
await detectSparseCheckoutCached('/repo-b', '/repo-b/wt-1')
|
|
expect(__getSparseCheckoutStateCacheSizeForTests()).toBe(2)
|
|
|
|
clearSparseCheckoutStateCache()
|
|
|
|
expect(__getSparseCheckoutStateCacheSizeForTests()).toBe(0)
|
|
})
|
|
})
|
|
|
|
// Regression coverage for the live Windows+WSL sequence: a distro-less listing (filesystem-auth
|
|
// root rebuild, worktree ownership checks) racing the real distro-carrying listing for the same
|
|
// repo. Before the distro joined the cache key they shared one entry, so whichever ran first
|
|
// decided the sparse badge for the whole reconcile window.
|
|
describe('detectSparseCheckoutCached with a WSL distro', () => {
|
|
// Mirrors the real probe: without the distro the gitdir pointer resolves to a fabricated Win32
|
|
// path, the sparse-checkout stat misses, and the worktree reads as non-sparse.
|
|
function detectOnlyWithDistro(distro: string): void {
|
|
detectSparseCheckoutMock.mockImplementation(
|
|
async (_worktreePath: string, options?: { wslDistro?: string }) =>
|
|
options?.wslDistro === distro
|
|
)
|
|
}
|
|
|
|
it('does not serve a distro-carrying read an answer derived without that distro', async () => {
|
|
detectOnlyWithDistro('Ubuntu')
|
|
|
|
expect(await detectSparseCheckoutCached('C:\\repo', 'C:\\repo\\wt')).toBe(false)
|
|
expect(
|
|
await detectSparseCheckoutCached('C:\\repo', 'C:\\repo\\wt', { wslDistro: 'Ubuntu' })
|
|
).toBe(true)
|
|
|
|
expect(detectSparseCheckoutMock).toHaveBeenCalledTimes(2)
|
|
})
|
|
|
|
it('keeps a distro-carrying answer correct when a distro-less read follows it', async () => {
|
|
detectOnlyWithDistro('Ubuntu')
|
|
|
|
expect(
|
|
await detectSparseCheckoutCached('C:\\repo', 'C:\\repo\\wt', { wslDistro: 'Ubuntu' })
|
|
).toBe(true)
|
|
expect(await detectSparseCheckoutCached('C:\\repo', 'C:\\repo\\wt')).toBe(false)
|
|
expect(
|
|
await detectSparseCheckoutCached('C:\\repo', 'C:\\repo\\wt', { wslDistro: 'Ubuntu' })
|
|
).toBe(true)
|
|
|
|
expect(detectSparseCheckoutMock).toHaveBeenCalledTimes(2)
|
|
})
|
|
|
|
it('treats distro spellings that name the same distro as one entry', async () => {
|
|
detectSparseCheckoutMock.mockResolvedValue(true)
|
|
|
|
expect(
|
|
await detectSparseCheckoutCached('C:\\repo', 'C:\\repo\\wt', { wslDistro: 'Ubuntu' })
|
|
).toBe(true)
|
|
expect(
|
|
await detectSparseCheckoutCached('C:\\repo', 'C:\\repo\\wt', { wslDistro: ' ubuntu ' })
|
|
).toBe(true)
|
|
|
|
expect(detectSparseCheckoutMock).toHaveBeenCalledTimes(1)
|
|
})
|
|
|
|
it('does not let a distro-less reader past the window revalidate a distro-carrying entry', async () => {
|
|
const listener = vi.fn()
|
|
onSparseCheckoutStateChanged(listener)
|
|
const nowSpy = vi.spyOn(Date, 'now')
|
|
try {
|
|
detectOnlyWithDistro('Ubuntu')
|
|
nowSpy.mockReturnValue(1_000)
|
|
await detectSparseCheckoutCached('C:\\repo', 'C:\\repo\\wt', { wslDistro: 'Ubuntu' })
|
|
await detectSparseCheckoutCached('C:\\repo', 'C:\\repo\\wt')
|
|
|
|
// Past the window the distro-less caller re-probes its own entry, not the sparse one, so the
|
|
// badge cannot blink off and fire the change listener that clears the whole repo's cache.
|
|
nowSpy.mockReturnValue(1_000 + RECONCILE_WINDOW_MS + 1)
|
|
expect(await detectSparseCheckoutCached('C:\\repo', 'C:\\repo\\wt')).toBe(false)
|
|
await flushBackgroundRevalidation()
|
|
|
|
expect(listener).not.toHaveBeenCalled()
|
|
expect(
|
|
await detectSparseCheckoutCached('C:\\repo', 'C:\\repo\\wt', { wslDistro: 'Ubuntu' })
|
|
).toBe(true)
|
|
} finally {
|
|
nowSpy.mockRestore()
|
|
onSparseCheckoutStateChanged(undefined)
|
|
}
|
|
})
|
|
|
|
it('drops every distro variant of a path on invalidate, so a removed worktree leaves nothing behind', async () => {
|
|
detectSparseCheckoutMock.mockResolvedValue(true)
|
|
await detectSparseCheckoutCached('C:\\repo', 'C:\\repo\\wt')
|
|
await detectSparseCheckoutCached('C:\\repo', 'C:\\repo\\wt', { wslDistro: 'Ubuntu' })
|
|
await detectSparseCheckoutCached('C:\\repo', 'C:\\repo\\other')
|
|
expect(__getSparseCheckoutStateCacheSizeForTests()).toBe(3)
|
|
|
|
invalidateSparseCheckoutState('C:\\repo', 'C:\\repo\\wt')
|
|
|
|
expect(__getSparseCheckoutStateCacheSizeForTests()).toBe(1)
|
|
})
|
|
|
|
it('still caches normally with no distro anywhere, as on macOS/Linux and native Windows', async () => {
|
|
detectSparseCheckoutMock.mockResolvedValue(true)
|
|
|
|
expect(await detectSparseCheckoutCached('/repo', '/repo/wt-a')).toBe(true)
|
|
expect(await detectSparseCheckoutCached('/repo', '/repo/wt-a', {})).toBe(true)
|
|
expect(await detectSparseCheckoutCached('/repo', '/repo/wt-a', { wslDistro: undefined })).toBe(
|
|
true
|
|
)
|
|
|
|
expect(detectSparseCheckoutMock).toHaveBeenCalledTimes(1)
|
|
expect(__getSparseCheckoutStateCacheSizeForTests()).toBe(1)
|
|
})
|
|
})
|