mirror of
https://github.com/stablyai/orca.git
synced 2026-10-20 00:03:45 +00:00
perf(gitlab): bound known-host auth probes
This commit is contained in:
@@ -6425,7 +6425,7 @@
|
||||
"id": "terminal-performance.store-and-git-hot-paths",
|
||||
"title": "Terminal-adjacent store and git polling work stays off hot interaction paths",
|
||||
"maturity": "experimental",
|
||||
"protection": "none",
|
||||
"protection": "partial",
|
||||
"owner": "terminal-performance",
|
||||
"layer": "renderer-main-perf-contract",
|
||||
"surfaces": [
|
||||
@@ -6439,31 +6439,56 @@
|
||||
],
|
||||
"platforms": ["macos", "linux", "windows"],
|
||||
"providers": ["local", "daemon", "ssh", "remote-runtime"],
|
||||
"coveredPlatforms": [],
|
||||
"coveredProviders": [],
|
||||
"coverageNotes": "Registered gap on main. The boot-hydration counters and their tests exist only on the pending reliability stack. It registers here with its owning split PR.",
|
||||
"coveredPlatforms": ["macos"],
|
||||
"coveredProviders": ["local", "ssh"],
|
||||
"coverageNotes": "Focused GitLab host-probe coverage on main bounds failed glab auth subprocesses per native or WSL execution context, shares the native probe across SSH targets, isolates connection-derived hosts by SSH generation, refreshes after a short cooldown, and caps retained state. Broader store, git-status, provider-listing, and boot-hydration counters remain gaps.",
|
||||
"motivatingLinks": ["https://github.com/stablyai/orca/pull/7002"],
|
||||
"invariant": "Terminal typing, focus, resize, tab/workspace switch, and agent/session restore must not trigger unbounded store projection, git status, provider listing, or per-pane polling work.",
|
||||
"oracle": "The current executable slice instruments boot-time local PTY registry hydration with repo counts, local-vs-remote repo skips, worktree enumeration counts, adapter/session listing counts, registration/skipped-session counts, duration, and failure phase. The broader oracle still needs instrumentation that counts store selector recomputes, git status requests, provider listings, and session scans during scripted hot interactions with many worktrees and terminal panes.",
|
||||
"commands": [],
|
||||
"testFiles": [],
|
||||
"assertionRefs": [],
|
||||
"evidenceRuns": [],
|
||||
"oracle": "Issue 64 sequential failed GitLab known-host requests across distinct SSH targets and require one native glab probe during the cooldown. Advance past the cooldown and require one retry; explicitly refresh auth and require immediate recovery. Replace the SSH provider generation and require connection-derived hosts to expire without repeating the native probe. Fill 129 distinct successful and failed WSL contexts and require the oldest entry to be re-probed after the 128-entry cap. The broader oracle still needs store-selector, git-status, provider-listing, and session-scan counters during scripted hot interactions.",
|
||||
"commands": [
|
||||
"pnpm exec vitest run src/main/gitlab/gl-utils.test.ts --config config/vitest.config.ts",
|
||||
"pnpm exec vitest run src/main/gitlab --config config/vitest.config.ts"
|
||||
],
|
||||
"testFiles": ["src/main/gitlab/gl-utils.test.ts"],
|
||||
"assertionRefs": [
|
||||
{
|
||||
"file": "src/main/gitlab/gl-utils.test.ts",
|
||||
"assertions": [
|
||||
"bounds sequential failed probes within the retry cooldown",
|
||||
"bounds successful host caches across execution contexts",
|
||||
"bounds failed-probe cooldowns across execution contexts",
|
||||
"shares local auth hosts across SSH lookups but isolates remembered remote hosts",
|
||||
"lets an explicit auth refresh bypass a failed-probe cooldown",
|
||||
"reuses local auth but drops remembered remote hosts after an SSH reconnect"
|
||||
]
|
||||
}
|
||||
],
|
||||
"evidenceRuns": [
|
||||
{
|
||||
"date": "2026-07-31",
|
||||
"runner": "local",
|
||||
"platform": "macos",
|
||||
"command": "pnpm exec vitest run src/main/gitlab --config config/vitest.config.ts",
|
||||
"result": "passed",
|
||||
"durationSeconds": 0.3,
|
||||
"summary": "On main 139f756064, 11 files and 214 GitLab tests passed; the focused red baseline made 64 probes and the candidate made one across 64 SSH lookup contexts. A same-base 35-second Electron A/B reduced glab auth spawns from 7 to 1 and spawn-initiation blocking from 236.39 ms to 18.50 ms."
|
||||
}
|
||||
],
|
||||
"runtimeBudget": {
|
||||
"p95Seconds": 60,
|
||||
"scope": "renderer-main perf/count gate"
|
||||
},
|
||||
"flakeHistory": {
|
||||
"status": "unknown",
|
||||
"evidence": "Focused boot-hydration counter slice passed locally once; needs CI/runtime history before promotion."
|
||||
"evidence": "The focused GitLab probe contract and one local Electron A/B run passed; broader hot-path counters and CI history are still required."
|
||||
},
|
||||
"redGreenEvidence": {
|
||||
"status": "partial",
|
||||
"evidence": "Tests would fail if boot hydration enumerated SSH worktrees, stopped recording provider-unavailable retry state, lost the startup counter surface, stopped recording fatal hydration failures, or missed router adapter fanout/list-failure counters. Needs intentional-break proof for git/status polling or store scans triggered by terminal hot paths."
|
||||
"evidence": "The byte-identical failed-probe contract was red on main with 64 glab calls and green on the candidate with one, including across 64 distinct SSH lookup contexts. Needs intentional-break proof for git-status polling, provider listings, or store scans triggered by terminal hot paths."
|
||||
},
|
||||
"performanceBudget": {
|
||||
"required": true,
|
||||
"evidence": "Current slice records boot-hydration repo, worktree, adapter, session, skip, register, and duration counters. Promotion still requires count budgets for listSessions, git status, provider scans, store recomputes, and elapsed hot-interaction time."
|
||||
"evidence": "Failed GitLab host discovery is capped at one subprocess probe per native or WSL execution context per 30 seconds; SSH targets share the native probe, and successful, failed, and connection-derived state are each capped at 128 entries. Promotion still requires count budgets for listSessions, git status, other provider scans, store recomputes, and elapsed hot-interaction time."
|
||||
},
|
||||
"promotionCriteria": [
|
||||
"Use deterministic counters before broad perf scenarios.",
|
||||
@@ -6471,8 +6496,8 @@
|
||||
"Keep stress variants non-blocking until stable."
|
||||
],
|
||||
"knownGaps": [
|
||||
"No executable coverage on main yet; the slice lives on the pending fix-terminal-reliability stack.",
|
||||
"Current command covers boot hydration counters only, not interactive terminal hot paths.",
|
||||
"Current executable coverage is limited to GitLab host probing, not interactive terminal hot paths.",
|
||||
"Boot-hydration counters remain on the pending reliability stack.",
|
||||
"No Electron count gate yet proves focus, typing, tab switch, workspace switch, render, or high-session provider fanout avoids broad store/git/provider scans."
|
||||
],
|
||||
"demotionRule": "Cannot promote without count budgets and actionable artifacts."
|
||||
|
||||
@@ -291,7 +291,7 @@ describe('gitlab client — MR operations', () => {
|
||||
})
|
||||
})
|
||||
|
||||
it('merges many authenticated hosts with one cache scan', async () => {
|
||||
it('merges many authenticated hosts with at most one cache scan', async () => {
|
||||
const hostCount = 256
|
||||
glabExecFileAsyncMock.mockResolvedValueOnce({
|
||||
stdout: Array.from(
|
||||
@@ -321,7 +321,7 @@ describe('gitlab client — MR operations', () => {
|
||||
} finally {
|
||||
mapSpy.mockRestore()
|
||||
}
|
||||
expect(knownHostCacheScans).toBe(1)
|
||||
expect(knownHostCacheScans).toBeLessThanOrEqual(1)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -7,24 +7,65 @@ export type LocalGitExecOptions = {
|
||||
}
|
||||
|
||||
const GLAB_KNOWN_HOSTS_TIMEOUT_MS = 10_000
|
||||
const GLAB_KNOWN_HOSTS_FAILURE_COOLDOWN_MS = 30_000
|
||||
const GLAB_KNOWN_HOSTS_CACHE_MAX_ENTRIES = 128
|
||||
const knownHostsCacheByExecutionContext = new Map<string, readonly string[]>()
|
||||
const knownHostsInFlightByExecutionContext = new Map<string, Promise<readonly string[]>>()
|
||||
const knownHostsFailureByExecutionContext = new Map<
|
||||
string,
|
||||
{ hosts: readonly string[]; retryAt: number }
|
||||
>()
|
||||
const rememberedHostsByLookupContext = new Map<string, readonly string[]>()
|
||||
|
||||
function rememberBounded<T>(cache: Map<string, T>, key: string, value: T): void {
|
||||
cache.delete(key)
|
||||
cache.set(key, value)
|
||||
while (cache.size > GLAB_KNOWN_HOSTS_CACHE_MAX_ENTRIES) {
|
||||
const oldestKey = cache.keys().next().value
|
||||
if (oldestKey === undefined) {
|
||||
return
|
||||
}
|
||||
cache.delete(oldestKey)
|
||||
}
|
||||
}
|
||||
|
||||
function knownHostsExecutionKey(
|
||||
connectionId?: string | null,
|
||||
localGitOptions: LocalGitExecOptions = {}
|
||||
): string {
|
||||
return !connectionId && localGitOptions.wslDistro ? `wsl:${localGitOptions.wslDistro}` : 'native'
|
||||
}
|
||||
|
||||
function knownHostsLookupKey(
|
||||
connectionId?: string | null,
|
||||
localGitOptions: LocalGitExecOptions = {}
|
||||
): string {
|
||||
if (connectionId) {
|
||||
// Why: reconnecting can replace the SSH/relay execution host under the same id.
|
||||
// Why: a reconnect can expose different repository hosts under the same connection id.
|
||||
return `connection:${connectionId}:${getSshGitProviderGeneration(connectionId)}`
|
||||
}
|
||||
return localGitOptions.wslDistro ? `wsl:${localGitOptions.wslDistro}` : 'native'
|
||||
return knownHostsExecutionKey(connectionId, localGitOptions)
|
||||
}
|
||||
|
||||
function mergeHosts(...groups: readonly (readonly string[])[]): readonly string[] {
|
||||
const hosts = new Set<string>()
|
||||
for (const group of groups) {
|
||||
for (const host of group) {
|
||||
const normalizedHost = normalizeGitLabHost(host)
|
||||
if (normalizedHost) {
|
||||
hosts.add(normalizedHost)
|
||||
}
|
||||
}
|
||||
}
|
||||
return Array.from(hosts)
|
||||
}
|
||||
|
||||
/** @internal - exposed for tests only */
|
||||
export function _resetKnownHostsCache(): void {
|
||||
knownHostsCacheByExecutionContext.clear()
|
||||
knownHostsInFlightByExecutionContext.clear()
|
||||
knownHostsFailureByExecutionContext.clear()
|
||||
rememberedHostsByLookupContext.clear()
|
||||
}
|
||||
|
||||
export function rememberGlabKnownHost(
|
||||
@@ -40,70 +81,96 @@ export function rememberGlabKnownHosts(
|
||||
connectionId?: string | null,
|
||||
localGitOptions: LocalGitExecOptions = {}
|
||||
): void {
|
||||
const key = knownHostsExecutionKey(connectionId, localGitOptions)
|
||||
const cached = knownHostsCacheByExecutionContext.get(key) ?? DEFAULT_GITLAB_HOSTS
|
||||
const seen = new Set(cached.map(normalizeGitLabHost))
|
||||
const additions: string[] = []
|
||||
for (const host of hosts) {
|
||||
const normalizedHost = normalizeGitLabHost(host)
|
||||
if (seen.has(normalizedHost)) {
|
||||
continue
|
||||
}
|
||||
seen.add(normalizedHost)
|
||||
additions.push(normalizedHost)
|
||||
}
|
||||
if (additions.length === 0) {
|
||||
const normalizedHosts = mergeHosts(hosts)
|
||||
if (normalizedHosts.length === 0) {
|
||||
return
|
||||
}
|
||||
knownHostsCacheByExecutionContext.set(key, [...cached, ...additions])
|
||||
if (connectionId) {
|
||||
const lookupKey = knownHostsLookupKey(connectionId, localGitOptions)
|
||||
const remembered = rememberedHostsByLookupContext.get(lookupKey) ?? []
|
||||
rememberBounded(
|
||||
rememberedHostsByLookupContext,
|
||||
lookupKey,
|
||||
mergeHosts(remembered, normalizedHosts)
|
||||
)
|
||||
return
|
||||
}
|
||||
const executionKey = knownHostsExecutionKey(connectionId, localGitOptions)
|
||||
const cached = knownHostsCacheByExecutionContext.get(executionKey) ?? DEFAULT_GITLAB_HOSTS
|
||||
knownHostsFailureByExecutionContext.delete(executionKey)
|
||||
rememberBounded(
|
||||
knownHostsCacheByExecutionContext,
|
||||
executionKey,
|
||||
mergeHosts(cached, normalizedHosts)
|
||||
)
|
||||
}
|
||||
|
||||
export async function getGlabKnownHosts(
|
||||
connectionId?: string | null,
|
||||
localGitOptions: LocalGitExecOptions = {}
|
||||
): Promise<readonly string[]> {
|
||||
const key = knownHostsExecutionKey(connectionId, localGitOptions)
|
||||
const cached = knownHostsCacheByExecutionContext.get(key)
|
||||
const executionKey = knownHostsExecutionKey(connectionId, localGitOptions)
|
||||
const lookupKey = knownHostsLookupKey(connectionId, localGitOptions)
|
||||
const remembered = (): readonly string[] => rememberedHostsByLookupContext.get(lookupKey) ?? []
|
||||
const cached = knownHostsCacheByExecutionContext.get(executionKey)
|
||||
if (cached) {
|
||||
return cached
|
||||
rememberBounded(knownHostsCacheByExecutionContext, executionKey, cached)
|
||||
const lookupHosts = remembered()
|
||||
return lookupHosts.length === 0 ? cached : mergeHosts(cached, lookupHosts)
|
||||
}
|
||||
const inFlight = knownHostsInFlightByExecutionContext.get(key)
|
||||
if (inFlight) {
|
||||
return inFlight
|
||||
const failed = knownHostsFailureByExecutionContext.get(executionKey)
|
||||
if (failed) {
|
||||
if (Date.now() < failed.retryAt) {
|
||||
rememberBounded(knownHostsFailureByExecutionContext, executionKey, failed)
|
||||
return mergeHosts(failed.hosts, remembered())
|
||||
}
|
||||
knownHostsFailureByExecutionContext.delete(executionKey)
|
||||
}
|
||||
const inFlight = knownHostsInFlightByExecutionContext.get(executionKey)
|
||||
const probe = inFlight ?? probeGlabKnownHosts(executionKey, connectionId ? {} : localGitOptions)
|
||||
if (!inFlight) {
|
||||
knownHostsInFlightByExecutionContext.set(executionKey, probe)
|
||||
}
|
||||
const probe = probeGlabKnownHosts(key, connectionId, localGitOptions)
|
||||
knownHostsInFlightByExecutionContext.set(key, probe)
|
||||
try {
|
||||
return await probe
|
||||
const probedHosts = await probe
|
||||
const lookupHosts = remembered()
|
||||
return lookupHosts.length === 0 ? probedHosts : mergeHosts(probedHosts, lookupHosts)
|
||||
} finally {
|
||||
if (knownHostsInFlightByExecutionContext.get(key) === probe) {
|
||||
knownHostsInFlightByExecutionContext.delete(key)
|
||||
if (knownHostsInFlightByExecutionContext.get(executionKey) === probe) {
|
||||
knownHostsInFlightByExecutionContext.delete(executionKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function probeGlabKnownHosts(
|
||||
key: string,
|
||||
connectionId?: string | null,
|
||||
localGitOptions: LocalGitExecOptions = {}
|
||||
): Promise<readonly string[]> {
|
||||
try {
|
||||
// Why: auth config belongs to the executing host; do not share native, WSL,
|
||||
// or reconnected SSH/relay results, and bound an otherwise global probe.
|
||||
// Why: SSH Git is remote, but glab runs beside Orca; share its probe with native lookups.
|
||||
const { stdout, stderr } = await glabExecFileAsync(['auth', 'status'], {
|
||||
timeout: GLAB_KNOWN_HOSTS_TIMEOUT_MS,
|
||||
...(!connectionId && localGitOptions.wslDistro
|
||||
? { wslDistro: localGitOptions.wslDistro }
|
||||
: {})
|
||||
...(localGitOptions.wslDistro ? { wslDistro: localGitOptions.wslDistro } : {})
|
||||
})
|
||||
const hosts = parseGlabAuthStatusHosts(`${stdout}\n${stderr}`)
|
||||
const remembered = knownHostsCacheByExecutionContext.get(key) ?? []
|
||||
const merged = Array.from(new Set([...DEFAULT_GITLAB_HOSTS, ...remembered, ...hosts]))
|
||||
knownHostsCacheByExecutionContext.set(key, merged)
|
||||
const merged = mergeHosts(DEFAULT_GITLAB_HOSTS, remembered, hosts)
|
||||
knownHostsFailureByExecutionContext.delete(key)
|
||||
rememberBounded(knownHostsCacheByExecutionContext, key, merged)
|
||||
return merged
|
||||
} catch {
|
||||
// Keep failures uncached so auth or tunnel recovery is discovered later.
|
||||
return knownHostsCacheByExecutionContext.get(key) ?? [...DEFAULT_GITLAB_HOSTS]
|
||||
const cached = knownHostsCacheByExecutionContext.get(key)
|
||||
if (cached) {
|
||||
knownHostsFailureByExecutionContext.delete(key)
|
||||
return cached
|
||||
}
|
||||
const hosts = [...DEFAULT_GITLAB_HOSTS]
|
||||
// Why: one missing glab on an SSH/WSL host must not respawn on every card refresh; the short cooldown still discovers recovery.
|
||||
rememberBounded(knownHostsFailureByExecutionContext, key, {
|
||||
hosts,
|
||||
retryAt: Date.now() + GLAB_KNOWN_HOSTS_FAILURE_COOLDOWN_MS
|
||||
})
|
||||
return hosts
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -428,6 +428,12 @@ describe('getGlabKnownHosts', () => {
|
||||
_resetKnownHostsCache()
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
unregisterSshGitProvider('conn-failure-reconnected')
|
||||
unregisterSshGitProvider('conn-reconnected')
|
||||
vi.useRealTimers()
|
||||
})
|
||||
|
||||
it('returns gitlab.com plus auth-status hosts, deduped', async () => {
|
||||
glabExecFileAsyncMock.mockResolvedValueOnce({
|
||||
stdout: '✓ Logged in to gitlab.com as user\n✓ Logged in to gitlab.example.com as user\n',
|
||||
@@ -473,12 +479,11 @@ describe('getGlabKnownHosts', () => {
|
||||
expect(results[0]).toEqual(['gitlab.com', 'gitlab.concurrent.test'])
|
||||
})
|
||||
|
||||
it('keeps simultaneous native, WSL distro, and connection probes isolated', async () => {
|
||||
it('shares the native probe with SSH while keeping WSL distro probes isolated', async () => {
|
||||
glabExecFileAsyncMock
|
||||
.mockResolvedValueOnce({ stdout: 'Logged in to ubuntu.test as user\n', stderr: '' })
|
||||
.mockResolvedValueOnce({ stdout: 'Logged in to debian.test as user\n', stderr: '' })
|
||||
.mockResolvedValueOnce({ stdout: 'Logged in to native.test as user\n', stderr: '' })
|
||||
.mockResolvedValueOnce({ stdout: 'Logged in to ssh.test as user\n', stderr: '' })
|
||||
|
||||
const [ubuntu, ubuntuAgain, debian, native, ssh] = await Promise.all([
|
||||
getGlabKnownHosts(undefined, { wslDistro: 'Ubuntu' }),
|
||||
@@ -492,8 +497,8 @@ describe('getGlabKnownHosts', () => {
|
||||
expect(ubuntu).toEqual(['gitlab.com', 'ubuntu.test'])
|
||||
expect(debian).toEqual(['gitlab.com', 'debian.test'])
|
||||
expect(native).toEqual(['gitlab.com', 'native.test'])
|
||||
expect(ssh).toEqual(['gitlab.com', 'ssh.test'])
|
||||
expect(glabExecFileAsyncMock).toHaveBeenCalledTimes(4)
|
||||
expect(ssh).toEqual(['gitlab.com', 'native.test'])
|
||||
expect(glabExecFileAsyncMock).toHaveBeenCalledTimes(3)
|
||||
expect(glabExecFileAsyncMock).toHaveBeenNthCalledWith(1, ['auth', 'status'], {
|
||||
timeout: 10_000,
|
||||
wslDistro: 'Ubuntu'
|
||||
@@ -538,11 +543,10 @@ describe('getGlabKnownHosts', () => {
|
||||
await expect(getGlabKnownHosts()).resolves.toEqual(['gitlab.com', 'gitlab.refreshed.test'])
|
||||
})
|
||||
|
||||
it('keeps a remembered native host out of WSL and SSH caches', async () => {
|
||||
it('shares remembered native auth with SSH but keeps it out of WSL', async () => {
|
||||
glabExecFileAsyncMock
|
||||
.mockResolvedValueOnce({ stdout: 'Logged in to native.test as user\n', stderr: '' })
|
||||
.mockResolvedValueOnce({ stdout: 'Logged in to wsl.test as user\n', stderr: '' })
|
||||
.mockResolvedValueOnce({ stdout: 'Logged in to ssh.test as user\n', stderr: '' })
|
||||
|
||||
await Promise.all([
|
||||
getGlabKnownHosts(),
|
||||
@@ -560,7 +564,11 @@ describe('getGlabKnownHosts', () => {
|
||||
'gitlab.com',
|
||||
'wsl.test'
|
||||
])
|
||||
await expect(getGlabKnownHosts('conn-1')).resolves.toEqual(['gitlab.com', 'ssh.test'])
|
||||
await expect(getGlabKnownHosts('conn-1')).resolves.toEqual([
|
||||
'gitlab.com',
|
||||
'native.test',
|
||||
'gitlab.refreshed.test'
|
||||
])
|
||||
})
|
||||
|
||||
it('batch-normalizes and deduplicates hosts in first-seen order per execution context', async () => {
|
||||
@@ -582,6 +590,8 @@ describe('getGlabKnownHosts', () => {
|
||||
])
|
||||
await expect(getGlabKnownHosts('conn-batch')).resolves.toEqual([
|
||||
'gitlab.com',
|
||||
'native-b.test',
|
||||
'native-a.test',
|
||||
'ssh-b.test',
|
||||
'ssh-a.test'
|
||||
])
|
||||
@@ -597,28 +607,108 @@ describe('getGlabKnownHosts', () => {
|
||||
await expect(getGlabKnownHosts()).resolves.toEqual(['gitlab.com', 'gitlab.example.com:8080'])
|
||||
})
|
||||
|
||||
it('caches per connection — the local probe does not satisfy a connection probe', async () => {
|
||||
glabExecFileAsyncMock
|
||||
.mockResolvedValueOnce({ stdout: '✓ Logged in to gitlab.com as user\n', stderr: '' })
|
||||
.mockResolvedValueOnce({
|
||||
stdout: '✓ Logged in to gitlab.example.com:8080 as user\n',
|
||||
stderr: ''
|
||||
})
|
||||
it('shares local auth hosts across SSH lookups but isolates remembered remote hosts', async () => {
|
||||
glabExecFileAsyncMock.mockResolvedValueOnce({
|
||||
stdout: '✓ Logged in to gitlab.example.com:8080 as user\n',
|
||||
stderr: ''
|
||||
})
|
||||
|
||||
await expect(getGlabKnownHosts('conn-1')).resolves.toEqual([
|
||||
'gitlab.com',
|
||||
'gitlab.example.com:8080'
|
||||
])
|
||||
rememberGlabKnownHost('gitlab.conn-1.test', 'conn-1')
|
||||
|
||||
await expect(getGlabKnownHosts('conn-2')).resolves.toEqual([
|
||||
'gitlab.com',
|
||||
'gitlab.example.com:8080'
|
||||
])
|
||||
await expect(getGlabKnownHosts('conn-1')).resolves.toEqual([
|
||||
'gitlab.com',
|
||||
'gitlab.example.com:8080',
|
||||
'gitlab.conn-1.test'
|
||||
])
|
||||
expect(glabExecFileAsyncMock).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('bounds sequential failed probes within the retry cooldown', async () => {
|
||||
vi.useFakeTimers()
|
||||
glabExecFileAsyncMock.mockRejectedValue(new Error('glab unavailable'))
|
||||
|
||||
for (let index = 0; index < 64; index += 1) {
|
||||
await expect(getGlabKnownHosts(`conn-failing-${index}`)).resolves.toEqual(['gitlab.com'])
|
||||
}
|
||||
|
||||
expect(glabExecFileAsyncMock).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('keeps the cooldown when an SSH lookup remembers only the default host', async () => {
|
||||
vi.useFakeTimers()
|
||||
glabExecFileAsyncMock.mockRejectedValue(new Error('glab unavailable'))
|
||||
|
||||
await expect(getGlabKnownHosts('conn-default')).resolves.toEqual(['gitlab.com'])
|
||||
rememberGlabKnownHost('gitlab.com', 'conn-default')
|
||||
await expect(getGlabKnownHosts('conn-default')).resolves.toEqual(['gitlab.com'])
|
||||
|
||||
expect(glabExecFileAsyncMock).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('bounds successful host caches across execution contexts', async () => {
|
||||
glabExecFileAsyncMock.mockResolvedValue({
|
||||
stdout: 'Logged in to gitlab.example.test as user\n',
|
||||
stderr: ''
|
||||
})
|
||||
|
||||
for (let index = 0; index < 129; index += 1) {
|
||||
await getGlabKnownHosts(undefined, { wslDistro: `Distro-${index}` })
|
||||
}
|
||||
await getGlabKnownHosts(undefined, { wslDistro: 'Distro-0' })
|
||||
|
||||
expect(glabExecFileAsyncMock).toHaveBeenCalledTimes(130)
|
||||
})
|
||||
|
||||
it('bounds failed-probe cooldowns across execution contexts', async () => {
|
||||
vi.useFakeTimers()
|
||||
glabExecFileAsyncMock.mockRejectedValue(new Error('glab unavailable'))
|
||||
|
||||
for (let index = 0; index < 129; index += 1) {
|
||||
await getGlabKnownHosts(undefined, { wslDistro: `Failing-${index}` })
|
||||
}
|
||||
await getGlabKnownHosts(undefined, { wslDistro: 'Failing-0' })
|
||||
|
||||
expect(glabExecFileAsyncMock).toHaveBeenCalledTimes(130)
|
||||
})
|
||||
|
||||
it('lets an explicit auth refresh bypass a failed-probe cooldown', async () => {
|
||||
vi.useFakeTimers()
|
||||
glabExecFileAsyncMock.mockRejectedValueOnce(new Error('glab unavailable'))
|
||||
|
||||
await expect(getGlabKnownHosts()).resolves.toEqual(['gitlab.com'])
|
||||
await expect(getGlabKnownHosts('conn-1')).resolves.toEqual([
|
||||
rememberGlabKnownHost('gitlab.reauthenticated.test')
|
||||
|
||||
await expect(getGlabKnownHosts()).resolves.toEqual([
|
||||
'gitlab.com',
|
||||
'gitlab.example.com:8080'
|
||||
'gitlab.reauthenticated.test'
|
||||
])
|
||||
// A second probe for the same connection is served from cache.
|
||||
await expect(getGlabKnownHosts('conn-1')).resolves.toEqual([
|
||||
'gitlab.com',
|
||||
'gitlab.example.com:8080'
|
||||
])
|
||||
expect(glabExecFileAsyncMock).toHaveBeenCalledTimes(2)
|
||||
expect(glabExecFileAsyncMock).toHaveBeenCalledTimes(1)
|
||||
})
|
||||
|
||||
it('does not repeat a local failed probe when an SSH provider reconnects', async () => {
|
||||
vi.useFakeTimers()
|
||||
const connectionId = 'conn-failure-reconnected'
|
||||
registerSshGitProvider(connectionId, {} as never)
|
||||
glabExecFileAsyncMock.mockRejectedValue(new Error('glab unavailable'))
|
||||
|
||||
await expect(getGlabKnownHosts(connectionId)).resolves.toEqual(['gitlab.com'])
|
||||
registerSshGitProvider(connectionId, {} as never)
|
||||
|
||||
await expect(getGlabKnownHosts(connectionId)).resolves.toEqual(['gitlab.com'])
|
||||
expect(glabExecFileAsyncMock).toHaveBeenCalledTimes(1)
|
||||
unregisterSshGitProvider(connectionId)
|
||||
})
|
||||
|
||||
it('does not permanently cache the failure fallback — a later probe can re-discover hosts', async () => {
|
||||
vi.useFakeTimers()
|
||||
glabExecFileAsyncMock
|
||||
.mockRejectedValueOnce(new Error('ssh tunnel not ready'))
|
||||
.mockResolvedValueOnce({
|
||||
@@ -626,9 +716,11 @@ describe('getGlabKnownHosts', () => {
|
||||
stderr: ''
|
||||
})
|
||||
|
||||
// First probe fails → canonical default, NOT cached.
|
||||
await expect(getGlabKnownHosts('conn-1')).resolves.toEqual(['gitlab.com'])
|
||||
// Re-probe (e.g. after tunnel comes up) discovers the real host.
|
||||
await expect(getGlabKnownHosts('conn-1')).resolves.toEqual(['gitlab.com'])
|
||||
expect(glabExecFileAsyncMock).toHaveBeenCalledTimes(1)
|
||||
|
||||
vi.advanceTimersByTime(30_001)
|
||||
await expect(getGlabKnownHosts('conn-1')).resolves.toEqual([
|
||||
'gitlab.com',
|
||||
'gitlab.example.com:8080'
|
||||
@@ -637,6 +729,7 @@ describe('getGlabKnownHosts', () => {
|
||||
})
|
||||
|
||||
it('removes a timed-out probe from in-flight state so a later call retries', async () => {
|
||||
vi.useFakeTimers()
|
||||
let rejectProbe!: (error: Error) => void
|
||||
glabExecFileAsyncMock
|
||||
.mockImplementationOnce(
|
||||
@@ -656,6 +749,12 @@ describe('getGlabKnownHosts', () => {
|
||||
['gitlab.com'],
|
||||
['gitlab.com']
|
||||
])
|
||||
await expect(getGlabKnownHosts(undefined, { wslDistro: 'Ubuntu' })).resolves.toEqual([
|
||||
'gitlab.com'
|
||||
])
|
||||
expect(glabExecFileAsyncMock).toHaveBeenCalledTimes(1)
|
||||
|
||||
vi.advanceTimersByTime(30_001)
|
||||
await expect(getGlabKnownHosts(undefined, { wslDistro: 'Ubuntu' })).resolves.toEqual([
|
||||
'gitlab.com',
|
||||
'recovered.test'
|
||||
@@ -663,23 +762,26 @@ describe('getGlabKnownHosts', () => {
|
||||
expect(glabExecFileAsyncMock).toHaveBeenCalledTimes(2)
|
||||
})
|
||||
|
||||
it('does not reuse a successful result after an SSH provider reconnects', async () => {
|
||||
it('reuses local auth but drops remembered remote hosts after an SSH reconnect', async () => {
|
||||
const connectionId = 'conn-reconnected'
|
||||
registerSshGitProvider(connectionId, {} as never)
|
||||
glabExecFileAsyncMock
|
||||
.mockResolvedValueOnce({ stdout: 'Logged in to old-tunnel.test as user\n', stderr: '' })
|
||||
.mockResolvedValueOnce({ stdout: 'Logged in to new-tunnel.test as user\n', stderr: '' })
|
||||
glabExecFileAsyncMock.mockResolvedValueOnce({
|
||||
stdout: 'Logged in to native-auth.test as user\n',
|
||||
stderr: ''
|
||||
})
|
||||
|
||||
await expect(getGlabKnownHosts(connectionId)).resolves.toEqual([
|
||||
'gitlab.com',
|
||||
'old-tunnel.test'
|
||||
'native-auth.test'
|
||||
])
|
||||
rememberGlabKnownHost('old-tunnel.test', connectionId)
|
||||
await expect(getGlabKnownHosts(connectionId)).resolves.toContain('old-tunnel.test')
|
||||
registerSshGitProvider(connectionId, {} as never)
|
||||
await expect(getGlabKnownHosts(connectionId)).resolves.toEqual([
|
||||
'gitlab.com',
|
||||
'new-tunnel.test'
|
||||
'native-auth.test'
|
||||
])
|
||||
expect(glabExecFileAsyncMock).toHaveBeenCalledTimes(2)
|
||||
expect(glabExecFileAsyncMock).toHaveBeenCalledTimes(1)
|
||||
unregisterSshGitProvider(connectionId)
|
||||
})
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user