fix(codex): cache weekly-only accounts when switching Codex accounts (#10467)

* fix(codex): cache weekly-only accounts when switching Codex accounts

refreshForCodexAccountChange snapshotted the outgoing account only when
this.state.codex.session was populated. Weekly-only plans report no session
window, so their snapshot was dropped and the account switcher's inline bars
rendered empty for exactly those accounts.

Accept a populated weekly window as well. #10136 made this reachable: before
duration-based classification, a weekly-only quota landed in the session slot,
so the gate happened to pass.

Claude is intentionally untouched; it has no weekly-only plan shape.

* test(rate-limits): pin that a windowless outgoing Codex account is not cached

The widened weekly-only gate had no test for its lower bound: replacing it with a bare truthy check on state.codex passed all 70 tests, which would cache an empty fetching placeholder and render a blank inline bar row in the switcher.
This commit is contained in:
Brennan Benson
2026-07-26 15:52:03 -07:00
committed by GitHub
parent 0b0e42ab60
commit fd7ebe3f47
2 changed files with 57 additions and 1 deletions
+54
View File
@@ -1751,6 +1751,60 @@ describe('RateLimitService', () => {
)
})
it('caches an outgoing weekly-only Codex account so the switcher keeps its inline bars', async () => {
const service = new RateLimitService()
service.setInactiveCodexAccountsResolver(() => [
{ id: 'account-weekly', managedHomePath: '/tmp/account-weekly/home' }
])
const weeklyOnly: ProviderRateLimits = {
provider: 'codex',
session: null,
weekly: { usedPercent: 76, windowMinutes: 10080, resetsAt: null, resetDescription: null },
updatedAt: Date.now(),
error: null,
status: 'ok'
}
vi.mocked(fetchClaudeRateLimits).mockResolvedValueOnce(okProvider('claude', 10, Date.now()))
vi.mocked(fetchCodexRateLimits)
.mockResolvedValueOnce(weeklyOnly)
.mockResolvedValueOnce(okProvider('codex', 40, Date.now()))
await service.refresh()
await service.refreshForCodexAccountChange('account-weekly')
expect(service.getState().inactiveCodexAccounts).toEqual(
expect.arrayContaining([
expect.objectContaining({
accountId: 'account-weekly',
rateLimits: expect.objectContaining({
session: null,
weekly: expect.objectContaining({ usedPercent: 76 })
})
})
])
)
})
it('does not cache an outgoing Codex account that has no usage windows', async () => {
const service = new RateLimitService()
service.setInactiveCodexAccountsResolver(() => [
{ id: 'account-empty', managedHomePath: '/tmp/account-empty/home' }
])
vi.mocked(fetchClaudeRateLimits).mockResolvedValueOnce(okProvider('claude', 10, Date.now()))
vi.mocked(fetchCodexRateLimits)
.mockResolvedValueOnce(errorProvider('codex', 'codex not signed in'))
.mockResolvedValueOnce(okProvider('codex', 40, Date.now()))
await service.refresh()
await service.refreshForCodexAccountChange('account-empty')
expect(service.getState().inactiveCodexAccounts).not.toEqual(
expect.arrayContaining([expect.objectContaining({ accountId: 'account-empty' })])
)
})
it('does not cache host Claude usage under an outgoing WSL account', async () => {
const service = new RateLimitService()
service.setInactiveClaudeAccountsResolver(() => [
+3 -1
View File
@@ -378,9 +378,11 @@ export class RateLimitService {
target?: CodexAccountSelectionTarget
): Promise<RateLimitState> {
const nextTarget = normalizeCodexAccountSelectionTarget(target)
// Why: weekly-only plans report no session window, so gating on session alone
// dropped their snapshot and left the switcher's inline bars empty.
if (
outgoingAccountId &&
this.state.codex?.session &&
(this.state.codex?.session || this.state.codex?.weekly) &&
this.isSameCodexTarget(this.codexFetchTarget, nextTarget)
) {
this.inactiveCodexCache.set(outgoingAccountId, this.state.codex)