mirror of
https://github.com/stablyai/orca.git
synced 2026-10-01 00:02:10 +00:00
* feat(git-status): batch, cancel, and cache git status polling to cut idl - Add a single duty-cycled refresh scheduler (activity debounce + 60s safety timer) replacing multiple overlapping intervals, so status polling no longer runs near-continuously on large repos (#7983). - Let safety refreshes reuse cached numstat line counts instead of re-running diff --numstat every cycle, invalidated by head change, known mutations, and a bounded TTL. - Thread AbortSignal/request-token cancellation through IPC, RPC, and relay layers so a superseded or backgrounded git:status call is killed instead of finishing wastefully. - Fix automatic upstream/status apply ordering so a slow, older refresh can no longer clobber a newer result, and so an earlier refresh still applies when a later one fails. * Fix aborted git status scans being mistaken for completed empty results - An aborted scan/numstat pass now always rejects instead of silently resolving, so a cancelled request can't look like a valid empty status. - Stop clearing the line-stats cache key on abort, since an aborted pass never wrote to it — clearing was evicting a concurrent scan's healthy snapshot and forcing a redundant numstat recompute. * Fix aborted git status scans resolving as completed results Cancelled scans could still resolve with partial or stale data instead of rejecting, letting callers treat an aborted refresh as a valid status. Also stop counting aborted scan duration toward catch-up refresh pacing, which was stretching the next refresh interval by the full length of a cancelled (often slow) scan. * Add cancellable, generation-aware git status polling to cut stale scans - Route git.status through an abortable subscription per requestToken so cancelStatus can actually abort the remote scan instead of being a no-op, preventing wasted work and stale responses overwriting fresher state. - Bump the git status polling generation on push-target changes so an in-flight refresh against the old remote/branch can't apply stale upstream data to the new one. - Guard the stale-conflict poller against writes after unmount. - Retire pre-purge line-stat scans in the cache so an older in-flight scan can't repopulate a key after a token-scoped purge.
203 lines
6.5 KiB
TypeScript
203 lines
6.5 KiB
TypeScript
import { beforeEach, describe, expect, it } from 'vitest'
|
|
import {
|
|
applyCachedGitStatusLineStats,
|
|
beginGitStatusLineStatsCacheWrite,
|
|
clearGitStatusLineStatsCache,
|
|
clearGitStatusLineStatsCacheKey,
|
|
GIT_STATUS_LINE_STATS_CACHE_MAX_AGE_MS,
|
|
reuseOrRecomputeGitStatusLineStats,
|
|
storeGitStatusLineStats
|
|
} from './git-status-line-stats-cache'
|
|
|
|
const cachedEntries = [
|
|
{
|
|
path: 'src/a.ts',
|
|
status: 'modified',
|
|
area: 'unstaged',
|
|
added: 3,
|
|
removed: 2
|
|
}
|
|
]
|
|
|
|
describe('git status line stats cache', () => {
|
|
beforeEach(() => {
|
|
clearGitStatusLineStatsCache()
|
|
})
|
|
|
|
it('reuses counts only for an exact head and status-entry identity', () => {
|
|
storeGitStatusLineStats({ cacheKey: 'native\0/repo', head: 'head-1', entries: cachedEntries })
|
|
const matchingEntries = [{ path: 'src/a.ts', status: 'modified', area: 'unstaged' }]
|
|
|
|
expect(
|
|
applyCachedGitStatusLineStats({
|
|
cacheKey: 'native\0/repo',
|
|
head: 'head-1',
|
|
entries: matchingEntries
|
|
})
|
|
).toBe(true)
|
|
expect(matchingEntries).toEqual(cachedEntries)
|
|
|
|
expect(
|
|
applyCachedGitStatusLineStats({
|
|
cacheKey: 'native\0/repo',
|
|
head: 'head-2',
|
|
entries: [{ path: 'src/a.ts', status: 'modified', area: 'unstaged' }]
|
|
})
|
|
).toBe(false)
|
|
})
|
|
|
|
it('invalidates a matching status identity at the bounded age', () => {
|
|
storeGitStatusLineStats({
|
|
cacheKey: 'native\0/repo',
|
|
head: 'head-1',
|
|
entries: cachedEntries,
|
|
now: 1_000
|
|
})
|
|
|
|
expect(
|
|
applyCachedGitStatusLineStats({
|
|
cacheKey: 'native\0/repo',
|
|
head: 'head-1',
|
|
entries: [{ path: 'src/a.ts', status: 'modified', area: 'unstaged' }],
|
|
now: 1_000 + GIT_STATUS_LINE_STATS_CACHE_MAX_AGE_MS
|
|
})
|
|
).toBe(false)
|
|
})
|
|
|
|
it('isolates execution-host keys and supports mutation invalidation', () => {
|
|
storeGitStatusLineStats({
|
|
cacheKey: 'wsl:ubuntu\0/repo',
|
|
head: 'head-1',
|
|
entries: cachedEntries
|
|
})
|
|
|
|
expect(
|
|
applyCachedGitStatusLineStats({
|
|
cacheKey: 'wsl:debian\0/repo',
|
|
head: 'head-1',
|
|
entries: [{ path: 'src/a.ts', status: 'modified', area: 'unstaged' }]
|
|
})
|
|
).toBe(false)
|
|
|
|
clearGitStatusLineStatsCacheKey('wsl:ubuntu\0/repo')
|
|
expect(
|
|
applyCachedGitStatusLineStats({
|
|
cacheKey: 'wsl:ubuntu\0/repo',
|
|
head: 'head-1',
|
|
entries: [{ path: 'src/a.ts', status: 'modified', area: 'unstaged' }]
|
|
})
|
|
).toBe(false)
|
|
})
|
|
|
|
it('lets an earlier recompute store after a later reuse-only read began', () => {
|
|
const recomputeWrite = beginGitStatusLineStatsCacheWrite('native\0/repo')
|
|
// A hinted safety read begins later, reuses, and never stores; it must not
|
|
// starve the older recompute's store.
|
|
beginGitStatusLineStatsCacheWrite('native\0/repo')
|
|
storeGitStatusLineStats({
|
|
cacheKey: 'native\0/repo',
|
|
head: 'head-1',
|
|
entries: cachedEntries,
|
|
writeToken: recomputeWrite
|
|
})
|
|
|
|
const entries: { path: string; status: string; area: string; added?: number }[] = [
|
|
{ path: 'src/a.ts', status: 'modified', area: 'unstaged' }
|
|
]
|
|
expect(
|
|
applyCachedGitStatusLineStats({ cacheKey: 'native\0/repo', head: 'head-1', entries })
|
|
).toBe(true)
|
|
expect(entries[0]?.added).toBe(3)
|
|
})
|
|
|
|
it('keeps a prior good snapshot reusable when an overlapping recompute is aborted', async () => {
|
|
// A completed scan stored healthy counts.
|
|
storeGitStatusLineStats({ cacheKey: 'native\0/repo', head: 'head-1', entries: cachedEntries })
|
|
|
|
// A later scan begins and recomputes but is aborted before it can store.
|
|
const abortedWrite = beginGitStatusLineStatsCacheWrite('native\0/repo')
|
|
let aborted = false
|
|
await expect(
|
|
reuseOrRecomputeGitStatusLineStats({
|
|
cacheKey: 'native\0/repo',
|
|
head: 'head-1',
|
|
entries: [{ path: 'src/a.ts', status: 'modified', area: 'unstaged' }],
|
|
writeToken: abortedWrite,
|
|
reuse: false,
|
|
isAborted: () => aborted,
|
|
recompute: async () => {
|
|
aborted = true
|
|
return true
|
|
}
|
|
})
|
|
).rejects.toMatchObject({ name: 'AbortError' })
|
|
|
|
// The aborted pass persisted nothing, so it must not evict the healthy
|
|
// snapshot — a following safety read still reuses the stored counts.
|
|
const entries: { path: string; status: string; area: string; added?: number }[] = [
|
|
{ path: 'src/a.ts', status: 'modified', area: 'unstaged' }
|
|
]
|
|
expect(
|
|
applyCachedGitStatusLineStats({ cacheKey: 'native\0/repo', head: 'head-1', entries })
|
|
).toBe(true)
|
|
expect(entries[0]?.added).toBe(3)
|
|
})
|
|
|
|
it('rejects a reuse-only read that is already aborted', async () => {
|
|
storeGitStatusLineStats({ cacheKey: 'native\0/repo', head: 'head-1', entries: cachedEntries })
|
|
const writeToken = beginGitStatusLineStatsCacheWrite('native\0/repo')
|
|
await expect(
|
|
reuseOrRecomputeGitStatusLineStats({
|
|
cacheKey: 'native\0/repo',
|
|
head: 'head-1',
|
|
entries: [{ path: 'src/a.ts', status: 'modified', area: 'unstaged' }],
|
|
writeToken,
|
|
reuse: true,
|
|
isAborted: () => true,
|
|
recompute: async () => true
|
|
})
|
|
).rejects.toMatchObject({ name: 'AbortError' })
|
|
})
|
|
|
|
it('rejects stale cache writes and writes started before invalidation', () => {
|
|
const olderWrite = beginGitStatusLineStatsCacheWrite('native\0/repo')
|
|
const latestWrite = beginGitStatusLineStatsCacheWrite('native\0/repo')
|
|
storeGitStatusLineStats({
|
|
cacheKey: 'native\0/repo',
|
|
head: 'head-1',
|
|
entries: [{ ...cachedEntries[0], added: 8 }],
|
|
writeToken: latestWrite
|
|
})
|
|
storeGitStatusLineStats({
|
|
cacheKey: 'native\0/repo',
|
|
head: 'head-1',
|
|
entries: cachedEntries,
|
|
writeToken: olderWrite
|
|
})
|
|
const entries: { path: string; status: string; area: string; added?: number }[] = [
|
|
{ path: 'src/a.ts', status: 'modified', area: 'unstaged' }
|
|
]
|
|
|
|
expect(
|
|
applyCachedGitStatusLineStats({ cacheKey: 'native\0/repo', head: 'head-1', entries })
|
|
).toBe(true)
|
|
expect(entries[0]?.added).toBe(8)
|
|
|
|
const invalidatedWrite = beginGitStatusLineStatsCacheWrite('native\0/other')
|
|
clearGitStatusLineStatsCache()
|
|
storeGitStatusLineStats({
|
|
cacheKey: 'native\0/other',
|
|
head: 'head-1',
|
|
entries: cachedEntries,
|
|
writeToken: invalidatedWrite
|
|
})
|
|
expect(
|
|
applyCachedGitStatusLineStats({
|
|
cacheKey: 'native\0/other',
|
|
head: 'head-1',
|
|
entries: [{ path: 'src/a.ts', status: 'modified', area: 'unstaged' }]
|
|
})
|
|
).toBe(false)
|
|
})
|
|
})
|