fix(bar): stop false re-auth on non-default native subscription profiles

Fixes the two collector-side root causes of #1601:

1. Claude per-profile credential reads were file-only, but on macOS Claude
   Code stores the OAuth token for an isolated CLAUDE_CONFIG_DIR in a
   per-directory Keychain item ("Claude Code-credentials-<sha256(dir)[0..8]>").
   The .credentials.json file never exists, so every isolated profile was
   parked with needsReauth:true forever. The reader now falls back to that
   Keychain item (file-first, same security-CLI read the shipped global
   fallback already performs; TTL-gated so it is not on every /summary).

2. Non-default profiles were cache-only forever, so they could never leave
   the parked state even with valid credentials. getNativeAccountRows now
   gives each surface ONE rotating live slot: the stalest eligible
   non-default profile is refreshed per pass, skipping profiles inside
   breaker/reauth cooldowns. Every account converges to real quota within a
   few polls while the per-pass upstream budget stays constant (<= 2 calls
   per surface regardless of profile count). Codex named profiles with valid
   auth but sparse payloads now yield an active quota-less row instead of a
   false needsReauth row.

Non-default rows keep paused:true (dimmed) even when freshly refreshed so
only the default renders active and rows do not flicker between polls.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
poomscandClaude Fable 5 committed 2026-08-08 14:18:00 +07:00
1 parent 7fb56d62e1
commit a5a5b742e4
6 files changed
+401 -67

No files matched your search

