diff --git a/src/web-server/usage/native-quota-collector.ts b/src/web-server/usage/native-quota-collector.ts index 65b20820..3d4517e4 100644 --- a/src/web-server/usage/native-quota-collector.ts +++ b/src/web-server/usage/native-quota-collector.ts @@ -669,6 +669,26 @@ function markDefault(row: BarSummaryRow, isDefault: boolean): BarSummaryRow { return { ...row, is_default: isDefault }; } +/** + * Tag the row with is_default AND write the flag back onto the cached copy. The + * collector caches a row before the default profile is known (the default is + * resolved in the enumerator), so without this the cache-fallback path + * (getCachedNativeAccountRows) would serve the default account with + * is_default:false and the UI would stop ordering/tagging it as the default. + */ +function markDefaultAndSyncCache( + map: Map, + profile: string, + row: BarSummaryRow, + isDefault: boolean +): BarSummaryRow { + const state = map.get(profile); + if (state?.cachedRow) { + state.cachedRow = { ...state.cachedRow, is_default: isDefault }; + } + return markDefault(row, isDefault); +} + async function collectClaudeRowForProfile( profile: string, deps: NativeQuotaDeps, @@ -1453,7 +1473,7 @@ async function getNativeAccountRowsMultiProfile( const isDefault = p === claudeDefault; tasks.push( collectClaudeRowForProfile(p, deps, force) - .then((r) => (r ? markDefault(r, isDefault) : null)) + .then((r) => (r ? markDefaultAndSyncCache(claudeProfileStates, p, r, isDefault) : null)) .catch(() => null) ); } @@ -1462,7 +1482,7 @@ async function getNativeAccountRowsMultiProfile( const isDefault = p === codexDefault; tasks.push( collectCodexRowForProfile(p, deps, force) - .then((r) => (r ? markDefault(r, isDefault) : null)) + .then((r) => (r ? markDefaultAndSyncCache(codexProfileStates, p, r, isDefault) : null)) .catch(() => null) ); } diff --git a/tests/unit/web-server/native-quota-collector.test.ts b/tests/unit/web-server/native-quota-collector.test.ts index 3022f478..2700e3bf 100644 --- a/tests/unit/web-server/native-quota-collector.test.ts +++ b/tests/unit/web-server/native-quota-collector.test.ts @@ -1306,4 +1306,32 @@ describe('review focus areas: reauth caching + codex local fallback', () => { expect(localCalls).toBe(1); expect(def?.quotaStatus).not.toBe('unsupported'); }); + + it('cache-fallback rows keep is_default for the default profile', async () => { + resetNativeQuotaState(); + const clock = { now: 6_000_000 }; + const deps = makeMultiProfileDeps({ + clock, + claudeProfiles: ['work', 'ck'], + codexProfiles: ['default', 'ck'], + claudeDefault: 'work', + codexDefault: 'default', + credsForProfile: () => maxCreds(), + claudeFetch: async () => successQuota(), + codexNativeAuth: (p) => ({ accessToken: `t-${p}`, accountId: `id-${p}` }), + codexNetworkFetch: async () => codexSuccessQuota(), + }); + + await getNativeAccountRows(deps); // populate the per-profile caches + + // The cache-fallback path must preserve is_default so the UI still orders and + // tags the default account when /summary serves from cache. + const cached = getCachedNativeAccountRows(); + expect(cached.find((r) => r.surface === 'ccs' && r.profile === 'work')?.is_default).toBe(true); + expect(cached.find((r) => r.surface === 'ccsx' && r.profile === 'default')?.is_default).toBe( + true + ); + expect(cached.find((r) => r.surface === 'ccs' && r.profile === 'ck')?.is_default).toBe(false); + expect(cached.find((r) => r.surface === 'ccsx' && r.profile === 'ck')?.is_default).toBe(false); + }); });