Files
orca/src/main/github/rate-limit.test.ts
T
OrcaWinandOrcaWin 4687e90a16 [comment audit] Continue main-process comment cleanup (#32946)
* [comment audit] Shorten daemon, hook, and emulator comments [skip ci]

* [comment audit] Shorten VM, conversation, SQLite, and theme-import comments [skip ci]

* [comment audit] Tighten Git execution and source-control comment contracts [skip ci]

* [comment audit] Clarify repository probes and maintenance comments [skip ci]

* [comment audit] Shorten status, diff-cache, and checkout comments [skip ci]

* [comment audit] Clarify worktree lifecycle and WSL routing comments [skip ci]

* [comment audit] Shorten forge identity and review-refresh comments [skip ci]

* [comment audit] Tighten hosted-review client comment contracts [skip ci]

* [comment audit] Clarify GitLab compatibility and review comments [skip ci]

* [comment audit] Tighten hook ownership and watchdog rationale [skip ci]

* [comment audit] Condense host and integration compatibility comments [skip ci]

* [comment audit] Clarify IPC authority and remote-routing rationale [skip ci]

* [comment audit] Finalize file-import and containment comment cleanup

* [comment audit] Fix stale Jira type imports blocking full CI

---------

Co-authored-by: OrcaWin <293788423+OrcaWin@users.noreply.github.com>
2026-10-23 16:27:37 -07:00

348 lines
11 KiB
TypeScript

import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
const { ghExecFileAsyncMock } = vi.hoisted(() => ({
ghExecFileAsyncMock: vi.fn()
}))
vi.mock('../git/runner', () => ({
ghExecFileAsync: ghExecFileAsyncMock
}))
vi.mock('./gh-utils', () => ({
acquire: vi.fn(),
release: vi.fn()
}))
import {
getRateLimit,
rateLimitGuard,
noteRateLimitSpend,
spendsSharedGitHubComQuota,
repositoryRateLimitGuard,
noteRepositoryRateLimitSpend,
_resetRateLimitCache
} from './rate-limit'
import {
clearGhRateLimitBlock,
getGhRateLimitBlockedUntilMs,
notifyGhPrimaryRateLimit,
type GhRateLimitBucket
} from '../git/gh-rate-limit-breaker'
const PAYLOAD = JSON.stringify({
resources: {
core: { limit: 5000, remaining: 4200, reset: 1_700_000_000 },
search: { limit: 30, remaining: 28, reset: 1_700_000_000 },
graphql: { limit: 5000, remaining: 4900, reset: 1_700_000_000 }
}
})
describe('getRateLimit', () => {
beforeEach(() => {
vi.useFakeTimers()
vi.setSystemTime(1_000)
ghExecFileAsyncMock.mockReset()
_resetRateLimitCache()
})
afterEach(() => {
vi.useRealTimers()
})
it('parses bucket counts from gh api rate_limit', async () => {
ghExecFileAsyncMock.mockResolvedValue({ stdout: PAYLOAD })
const result = await getRateLimit()
expect(result).toEqual({
ok: true,
snapshot: {
core: { limit: 5000, remaining: 4200, resetAt: 1_700_000_000 },
search: { limit: 30, remaining: 28, resetAt: 1_700_000_000 },
graphql: { limit: 5000, remaining: 4900, resetAt: 1_700_000_000 },
fetchedAt: 1_000
}
})
// The real runner adds --hostname after this mocked boundary.
expect(ghExecFileAsyncMock).toHaveBeenCalledWith(['api', 'rate_limit'], {
encoding: 'utf-8',
host: 'github.com'
})
})
it('caches a failed probe for the TTL instead of re-spawning gh', async () => {
// Negative-cache failed probes so repeated refreshes do not respawn doomed gh calls.
ghExecFileAsyncMock.mockRejectedValue(new Error('HTTP 404: Rate limiting is not enabled.'))
const first = await getRateLimit()
const second = await getRateLimit()
expect(first).toEqual({ ok: false, error: 'HTTP 404: Rate limiting is not enabled.' })
expect(second).toEqual(first)
expect(ghExecFileAsyncMock).toHaveBeenCalledTimes(1)
})
it('re-probes after the failure TTL elapses and recovers on success', async () => {
ghExecFileAsyncMock.mockRejectedValueOnce(new Error('boom'))
ghExecFileAsyncMock.mockResolvedValue({ stdout: PAYLOAD })
expect((await getRateLimit()).ok).toBe(false)
expect((await getRateLimit()).ok).toBe(false)
expect(ghExecFileAsyncMock).toHaveBeenCalledTimes(1)
vi.setSystemTime(1_000 + 30_000)
expect((await getRateLimit()).ok).toBe(true)
expect(ghExecFileAsyncMock).toHaveBeenCalledTimes(2)
})
it('bypasses the failure cache when forced', async () => {
ghExecFileAsyncMock.mockRejectedValueOnce(new Error('boom'))
ghExecFileAsyncMock.mockResolvedValue({ stdout: PAYLOAD })
expect((await getRateLimit()).ok).toBe(false)
expect((await getRateLimit({ force: true })).ok).toBe(true)
expect(ghExecFileAsyncMock).toHaveBeenCalledTimes(2)
})
})
describe('rateLimitGuard', () => {
beforeEach(() => {
vi.useFakeTimers()
vi.setSystemTime(1_000)
ghExecFileAsyncMock.mockReset()
_resetRateLimitCache()
})
afterEach(() => {
vi.useRealTimers()
})
async function cacheExhaustedSnapshot(resetEpochSeconds: number): Promise<void> {
ghExecFileAsyncMock.mockResolvedValueOnce({
stdout: JSON.stringify({
resources: {
core: { limit: 5000, remaining: 3, reset: resetEpochSeconds },
search: { limit: 30, remaining: 28, reset: resetEpochSeconds },
graphql: { limit: 5000, remaining: 4900, reset: resetEpochSeconds }
}
})
})
expect((await getRateLimit()).ok).toBe(true)
}
it('blocks below the floor while resetAt is in the future', async () => {
await cacheExhaustedSnapshot(61)
expect(rateLimitGuard('core')).toEqual({
blocked: true,
remaining: 3,
limit: 5000,
resetAt: 61
})
})
it('fails open once the blocking bucket resetAt has elapsed', async () => {
// Exhausted snapshots from an ended window must not keep the refresh loop paused.
await cacheExhaustedSnapshot(61)
vi.setSystemTime(61_000)
expect(rateLimitGuard('core')).toEqual({ blocked: false })
})
})
// Only native github.com calls share this snapshot; Enterprise and WSL have separate quotas.
describe('shared-quota host scoping', () => {
beforeEach(() => {
vi.useFakeTimers()
vi.setSystemTime(1_000)
ghExecFileAsyncMock.mockReset()
_resetRateLimitCache()
})
afterEach(() => {
vi.useRealTimers()
})
async function cacheExhaustedSnapshot(resetEpochSeconds: number): Promise<void> {
ghExecFileAsyncMock.mockResolvedValueOnce({
stdout: JSON.stringify({
resources: {
core: { limit: 5000, remaining: 3, reset: resetEpochSeconds },
search: { limit: 30, remaining: 28, reset: resetEpochSeconds },
graphql: { limit: 5000, remaining: 4900, reset: resetEpochSeconds }
}
})
})
expect((await getRateLimit()).ok).toBe(true)
}
it('treats github.com / no host on the native runtime as sharing the budget', () => {
expect(spendsSharedGitHubComQuota(undefined)).toBe(true)
expect(spendsSharedGitHubComQuota(null)).toBe(true)
expect(spendsSharedGitHubComQuota({ host: 'github.com' })).toBe(true)
expect(spendsSharedGitHubComQuota({ host: 'GitHub.com' })).toBe(true)
})
it('excludes GHES hosts and WSL runtimes from the shared budget', () => {
expect(spendsSharedGitHubComQuota({ host: 'github.acme-corp.com' })).toBe(false)
expect(spendsSharedGitHubComQuota({ host: 'github.com' }, { wslDistro: 'Ubuntu' })).toBe(false)
expect(spendsSharedGitHubComQuota(undefined, { wslDistro: 'Ubuntu' })).toBe(false)
})
it('treats a WSL UNC cwd as non-shared even without an explicit wslDistro', () => {
// UNC cwd routes execution into the distro and spends its quota.
expect(
spendsSharedGitHubComQuota(
{ host: 'github.com' },
{ cwd: '\\\\wsl.localhost\\Ubuntu\\home\\me\\repo' }
)
).toBe(false)
expect(
spendsSharedGitHubComQuota(
{ host: 'github.com' },
{ cwd: '\\\\wsl$\\Ubuntu\\home\\me\\repo' }
)
).toBe(false)
expect(spendsSharedGitHubComQuota({ host: 'github.com' }, { cwd: 'C:\\repos\\a' })).toBe(true)
expect(spendsSharedGitHubComQuota({ host: 'github.com' }, { cwd: '/Users/me/repo' })).toBe(true)
})
it('bypasses the guard and spend for a WSL UNC cwd', async () => {
await cacheExhaustedSnapshot(61)
const cwd = '\\\\wsl.localhost\\Ubuntu\\home\\me\\repo'
expect(repositoryRateLimitGuard({ host: 'github.com' }, 'core', { cwd })).toEqual({
blocked: false
})
noteRepositoryRateLimitSpend({ host: 'github.com' }, 'core', 1, { cwd })
const guard = rateLimitGuard('core')
expect(guard.blocked && guard.remaining).toBe(3)
})
it('applies the shared guard for github.com but bypasses non-shared scopes', async () => {
await cacheExhaustedSnapshot(61)
expect(repositoryRateLimitGuard({ host: 'github.com' }, 'core')).toEqual({
blocked: true,
remaining: 3,
limit: 5000,
resetAt: 61
})
expect(repositoryRateLimitGuard({ host: 'github.acme-corp.com' }, 'core')).toEqual({
blocked: false
})
expect(
repositoryRateLimitGuard({ host: 'github.com' }, 'core', { wslDistro: 'Ubuntu' })
).toEqual({ blocked: false })
})
it('debits shared-budget spend but no-ops for non-shared scopes', async () => {
await cacheExhaustedSnapshot(61)
noteRepositoryRateLimitSpend({ host: 'github.acme-corp.com' }, 'core')
const afterEnterprise = rateLimitGuard('core')
expect(afterEnterprise.blocked && afterEnterprise.remaining).toBe(3)
noteRepositoryRateLimitSpend({ host: 'github.com' }, 'core')
const afterShared = rateLimitGuard('core')
expect(afterShared.blocked && afterShared.remaining).toBe(2)
})
it('matches noteRateLimitSpend when the scope is shared github.com', async () => {
await cacheExhaustedSnapshot(61)
noteRateLimitSpend('core')
const guard = rateLimitGuard('core')
expect(guard.blocked && guard.remaining).toBe(2)
})
})
// Refine only the tripping host/runtime's breaker; preserve the shared native snapshot.
describe('scope-aware breaker reset probe', () => {
const WSL_SCOPE = 'wsl:ubuntu:github.com'
const GHES_SCOPE = 'native:github.acme-corp.com'
const ALL_BUCKETS: GhRateLimitBucket[] = ['core', 'search', 'graphql']
beforeEach(() => {
vi.useFakeTimers()
vi.setSystemTime(1_000)
ghExecFileAsyncMock.mockReset()
_resetRateLimitCache()
})
afterEach(() => {
// Preserve the module-load probe while clearing scoped breaker state.
for (const scope of [WSL_SCOPE, GHES_SCOPE]) {
for (const bucket of ALL_BUCKETS) {
clearGhRateLimitBlock(bucket, scope)
}
}
vi.useRealTimers()
})
async function flushProbe(): Promise<void> {
for (let i = 0; i < 20; i++) {
await Promise.resolve()
}
}
it('refines a WSL-scope trip via a probe matching that scope', async () => {
// Choose a reset beyond the fallback window so refinement is observable.
ghExecFileAsyncMock.mockResolvedValue({
stdout: JSON.stringify({
resources: {
core: { limit: 5000, remaining: 0, reset: 601 },
search: { limit: 30, remaining: 20, reset: 601 },
graphql: { limit: 5000, remaining: 100, reset: 601 }
}
})
})
notifyGhPrimaryRateLimit('core', WSL_SCOPE)
notifyGhPrimaryRateLimit('core', WSL_SCOPE)
await flushProbe()
expect(ghExecFileAsyncMock).toHaveBeenCalledTimes(1)
expect(ghExecFileAsyncMock).toHaveBeenCalledWith(['api', 'rate_limit'], {
encoding: 'utf-8',
host: 'github.com',
wslDistro: 'ubuntu'
})
expect(getGhRateLimitBlockedUntilMs('core', 1_000, WSL_SCOPE)).toBe(601_000)
expect(getGhRateLimitBlockedUntilMs('search', 1_000, WSL_SCOPE)).toBeNull()
expect(rateLimitGuard('core')).toEqual({ blocked: false })
expect(getGhRateLimitBlockedUntilMs('core', 1_000)).toBeNull()
})
it('fails open on a GHES probe failure and negative-caches the scope', async () => {
ghExecFileAsyncMock.mockRejectedValue(new Error('HTTP 404: Rate limiting is not enabled.'))
notifyGhPrimaryRateLimit('core', GHES_SCOPE)
await flushProbe()
notifyGhPrimaryRateLimit('core', GHES_SCOPE)
await flushProbe()
expect(ghExecFileAsyncMock).toHaveBeenCalledTimes(1)
// Failed refinement leaves the existing fallback block in force.
expect(getGhRateLimitBlockedUntilMs('core', 1_000, GHES_SCOPE)).toBe(301_000)
vi.setSystemTime(1_000 + 30_000)
ghExecFileAsyncMock.mockResolvedValue({
stdout: JSON.stringify({
resources: { core: { limit: 5000, remaining: 4999, reset: 700 } }
})
})
notifyGhPrimaryRateLimit('core', GHES_SCOPE)
await flushProbe()
expect(ghExecFileAsyncMock).toHaveBeenCalledTimes(2)
expect(ghExecFileAsyncMock).toHaveBeenLastCalledWith(['api', 'rate_limit'], {
encoding: 'utf-8',
host: 'github.acme-corp.com'
})
expect(getGhRateLimitBlockedUntilMs('core', 31_000, GHES_SCOPE)).toBeNull()
})
})