+3 -3
View File
@@ -1,9 +1,9 @@
{
"scope": "src/**/*.{ts,tsx,js,jsx,mjs,cjs}",
"syncFs": {
"totalOccurrences": 2427,
"totalOccurrences": 2426,
"filesAffected": 258,
"hotpathOccurrences": 1142,
"hotpathOccurrences": 1141,
"hotpathFilesAffected": 152,
"topHotpathFiles": [
{
@@ -725,7 +725,7 @@
"topOver400": [
{
"file": "src/web-server/usage/native-quota-collector.ts",
"loc": 1662
"loc": 1730
},
{
"file": "src/web-server/routes/cliproxy-auth-routes.ts",
+3 -3
View File
@@ -6,9 +6,9 @@ Scope: `src/**/*.{ts,tsx,js,jsx,mjs,cjs}`
| Metric | Value |
|---|---:|
| Sync fs occurrences (all) | 2427 |
| Sync fs occurrences (all) | 2426 |
| Sync fs files affected (all) | 258 |
| Sync fs occurrences (runtime hotpaths) | 1142 |
| Sync fs occurrences (runtime hotpaths) | 1141 |
| Sync fs files affected (runtime hotpaths) | 152 |
| Legacy shim markers | 458 |
| Legacy shim files affected | 173 |
@@ -89,7 +89,7 @@ Scope: `src/**/*.{ts,tsx,js,jsx,mjs,cjs}`
| File | LOC |
|---|---:|
| `src/web-server/usage/native-quota-collector.ts` | 1662 |
| `src/web-server/usage/native-quota-collector.ts` | 1730 |
| `src/web-server/routes/cliproxy-auth-routes.ts` | 1531 |
| `src/cliproxy/auth/oauth-handler.ts` | 1510 |
| `src/cursor/cursor-executor.ts` | 1234 |
@@ -16,6 +16,7 @@
import { existsSync, readFileSync } from 'node:fs';
import { execSync } from 'node:child_process';
import * as crypto from 'node:crypto';
import * as os from 'node:os';
import * as path from 'node:path';
@@ -87,20 +88,77 @@ export function readClaudeCredentials(
}
if (platform === 'darwin') {
try {
const out = execImpl(`security find-generic-password -s "${KEYCHAIN_SERVICE}" -w`, {
timeout: KEYCHAIN_TIMEOUT_MS,
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'ignore'],
});
const raw = (typeof out === 'string' ? out : out.toString('utf8')).trim();
if (raw) {
const parsed = parseCredentials(raw);
if (parsed) return parsed;
}
} catch {
// no Keychain entry / access denied -> null
const parsed = readCredentialsFromKeychainService(KEYCHAIN_SERVICE, execImpl);
if (parsed) return parsed;
}
return null;
}
/** Read + parse one Keychain generic-password item. Returns null on any failure. */
function readCredentialsFromKeychainService(
service: string,
execImpl: NonNullable<CredentialReaderDeps['execSyncImpl']>
): ClaudeNativeCredentials | null {
try {
const out = execImpl(`security find-generic-password -s "${service}" -w`, {
timeout: KEYCHAIN_TIMEOUT_MS,
encoding: 'utf8',
stdio: ['pipe', 'pipe', 'ignore'],
});
const raw = (typeof out === 'string' ? out : out.toString('utf8')).trim();
if (raw) {
const parsed = parseCredentials(raw);
if (parsed) return parsed;
}
} catch {
// no Keychain entry / access denied -> null
}
return null;
}
/**
* Keychain service name Claude Code uses for a non-default CLAUDE_CONFIG_DIR:
* "Claude Code-credentials-<first 8 hex chars of sha256(configDir)>".
*/
export function claudeKeychainServiceForConfigDir(configDir: string): string {
const hash = crypto.createHash('sha256').update(configDir).digest('hex').slice(0, 8);
return `${KEYCHAIN_SERVICE}-${hash}`;
}
/**
* Read the Claude Code credentials for a specific CLAUDE_CONFIG_DIR (e.g. an
* isolated `ccs auth` instance directory).
*
* File-first (<configDir>/.credentials.json, no prompt), then the macOS
* Keychain item derived from the config dir path. On macOS Claude Code stores
* OAuth tokens in the Keychain by default, so without the Keychain fallback
* every isolated profile looks permanently logged-out to the bar.
*/
export function readClaudeCredentialsForConfigDir(
configDir: string,
deps: CredentialReaderDeps = {}
): ClaudeNativeCredentials | null {
const platform = deps.platform ?? os.platform();
const existsImpl = deps.existsSyncImpl ?? existsSync;
const readImpl = deps.readFileSyncImpl ?? ((p: string) => readFileSync(p, 'utf8'));
const execImpl = deps.execSyncImpl ?? execSync;
const credentialsPath = path.join(configDir, '.credentials.json');
if (existsImpl(credentialsPath)) {
try {
const parsed = parseCredentials(readImpl(credentialsPath));
if (parsed) return parsed;
} catch {
// fall through to Keychain
}
}
if (platform === 'darwin') {
return readCredentialsFromKeychainService(
claudeKeychainServiceForConfigDir(configDir),
execImpl
);
}
return null;
+100 -32
View File
@@ -14,9 +14,9 @@
* - circuit breaker stops calling after repeated 429s for a cooldown
* - serve-stale-on-failure; only omit a row when there is genuinely no data
*
* Claude path: reads per-profile .credentials.json (file-only, NO keychain)
* and polls api.anthropic.com/api/oauth/usage. If the file is absent the
* profile is emitted as a parked row (paused:true) — never a keychain call.
* Claude path: reads per-profile .credentials.json, then the per-config-dir
* Keychain item, and polls api.anthropic.com/api/oauth/usage. If neither source
* yields credentials the profile is emitted as a parked row (paused:true).
*
* Codex path: PRIMARY = live network (chatgpt.com/backend-api/wham/usage, via
* fetchCodexQuota), FALLBACK = local session logs (getCodexLocalQuota), mirroring
@@ -29,13 +29,15 @@
* is maintained — at most 2 live upstream calls per /summary regardless of
* profile count.
*
* NO macOS Keychain access anywhere in this module. The old global-default
* Claude reader (readClaudeCredentials) is kept for back-compat but is no longer
* used by the multi-profile path.
* Claude per-profile reads are file-first with a per-config-dir macOS Keychain
* fallback (Claude Code stores OAuth tokens in the Keychain by default on
* macOS). The old global-default Claude reader (readClaudeCredentials) is kept
* for back-compat but is no longer used by the multi-profile path.
*/
import {
readClaudeCredentials,
readClaudeCredentialsForConfigDir,
getAccessToken,
getSubscriptionTier,
hasSupportedSubscription,
@@ -110,7 +112,7 @@ export interface NativeQuotaDeps {
/** Read the native Claude Code credentials (global default path). */
readCredentials?: () => ClaudeNativeCredentials | null;
/**
* Read credentials for a specific Claude profile (file-only, no keychain).
* Read credentials for a specific Claude profile (file-first, Keychain fallback).
* Injected so tests never touch real fs or Keychain.
* profile: the profile name (e.g. "work"); returns null when absent/unparseable.
*/
@@ -487,11 +489,11 @@ function serveCached(state: ProviderState): BarSummaryRow | null {
}
// ============================================================================
// File-only Claude credentials reader for per-profile paths (NO keychain)
// Per-profile Claude credentials reader (file-first, Keychain fallback)
// ============================================================================
/**
* Read credentials for a specific Claude Code profile (file-only, no keychain).
* Read credentials for a specific Claude Code profile (file-first, Keychain fallback).
*
* Looks for .credentials.json in the profile's instance directory. If the file
* is absent or unparseable, returns null — the caller emits a parked row.
@@ -503,27 +505,20 @@ function readClaudeCredentialsForProfileFromDisk(
): ClaudeNativeCredentials | null {
try {
const instanceDir = path.join(getCcsDir(), 'instances', profile);
const credFile = path.join(instanceDir, '.credentials.json');
if (fs.existsSync(credFile)) {
const raw = fs.readFileSync(credFile, 'utf8');
const parsed = JSON.parse(raw) as unknown;
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
return parsed as ClaudeNativeCredentials;
}
return null;
}
// The bare `ccs` default login uses the standard global credential lookup:
// ~/.claude/.credentials.json, falling back to the single global
// "Claude Code-credentials" Keychain item that Claude Code itself maintains.
// This is the ONE pre-existing global read the shipped Bar already performs --
// NOT a per-profile Keychain scan. Isolated `ccs auth` profiles stay
// file-only and never touch the Keychain; a real instance directory named
// "default" is therefore parked when its file is absent.
if (profile === DEFAULT_PROFILE && !fs.existsSync(instanceDir)) {
return readDefaultCredentials();
}
return null;
// Isolated `ccs auth` instance: <instanceDir>/.credentials.json first, then
// the per-config-dir Keychain item Claude Code maintains for this
// CLAUDE_CONFIG_DIR ("Claude Code-credentials-<sha256(dir)[0..8]>"). On
// macOS Claude Code stores tokens in the Keychain by default, so without
// the Keychain read every isolated profile is permanently parked.
return readClaudeCredentialsForConfigDir(instanceDir);
} catch {
return null;
}
@@ -787,6 +782,56 @@ function markDefault(row: BarSummaryRow, isDefault: boolean): BarSummaryRow {
return { ...row, is_default: isDefault };
}
/**
* Force paused:true on a non-default row AND its cached copy. Non-default rows
* always render dimmed (only the default profile is "active"), including the
* pass where the rotating live slot refreshed them — otherwise the row would
* flicker active for one poll and dim again on the next.
*/
function markPausedAndSyncCache(
map: Map<string, ProviderState>,
profile: string,
row: BarSummaryRow
): BarSummaryRow {
const state = map.get(profile);
if (state?.cachedRow) {
state.cachedRow = { ...state.cachedRow, paused: true };
}
return { ...row, paused: true };
}
/**
* Pick the non-default profile the rotating live slot should refresh this pass:
* the stalest profile whose cached row is missing or past its TTL, skipping
* profiles inside a breaker/cooldown window (their collector would refuse the
* fetch anyway, wasting the slot). Returns null when every profile is fresh.
*/
function pickRotatingLiveProfile(
map: Map<string, ProviderState>,
profiles: string[],
defaultProfile: string | null,
now: number
): string | null {
let picked: string | null = null;
let pickedAt = Number.POSITIVE_INFINITY;
for (const p of profiles) {
if (p === defaultProfile) continue;
const state = map.get(p);
if (state && (now < state.breakerOpenUntil || now < state.cooldownUntil)) continue;
const cachedRow = state?.cachedRow ?? null;
const cachedAt = state?.cachedAt ?? 0;
if (cachedRow) {
const ttl = cachedRow.quotaStatus === 'unsupported' ? PARKED_TTL_MS : NATIVE_QUOTA_TTL_MS;
if (now - cachedAt < ttl) continue;
}
if (cachedAt < pickedAt) {
pickedAt = cachedAt;
picked = p;
}
}
return picked;
}
/**
* 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
@@ -835,7 +880,7 @@ async function collectClaudeRowForProfile(
return state.pending;
}
// For per-profile reads: use the injected seam (file-only, no keychain).
// For per-profile reads: use the injected seam (file-first, Keychain fallback).
const readDefaultCredentials = deps.readCredentials ?? readClaudeCredentials;
const readCreds =
deps.readClaudeCredentialsForProfile ??
@@ -1579,13 +1624,28 @@ async function getNativeAccountRowsMultiProfile(
const results: (BarSummaryRow | null)[] = [];
const now = (deps.now ?? Date.now)();
// Preserve the safety budget: only the active/default profile for each surface
// may perform a live refresh. Non-default profiles are cache-only (or parked)
// so one /summary request can trigger at most one Claude and one Codex live
// upstream call regardless of configured profile count.
// Preserve the safety budget: the active/default profile for each surface is
// live-polled every pass, plus ONE rotating live slot for the stalest
// non-default profile. All other profiles are cache-only (or parked), so one
// /summary request triggers at most two Claude and two Codex live upstream
// calls regardless of configured profile count — every account converges to
// real quota within a few polls without per-profile fan-out.
const claudeRotating = pickRotatingLiveProfile(
claudeProfileStates,
claudeProfiles,
claudeDefault,
now
);
const codexRotating = pickRotatingLiveProfile(
codexProfileStates,
codexProfiles,
codexDefault,
now
);
for (const p of claudeProfiles) {
const isDefault = p === claudeDefault;
if (!isDefault) {
if (!isDefault && p !== claudeRotating) {
results.push(
markDefaultAndSyncCache(
claudeProfileStates,
@@ -1602,14 +1662,18 @@ async function getNativeAccountRowsMultiProfile(
deps,
force || claudeProfileStates.get(p)?.cachedRow?.quotaStatus === 'unsupported'
)
.then((r) => (r ? markDefaultAndSyncCache(claudeProfileStates, p, r, true) : null))
.then((r) => {
if (!r) return null;
const marked = markDefaultAndSyncCache(claudeProfileStates, p, r, isDefault);
return isDefault ? marked : markPausedAndSyncCache(claudeProfileStates, p, marked);
})
.catch(() => null)
);
}
for (const p of codexProfiles) {
const isDefault = p === codexDefault;
if (!isDefault) {
if (!isDefault && p !== codexRotating) {
results.push(
markDefaultAndSyncCache(
codexProfileStates,
@@ -1626,7 +1690,11 @@ async function getNativeAccountRowsMultiProfile(
deps,
force || codexProfileStates.get(p)?.cachedRow?.quotaStatus === 'unsupported'
)
.then((r) => (r ? markDefaultAndSyncCache(codexProfileStates, p, r, true) : null))
.then((r) => {
if (!r) return null;
const marked = markDefaultAndSyncCache(codexProfileStates, p, r, isDefault);
return isDefault ? marked : markPausedAndSyncCache(codexProfileStates, p, marked);
})
.catch(() => null)
);
}
@@ -8,6 +8,8 @@
import { describe, expect, it } from 'bun:test';
import {
readClaudeCredentials,
readClaudeCredentialsForConfigDir,
claudeKeychainServiceForConfigDir,
getAccessToken,
getSubscriptionTier,
hasSupportedSubscription,
@@ -124,3 +126,92 @@ describe('token + tier extraction', () => {
expect(getSubscriptionTier(null)).toBeNull();
});
});
// ---------------------------------------------------------------------------
// Per-config-dir credential reading (isolated `ccs auth` profiles on macOS)
//
// Claude Code stores OAuth credentials for a non-default CLAUDE_CONFIG_DIR in
// a per-directory Keychain item: service "Claude Code-credentials-<hash>",
// where <hash> is the first 8 hex chars of sha256(configDir). These tests pin
// that derivation and the file-first / Keychain-fallback read order.
// ---------------------------------------------------------------------------
describe('claudeKeychainServiceForConfigDir', () => {
it('derives the service name from sha256 of the config dir path (first 8 hex chars)', () => {
// sha256("/home/test/.ccs/instances/work") = ffeb4b45...
expect(claudeKeychainServiceForConfigDir('/home/test/.ccs/instances/work')).toBe(
'Claude Code-credentials-ffeb4b45'
);
});
});
describe('readClaudeCredentialsForConfigDir', () => {
const configDir = '/home/test/.ccs/instances/work';
const credFile = `${configDir}/.credentials.json`;
it('reads <configDir>/.credentials.json when present (file-first, no Keychain)', () => {
let keychainCalled = false;
const creds = readClaudeCredentialsForConfigDir(configDir, {
platform: 'darwin',
existsSyncImpl: (p: string) => p === credFile,
readFileSyncImpl: (p: string) => {
expect(p).toBe(credFile);
return JSON.stringify(makeCreds());
},
execSyncImpl: () => {
keychainCalled = true;
return '';
},
});
expect(creds?.claudeAiOauth?.accessToken).toBe('tok-abc');
expect(keychainCalled).toBe(false);
});
it('falls back to the per-config-dir Keychain service when the file is absent', () => {
let keychainCmd = '';
const creds = readClaudeCredentialsForConfigDir(configDir, {
platform: 'darwin',
existsSyncImpl: () => false,
readFileSyncImpl: () => {
throw new Error('should not read file');
},
execSyncImpl: (cmd: string) => {
keychainCmd = cmd;
return JSON.stringify(makeCreds({ subscriptionType: 'team' }));
},
});
expect(creds?.claudeAiOauth?.subscriptionType).toBe('team');
expect(keychainCmd).toContain('Claude Code-credentials-ffeb4b45');
});
it('returns null when both file and Keychain are absent', () => {
const creds = readClaudeCredentialsForConfigDir(configDir, {
platform: 'darwin',
existsSyncImpl: () => false,
readFileSyncImpl: () => {
throw new Error('no file');
},
execSyncImpl: () => {
throw new Error('no keychain entry');
},
});
expect(creds).toBeNull();
});
it('does not consult the Keychain on non-darwin platforms', () => {
let keychainCalled = false;
const creds = readClaudeCredentialsForConfigDir(configDir, {
platform: 'linux',
existsSyncImpl: () => false,
readFileSyncImpl: () => {
throw new Error('no file');
},
execSyncImpl: () => {
keychainCalled = true;
return '';
},
});
expect(creds).toBeNull();
expect(keychainCalled).toBe(false);
});
});
@@ -1170,8 +1170,10 @@ describe('multi-profile: account_id and wire fields', () => {
const rows = await getNativeAccountRows(deps);
expect(rows.length).toBe(claudeProfiles.length + codexProfiles.length);
expect(deps.claudeFetchCount()).toBe(1);
expect(deps.codexNetworkCount()).toBe(1);
// Budget per pass: the default + one rotating non-default live slot per
// surface — constant regardless of profile count.
expect(deps.claudeFetchCount()).toBe(2);
expect(deps.codexNetworkCount()).toBe(2);
});
it('rows are sorted by (surface, profile)', async () => {
@@ -1396,8 +1398,9 @@ describe('multi-profile: per-profile circuit breaker isolation', () => {
const ckRow = rows.find((r) => r.profile === 'ck');
const workRow = rows.find((r) => r.profile === 'work');
// 'ck' stays cache-only, independent of work's breaker.
expect(ckRow?.quotaStatus).toBe('unsupported');
// 'ck' gets its own live row via the rotating slot, independent of work's
// breaker history.
expect(ckRow?.quotaStatus).toBe('ok');
// 'work' is also fine after reset (no breaker state).
expect(workRow?.quotaStatus).toBe('ok');
});
@@ -1431,10 +1434,11 @@ describe('multi-profile: per-profile circuit breaker isolation', () => {
},
});
// First call: 'work' gets a 429, 'ck' remains cache-only.
// First call: 'work' gets a 429; 'ck' is refreshed by the rotating slot and
// succeeds — work's failures do not leak into ck's state.
const rows1 = await getNativeAccountRows(deps);
const ck1 = rows1.find((r) => r.profile === 'ck');
expect(ck1?.quotaStatus).toBe('unsupported');
expect(ck1?.quotaStatus).toBe('ok');
expect(workCall429Count).toBeGreaterThanOrEqual(1);
// Skip past cooldown for 'work' only; 'ck' is within TTL.
@@ -1443,8 +1447,8 @@ describe('multi-profile: per-profile circuit breaker isolation', () => {
// Second call past 'work' cooldown: work tries again (429 again); ck cached.
const rows2 = await getNativeAccountRows(deps);
const ck2 = rows2.find((r) => r.profile === 'ck');
// 'ck' remains cache-only and is not affected by work's breaker.
expect(ck2?.quotaStatus).toBe('unsupported');
// 'ck' keeps its healthy cached row and is not affected by work's breaker.
expect(ck2?.quotaStatus).toBe('ok');
});
});
@@ -1537,15 +1541,18 @@ describe('review focus areas: reauth caching + codex local fallback', () => {
codexNetworkFetch: async () => ({ success: false, needsReauth: true }) as CodexQuotaResult,
});
// The rotating slot polls 'ck' once; the 401 parks it into the reauth
// cooldown.
const first = await getNativeAccountRows(deps);
const r1 = first.find((r) => r.profile === 'ck');
expect(r1?.needsReauth).toBe(true);
expect(r1?.paused).toBe(true);
expect(deps.codexNetworkCount()).toBe(0);
expect(deps.codexNetworkCount()).toBe(1);
// Within the cooldown it is NOT re-polled (no repeated 401s), even forced.
const second = await getNativeAccountRows(deps, { force: true });
expect(second.find((r) => r.profile === 'ck')?.cached).toBe(true);
expect(deps.codexNetworkCount()).toBe(0);
expect(deps.codexNetworkCount()).toBe(1);
});
it('Codex named profile without on-disk auth is parked, never filled from global local data', async () => {
@@ -1655,7 +1662,7 @@ describe('review focus areas: reauth caching + codex local fallback', () => {
expect(deps.claudeFetchCount()).toBe(1); // re-checked -> fetched
});
it('Codex named profile with valid auth but sparse payload stays parked when not default', async () => {
it('Codex named profile with valid auth but sparse payload yields an active quota-less row', async () => {
resetNativeQuotaState();
const clock = { now: 7_000_000 };
const deps = makeMultiProfileDeps({
@@ -1668,13 +1675,15 @@ describe('review focus areas: reauth caching + codex local fallback', () => {
codexNetworkFetch: async () => ({ success: true }) as CodexQuotaResult,
});
// The rotating slot polls 'ck'; the token authenticated, so it is a valid
// active subscription with a sparse payload — an active quota-less row,
// still dimmed because it is not the default profile.
const rows = await getNativeAccountRows(deps);
const ck = rows.find((r) => r.profile === 'ck');
expect(ck).toBeDefined();
expect(ck?.paused).toBe(true); // cache-only, parked until selected as default
expect(ck?.needsReauth).toBe(true);
expect(ck?.quotaStatus).toBe('unsupported');
expect(ck?.quota_percentage).toBeNull(); // no windows while parked
expect(ck?.paused).toBe(true); // non-default rows always render dimmed
expect(ck?.needsReauth).toBe(false);
expect(ck?.quota_percentage).toBeNull(); // no windows in the payload
});
it('non-default cache-only rows do not overwrite the canonical live cache', async () => {
@@ -1691,9 +1700,11 @@ describe('review focus areas: reauth caching + codex local fallback', () => {
});
deps.defaultCodexProfile = () => codexDefault;
// First pass: 'ck' (default) is live-polled and the rotating slot also
// refreshes 'default' — two upstream calls.
const first = await getNativeAccountRows(deps);
expect(first.find((r) => r.profile === 'ck')?.paused).toBe(false);
expect(deps.codexNetworkCount()).toBe(1);
expect(deps.codexNetworkCount()).toBe(2);
codexDefault = 'default';
const second = await getNativeAccountRows(deps);
@@ -1710,3 +1721,109 @@ describe('review focus areas: reauth caching + codex local fallback', () => {
expect(deps.codexNetworkCount()).toBe(2);
});
});
// ============================================================================
// Multi-profile: rotating live slot for non-default profiles
//
// Non-default profiles used to be cache-only forever, so accounts other than
// the default never showed real quota — they sat parked ("needs re-auth") for
// the lifetime of the server. One rotating live slot per surface refreshes the
// stalest eligible non-default profile per pass, so every account converges to
// real data within a few polls while the per-pass upstream budget stays
// constant (<= 2 calls per surface) regardless of profile count.
// ============================================================================
describe('multi-profile: rotating live slot for non-default profiles', () => {
it('live-polls the default plus one stale non-default Claude profile per pass', async () => {
const clock = { now: 1_000_000 };
const deps = makeMultiProfileDeps({
clock,
claudeProfiles: ['work', 'personal', 'fc'],
codexProfiles: [],
claudeDefault: 'work',
credsForProfile: () => maxCreds(),
claudeFetch: async () => successQuota(),
});
// Pass 1: default (work) + one stale non-default get live data.
let rows = await getNativeAccountRows(deps);
expect(deps.claudeFetchCount()).toBe(2);
expect(rows.filter((r) => r.quotaStatus === 'ok').length).toBe(2);
// Pass 2 after the parked TTL: the remaining profile gets its live row.
clock.now += 31_000;
rows = await getNativeAccountRows(deps);
expect(deps.claudeFetchCount()).toBe(3);
expect(rows.filter((r) => r.quotaStatus === 'ok').length).toBe(3);
// Pass 3 while everything is within TTL: fully cached, zero upstream calls.
clock.now += 1_000;
rows = await getNativeAccountRows(deps);
expect(deps.claudeFetchCount()).toBe(3);
expect(rows.filter((r) => r.quotaStatus === 'ok').length).toBe(3);
});
it('rotated non-default rows keep paused:true (only the default renders active)', async () => {
const clock = { now: 1_000_000 };
const deps = makeMultiProfileDeps({
clock,
claudeProfiles: ['work', 'personal'],
codexProfiles: [],
claudeDefault: 'work',
credsForProfile: () => maxCreds(),
claudeFetch: async () => successQuota(),
});
const rows = await getNativeAccountRows(deps);
const personal = rows.find((r) => r.profile === 'personal');
expect(personal?.quotaStatus).toBe('ok');
expect(personal?.needsReauth).toBe(false);
expect(personal?.paused).toBe(true);
expect(personal?.is_default).toBe(false);
});
it('codex non-default profiles also get one rotating live slot per pass', async () => {
const clock = { now: 1_000_000 };
const deps = makeMultiProfileDeps({
clock,
claudeProfiles: [],
codexProfiles: ['personal', 'ck'],
codexDefault: 'personal',
codexNativeAuth: (p) => ({ accessToken: `tok-${p}`, accountId: `id-${p}` }),
codexNetworkFetch: async () => codexSuccessQuota(),
});
const rows = await getNativeAccountRows(deps);
expect(deps.codexNetworkCount()).toBe(2);
const ck = rows.find((r) => r.profile === 'ck');
expect(ck?.paused).toBe(true);
expect(ck?.needsReauth).toBe(false);
});
it('profiles in reauth cooldown are skipped by the rotating slot', async () => {
const clock = { now: 1_000_000 };
let failProfile: string | null = 'personal';
const deps = makeMultiProfileDeps({
clock,
claudeProfiles: ['work', 'personal'],
codexProfiles: [],
claudeDefault: 'work',
credsForProfile: () => maxCreds(),
claudeFetch: async (_token: string, accountId?: string) =>
accountId === `ccs:${failProfile}`
? ({ success: false, needsReauth: true, retryable: false } as ClaudeQuotaResult)
: successQuota(),
});
// Pass 1: personal is rotated in, 401s, and enters the reauth cooldown.
let rows = await getNativeAccountRows(deps);
expect(rows.find((r) => r.profile === 'personal')?.needsReauth).toBe(true);
expect(deps.claudeFetchCount()).toBe(2);
// Pass 2 inside the cooldown: the slot must NOT re-poll (and re-401) it.
clock.now += 31_000;
rows = await getNativeAccountRows(deps);
expect(deps.claudeFetchCount()).toBe(2);
expect(rows.find((r) => r.profile === 'personal')?.needsReauth).toBe(true);
});
});