diff --git a/src/providers/quota.ts b/src/providers/quota.ts index 3335679e63..ca6411043f 100644 --- a/src/providers/quota.ts +++ b/src/providers/quota.ts @@ -1377,6 +1377,7 @@ async function fetchAnthropicQuota(provider: string): Promise { + hydrateAccountQuotaCache(); const key = accountCacheKey(provider, accountId); const writerGeneration = captureConfigGeneration(); const cached = accountQuotaCache.get(key); @@ -1641,6 +1653,7 @@ async function fetchAccountQuota( // superseded config generation must not publish either half. if (provider === "kiro") commitKiroAccountUsageState(key, kiroSnapshot); sweepExpiredOnWrite(entry.ts); + persistAccountQuotaCache(); } return entry; } catch { diff --git a/tests/provider-account-quota-persistence.test.ts b/tests/provider-account-quota-persistence.test.ts index fd98cf141f..5a9a4a02b7 100644 --- a/tests/provider-account-quota-persistence.test.ts +++ b/tests/provider-account-quota-persistence.test.ts @@ -7,6 +7,10 @@ import { readPersistedAccountQuotas, schedulePersistAccountQuotas, } from "../src/providers/account-quota-disk"; +import { + getCachedProviderAccountQuota, + resetProviderAccountQuotaDiskStateForTests, +} from "../src/providers/quota"; import type { ProviderQuota } from "../src/providers/quota-types"; const previousHome = process.env.OPENCODEX_HOME; @@ -22,6 +26,7 @@ beforeEach(() => { }); afterEach(() => { + resetProviderAccountQuotaDiskStateForTests(); cancelPendingAccountQuotaPersist(); if (previousHome === undefined) delete process.env.OPENCODEX_HOME; else process.env.OPENCODEX_HOME = previousHome; @@ -40,6 +45,14 @@ describe("provider account quota persistence", () => { expect(readPersistedAccountQuotas().get(KEY)?.monthlyPercent).toBe(15); }); + test("routing hydrates the runtime cache from a persisted snapshot", async () => { + schedulePersistAccountQuotas(() => [[KEY, quota(95)]]); + await settle(); + + resetProviderAccountQuotaDiskStateForTests(); + expect(getCachedProviderAccountQuota("kiro", "acct-a")?.monthlyPercent).toBe(95); + }); + test("a stale snapshot is discarded rather than ordering the pool on old data", async () => { schedulePersistAccountQuotas(() => [[KEY, quota(15, Date.now() - 7 * 60 * 60_000)]]); await settle(); diff --git a/tests/provider-account-quota.test.ts b/tests/provider-account-quota.test.ts index 2bb21f13a2..907dce70f1 100644 --- a/tests/provider-account-quota.test.ts +++ b/tests/provider-account-quota.test.ts @@ -3,6 +3,7 @@ import { mkdtempSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { saveCredential } from "../src/oauth/store"; +import { readPersistedAccountQuotas } from "../src/providers/account-quota-disk"; import type { OcxConfig } from "../src/types"; import { clearAccountQuotaCache, @@ -76,6 +77,10 @@ describe("fetchProviderAccountQuotas", () => { expect(seenTokens.sort()).toEqual(["Bearer token-first", "Bearer token-second"]); // The 5-hour window lands in the canonical fields, not in customWindows. for (const row of rows) expect(row.quota?.customWindows).toBeUndefined(); + + await new Promise(resolve => setTimeout(resolve, 400)); + const persisted = readPersistedAccountQuotas(); + expect(rows.map(row => persisted.get(`anthropic\u0000${row.accountId}`)?.fiveHourPercent).sort()).toEqual([3, 70]); }); test("a cached row is reused instead of re-probing upstream", async () => {