feat(bar): native Claude Code + Codex subscription quota (safe, server-side)

Surface the logged-in Claude Code and Codex subscription quota as first-class
summary rows so the existing gauge and alert engine render them with no new UI.

Claude Code: read the native token (~/.claude/.credentials.json, macOS Keychain
fallback) and reuse the existing Anthropic usage fetch+normalize via a new
token-fed entry point. The /oauth/usage endpoint is hostile to polling, so the
fetch is server-side only behind a 10-minute on-demand cache, in-flight
coalescing, Retry-After + exponential backoff with jitter, a 3-strike circuit
breaker with a 15-minute cooldown, and serve-stale-on-failure; logged-out or
unsupported subscriptions never spend a token call. Codex: zero-network read of
the latest rate_limits from local session rollouts, omitted when absent.
Native rows side-load bounded so a slow or failed fetch degrades to CLIProxy
rows, never an error.
This commit is contained in:
Tam Nhu Tran
2026-06-09 13:37:38 -04:00
parent 0fed2982f5
commit dbeb0c543a
9 changed files with 1481 additions and 21 deletions
+64 -20
View File
@@ -218,25 +218,19 @@ function buildEmptyResult(
}
/**
* Fetch quota for a single Claude account.
* Run the Anthropic OAuth usage fetch loop for a known-good access token.
*
* This is the single Anthropic-call surface shared by both the CLIProxy-managed
* path (fetchClaudeQuota) and the native-login path (fetchClaudeQuotaWithToken).
* It owns the 401/403/404/429/5xx branch logic, the bounded retry loop, and the
* window normalization so neither caller re-implements the hostile-endpoint
* handling. The callers differ only in WHERE the token comes from.
*/
export async function fetchClaudeQuota(
async function runClaudeUsageFetch(
accessToken: string,
accountId: string,
verbose = false
verbose: boolean
): Promise<ClaudeQuotaResult> {
const authData = await readClaudeAuthData(accountId);
if (!authData) {
return buildEmptyResult('Auth file not found for Claude account', accountId);
}
if (authData.isExpired) {
return buildEmptyResult(
'Token expired - re-authenticate with ccs cliproxy auth claude',
accountId,
true
);
}
let lastError = 'Unknown error';
for (let attempt = 1; attempt <= CLAUDE_QUOTA_MAX_ATTEMPTS; attempt++) {
@@ -248,7 +242,7 @@ export async function fetchClaudeQuota(
method: 'GET',
signal: controller.signal,
headers: {
Authorization: `Bearer ${authData.accessToken}`,
Authorization: `Bearer ${accessToken}`,
Accept: 'application/json',
'Content-Type': 'application/json',
'anthropic-beta': CLAUDE_OAUTH_BETA_HEADER,
@@ -283,6 +277,9 @@ export async function fetchClaudeQuota(
lastError =
(await readResponseErrorMessage(response)) ||
`Claude OAuth usage API error: ${response.status}`;
// Surface the upstream status + Retry-After so an outer caller (the
// native collector's circuit-breaker) can honor backoff guidance.
const retryAfter = response.headers.get('retry-after');
if (
attempt < CLAUDE_QUOTA_MAX_ATTEMPTS &&
(response.status === 429 || response.status >= 500)
@@ -291,7 +288,12 @@ export async function fetchClaudeQuota(
continue;
}
clearTimeout(timeoutId);
return buildEmptyResult(lastError, accountId);
return {
...buildEmptyResult(lastError, accountId),
httpStatus: response.status,
retryable: response.status === 429 || response.status >= 500,
...(retryAfter ? { errorDetail: `retry-after:${retryAfter}` } : {}),
};
}
let payload: unknown;
@@ -338,12 +340,54 @@ export async function fetchClaudeQuota(
if (attempt >= CLAUDE_QUOTA_MAX_ATTEMPTS) {
clearTimeout(timeoutId);
return buildEmptyResult(lastError, accountId);
return { ...buildEmptyResult(lastError, accountId), retryable: true };
}
}
}
return buildEmptyResult(lastError, accountId);
return { ...buildEmptyResult(lastError, accountId), retryable: true };
}
/**
* Fetch quota using a directly-supplied native OAuth access token.
*
* Reuses the exact Anthropic call + normalization as fetchClaudeQuota; the only
* difference is the token source (the logged-in Claude Code credential rather
* than a CLIProxy-managed auth file). Lives in this file so it can share the
* file-private beta header, timeout, attempt count, and branch logic.
*/
export async function fetchClaudeQuotaWithToken(
accessToken: string,
accountId = 'claude-code',
verbose = false
): Promise<ClaudeQuotaResult> {
if (!accessToken || accessToken.trim().length === 0) {
return buildEmptyResult('Missing native Claude access token', accountId, true);
}
return runClaudeUsageFetch(accessToken.trim(), accountId, verbose);
}
/**
* Fetch quota for a single Claude account.
*/
export async function fetchClaudeQuota(
accountId: string,
verbose = false
): Promise<ClaudeQuotaResult> {
const authData = await readClaudeAuthData(accountId);
if (!authData) {
return buildEmptyResult('Auth file not found for Claude account', accountId);
}
if (authData.isExpired) {
return buildEmptyResult(
'Token expired - re-authenticate with ccs cliproxy auth claude',
accountId,
true
);
}
return runClaudeUsageFetch(authData.accessToken, accountId, verbose);
}
/**
+24 -1
View File
@@ -104,6 +104,13 @@ export interface BarRouterDeps {
* audit shells out via a synchronous execSync that must never run here.
*/
runHealthChecks?: () => Promise<HealthReport>;
/**
* Native subscription quota rows (Claude Code + Codex). Defaults to an empty
* async so older tests that build deps without it keep passing. The native
* collector owns its own long-TTL cache + safety controls, so this is cheap
* to call per request.
*/
getNativeAccountRows?: () => Promise<BarSummaryRow[]>;
}
// ============================================================================
@@ -122,6 +129,13 @@ const PER_ACCOUNT_TIMEOUT_MS = 5_000;
/** Bound for the cost side-load so a slow snapshot read can't dominate the response. */
const SIDELOAD_TIMEOUT_MS = 1_500;
/**
* Bound for the native-subscription side-load. A slow or failed native fetch
* resolves to [] so the response paints CLIProxy rows only — never errors, never
* blocks. Native rows have their own 10-min cache, so the common path is instant.
*/
const NATIVE_SIDELOAD_TIMEOUT_MS = 1_500;
/** Timestamp of the last successful force-fresh pull (epoch ms, 0 = never) */
let lastForceFreshAt = 0;
@@ -475,7 +489,14 @@ export function createBarRouter(deps: BarRouterDeps): Router {
});
const rows = await Promise.race([gather, deadline]);
res.json(rows);
// Native subscription rows (Claude Code + Codex) are side-loaded AFTER the
// CLIProxy rows resolve, bounded so a slow/failed native fetch degrades to
// [] rather than blocking or erroring the response.
const getNative = deps.getNativeAccountRows ?? (async () => [] as BarSummaryRow[]);
const nativeRows = (await withTimeout(getNative(), NATIVE_SIDELOAD_TIMEOUT_MS)) ?? [];
res.json([...rows, ...nativeRows]);
} catch (err) {
console.error('[bar-routes] /summary error:', (err as Error).message);
res.status(500).json({ error: 'Internal server error' });
@@ -523,6 +544,7 @@ import { fetchAccountQuota } from '../../cliproxy/quota/quota-fetcher';
import { getTodayCostByAccount } from '../usage/data-aggregator';
import { loadCliproxySnapshotDetails } from '../usage/cliproxy-snapshot-reader';
import { getCachedDailyData, getCachedHourlyData } from '../usage/aggregator';
import { getNativeAccountRows } from '../usage/native-quota-collector';
/** Production bar router — wired to real dependencies */
const barRouter: Router = createBarRouter({
@@ -535,6 +557,7 @@ const barRouter: Router = createBarRouter({
loadCliproxyDetails: loadCliproxySnapshotDetails,
loadDailyUsage: () => getCachedDailyData(),
loadHourlyUsage: () => getCachedHourlyData(),
getNativeAccountRows: () => getNativeAccountRows(),
});
export default barRouter;
@@ -0,0 +1,139 @@
/**
* Native Claude Code credential reader.
*
* Reads the LOGGED-IN Claude Code OAuth token so the bar can show the user's
* own subscription quota (Max/Pro/Team) without going through CLIProxy-managed
* auth files.
*
* File-first, Keychain-fallback: we read ~/.claude/.credentials.json directly
* because hitting the macOS Keychain pops a permission dialog. The Keychain
* fallback is load-bearing on machines where Claude Code stores the token there
* instead of on disk, so it must be kept even though the file path is preferred.
*
* Only the user's own token is read here; the single read-only Anthropic usage
* endpoint is the only thing that ever sees it (see native-quota-collector).
*/
import { existsSync, readFileSync } from 'node:fs';
import { execSync } from 'node:child_process';
import * as os from 'node:os';
import * as path from 'node:path';
/** Shape of the relevant slice of ~/.claude/.credentials.json */
export interface ClaudeNativeCredentials {
claudeAiOauth?: {
accessToken?: string;
subscriptionType?: string;
rateLimitTier?: string;
[key: string]: unknown;
};
[key: string]: unknown;
}
/** Injectable seams so unit tests never touch the real fs/Keychain. */
export interface CredentialReaderDeps {
platform?: NodeJS.Platform;
homedir?: string;
existsSyncImpl?: (p: string) => boolean;
readFileSyncImpl?: (p: string) => string;
execSyncImpl?: (cmd: string, opts: Record<string, unknown>) => string | Buffer;
}
const KEYCHAIN_SERVICE = 'Claude Code-credentials';
const KEYCHAIN_TIMEOUT_MS = 5000;
/** Subscription types that mean "no real subscription" -> skip the fetch. */
const UNSUPPORTED_SUBSCRIPTION_TYPES = new Set(['', 'free', 'none']);
/** rateLimitTier values that imply an entitled subscription. */
const SUPPORTED_RATE_LIMIT_TIER = /claude|max|pro|team|enterprise/;
function parseCredentials(raw: string): ClaudeNativeCredentials | null {
try {
const parsed = JSON.parse(raw) as unknown;
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
return parsed as ClaudeNativeCredentials;
}
} catch {
// fall through
}
return null;
}
/**
* Read the native Claude Code credentials.
*
* Order: the on-disk credentials file first (no prompt), then the macOS
* Keychain as a fallback. Returns null when neither source yields a parseable
* object.
*/
export function readClaudeCredentials(
deps: CredentialReaderDeps = {}
): ClaudeNativeCredentials | null {
const platform = deps.platform ?? os.platform();
const homedir = deps.homedir ?? os.homedir();
const existsImpl = deps.existsSyncImpl ?? existsSync;
const readImpl = deps.readFileSyncImpl ?? ((p: string) => readFileSync(p, 'utf8'));
const execImpl = deps.execSyncImpl ?? execSync;
const credentialsPath = path.join(homedir, '.claude', '.credentials.json');
if (existsImpl(credentialsPath)) {
try {
const parsed = parseCredentials(readImpl(credentialsPath));
if (parsed) return parsed;
} catch {
// fall through to Keychain
}
}
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
}
}
return null;
}
/** Pull the OAuth access token, or null when absent. */
export function getAccessToken(creds: ClaudeNativeCredentials | null): string | null {
const token = creds?.claudeAiOauth?.accessToken;
return typeof token === 'string' && token.trim().length > 0 ? token : null;
}
/** Pull the subscription tier (e.g. "max" / "pro"), or null. */
export function getSubscriptionTier(creds: ClaudeNativeCredentials | null): string | null {
const tier = creds?.claudeAiOauth?.subscriptionType;
return typeof tier === 'string' && tier.trim().length > 0 ? tier.trim() : null;
}
/**
* True when the credentials describe a real, entitled subscription.
*
* Gating on this BEFORE fetching means a free/logged-out user never spends a
* token call against the hostile usage endpoint and never gets a phantom row.
*/
export function hasSupportedSubscription(creds: ClaudeNativeCredentials | null): boolean {
const subscriptionType = String(creds?.claudeAiOauth?.subscriptionType ?? '')
.trim()
.toLowerCase();
if (subscriptionType && !UNSUPPORTED_SUBSCRIPTION_TYPES.has(subscriptionType)) {
return true;
}
const rateLimitTier = String(creds?.claudeAiOauth?.rateLimitTier ?? '')
.trim()
.toLowerCase();
return SUPPORTED_RATE_LIMIT_TIER.test(rateLimitTier);
}
@@ -0,0 +1,221 @@
/**
* Codex local quota collector (zero network).
*
* Codex writes a `rate_limits` object into its rollout session logs
* (~/.codex/sessions/<y>/<m>/<d>/rollout-*.jsonl). We read the most recent
* session's last non-null rate_limits to surface the user's Codex subscription
* quota WITHOUT any network call — pure local file read.
*
* Exec-mode sessions often never emit rate_limits (the field stays null); in
* that case we return null so the bar simply omits the Codex row rather than
* inventing a fake one.
*/
import * as fs from 'node:fs';
import * as path from 'node:path';
import { resolveCodexConfigPaths } from '../services/codex-dashboard-service';
/** Normalized Codex quota snapshot from a local session log. */
export interface CodexLocalQuota {
/** Remaining percentage (0-100): min across primary/secondary windows. */
quotaPercentage: number;
/** ISO timestamp of the soonest window reset, null if unknown. */
nextReset: string | null;
/** plan_type from the session (e.g. "pro"/"plus"), null if absent. */
tier: string | null;
/** True when the source file is older than the freshness window. */
stale: boolean;
}
/** Injectable seams for deterministic tests (no real fs / no tail subprocess). */
export interface CodexLocalQuotaDeps {
env?: NodeJS.ProcessEnv;
homeDir?: string;
existsSyncImpl?: (p: string) => boolean;
readdirImpl?: (dir: string) => fs.Dirent[];
statMtimeMsImpl?: (p: string) => number;
/** Returns the last N lines of a file (default: Bun tail). */
tailLinesImpl?: (file: string, lines: number) => Promise<string[]>;
now?: number;
}
/** Lines to scan from the tail; rate_limits sits near the end of a session. */
const TAIL_LINES = 200;
/** A source older than this is reported stale (but still emitted). */
const STALE_AFTER_MS = 5 * 60 * 1000;
interface CodexRateWindow {
usedPercent: number;
resetsAtSeconds: number | null;
}
interface CodexRateLimits {
primary: CodexRateWindow | null;
secondary: CodexRateWindow | null;
planType: string | null;
}
function asObject(value: unknown): Record<string, unknown> | null {
if (typeof value !== 'object' || value === null || Array.isArray(value)) return null;
return value as Record<string, unknown>;
}
function asFiniteNumber(value: unknown): number | null {
return typeof value === 'number' && Number.isFinite(value) ? value : null;
}
function parseWindow(value: unknown): CodexRateWindow | null {
const obj = asObject(value);
if (!obj) return null;
const usedPercent = asFiniteNumber(obj['used_percent']);
if (usedPercent === null) return null;
return {
usedPercent,
resetsAtSeconds: asFiniteNumber(obj['resets_at']),
};
}
/**
* Extract a non-null rate_limits object from a parsed JSONL line.
* rate_limits lives under `payload` in the token_count event.
*/
function extractRateLimits(line: string): CodexRateLimits | null {
let parsed: unknown;
try {
parsed = JSON.parse(line);
} catch {
return null;
}
const root = asObject(parsed);
if (!root) return null;
const payload = asObject(root['payload']) ?? root;
const rateLimits = asObject(payload['rate_limits']);
if (!rateLimits) return null;
const primary = parseWindow(rateLimits['primary']);
const secondary = parseWindow(rateLimits['secondary']);
// A rate_limits object with neither window carries no usable signal.
if (!primary && !secondary) return null;
const planType =
typeof rateLimits['plan_type'] === 'string' && rateLimits['plan_type'].trim().length > 0
? (rateLimits['plan_type'] as string).trim()
: null;
return { primary, secondary, planType };
}
/** Recursive rollout-*.jsonl walker, lexicographically sorted (ISO ts in name). */
function collectRolloutFiles(
dir: string,
existsImpl: (p: string) => boolean,
readdirImpl: (d: string) => fs.Dirent[]
): string[] {
if (!existsImpl(dir)) return [];
const files: string[] = [];
for (const entry of readdirImpl(dir)) {
const entryPath = path.join(dir, entry.name);
if (entry.isDirectory()) {
files.push(...collectRolloutFiles(entryPath, existsImpl, readdirImpl));
continue;
}
if (entry.isFile() && entry.name.startsWith('rollout-') && entry.name.endsWith('.jsonl')) {
files.push(entryPath);
}
}
return files.sort();
}
/**
* Default tail using Bun.spawn (macOS-safe: no tac, no GNU timeout).
* Reads the last `lines` lines so we never load a huge session into memory.
*/
async function defaultTailLines(file: string, lines: number): Promise<string[]> {
const proc = Bun.spawn(['tail', `-${lines}`, file], {
stdout: 'pipe',
stderr: 'ignore',
});
const text = await new Response(proc.stdout).text();
await proc.exited;
return text.split('\n').filter((l) => l.trim().length > 0);
}
function computeQuotaPercentage(rate: CodexRateLimits): number {
const remaining: number[] = [];
if (rate.primary) remaining.push(100 - rate.primary.usedPercent);
if (rate.secondary) remaining.push(100 - rate.secondary.usedPercent);
// Clamp into [0,100] to guard against odd upstream values.
return Math.max(0, Math.min(100, Math.min(...remaining)));
}
function computeNextReset(rate: CodexRateLimits): string | null {
const resets: number[] = [];
if (rate.primary?.resetsAtSeconds !== null && rate.primary?.resetsAtSeconds !== undefined) {
resets.push(rate.primary.resetsAtSeconds);
}
if (rate.secondary?.resetsAtSeconds !== null && rate.secondary?.resetsAtSeconds !== undefined) {
resets.push(rate.secondary.resetsAtSeconds);
}
if (resets.length === 0) return null;
return new Date(Math.min(...resets) * 1000).toISOString();
}
/**
* Read the latest Codex session's most recent rate_limits and normalize it.
* Returns null when no session carries a usable rate_limits object.
*/
export async function getCodexLocalQuota(
deps: CodexLocalQuotaDeps = {}
): Promise<CodexLocalQuota | null> {
const existsImpl = deps.existsSyncImpl ?? fs.existsSync;
const readdirImpl =
deps.readdirImpl ?? ((dir: string) => fs.readdirSync(dir, { withFileTypes: true }));
const statMtimeMsImpl = deps.statMtimeMsImpl ?? ((p: string) => fs.statSync(p).mtimeMs);
const tailLinesImpl = deps.tailLinesImpl ?? defaultTailLines;
const now = deps.now ?? Date.now();
const { baseDir } = resolveCodexConfigPaths({ env: deps.env, homeDir: deps.homeDir });
const sessionsDir = path.join(baseDir, 'sessions');
const rolloutFiles = collectRolloutFiles(sessionsDir, existsImpl, readdirImpl);
if (rolloutFiles.length === 0) return null;
// Filename carries an ISO timestamp, so the lexicographically-last file is
// the most recent session.
const latest = rolloutFiles[rolloutFiles.length - 1];
let lines: string[];
try {
lines = await tailLinesImpl(latest, TAIL_LINES);
} catch {
return null;
}
// Iterate backwards for the most recent non-null rate_limits.
let rate: CodexRateLimits | null = null;
for (let i = lines.length - 1; i >= 0; i--) {
const found = extractRateLimits(lines[i]);
if (found) {
rate = found;
break;
}
}
if (!rate) return null;
let stale = false;
try {
stale = now - statMtimeMsImpl(latest) > STALE_AFTER_MS;
} catch {
// Unknown mtime -> treat as fresh; the data itself is still valid.
stale = false;
}
return {
quotaPercentage: computeQuotaPercentage(rate),
nextReset: computeNextReset(rate),
tier: rate.planType,
stale,
};
}
@@ -0,0 +1,376 @@
/**
* Native subscription quota collector — the ONLY server-side fetch surface for
* the user's own Claude Code + Codex subscription quota.
*
* The macOS bar reads localhost /api/bar/summary and NEVER calls Anthropic. All
* Anthropic traffic originates here, under strict safety controls, because the
* OAuth usage endpoint is undocumented and hostile to polling (persistent 429s,
* no Retry-After, first-party-only policy). The controls below exist to protect
* the user's account:
*
* - long TTL (10 min) on-demand cache, never a tight timer loop
* - in-flight coalescing so concurrent /summary calls share one fetch
* - Retry-After honored; exponential backoff + jitter on 429/5xx
* - circuit breaker stops calling after repeated 429s for a cooldown
* - serve-stale-on-failure; only omit a row when there is genuinely no data
*
* Codex is a pure local file read (no network), so it skips the network guards.
*/
import {
readClaudeCredentials,
getAccessToken,
getSubscriptionTier,
hasSupportedSubscription,
type ClaudeNativeCredentials,
} from './claude-native-credentials';
import { fetchClaudeQuotaWithToken } from '../../cliproxy/quota/quota-fetcher-claude';
import { getCodexLocalQuota, type CodexLocalQuota } from './codex-local-quota-collector';
import type { ClaudeQuotaResult } from '../../cliproxy/quota/quota-types';
import type { BarSummaryRow } from '../routes/bar-routes';
// ============================================================================
// Safety constants (concrete, named, module-level)
// ============================================================================
/** On-demand cache TTL. Floor is 5 min; we use 10 min because the bar polls
* /summary far more often than a hook fires. */
const NATIVE_QUOTA_TTL_MS = 600_000; // 10 minutes
/** Exponential backoff base; delay = min(base * 2^n, MAX) + jitter. */
const RETRY_BASE_MS = 1_000;
/** Ceiling for any single backoff / Retry-After cooldown derived from one call. */
const MAX_BACKOFF_MS = 60_000; // 1 minute
/** Jitter added to backoff to avoid synchronized retries. */
const JITTER_MAX_MS = 500;
/** Consecutive 429s that trip the breaker open. */
const CB_TRIP_THRESHOLD = 3;
/** How long the breaker stays open (zero network) once tripped. */
const CB_COOLDOWN_MS = 900_000; // 15 minutes
const CLAUDE_PROVIDER = 'claude-code';
const CODEX_PROVIDER = 'codex';
// ============================================================================
// Injectable dependencies (tests inject mocks; never live Anthropic in CI)
// ============================================================================
export interface NativeQuotaDeps {
/** Read the native Claude Code credentials. */
readCredentials?: () => ClaudeNativeCredentials | null;
/** Fetch Claude quota with a directly-supplied native token. */
fetchClaudeQuota?: (accessToken: string, accountId?: string) => Promise<ClaudeQuotaResult>;
/** Read Codex quota from local session logs (zero network). */
getCodexQuota?: () => Promise<CodexLocalQuota | null>;
/** Clock seam for deterministic backoff/TTL/breaker tests. */
now?: () => number;
/** Sleep seam (no real delay in tests). */
sleep?: (ms: number) => Promise<void>;
}
// ============================================================================
// Per-provider mutable state (module-scoped; reset() for tests)
// ============================================================================
interface ProviderState {
/** Last successfully-built row, kept for stale-on-fail and TTL serving. */
cachedRow: BarSummaryRow | null;
/** Epoch ms when cachedRow was produced. */
cachedAt: number;
/** Shared in-flight promise; concurrent callers await this, not a new fetch. */
pending: Promise<BarSummaryRow | null> | null;
/** Consecutive 429 count toward the breaker threshold. */
consecutive429: number;
/** Epoch ms until which the breaker is open (no network). */
breakerOpenUntil: number;
/** Epoch ms until which a Retry-After / backoff cooldown holds. */
cooldownUntil: number;
/** Attempt counter feeding exponential backoff. */
backoffAttempt: number;
}
function freshProviderState(): ProviderState {
return {
cachedRow: null,
cachedAt: 0,
pending: null,
consecutive429: 0,
breakerOpenUntil: 0,
cooldownUntil: 0,
backoffAttempt: 0,
};
}
const claudeState = freshProviderState();
/** Reset all module state. Tests call this to avoid cross-test pollution. */
export function resetNativeQuotaState(): void {
Object.assign(claudeState, freshProviderState());
}
// ============================================================================
// Helpers
// ============================================================================
function defaultSleep(ms: number): Promise<void> {
return new Promise((resolve) => setTimeout(resolve, ms));
}
/** Parse Retry-After (seconds or HTTP-date) into ms, capped at MAX_BACKOFF_MS. */
function parseRetryAfterMs(detail: string | undefined, now: number): number | null {
if (!detail) return null;
const match = /retry-after:(.+)$/.exec(detail);
if (!match) return null;
const raw = match[1].trim();
const asSeconds = Number(raw);
if (Number.isFinite(asSeconds)) {
return Math.min(Math.max(0, asSeconds) * 1000, MAX_BACKOFF_MS);
}
const asDate = Date.parse(raw);
if (Number.isFinite(asDate)) {
return Math.min(Math.max(0, asDate - now), MAX_BACKOFF_MS);
}
return null;
}
function computeBackoffMs(attempt: number): number {
const exp = Math.min(RETRY_BASE_MS * 2 ** attempt, MAX_BACKOFF_MS);
const jitter = Math.floor(Math.random() * JITTER_MAX_MS);
return exp + jitter;
}
/**
* Best-guess remaining percentage across the 5h + weekly core windows, mirroring
* the CLIProxy quota-manager derivation: min of remaining across non-overage
* windows, falling back to all windows if core summary is absent.
*/
function deriveClaudeQuotaPercentage(quota: ClaudeQuotaResult): number | null {
const coreWindows = [quota.coreUsage?.fiveHour, quota.coreUsage?.weekly].filter(
(w): w is NonNullable<typeof w> => !!w
);
if (coreWindows.length > 0) {
return Math.min(...coreWindows.map((w) => w.remainingPercent));
}
const usageWindows = quota.windows.filter((w) => w.rateLimitType !== 'overage');
if (usageWindows.length > 0) {
return Math.min(...usageWindows.map((w) => w.remainingPercent));
}
return null;
}
/** Soonest non-null reset ISO across the two core windows. */
function deriveClaudeNextReset(quota: ClaudeQuotaResult): string | null {
const resets = [quota.coreUsage?.fiveHour?.resetAt, quota.coreUsage?.weekly?.resetAt]
.filter((r): r is string => typeof r === 'string')
.map((r) => ({ iso: r, ms: new Date(r).getTime() }))
.filter((r) => Number.isFinite(r.ms))
.sort((a, b) => a.ms - b.ms);
return resets.length > 0 ? resets[0].iso : null;
}
function buildClaudeRow(quota: ClaudeQuotaResult, tier: string | null, now: number): BarSummaryRow {
return {
account_id: CLAUDE_PROVIDER,
provider: CLAUDE_PROVIDER,
displayName: 'Claude Code',
tier,
paused: false,
quota_percentage: deriveClaudeQuotaPercentage(quota),
quotaStatus: 'ok',
next_reset: deriveClaudeNextReset(quota),
is_default: false,
last_activity_at: null,
today_cost: null,
health: 'ok',
cached: false,
fetchedAt: new Date(now).toISOString(),
needsReauth: false,
};
}
function buildCodexRow(quota: CodexLocalQuota, now: number): BarSummaryRow {
return {
account_id: CODEX_PROVIDER,
provider: CODEX_PROVIDER,
displayName: 'Codex',
tier: quota.tier,
paused: false,
quota_percentage: quota.quotaPercentage,
quotaStatus: 'ok',
next_reset: quota.nextReset,
is_default: false,
last_activity_at: null,
today_cost: null,
// Codex is a local read; a stale source still reflects real usage so we keep
// quotaStatus 'ok' but flag health 'warning' to hint freshness.
health: quota.stale ? 'warning' : 'ok',
cached: false,
fetchedAt: new Date(now).toISOString(),
needsReauth: false,
};
}
/** Return the cached row marked cached=true (used for TTL + stale serving). */
function serveCached(state: ProviderState): BarSummaryRow | null {
if (!state.cachedRow) return null;
return { ...state.cachedRow, cached: true };
}
// ============================================================================
// Claude path with full safety controls
// ============================================================================
async function collectClaudeRow(deps: NativeQuotaDeps): Promise<BarSummaryRow | null> {
const now = (deps.now ?? Date.now)();
const state = claudeState;
// Serve from cache while within TTL — on-demand only, NO network.
if (state.cachedRow && now - state.cachedAt < NATIVE_QUOTA_TTL_MS) {
return serveCached(state);
}
// Breaker open or cooldown active -> zero network, serve stale (may be null).
if (now < state.breakerOpenUntil || now < state.cooldownUntil) {
return serveCached(state);
}
// Coalesce: concurrent callers past TTL share one in-flight fetch.
if (state.pending) {
return state.pending;
}
const readCredentials = deps.readCredentials ?? readClaudeCredentials;
const fetchQuota = deps.fetchClaudeQuota ?? fetchClaudeQuotaWithToken;
const sleep = deps.sleep ?? defaultSleep;
state.pending = (async (): Promise<BarSummaryRow | null> => {
try {
const creds = readCredentials();
// No token / unsupported subscription -> never spend a call, omit the row.
if (!creds || !hasSupportedSubscription(creds)) {
return serveCached(state);
}
const token = getAccessToken(creds);
if (!token) {
return serveCached(state);
}
const tier = getSubscriptionTier(creds);
const quota = await fetchQuota(token, CLAUDE_PROVIDER);
if (quota.success) {
// Success closes the breaker and clears backoff.
state.consecutive429 = 0;
state.breakerOpenUntil = 0;
state.cooldownUntil = 0;
state.backoffAttempt = 0;
const row = buildClaudeRow(quota, tier, now);
state.cachedRow = row;
state.cachedAt = now;
return { ...row, cached: false };
}
// 401 -> token expired. Emit a reauth row so the bar can prompt; this is
// a real, actionable state distinct from a transient failure.
if (quota.needsReauth) {
const row: BarSummaryRow = {
account_id: CLAUDE_PROVIDER,
provider: CLAUDE_PROVIDER,
displayName: 'Claude Code',
tier,
paused: false,
quota_percentage: null,
quotaStatus: 'error',
next_reset: null,
is_default: false,
last_activity_at: null,
today_cost: null,
health: 'error',
cached: false,
fetchedAt: new Date(now).toISOString(),
needsReauth: true,
};
// Do not cache the reauth row as a good value; it should re-evaluate
// once the user re-auths. But return it now.
return row;
}
// 429 / 5xx / transient. Apply backoff + breaker, then serve stale.
const is429 = quota.httpStatus === 429;
if (is429) {
state.consecutive429 += 1;
if (state.consecutive429 >= CB_TRIP_THRESHOLD) {
state.breakerOpenUntil = now + CB_COOLDOWN_MS;
}
const retryAfter = parseRetryAfterMs(quota.errorDetail, now);
const backoff = retryAfter ?? computeBackoffMs(state.backoffAttempt);
state.cooldownUntil = now + backoff;
state.backoffAttempt += 1;
// We do NOT sleep-then-retry inside the request path (that would burn
// the request budget). The cooldown gates the NEXT call instead.
void sleep; // retained as an injectable seam for future inline retry
} else if (quota.retryable) {
const backoff = computeBackoffMs(state.backoffAttempt);
state.cooldownUntil = now + backoff;
state.backoffAttempt += 1;
}
// Serve last good row on failure; omit if we never succeeded.
return serveCached(state);
} catch {
// Network/parse rejection -> treat as transient, serve stale.
const backoff = computeBackoffMs(state.backoffAttempt);
state.cooldownUntil = now + backoff;
state.backoffAttempt += 1;
return serveCached(state);
} finally {
state.pending = null;
}
})();
return state.pending;
}
// ============================================================================
// Codex path (local read, no network guards needed)
// ============================================================================
async function collectCodexRow(deps: NativeQuotaDeps): Promise<BarSummaryRow | null> {
const now = (deps.now ?? Date.now)();
const getCodex = deps.getCodexQuota ?? getCodexLocalQuota;
try {
const quota = await getCodex();
if (!quota) return null; // exec-mode / no rate_limits -> omit the row
return buildCodexRow(quota, now);
} catch {
return null;
}
}
// ============================================================================
// Public entry point
// ============================================================================
/**
* Build the native subscription rows (Claude Code + Codex) for /summary.
*
* Each path is independently try/caught so one failing source never blocks the
* other or the response. Returns only rows that represent real data.
*/
export async function getNativeAccountRows(deps: NativeQuotaDeps = {}): Promise<BarSummaryRow[]> {
const [claude, codex] = await Promise.all([
collectClaudeRow(deps).catch(() => null),
collectCodexRow(deps).catch(() => null),
]);
const rows: BarSummaryRow[] = [];
if (claude) rows.push(claude);
if (codex) rows.push(codex);
return rows;
}
+103
View File
@@ -986,3 +986,106 @@ describe('today_cost: duplicate-email accounts get null (finding #11)', () => {
expect(body[0].today_cost).toBeCloseTo(3.75);
});
});
// ============================================================================
// Native subscription rows side-load into /summary
// ============================================================================
describe('/summary native subscription rows', () => {
function nativeRow(provider: 'claude-code' | 'codex'): BarSummaryRow {
return {
account_id: provider,
provider,
displayName: provider === 'codex' ? 'Codex' : 'Claude Code',
tier: provider === 'codex' ? 'pro' : 'max',
paused: false,
quota_percentage: 42,
quotaStatus: 'ok',
next_reset: '2026-06-09T20:00:00.000Z',
is_default: false,
last_activity_at: null,
today_cost: null,
health: 'ok',
cached: false,
fetchedAt: '2026-06-09T14:00:00.000Z',
needsReauth: false,
};
}
async function buildRouter(getNativeAccountRows: () => Promise<BarSummaryRow[]>) {
const { createBarRouter, resetForceFreshDebounce: resetDebounce } = await import(
'../../../src/web-server/routes/bar-routes'
);
const app = express();
app.use(express.json());
const cliproxyAccount = makeAccountInfo({ id: 'pool@example.com', provider: 'agy' });
const router = createBarRouter({
// eslint-disable-next-line @typescript-eslint/no-explicit-any
getAllAccountsSummary: () => ({ agy: [cliproxyAccount] }) as any,
getCachedQuota: () => null,
setCachedQuota: () => {},
invalidateQuotaCache: () => {},
fetchAccountQuota: async () => makeQuotaResult(),
getTodayCostByAccount: () => ({}),
loadCliproxyDetails: async () => [],
loadDailyUsage: async () => [],
loadHourlyUsage: async () => [],
runHealthChecks: async () => makeHealthReport(),
getNativeAccountRows,
});
app.use('/api/bar', router);
const srv = await new Promise<Server>((resolve, reject) => {
const instance = app.listen(0, '127.0.0.1');
instance.once('error', reject);
instance.once('listening', () => resolve(instance));
});
const addr = srv.address();
if (!addr || typeof addr === 'string') throw new Error('No server address');
resetDebounce();
return { srv, url: `http://127.0.0.1:${(addr as { port: number }).port}` };
}
it('appends native rows after the CLIProxy rows', async () => {
const { srv, url } = await buildRouter(async () => [
nativeRow('claude-code'),
nativeRow('codex'),
]);
const { body } = await getJson<BarSummaryRow[]>(url, '/api/bar/summary');
await new Promise<void>((resolve) => srv.close(() => resolve()));
expect(body.length).toBe(3);
expect(body[0].provider).toBe('agy'); // CLIProxy row first
expect(body[1].provider).toBe('claude-code');
expect(body[2].provider).toBe('codex');
});
it('degrades to CLIProxy-only rows when the native fetch never resolves (bounded side-load)', async () => {
const { srv, url } = await buildRouter(() => new Promise<BarSummaryRow[]>(() => {}));
const start = Date.now();
const { status, body } = await getJson<BarSummaryRow[]>(url, '/api/bar/summary');
const elapsed = Date.now() - start;
await new Promise<void>((resolve) => srv.close(() => resolve()));
expect(status).toBe(200);
expect(body.length).toBe(1);
expect(body[0].provider).toBe('agy');
// Must not block longer than the native side-load budget (+ small slack).
expect(elapsed).toBeLessThan(2_500);
});
it('returns CLIProxy-only rows (never 500) when the native fetch rejects', async () => {
const { srv, url } = await buildRouter(async () => {
throw new Error('native blew up');
});
const { status, body } = await getJson<BarSummaryRow[]>(url, '/api/bar/summary');
await new Promise<void>((resolve) => srv.close(() => resolve()));
expect(status).toBe(200);
expect(body.length).toBe(1);
expect(body[0].provider).toBe('agy');
});
});
@@ -0,0 +1,126 @@
/**
* Tests for the native Claude Code credential reader.
*
* All fs / Keychain access is injected, so these tests never touch the real
* filesystem or pop a macOS Keychain prompt.
*/
import { describe, expect, it } from 'bun:test';
import {
readClaudeCredentials,
getAccessToken,
getSubscriptionTier,
hasSupportedSubscription,
type ClaudeNativeCredentials,
} from '../../../src/web-server/usage/claude-native-credentials';
function makeCreds(overrides: Record<string, unknown> = {}): ClaudeNativeCredentials {
return {
claudeAiOauth: {
accessToken: 'tok-abc',
subscriptionType: 'max',
...overrides,
},
};
}
describe('readClaudeCredentials', () => {
it('parses the on-disk credentials file when present (file-first, no Keychain)', () => {
let keychainCalled = false;
const creds = readClaudeCredentials({
platform: 'darwin',
homedir: '/home/test',
existsSyncImpl: () => true,
readFileSyncImpl: () => JSON.stringify(makeCreds()),
execSyncImpl: () => {
keychainCalled = true;
return '';
},
});
expect(creds?.claudeAiOauth?.accessToken).toBe('tok-abc');
// File present means the Keychain must NOT be consulted (avoids prompt).
expect(keychainCalled).toBe(false);
});
it('falls back to the macOS Keychain when the file is absent', () => {
const creds = readClaudeCredentials({
platform: 'darwin',
homedir: '/home/test',
existsSyncImpl: () => false,
readFileSyncImpl: () => {
throw new Error('should not read file');
},
execSyncImpl: () => JSON.stringify(makeCreds({ subscriptionType: 'pro' })),
});
expect(creds?.claudeAiOauth?.subscriptionType).toBe('pro');
});
it('returns null when both file and Keychain are absent', () => {
const creds = readClaudeCredentials({
platform: 'darwin',
homedir: '/home/test',
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 = readClaudeCredentials({
platform: 'linux',
homedir: '/home/test',
existsSyncImpl: () => false,
readFileSyncImpl: () => {
throw new Error('no file');
},
execSyncImpl: () => {
keychainCalled = true;
return '';
},
});
expect(creds).toBeNull();
expect(keychainCalled).toBe(false);
});
});
describe('hasSupportedSubscription', () => {
it.each(['', 'free', 'none'])('returns false for unsupported subscriptionType %p', (sub) => {
expect(hasSupportedSubscription(makeCreds({ subscriptionType: sub }))).toBe(false);
});
it.each(['max', 'pro', 'team', 'enterprise'])(
'returns true for supported subscriptionType %p',
(sub) => {
expect(hasSupportedSubscription(makeCreds({ subscriptionType: sub }))).toBe(true);
}
);
it('returns true via rateLimitTier regex when subscriptionType is empty', () => {
const creds = makeCreds({ subscriptionType: '', rateLimitTier: 'claude_max_20x' });
expect(hasSupportedSubscription(creds)).toBe(true);
});
it('returns false for null credentials', () => {
expect(hasSupportedSubscription(null)).toBe(false);
});
});
describe('token + tier extraction', () => {
it('getAccessToken returns the token or null', () => {
expect(getAccessToken(makeCreds())).toBe('tok-abc');
expect(getAccessToken(makeCreds({ accessToken: '' }))).toBeNull();
expect(getAccessToken(null)).toBeNull();
});
it('getSubscriptionTier returns the tier or null', () => {
expect(getSubscriptionTier(makeCreds({ subscriptionType: 'max' }))).toBe('max');
expect(getSubscriptionTier(makeCreds({ subscriptionType: '' }))).toBeNull();
expect(getSubscriptionTier(null)).toBeNull();
});
});
@@ -0,0 +1,100 @@
/**
* Tests for the Codex local quota collector (zero network).
*
* Uses a temp fixture rollout-*.jsonl read via the real Bun.spawn(['tail', ...])
* default impl (macOS-safe) plus injected fs seams for the directory walk.
*/
import { afterAll, beforeAll, describe, expect, it } from 'bun:test';
import * as fs from 'node:fs';
import * as os from 'node:os';
import * as path from 'node:path';
import { getCodexLocalQuota } from '../../../src/web-server/usage/codex-local-quota-collector';
let tmpDir: string;
let codexHome: string;
let sessionsDir: string;
function tokenCountLine(rateLimits: unknown): string {
return JSON.stringify({
timestamp: '2026-06-09T14:36:48.896Z',
type: 'event_msg',
payload: { type: 'token_count', info: {}, rate_limits: rateLimits },
});
}
function writeRollout(name: string, lines: string[]): string {
const day = path.join(sessionsDir, '2026', '06', '09');
fs.mkdirSync(day, { recursive: true });
const file = path.join(day, name);
fs.writeFileSync(file, lines.join('\n') + '\n');
return file;
}
beforeAll(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'codex-local-quota-'));
codexHome = path.join(tmpDir, '.codex');
sessionsDir = path.join(codexHome, 'sessions');
fs.mkdirSync(sessionsDir, { recursive: true });
});
afterAll(() => {
fs.rmSync(tmpDir, { recursive: true, force: true });
});
describe('getCodexLocalQuota', () => {
it('parses the last non-null rate_limits into a normalized quota', async () => {
writeRollout('rollout-2026-06-09T10-00-00-aaaa.jsonl', [
tokenCountLine(null),
tokenCountLine({
primary: { used_percent: 0.0, window_minutes: 300, resets_at: 1781033803 },
secondary: { used_percent: 48.0, window_minutes: 10080, resets_at: 1781192122 },
plan_type: 'pro',
}),
]);
const quota = await getCodexLocalQuota({ env: { CODEX_HOME: codexHome }, now: Date.now() });
expect(quota).not.toBeNull();
// min(100-0, 100-48) = 52
expect(quota?.quotaPercentage).toBe(52);
expect(quota?.tier).toBe('pro');
// soonest reset = min(1781033803, 1781192122) -> primary
expect(quota?.nextReset).toBe(new Date(1781033803 * 1000).toISOString());
expect(quota?.stale).toBe(false);
});
it('returns null when every rate_limits is null (exec-mode session)', async () => {
// A newer session (later filename) with only null rate_limits wins the sort.
writeRollout('rollout-2026-06-09T11-00-00-bbbb.jsonl', [
tokenCountLine(null),
tokenCountLine(null),
]);
const quota = await getCodexLocalQuota({ env: { CODEX_HOME: codexHome }, now: Date.now() });
expect(quota).toBeNull();
});
it('flags stale when the source file mtime is older than 5 minutes', async () => {
const file = writeRollout('rollout-2026-06-09T12-00-00-cccc.jsonl', [
tokenCountLine({
primary: { used_percent: 10, window_minutes: 300, resets_at: 1781033803 },
secondary: { used_percent: 5, window_minutes: 10080, resets_at: 1781192122 },
plan_type: 'plus',
}),
]);
const mtime = fs.statSync(file).mtimeMs;
// now = mtime + 6 minutes -> stale
const quota = await getCodexLocalQuota({
env: { CODEX_HOME: codexHome },
now: mtime + 6 * 60 * 1000,
});
expect(quota?.stale).toBe(true);
expect(quota?.tier).toBe('plus');
});
it('returns null when there are no rollout files at all', async () => {
const emptyHome = path.join(tmpDir, '.codex-empty');
fs.mkdirSync(path.join(emptyHome, 'sessions'), { recursive: true });
const quota = await getCodexLocalQuota({ env: { CODEX_HOME: emptyHome }, now: Date.now() });
expect(quota).toBeNull();
});
});
@@ -0,0 +1,328 @@
/**
* Tests for the native subscription quota collector.
*
* The Anthropic fetch is ALWAYS mocked — these tests NEVER hit the live usage
* endpoint. A controllable clock drives TTL / backoff / breaker assertions.
*/
import { beforeEach, describe, expect, it } from 'bun:test';
import {
getNativeAccountRows,
resetNativeQuotaState,
type NativeQuotaDeps,
} from '../../../src/web-server/usage/native-quota-collector';
import type { ClaudeQuotaResult } from '../../../src/cliproxy/quota/quota-types';
import type { ClaudeNativeCredentials } from '../../../src/web-server/usage/claude-native-credentials';
// A jump comfortably past any single-call backoff cooldown (<= 60s), used so a
// breaker-test fetch is not blocked by the prior 429's per-call cooldown.
const MAX_COOLDOWN_JUMP = 61_000;
function maxCreds(): ClaudeNativeCredentials {
return { claudeAiOauth: { accessToken: 'native-tok', subscriptionType: 'max' } };
}
function successQuota(): ClaudeQuotaResult {
return {
success: true,
windows: [],
coreUsage: {
fiveHour: {
rateLimitType: 'five_hour',
label: 'Session limit',
remainingPercent: 42,
resetAt: '2026-06-09T20:00:00.000Z',
status: 'allowed',
},
weekly: {
rateLimitType: 'seven_day',
label: 'Weekly limit',
remainingPercent: 70,
resetAt: '2026-06-15T00:00:00.000Z',
status: 'allowed',
},
},
lastUpdated: Date.now(),
accountId: 'claude-code',
};
}
function reauthQuota(): ClaudeQuotaResult {
return {
success: false,
windows: [],
coreUsage: { fiveHour: null, weekly: null },
lastUpdated: Date.now(),
accountId: 'claude-code',
needsReauth: true,
error: 'Authentication required',
};
}
function rateLimitedQuota(retryAfter?: string): ClaudeQuotaResult {
return {
success: false,
windows: [],
coreUsage: { fiveHour: null, weekly: null },
lastUpdated: Date.now(),
accountId: 'claude-code',
httpStatus: 429,
retryable: true,
...(retryAfter ? { errorDetail: `retry-after:${retryAfter}` } : {}),
error: 'rate limited',
};
}
/** Build a deps object with a controllable clock + counted fetch. */
function makeDeps(
fetchImpl: (token: string) => Promise<ClaudeQuotaResult>,
clock: { now: number },
credsImpl: () => ClaudeNativeCredentials | null = maxCreds
): NativeQuotaDeps & { fetchCount: () => number } {
let count = 0;
return {
readCredentials: credsImpl,
fetchClaudeQuota: async (token: string) => {
count += 1;
return fetchImpl(token);
},
getCodexQuota: async () => null,
now: () => clock.now,
sleep: async () => {},
fetchCount: () => count,
};
}
beforeEach(() => {
resetNativeQuotaState();
});
describe('Claude native row mapping', () => {
it('maps a successful fetch into a claude-code ok row with min remaining', async () => {
const clock = { now: 1_000_000 };
const deps = makeDeps(async () => successQuota(), clock);
const rows = await getNativeAccountRows(deps);
const row = rows.find((r) => r.provider === 'claude-code');
expect(row).toBeDefined();
expect(row?.quotaStatus).toBe('ok');
// min(42, 70)
expect(row?.quota_percentage).toBe(42);
expect(row?.next_reset).toBe('2026-06-09T20:00:00.000Z');
expect(row?.tier).toBe('max');
expect(row?.displayName).toBe('Claude Code');
expect(row?.account_id).toBe('claude-code');
expect(row?.needsReauth).toBe(false);
});
it('emits a reauth error row on 401/needsReauth', async () => {
const clock = { now: 1_000_000 };
const deps = makeDeps(async () => reauthQuota(), clock);
const rows = await getNativeAccountRows(deps);
const row = rows.find((r) => r.provider === 'claude-code');
expect(row?.quotaStatus).toBe('error');
expect(row?.health).toBe('error');
expect(row?.needsReauth).toBe(true);
});
it('omits the claude row when there is no token', async () => {
const clock = { now: 1_000_000 };
const deps = makeDeps(
async () => successQuota(),
clock,
() => null
);
const rows = await getNativeAccountRows(deps);
expect(rows.find((r) => r.provider === 'claude-code')).toBeUndefined();
});
it('omits the claude row for an unsupported (free) subscription without spending a call', async () => {
const clock = { now: 1_000_000 };
const deps = makeDeps(
async () => successQuota(),
clock,
() => ({
claudeAiOauth: { accessToken: 'x', subscriptionType: 'free' },
})
);
const rows = await getNativeAccountRows(deps);
expect(rows.find((r) => r.provider === 'claude-code')).toBeUndefined();
expect(deps.fetchCount()).toBe(0);
});
});
describe('cache / TTL', () => {
it('serves cache within TTL and does NOT re-fetch', async () => {
const clock = { now: 1_000_000 };
const deps = makeDeps(async () => successQuota(), clock);
await getNativeAccountRows(deps);
expect(deps.fetchCount()).toBe(1);
// Advance < 10 min: still cached.
clock.now += 5 * 60 * 1000;
const rows = await getNativeAccountRows(deps);
expect(deps.fetchCount()).toBe(1);
expect(rows.find((r) => r.provider === 'claude-code')?.cached).toBe(true);
});
it('re-fetches after the TTL expires', async () => {
const clock = { now: 1_000_000 };
const deps = makeDeps(async () => successQuota(), clock);
await getNativeAccountRows(deps);
clock.now += 11 * 60 * 1000; // past 10-min TTL
await getNativeAccountRows(deps);
expect(deps.fetchCount()).toBe(2);
});
});
describe('in-flight coalescing', () => {
it('shares ONE fetch across concurrent callers past TTL', async () => {
const clock = { now: 1_000_000 };
let resolveFetch: (q: ClaudeQuotaResult) => void = () => {};
const gate = new Promise<ClaudeQuotaResult>((resolve) => {
resolveFetch = resolve;
});
const deps = makeDeps(async () => gate, clock);
const p1 = getNativeAccountRows(deps);
const p2 = getNativeAccountRows(deps);
resolveFetch(successQuota());
await Promise.all([p1, p2]);
expect(deps.fetchCount()).toBe(1);
});
});
describe('Retry-After + backoff + circuit breaker', () => {
it('honors Retry-After: no fetch until the cooldown elapses', async () => {
const t0 = 1_000_000;
const clock = { now: t0 };
const deps = makeDeps(async () => rateLimitedQuota('30'), clock);
await getNativeAccountRows(deps); // fetch #1 -> 429, cooldown = t0 + 30s
expect(deps.fetchCount()).toBe(1);
// Within the 30s Retry-After cooldown -> zero network even though there is
// no cached row yet.
clock.now = t0 + 10_000;
await getNativeAccountRows(deps);
expect(deps.fetchCount()).toBe(1);
// Past the 30s cooldown -> a fetch is allowed again.
clock.now = t0 + 31_000;
await getNativeAccountRows(deps);
expect(deps.fetchCount()).toBe(2);
});
it('trips the breaker after 3 consecutive 429s, then a success closes it', async () => {
const clock = { now: 1_000_000 };
let mode: 'fail' | 'ok' = 'fail';
const deps = makeDeps(
async () => (mode === 'fail' ? rateLimitedQuota() : successQuota()),
clock
);
// Three 429s; each separated past the per-call cooldown so they actually fetch.
for (let i = 0; i < 3; i++) {
await getNativeAccountRows(deps);
// jump past TTL and any backoff cooldown
clock.now += 11 * 60 * 1000 + MAX_COOLDOWN_JUMP;
}
expect(deps.fetchCount()).toBe(3);
// Breaker is open now -> zero network even past TTL.
await getNativeAccountRows(deps);
expect(deps.fetchCount()).toBe(3);
// Advance past the 15-min breaker cooldown; allow a success which closes it.
clock.now += 16 * 60 * 1000;
mode = 'ok';
await getNativeAccountRows(deps);
expect(deps.fetchCount()).toBe(4);
// After success, breaker closed: another fetch past TTL proceeds.
clock.now += 11 * 60 * 1000;
await getNativeAccountRows(deps);
expect(deps.fetchCount()).toBe(5);
});
});
describe('stale-on-fail', () => {
it('returns the last good row when a subsequent fetch rejects', async () => {
const clock = { now: 1_000_000 };
let mode: 'ok' | 'throw' = 'ok';
const deps = makeDeps(async () => {
if (mode === 'throw') throw new Error('network down');
return successQuota();
}, clock);
await getNativeAccountRows(deps); // good row cached
mode = 'throw';
clock.now += 11 * 60 * 1000; // force re-fetch
const rows = await getNativeAccountRows(deps);
const row = rows.find((r) => r.provider === 'claude-code');
expect(row).toBeDefined();
expect(row?.quotaStatus).toBe('ok');
expect(row?.cached).toBe(true);
});
it('omits the row when the first-ever fetch fails (no prior cache)', async () => {
const clock = { now: 1_000_000 };
const deps = makeDeps(async () => {
throw new Error('network down');
}, clock);
const rows = await getNativeAccountRows(deps);
expect(rows.find((r) => r.provider === 'claude-code')).toBeUndefined();
});
});
describe('Codex path', () => {
it('maps a local Codex quota into a codex ok row', async () => {
const clock = { now: 1_000_000 };
const deps: NativeQuotaDeps = {
readCredentials: () => null, // no claude row
getCodexQuota: async () => ({
quotaPercentage: 52,
nextReset: '2026-06-09T19:00:00.000Z',
tier: 'pro',
stale: false,
}),
now: () => clock.now,
};
const rows = await getNativeAccountRows(deps);
const row = rows.find((r) => r.provider === 'codex');
expect(row?.quotaStatus).toBe('ok');
expect(row?.quota_percentage).toBe(52);
expect(row?.tier).toBe('pro');
expect(row?.health).toBe('ok');
});
it('flags health warning when the Codex source is stale', async () => {
const clock = { now: 1_000_000 };
const deps: NativeQuotaDeps = {
readCredentials: () => null,
getCodexQuota: async () => ({
quotaPercentage: 10,
nextReset: null,
tier: null,
stale: true,
}),
now: () => clock.now,
};
const rows = await getNativeAccountRows(deps);
expect(rows.find((r) => r.provider === 'codex')?.health).toBe('warning');
});
it('omits the codex row when there is no rate_limits (exec-mode)', async () => {
const clock = { now: 1_000_000 };
const deps: NativeQuotaDeps = {
readCredentials: () => null,
getCodexQuota: async () => null,
now: () => clock.now,
};
const rows = await getNativeAccountRows(deps);
expect(rows.find((r) => r.provider === 'codex')).toBeUndefined();
});
});