Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/main/global-fetch-call-site-audit.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ const AUDITED_GLOBAL_FETCH_LINES = new Map<string, number>([
['main/orca-profiles/profile-cloud-client.ts', 1],
['main/orca-profiles/profile-cloud-org-members-client.ts', 1],
['main/rate-limits/codex-fetcher.ts', 3],
['main/rate-limits/zcode-usage-fetcher.ts', 1],
['main/runtime/push/push-gateway-client.ts', 1],
['main/runtime/relay/relay-http-client.ts', 2],
['main/runtime/relay/relay-region-catalog-fetch.ts', 1],
Expand Down
3 changes: 3 additions & 0 deletions src/main/rate-limits/rate-limit-service-test-harness.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { readGrokAuthSession } from './grok-auth'
import { fetchCursorRateLimits } from './cursor-fetcher'
import { readCursorAuthSession } from './cursor-auth'
import { fetchOpenCodeGoUsage } from './opencode-go-usage-source-selection'
import { fetchZcodeRateLimits } from './zcode-usage-fetcher'
import { hasMiniMaxSessionCookie } from '../minimax/minimax-cookie-store'

export type Deferred<T> = {
Expand Down Expand Up @@ -92,6 +93,7 @@ export function mockFreshBackgroundProviderFetches(): void {
vi.mocked(fetchMiniMaxRateLimits).mockImplementation(async () => okProvider('minimax', 0))
vi.mocked(fetchGrokRateLimits).mockImplementation(async () => unavailableProvider('grok'))
vi.mocked(fetchCursorRateLimits).mockImplementation(async () => unavailableProvider('cursor'))
vi.mocked(fetchZcodeRateLimits).mockImplementation(async () => unavailableProvider('zcode'))
}

/** Shared `beforeEach` body: healthy stubs for every provider the service polls. */
Expand All @@ -110,6 +112,7 @@ export function resetRateLimitProviderMocks(): void {
status: 'unavailable'
})
vi.mocked(fetchCursorRateLimits).mockResolvedValue(unavailableProvider('cursor'))
vi.mocked(fetchZcodeRateLimits).mockResolvedValue(unavailableProvider('zcode'))
vi.mocked(hasMiniMaxSessionCookie).mockReturnValue(false)
vi.mocked(readGrokAuthSession).mockReturnValue({ status: 'missing' })
vi.mocked(readCursorAuthSession).mockResolvedValue({ status: 'missing' })
Expand Down
4 changes: 4 additions & 0 deletions src/main/rate-limits/service-account-target-selection.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ vi.mock('./opencode-go-usage-source-selection', () => ({
fetchOpenCodeGoUsage: vi.fn()
}))

vi.mock('./zcode-usage-fetcher', () => ({
fetchZcodeRateLimits: vi.fn()
}))

vi.mock('./minimax/minimax-fetcher', () => ({
fetchMiniMaxRateLimits: vi.fn()
}))
Expand Down
2 changes: 2 additions & 0 deletions src/main/rate-limits/service-antigravity-usage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,8 @@ vi.mock('./grok-fetcher', () => ({
fetchGrokRateLimits: vi.fn()
}))

vi.mock('./zcode-usage-fetcher', () => ({ fetchZcodeRateLimits: vi.fn() }))

vi.mock('./cursor-fetcher', () => ({
fetchCursorRateLimits: vi.fn()
}))
Expand Down
2 changes: 2 additions & 0 deletions src/main/rate-limits/service-cursor-usage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ vi.mock('./opencode-go-usage-source-selection', () => ({ fetchOpenCodeGoUsage: v
vi.mock('./minimax/minimax-fetcher', () => ({ fetchMiniMaxRateLimits: vi.fn() }))
vi.mock('./grok-fetcher', () => ({ fetchGrokRateLimits: vi.fn() }))
vi.mock('./grok-auth', () => ({ readGrokAuthSession: vi.fn(() => ({ status: 'missing' })) }))
vi.mock('./zcode-usage-fetcher', () => ({ fetchZcodeRateLimits: vi.fn() }))

vi.mock('./cursor-fetcher', () => ({ fetchCursorRateLimits: vi.fn() }))
vi.mock('./cursor-auth', () => ({ readCursorAuthSession: vi.fn() }))
vi.mock('../minimax/minimax-cookie-store', () => ({ hasMiniMaxSessionCookie: vi.fn(() => false) }))
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,10 @@ vi.mock('./opencode-go-usage-source-selection', () => ({
fetchOpenCodeGoUsage: vi.fn()
}))

vi.mock('./zcode-usage-fetcher', () => ({
fetchZcodeRateLimits: vi.fn()
}))

vi.mock('./minimax/minimax-fetcher', () => ({
fetchMiniMaxRateLimits: vi.fn()
}))
Expand Down
4 changes: 4 additions & 0 deletions src/main/rate-limits/service-live-claude-usage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ vi.mock('./opencode-go-usage-source-selection', () => ({
fetchOpenCodeGoUsage: vi.fn()
}))

vi.mock('./zcode-usage-fetcher', () => ({
fetchZcodeRateLimits: vi.fn()
}))

vi.mock('./minimax/minimax-fetcher', () => ({
fetchMiniMaxRateLimits: vi.fn()
}))
Expand Down
4 changes: 4 additions & 0 deletions src/main/rate-limits/service-minimax-usage.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ vi.mock('./opencode-go-usage-source-selection', () => ({
fetchOpenCodeGoUsage: vi.fn()
}))

vi.mock('./zcode-usage-fetcher', () => ({
fetchZcodeRateLimits: vi.fn()
}))

vi.mock('./minimax/minimax-fetcher', () => ({
fetchMiniMaxRateLimits: vi.fn()
}))
Expand Down
56 changes: 56 additions & 0 deletions src/main/rate-limits/service-refresh-orchestration.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { fetchGeminiRateLimits } from './gemini-usage-fetcher'
import { fetchKimiRateLimits } from './kimi-fetcher'
import { fetchMiniMaxRateLimits } from './minimax/minimax-fetcher'
import { fetchGrokRateLimits } from './grok-fetcher'
import { fetchZcodeRateLimits } from './zcode-usage-fetcher'
import { readGrokAuthSession } from './grok-auth'
import { fetchOpenCodeGoUsage } from './opencode-go-usage-source-selection'
import {
Expand Down Expand Up @@ -40,6 +41,10 @@ vi.mock('./opencode-go-usage-source-selection', () => ({
fetchOpenCodeGoUsage: vi.fn()
}))

vi.mock('./zcode-usage-fetcher', () => ({
fetchZcodeRateLimits: vi.fn()
}))

vi.mock('./minimax/minimax-fetcher', () => ({
fetchMiniMaxRateLimits: vi.fn()
}))
Expand Down Expand Up @@ -73,6 +78,57 @@ describe('RateLimitService', () => {
resetRateLimitProviderMocks()
})

it('publishes a ZCode quota snapshot alongside the other providers', async () => {
vi.mocked(fetchClaudeRateLimits).mockResolvedValue(okProvider('claude', 7))
vi.mocked(fetchCodexRateLimits).mockResolvedValue(okProvider('codex', 8))
vi.mocked(fetchZcodeRateLimits).mockResolvedValue(okProvider('zcode', 42))
const service = new RateLimitService()

await service.refresh()

expect(fetchZcodeRateLimits).toHaveBeenCalledTimes(1)
expect(service.getState().zcode?.session?.usedPercent).toBe(42)
expect(service.getState().codex?.session?.usedPercent).toBe(8)
})

it('does not keep a previous ZCode account quota after a failed account switch', async () => {
vi.mocked(fetchZcodeRateLimits)
.mockResolvedValueOnce({
...okProvider('zcode', 42),
usageMetadata: { source: 'web', authProvenance: 'account-a' }
})
.mockResolvedValueOnce({
...errorProvider('zcode', 'request failed'),
usageMetadata: { source: 'web', authProvenance: 'account-b', failureKind: 'network' }
})
const service = new RateLimitService()

await service.refresh()
await service.refresh()

expect(service.getState().zcode?.status).toBe('error')
expect(service.getState().zcode?.session).toBeNull()
})

it('keeps a recent ZCode quota after a failed retry for the same account', async () => {
vi.mocked(fetchZcodeRateLimits)
.mockResolvedValueOnce({
...okProvider('zcode', 42),
usageMetadata: { source: 'web', authProvenance: 'account-a' }
})
.mockResolvedValueOnce({
...errorProvider('zcode', 'request failed'),
usageMetadata: { source: 'web', authProvenance: 'account-a', failureKind: 'network' }
})
const service = new RateLimitService()

await service.refresh()
await service.refresh()

expect(service.getState().zcode?.status).toBe('error')
expect(service.getState().zcode?.session?.usedPercent).toBe(42)
})

it('does not reread Grok auth when callers read state snapshots', () => {
vi.mocked(readGrokAuthSession).mockReturnValue({
status: 'ok',
Expand Down
4 changes: 4 additions & 0 deletions src/main/rate-limits/service-window-activation.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,10 @@ vi.mock('./opencode-go-usage-source-selection', () => ({
fetchOpenCodeGoUsage: vi.fn()
}))

vi.mock('./zcode-usage-fetcher', () => ({
fetchZcodeRateLimits: vi.fn()
}))

vi.mock('./minimax/minimax-fetcher', () => ({
fetchMiniMaxRateLimits: vi.fn()
}))
Expand Down
23 changes: 20 additions & 3 deletions src/main/rate-limits/service/service-full-cycle-application.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,8 @@ export abstract class RateLimitServiceFullCycleApplication extends RateLimitServ
miniMaxResult
],
grokResultPromise,
cursorResultPromise
cursorResultPromise,
zcodeResultPromise
} = prepared
if (signal.aborted) {
return
Expand Down Expand Up @@ -193,12 +194,17 @@ export abstract class RateLimitServiceFullCycleApplication extends RateLimitServ
: this.state.minimax
})

const [grokSettled, cursorSettled] = await Promise.all([grokResultPromise, cursorResultPromise])
const [grokSettled, cursorSettled, zcodeSettled] = await Promise.all([
grokResultPromise,
cursorResultPromise,
zcodeResultPromise
])
if (signal.aborted) {
return
}
const grok = settleSiblingProviderResult('grok', grokSettled)
const cursor = settleSiblingProviderResult('cursor', cursorSettled)
const zcode = settleSiblingProviderResult('zcode', zcodeSettled)
// Why: the stale policy keeps a recent snapshot through a failed refresh, but
// a snapshot belonging to a different Cursor account must not survive the
// switch — the Accounts pane would name the new account beside the old
Expand All @@ -210,12 +216,23 @@ export abstract class RateLimitServiceFullCycleApplication extends RateLimitServ
previousCursorAccount !== undefined &&
cursorAccount !== undefined &&
previousCursorAccount !== cursorAccount
const previousZcodeAccount = previousState.zcode?.usageMetadata?.authProvenance
const zcodeAccount = zcode.usageMetadata?.authProvenance
const sameZcodeAccount =
previousZcodeAccount !== undefined &&
zcodeAccount !== undefined &&
previousZcodeAccount === zcodeAccount
this.trackActiveFailureStreak('grok', grok)
this.trackActiveFailureStreak('cursor', cursor)
this.trackActiveFailureStreak('zcode', zcode)
this.updateState({
...this.state,
grok: this.applyStalePolicy(grok, previousState.grok),
cursor: cursorAccountChanged ? cursor : this.applyStalePolicy(cursor, previousState.cursor)
cursor: cursorAccountChanged ? cursor : this.applyStalePolicy(cursor, previousState.cursor),
zcode:
zcode.status === 'error' && !sameZcodeAccount
? zcode
: this.applyStalePolicy(zcode, previousState.zcode)
})
}
}
13 changes: 11 additions & 2 deletions src/main/rate-limits/service/service-full-cycle-preparation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { fetchGrokRateLimits } from '../grok-fetcher'
import { readGrokAuthSession } from '../grok-auth'
import { fetchCursorRateLimits } from '../cursor-fetcher'
import { readCursorAuthSession } from '../cursor-auth'
import { fetchZcodeRateLimits } from '../zcode-usage-fetcher'
import { fetchMiniMaxRateLimits } from '../minimax/minimax-fetcher'
import { createHash } from 'node:crypto'
import { fetchOpenCodeGoUsage } from '../opencode-go-usage-source-selection'
Expand Down Expand Up @@ -44,6 +45,7 @@ export type FetchAllCyclePrepared = {
]
grokResultPromise: Promise<SettledProviderResult>
cursorResultPromise: Promise<SettledProviderResult>
zcodeResultPromise: Promise<SettledProviderResult>
}

export abstract class RateLimitServiceFullCyclePreparation extends RateLimitServiceFetchPolicy {
Expand Down Expand Up @@ -130,7 +132,8 @@ export abstract class RateLimitServiceFullCyclePreparation extends RateLimitServ
? this.withFetchingStatus(null, 'minimax')
: this.withFetchingStatus(previousState.minimax, 'minimax'),
grok: this.withFetchingStatus(previousState.grok, 'grok'),
cursor: this.withFetchingStatus(previousState.cursor, 'cursor')
cursor: this.withFetchingStatus(previousState.cursor, 'cursor'),
zcode: this.withFetchingStatus(previousState.zcode, 'zcode')
})

// Why: the Cursor probe reads the macOS Keychain, so it is awaited inside the
Expand All @@ -145,6 +148,11 @@ export abstract class RateLimitServiceFullCyclePreparation extends RateLimitServ
(reason) => ({ status: 'rejected', reason }) as const
)

const zcodeResultPromise = fetchZcodeRateLimits({ signal }).then(
(value) => ({ status: 'fulfilled', value }) as const,
(reason) => ({ status: 'rejected', reason }) as const
)

const missingWslCodexHome =
codexFetchGated || codexHomePath ? null : this.getMissingWslCodexHomeResult(codexTarget)
const grokResultPromise = fetchGrokRateLimits({
Expand Down Expand Up @@ -231,7 +239,8 @@ export abstract class RateLimitServiceFullCyclePreparation extends RateLimitServ
miniMaxResult
],
grokResultPromise,
cursorResultPromise
cursorResultPromise,
zcodeResultPromise
}
}
}
3 changes: 2 additions & 1 deletion src/main/rate-limits/service/service-polling.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,8 @@ export abstract class RateLimitServicePolling extends RateLimitServiceFetchQueue
minimax: this.state.minimax,
grok: this.state.grok,
antigravity: this.state.antigravity,
cursor: this.state.cursor
cursor: this.state.cursor,
zcode: this.state.zcode
}
return Object.entries(byProvider).map(([provider, limits]) => ({
provider: provider as ActiveRateLimitProvider,
Expand Down
9 changes: 6 additions & 3 deletions src/main/rate-limits/service/service-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ export abstract class RateLimitServiceState {
antigravity: null,
minimax: null,
grok: null,
cursor: null
cursor: null,
zcode: null
}
protected grokAuthConfigured = readGrokAuthSession().status === 'ok'
// Why: the Cursor probe reads the macOS Keychain, so it cannot run synchronously
Expand All @@ -52,7 +53,8 @@ export abstract class RateLimitServiceState {
minimax: 0,
grok: 0,
antigravity: 0,
cursor: 0
cursor: 0,
zcode: 0
}
// Why: consecutive failures drive exponential backoff of the fast activation-retry lane; reset on any success/unavailable result.
protected activeFailureStreakByProvider: Record<ActiveRateLimitProvider, number> = {
Expand All @@ -64,7 +66,8 @@ export abstract class RateLimitServiceState {
minimax: 0,
grok: 0,
antigravity: 0,
cursor: 0
cursor: 0,
zcode: 0
}
protected mainWindow: BrowserWindow | null = null
protected detachWindowListeners: (() => void) | null = null
Expand Down
1 change: 1 addition & 0 deletions src/main/rate-limits/service/service-types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -110,6 +110,7 @@ export type InternalRateLimitState = {
minimax: ProviderRateLimits | null
grok: ProviderRateLimits | null
cursor: ProviderRateLimits | null
zcode: ProviderRateLimits | null
}

export function normalizePollingInterval(ms: number): number {
Expand Down
Loading
Loading