Files
orca/src/main/git/coalesced-probe.test.ts
T
Jinjing ced4a2a959 fix(P1-D): bound hosted-review in-flight lookups so a wedged provider cannot pin a branch (#12030)
* fix(P1-D): bound hosted-review lookups with a detachable deadline

The `inflight` map in the hosted-review branch cache was only ever cleared
when the lookup settled, and nothing bounded how long that took. One wedged
provider call pinned its branch for the life of the process: every later poll
joined the same dead promise, so the card loaded forever with no in-session
recovery.

Each lookup now runs under a 120s deadline. Nothing below the funnel can be
cancelled, so the deadline detaches instead: the record is released, the
callers get the last known review (or a timeout error), and the branch enters
the existing failure backoff. The lookup keeps running and its answer is still
adopted if it lands, so a slow-but-alive host converges rather than failing
forever. A token identity keeps a detached lookup from evicting the record
that replaced it, and a wall-clock sweep expires records whose timer never
fired — main's timers are suspended across system sleep. `inflight` is capped
independently of the completed cache.

The failure backoff moves to its own module: it has a different lifetime from
the answer cache and is what a deadline records against.

* fix(P1-D): bound `git remote get-url` on the local/WSL path

`getRemoteUrlForRepo` ran the git child with no timeout, which is the one
unbounded step under the hosted-review lookup funnel: `git/runner.ts` only
arms its kill path when a timeout is passed, so a dead network mount or a
stalled WSL interop hangs the call and everything above it. The SSH branch is
already bounded by the relay mux's 30s request timeout, so it is unchanged.

* rm review doc

* rm review doc

* test(P1-D): add probe tests and transient-failure recovery verification

Add tests for coalesced-probe and remote-url-probe infrastructure. Add integration test verifying that transient Bitbucket API failures don't cache as a definitive no-review result, allowing recovery after cache TTL expiration.

* fix(P1-D): track lookups from start, prevent stale scope adoption

- Count unsettled lookups when they start, not after deadline expires: prevents multiple concurrent lookups for the same branch.
- Add evicted generation floor: prevents adopting stale results when scope is invalidated and evicted from the map.
- Consolidate duplicate repository reference cache logic into createRemoteRefProbeCache utility.
- Fix deadline wrapper in git config signature lookup: bound the caller's deadline only, not the coalesced probe itself.

* feat(P1-D): add remote-ref-probe-cache utility

Cache successful remote URL probes per repo/runtime to avoid duplicate work.
Skip caching transient errors and SSH failures so providers can retry on
reconnect, preventing stale scope adoption during the session.
2026-08-01 22:10:18 -07:00

75 lines
2.4 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { runCoalescedProbe, type CoalescedProbes } from './coalesced-probe'
function deferred<T>(): {
promise: Promise<T>
resolve: (value: T) => void
reject: (error: unknown) => void
} {
let resolve: (value: T) => void = () => {}
let reject: (error: unknown) => void = () => {}
const promise = new Promise<T>((settle, fail) => {
resolve = settle
reject = fail
})
return { promise, resolve, reject }
}
describe('runCoalescedProbe', () => {
beforeEach(() => {
vi.useFakeTimers()
vi.setSystemTime(1_000_000)
})
afterEach(() => {
vi.useRealTimers()
})
it('replaces a stale probe without letting the older cleanup remove its successor', async () => {
const probes: CoalescedProbes<string> = new Map()
const first = deferred<string>()
const second = deferred<string>()
const createProbe = vi
.fn<() => Promise<string>>()
.mockReturnValueOnce(first.promise)
.mockReturnValueOnce(second.promise)
const firstRequest = runCoalescedProbe(probes, 'repo', createProbe, 60_000)
vi.setSystemTime(1_060_001)
const secondRequest = runCoalescedProbe(probes, 'repo', createProbe, 60_000)
expect(createProbe).toHaveBeenCalledTimes(2)
first.resolve('old')
await expect(firstRequest).resolves.toBe('old')
const joinedRequest = runCoalescedProbe(probes, 'repo', createProbe, 60_000)
expect(createProbe).toHaveBeenCalledTimes(2)
second.resolve('new')
await expect(secondRequest).resolves.toBe('new')
await expect(joinedRequest).resolves.toBe('new')
expect(probes).toEqual(new Map())
})
it('removes a settled probe only when it still owns the key', async () => {
const probes: CoalescedProbes<string> = new Map()
const first = deferred<string>()
const second = deferred<string>()
const createProbe = vi
.fn<() => Promise<string>>()
.mockReturnValueOnce(first.promise)
.mockReturnValueOnce(second.promise)
const firstRequest = runCoalescedProbe(probes, 'repo', createProbe, 1)
vi.advanceTimersByTime(2)
const secondRequest = runCoalescedProbe(probes, 'repo', createProbe, 1)
first.reject(new Error('old probe failed'))
await expect(firstRequest).rejects.toThrow('old probe failed')
expect(probes.get('repo')?.promise).toBe(second.promise)
second.resolve('new')
await expect(secondRequest).resolves.toBe('new')
})
})