Files
orca/src/shared/git-status-branch-line-total-cache.test.ts
T

344 lines
12 KiB
TypeScript

import { beforeEach, describe, expect, it, vi } from 'vitest'
import {
applyCachedGitStatusLineStats,
beginGitStatusLineStatsCacheWrite,
clearGitStatusLineStatsCache,
readCachedGitBranchLineTotal,
reuseOrRecomputeGitStatusLineStats,
storeGitStatusLineStats
} from './git-status-line-stats-cache'
import type { GitBranchLineTotal } from './git-status-types'
const CACHE_KEY = 'native\0/repo'
const MERGE_BASE = 'a1b2c3d4e5f60718293a4b5c6d7e8f90a1b2c3d4'
const OTHER_MERGE_BASE = '0f1e2d3c4b5a69788796a5b4c3d2e1f00f1e2d3c'
const TOTAL: GitBranchLineTotal = { added: 12, removed: 3, mergeBase: MERGE_BASE }
function entries(added?: number): { path: string; status: string; area: string; added?: number }[] {
return [{ path: 'src/a.ts', status: 'modified', area: 'unstaged', ...(added ? { added } : {}) }]
}
function seedSnapshot(branchLineTotal?: GitBranchLineTotal): void {
storeGitStatusLineStats({
cacheKey: CACHE_KEY,
head: 'head-1',
entries: entries(3),
...(branchLineTotal ? { branchLineTotal } : {})
})
}
function runTotalPass(
compute: () => Promise<GitBranchLineTotal | null | undefined>,
options: { reuse?: boolean; mergeBase?: string; complete?: boolean } = {}
) {
return reuseOrRecomputeGitStatusLineStats({
cacheKey: CACHE_KEY,
head: 'head-1',
entries: entries(4),
writeToken: beginGitStatusLineStatsCacheWrite(CACHE_KEY),
reuse: options.reuse === true,
isAborted: () => false,
recompute: async () => options.complete !== false,
branchLineTotal: { mergeBase: options.mergeBase ?? MERGE_BASE, compute }
})
}
describe('branch line total inside the status line-stats cache', () => {
beforeEach(() => {
clearGitStatusLineStatsCache()
})
it('costs nothing when the caller did not ask for a total', async () => {
const recompute = vi.fn(async () => true)
const result = await reuseOrRecomputeGitStatusLineStats({
cacheKey: CACHE_KEY,
head: 'head-1',
entries: entries(),
writeToken: beginGitStatusLineStatsCacheWrite(CACHE_KEY),
reuse: false,
isAborted: () => false,
recompute
})
expect(result).toEqual({})
expect(recompute).toHaveBeenCalledTimes(1)
})
it('reuses the cached total on a line-stats reuse hit without re-running the diff', async () => {
seedSnapshot(TOTAL)
const compute = vi.fn(async () => TOTAL)
const recompute = vi.fn(async () => true)
const result = await reuseOrRecomputeGitStatusLineStats({
cacheKey: CACHE_KEY,
head: 'head-1',
entries: entries(),
writeToken: beginGitStatusLineStatsCacheWrite(CACHE_KEY),
reuse: true,
isAborted: () => false,
recompute,
branchLineTotal: { mergeBase: MERGE_BASE, compute }
})
expect(result).toEqual({ branchLineTotal: TOTAL })
expect(compute).not.toHaveBeenCalled()
expect(recompute).not.toHaveBeenCalled()
})
it('recomputes and backfills when the snapshot has no total for this fork point', async () => {
seedSnapshot({ added: 99, removed: 99, mergeBase: OTHER_MERGE_BASE })
const compute = vi.fn(async () => TOTAL)
const first = await reuseOrRecomputeGitStatusLineStats({
cacheKey: CACHE_KEY,
head: 'head-1',
entries: entries(),
writeToken: beginGitStatusLineStatsCacheWrite(CACHE_KEY),
reuse: true,
isAborted: () => false,
recompute: async () => true,
branchLineTotal: { mergeBase: MERGE_BASE, compute }
})
const second = await reuseOrRecomputeGitStatusLineStats({
cacheKey: CACHE_KEY,
head: 'head-1',
entries: entries(),
writeToken: beginGitStatusLineStatsCacheWrite(CACHE_KEY),
reuse: true,
isAborted: () => false,
recompute: async () => true,
branchLineTotal: { mergeBase: MERGE_BASE, compute }
})
expect(first).toEqual({ branchLineTotal: TOTAL })
expect(second).toEqual({ branchLineTotal: TOTAL })
expect(compute).toHaveBeenCalledTimes(1)
expect(readCachedGitBranchLineTotal({ cacheKey: CACHE_KEY, mergeBase: MERGE_BASE })).toEqual(
TOTAL
)
})
it('never serves a cached total that was measured against another fork point', () => {
seedSnapshot(TOTAL)
expect(
readCachedGitBranchLineTotal({ cacheKey: CACHE_KEY, mergeBase: OTHER_MERGE_BASE })
).toBeUndefined()
})
it('starts the ranged diff before waiting on the per-area numstat', async () => {
const order: string[] = []
let releaseRecompute = (): void => {}
const recomputeGate = new Promise<void>((resolve) => {
releaseRecompute = resolve
})
const pending = reuseOrRecomputeGitStatusLineStats({
cacheKey: CACHE_KEY,
head: 'head-1',
entries: entries(),
writeToken: beginGitStatusLineStatsCacheWrite(CACHE_KEY),
reuse: false,
isAborted: () => false,
recompute: async () => {
order.push('recompute')
await recomputeGate
return true
},
branchLineTotal: {
mergeBase: MERGE_BASE,
compute: async () => {
order.push('compute')
return TOTAL
}
}
})
releaseRecompute()
expect(await pending).toEqual({ branchLineTotal: TOTAL })
expect(order).toEqual(['compute', 'recompute'])
})
it('keeps the exact total when the per-area numstat pass failed', async () => {
const result = await reuseOrRecomputeGitStatusLineStats({
cacheKey: CACHE_KEY,
head: 'head-1',
entries: entries(),
writeToken: beginGitStatusLineStatsCacheWrite(CACHE_KEY),
reuse: false,
isAborted: () => false,
recompute: async () => false,
branchLineTotal: { mergeBase: MERGE_BASE, compute: async () => TOTAL }
})
expect(result).toEqual({ branchLineTotal: TOTAL })
// The incomplete pass stays uncacheable, so nothing was pinned for reuse.
expect(
applyCachedGitStatusLineStats({ cacheKey: CACHE_KEY, head: 'head-1', entries: entries() })
).toBe(false)
})
it('omits the field entirely when total computation was deferred', async () => {
const result = await reuseOrRecomputeGitStatusLineStats({
cacheKey: CACHE_KEY,
head: 'head-1',
entries: entries(4),
writeToken: beginGitStatusLineStatsCacheWrite(CACHE_KEY),
reuse: false,
isAborted: () => false,
recompute: async () => true,
branchLineTotal: { mergeBase: MERGE_BASE, compute: async () => undefined }
})
expect(result).toEqual({})
expect('branchLineTotal' in result).toBe(false)
expect(readCachedGitBranchLineTotal({ cacheKey: CACHE_KEY, mergeBase: MERGE_BASE })).toBe(
undefined
)
})
it('replaces an exact total with cached unavailability for the same porcelain identity, then recovers', async () => {
seedSnapshot(TOTAL)
expect(await runTotalPass(async () => null)).toEqual({
branchLineTotalUnavailable: MERGE_BASE
})
expect(readCachedGitBranchLineTotal({ cacheKey: CACHE_KEY, mergeBase: MERGE_BASE })).toBeNull()
const compute = vi.fn(async () => TOTAL)
expect(await runTotalPass(compute, { reuse: true })).toEqual({
branchLineTotalUnavailable: MERGE_BASE
})
expect(compute).not.toHaveBeenCalled()
expect(await runTotalPass(compute)).toEqual({ branchLineTotal: TOTAL })
expect(await runTotalPass(compute, { reuse: true })).toEqual({ branchLineTotal: TOTAL })
expect(compute).toHaveBeenCalledTimes(1)
})
it('only republishes cached unavailability for its own merge base', async () => {
seedSnapshot(TOTAL)
await runTotalPass(async () => null)
expect(
readCachedGitBranchLineTotal({ cacheKey: CACHE_KEY, mergeBase: OTHER_MERGE_BASE })
).toBeUndefined()
const otherTotal = { ...TOTAL, mergeBase: OTHER_MERGE_BASE }
const compute = vi.fn(async () => otherTotal)
expect(await runTotalPass(compute, { reuse: true, mergeBase: OTHER_MERGE_BASE })).toEqual({
branchLineTotal: otherTotal
})
expect(compute).toHaveBeenCalledTimes(1)
expect(readCachedGitBranchLineTotal({ cacheKey: CACHE_KEY, mergeBase: MERGE_BASE })).toBeNull()
})
it('carries exact totals or unavailable markers through deferred recomputes', async () => {
seedSnapshot(TOTAL)
expect(await runTotalPass(async () => undefined)).toEqual({ branchLineTotal: TOTAL })
await runTotalPass(async () => null)
expect(await runTotalPass(async () => undefined)).toEqual({
branchLineTotalUnavailable: MERGE_BASE
})
expect(readCachedGitBranchLineTotal({ cacheKey: CACHE_KEY, mergeBase: MERGE_BASE })).toBeNull()
})
it.each([false, true])(
'keeps a good total when another merge base becomes unavailable (reuse=%s)',
async (reuse) => {
seedSnapshot(TOTAL)
expect(await runTotalPass(async () => null, { mergeBase: OTHER_MERGE_BASE, reuse })).toEqual({
branchLineTotalUnavailable: OTHER_MERGE_BASE
})
expect(readCachedGitBranchLineTotal({ cacheKey: CACHE_KEY, mergeBase: MERGE_BASE })).toEqual(
TOTAL
)
expect(
readCachedGitBranchLineTotal({ cacheKey: CACHE_KEY, mergeBase: OTHER_MERGE_BASE })
).toBeNull()
}
)
it('keeps a newer reuse-only invalidation when an older full scan stores exact totals', async () => {
seedSnapshot()
const olderToken = beginGitStatusLineStatsCacheWrite(CACHE_KEY)
await runTotalPass(async () => null, { reuse: true })
storeGitStatusLineStats({
cacheKey: CACHE_KEY,
head: 'head-1',
entries: entries(9),
writeToken: olderToken,
branchLineTotal: TOTAL
})
expect(readCachedGitBranchLineTotal({ cacheKey: CACHE_KEY, mergeBase: MERGE_BASE })).toBeNull()
const reused = entries()
expect(
applyCachedGitStatusLineStats({ cacheKey: CACHE_KEY, head: 'head-1', entries: reused })
).toBe(true)
expect(reused[0].added).toBe(9)
})
it('invalidates a cached exact total even when per-area stats fail', async () => {
seedSnapshot(TOTAL)
expect(await runTotalPass(async () => null, { complete: false })).toEqual({
branchLineTotalUnavailable: MERGE_BASE
})
expect(readCachedGitBranchLineTotal({ cacheKey: CACHE_KEY, mergeBase: MERGE_BASE })).toBeNull()
const reused = entries()
expect(
applyCachedGitStatusLineStats({ cacheKey: CACHE_KEY, head: 'head-1', entries: reused })
).toBe(true)
expect(reused[0].added).toBe(3)
})
it('rejects instead of returning a partial total when the pass aborts mid-diff', async () => {
let aborted = false
await expect(
reuseOrRecomputeGitStatusLineStats({
cacheKey: CACHE_KEY,
head: 'head-1',
entries: entries(),
writeToken: beginGitStatusLineStatsCacheWrite(CACHE_KEY),
reuse: false,
isAborted: () => aborted,
recompute: async () => true,
branchLineTotal: {
mergeBase: MERGE_BASE,
compute: async () => {
aborted = true
const error = new Error('The operation was aborted.')
error.name = 'AbortError'
throw error
}
}
})
).rejects.toMatchObject({ name: 'AbortError' })
expect(readCachedGitBranchLineTotal({ cacheKey: CACHE_KEY, mergeBase: MERGE_BASE })).toBe(
undefined
)
})
it('rejects a reuse hit whose backfill compute is aborted', async () => {
seedSnapshot()
let aborted = false
await expect(
reuseOrRecomputeGitStatusLineStats({
cacheKey: CACHE_KEY,
head: 'head-1',
entries: entries(),
writeToken: beginGitStatusLineStatsCacheWrite(CACHE_KEY),
reuse: true,
isAborted: () => aborted,
recompute: async () => true,
branchLineTotal: {
mergeBase: MERGE_BASE,
compute: async () => {
aborted = true
return TOTAL
}
}
})
).rejects.toMatchObject({ name: 'AbortError' })
})
})