Files
orca/src/main/git/remote-ref-probe-cache.test.ts
T
JinjingandOrca 1562f12f78 fix(P1-D): coalesce remote-ref probes, TTL negatives, and bound unsettled keys (#12065)
* fix(P1-D): coalesce remote-ref probes, TTL negatives, and bound unsettled keys

Keep forge resolution from stampeding git under worktree fan-out, let
remotes added mid-session be discovered without a restart, and refuse
pathological new-branch waves once the unsettled map is full.

* fix(P1-D): stop abandoned probes publishing, and split capacity refusals

A coalesced probe abandoned as stale kept running and still wrote its answer
to the cache, so a late permanent miss could land over the successor's fresher
one. Probes now publish only while they still own the in-flight key.

The hosted-review capacity refusal told brand-new branches that an earlier
attempt of their own never answered when the refusal was really the unsettled
map or the process-wide detached cap; each cap now says what it is.

Also caches stable "no such remote" SSH misses under the negative TTL instead
of re-spawning the probe on every poll.

Co-authored-by: Orca <help@stably.ai>

* Bound SSH remote URL probe with deadline to prevent hangs

The SSH branch of remote URL probes was unbounded — the relay's bounds
are per-phase and reset on every frame, so a relay dribbling output would
outlive them. Pass AbortSignal.timeout to the SSH provider's exec call to
enforce the same 30s deadline as local probes.

Treat AbortError as a transient probe error: it signals unavailable
infrastructure (deadline or cancellation), not a negative answer about
the remote.

---------

Co-authored-by: Orca <help@stably.ai>
2026-08-02 00:31:54 -07:00

199 lines
8.3 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
const { getSshGitProviderMock, getSshGitProviderGenerationMock, gitExecFileAsyncMock } = vi.hoisted(
() => ({
getSshGitProviderMock: vi.fn(),
getSshGitProviderGenerationMock: vi.fn(() => 0),
gitExecFileAsyncMock: vi.fn()
})
)
vi.mock('../providers/ssh-git-dispatch', () => ({
getSshGitProvider: getSshGitProviderMock,
getSshGitProviderGeneration: getSshGitProviderGenerationMock,
SSH_GIT_PROVIDER_UNAVAILABLE_MESSAGE: 'SSH Git provider unavailable'
}))
vi.mock('./runner', () => ({ gitExecFileAsync: gitExecFileAsyncMock }))
import { PROBE_COALESCE_STALE_MS } from './coalesced-probe'
import { createRemoteRefProbeCache, NEGATIVE_ENTRY_TTL_MS } from './remote-ref-probe-cache'
/** Stands in for a forge's parser: claims one host, rejects everything else. */
function parseExampleRef(remoteUrl: string): { repo: string } | null {
const match = remoteUrl.trim().match(/^git@example\.com:(.+?)(?:\.git)?$/)
return match ? { repo: match[1] } : null
}
function deferred<T>(): { promise: Promise<T>; resolve: (value: T) => void } {
let resolve: (value: T) => void = () => {}
const promise = new Promise<T>((settle) => {
resolve = settle
})
return { promise, resolve }
}
describe('remote ref probe cache (P1-D)', () => {
beforeEach(() => {
getSshGitProviderMock.mockReset()
getSshGitProviderGenerationMock.mockReset()
getSshGitProviderGenerationMock.mockReturnValue(0)
gitExecFileAsyncMock.mockReset()
vi.useFakeTimers()
vi.setSystemTime(1_000_000)
})
afterEach(() => {
vi.useRealTimers()
})
it('answers concurrent lookups for one repo with a single probe', async () => {
const cache = createRemoteRefProbeCache(parseExampleRef)
gitExecFileAsyncMock.mockResolvedValue({ stdout: 'git@example.com:team/repo.git\n' })
const answers = await Promise.all([
cache.get('/repo', 'origin'),
cache.get('/repo', 'origin'),
cache.get('/repo', 'origin')
])
expect(answers).toEqual([{ repo: 'team/repo' }, { repo: 'team/repo' }, { repo: 'team/repo' }])
expect(gitExecFileAsyncMock).toHaveBeenCalledTimes(1)
})
it('re-probes a repo whose remotes could have changed since the miss', async () => {
const cache = createRemoteRefProbeCache(parseExampleRef)
gitExecFileAsyncMock.mockRejectedValueOnce(new Error("fatal: No such remote 'origin'"))
await expect(cache.get('/repo', 'origin')).resolves.toBeNull()
await expect(cache.get('/repo', 'origin')).resolves.toBeNull()
expect(gitExecFileAsyncMock).toHaveBeenCalledTimes(1)
// A remote added after the miss is only visible once the negative expires;
// nothing here watches .git/config, and SSH/WSL repos have no file to watch.
gitExecFileAsyncMock.mockResolvedValue({ stdout: 'git@example.com:team/repo.git\n' })
vi.setSystemTime(1_000_000 + NEGATIVE_ENTRY_TTL_MS + 1)
await expect(cache.get('/repo', 'origin')).resolves.toEqual({ repo: 'team/repo' })
expect(gitExecFileAsyncMock).toHaveBeenCalledTimes(2)
})
it('keeps a resolved ref without re-probing', async () => {
const cache = createRemoteRefProbeCache(parseExampleRef)
gitExecFileAsyncMock.mockResolvedValue({ stdout: 'git@example.com:team/repo.git\n' })
await expect(cache.get('/repo', 'origin')).resolves.toEqual({ repo: 'team/repo' })
vi.setSystemTime(1_000_000 + NEGATIVE_ENTRY_TTL_MS * 10)
await expect(cache.get('/repo', 'origin')).resolves.toEqual({ repo: 'team/repo' })
expect(gitExecFileAsyncMock).toHaveBeenCalledTimes(1)
})
it('does not let a lookup on a reconnected provider join the old connection probe', async () => {
const cache = createRemoteRefProbeCache(parseExampleRef)
let releaseStalled = (): void => {}
const execMock = vi
.fn()
.mockImplementationOnce(
async () => await new Promise((resolve) => (releaseStalled = () => resolve({ stdout: '' })))
)
.mockResolvedValue({ stdout: 'git@example.com:team/repo.git\n' })
getSshGitProviderMock.mockReturnValue({ exec: execMock })
const stalled = cache.get('/repo', 'origin', 'conn-1')
getSshGitProviderGenerationMock.mockReturnValue(1)
await expect(cache.get('/repo', 'origin', 'conn-1')).resolves.toEqual({ repo: 'team/repo' })
expect(execMock).toHaveBeenCalledTimes(2)
releaseStalled()
await expect(stalled).resolves.toBeNull()
})
it('does not let a probe abandoned as stale overwrite its successor answer', async () => {
const cache = createRemoteRefProbeCache(parseExampleRef)
const stalled = deferred<{ stdout: string }>()
gitExecFileAsyncMock
.mockImplementationOnce(async () => await stalled.promise)
.mockResolvedValue({ stdout: 'git@example.com:team/repo.git\n' })
const abandoned = cache.get('/repo', 'origin')
vi.setSystemTime(1_000_000 + PROBE_COALESCE_STALE_MS + 1)
await expect(cache.get('/repo', 'origin')).resolves.toEqual({ repo: 'team/repo' })
// The abandoned probe still answers the caller that started it, but the repo
// state it read is older than the one already cached — it must not publish.
stalled.resolve({ stdout: 'git@elsewhere.example:team/repo.git\n' })
await expect(abandoned).resolves.toBeNull()
await expect(cache.get('/repo', 'origin')).resolves.toEqual({ repo: 'team/repo' })
expect(gitExecFileAsyncMock).toHaveBeenCalledTimes(2)
})
it('holds a parsed-as-not-mine remote for the negative interval, then re-asks', async () => {
const cache = createRemoteRefProbeCache(parseExampleRef)
gitExecFileAsyncMock.mockResolvedValue({ stdout: 'git@elsewhere.example:team/repo.git\n' })
await expect(cache.get('/repo', 'origin')).resolves.toBeNull()
await expect(cache.get('/repo', 'origin')).resolves.toBeNull()
expect(gitExecFileAsyncMock).toHaveBeenCalledTimes(1)
vi.setSystemTime(1_000_000 + NEGATIVE_ENTRY_TTL_MS + 1)
await expect(cache.get('/repo', 'origin')).resolves.toBeNull()
expect(gitExecFileAsyncMock).toHaveBeenCalledTimes(2)
})
it('never caches a miss from a runtime that could not be asked at all', async () => {
const cache = createRemoteRefProbeCache(parseExampleRef)
getSshGitProviderMock.mockReturnValueOnce(undefined)
await expect(cache.get('/repo', 'origin', 'conn-1')).resolves.toBeNull()
// The provider was gone, not the remote: reconnecting must not need the TTL.
getSshGitProviderMock.mockReturnValue({
exec: vi.fn(async () => ({ stdout: 'git@example.com:team/repo.git\n' }))
})
await expect(cache.get('/repo', 'origin', 'conn-1')).resolves.toEqual({ repo: 'team/repo' })
})
it('holds an SSH repo that has no such remote instead of re-asking every poll', async () => {
const cache = createRemoteRefProbeCache(parseExampleRef)
const exec = vi.fn(async () => {
throw new Error("fatal: No such remote 'origin'")
})
getSshGitProviderMock.mockReturnValue({ exec })
await expect(cache.get('/repo', 'origin', 'conn-1')).resolves.toBeNull()
await expect(cache.get('/repo', 'origin', 'conn-1')).resolves.toBeNull()
expect(exec).toHaveBeenCalledTimes(1)
vi.setSystemTime(1_000_000 + NEGATIVE_ENTRY_TTL_MS + 1)
await expect(cache.get('/repo', 'origin', 'conn-1')).resolves.toBeNull()
expect(exec).toHaveBeenCalledTimes(2)
})
it('keeps re-asking an SSH repo whose probe died with its transport', async () => {
const cache = createRemoteRefProbeCache(parseExampleRef)
const exec = vi
.fn()
.mockRejectedValueOnce(new Error('relay request failed: connection closed'))
.mockResolvedValue({ stdout: 'git@example.com:team/repo.git\n' })
getSshGitProviderMock.mockReturnValue({ exec })
await expect(cache.get('/repo', 'origin', 'conn-1')).resolves.toBeNull()
await expect(cache.get('/repo', 'origin', 'conn-1')).resolves.toEqual({ repo: 'team/repo' })
expect(exec).toHaveBeenCalledTimes(2)
})
it('does not cache a probe killed on its deadline as a definitive miss', async () => {
const cache = createRemoteRefProbeCache(parseExampleRef)
gitExecFileAsyncMock
.mockRejectedValueOnce(new Error('git timed out.'))
.mockResolvedValueOnce({ stdout: 'git@example.com:team/repo.git\n' })
await expect(cache.get('/repo', 'origin')).resolves.toBeNull()
await expect(cache.get('/repo', 'origin')).resolves.toEqual({ repo: 'team/repo' })
expect(gitExecFileAsyncMock).toHaveBeenCalledTimes(2)
})
})