mirror of
https://github.com/stablyai/orca.git
synced 2026-10-25 00:03:59 +00:00
* 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.
75 lines
2.4 KiB
TypeScript
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')
|
|
})
|
|
})
|