fix(cliproxy): harden quota guards and review follow-ups

This commit is contained in:
Tam Nhu Tran committed 2026-02-22 02:35:07 +07:00
1 parent 8c790f41ff
commit ca58cb5a08
11 files changed
+251 -58

No files matched your search

@@ -315,7 +315,7 @@ export function AccountCard({
</div>
</TooltipTrigger>
<TooltipContent side="top" className="max-w-xs">
{quota && <QuotaTooltipContent quota={quota} resetTime={resetTime} />}
<QuotaTooltipContent quota={quota} resetTime={resetTime} />
</TooltipContent>
</Tooltip>
</TooltipProvider>
@@ -424,7 +424,7 @@ export function AccountItem({
)}
</TooltipTrigger>
<TooltipContent side="bottom" className="max-w-xs">
{quota && <QuotaTooltipContent quota={quota} resetTime={nextReset} />}
<QuotaTooltipContent quota={quota} resetTime={nextReset} />
</TooltipContent>
</Tooltip>
</TooltipProvider>
@@ -22,7 +22,7 @@ import {
} from '@/lib/utils';
interface QuotaTooltipContentProps {
quota: UnifiedQuotaResult;
quota: UnifiedQuotaResult | null | undefined;
resetTime: string | null;
}
@@ -62,7 +62,13 @@ function getClaudeWindowDisplayLabel(rateLimitType: string, fallback: string): s
* Uses type guards for proper TypeScript narrowing
*/
export function QuotaTooltipContent({ quota, resetTime }: QuotaTooltipContentProps) {
if (!quota?.success) return null;
if (!quota) {
return <p className="text-xs text-muted-foreground">Loading quota...</p>;
}
if (!quota.success) {
return <p className="text-xs text-destructive">{quota.error || 'Failed to load quota'}</p>;
}
// Antigravity (agy) provider tooltip
if (isAgyQuotaResult(quota)) {
+70 -18
View File
@@ -618,45 +618,97 @@ export type UnifiedQuotaResult =
| GeminiCliQuotaResult
| GhcpQuotaResult;
function isRecord(value: unknown): value is Record<string, unknown> {
return typeof value === 'object' && value !== null;
}
function isFiniteNumber(value: unknown): value is number {
return typeof value === 'number' && Number.isFinite(value);
}
/** Type guard: Check if quota result is from Antigravity (agy) provider */
export function isAgyQuotaResult(quota: UnifiedQuotaResult): quota is QuotaResult {
return 'models' in quota && Array.isArray((quota as QuotaResult).models);
if (!isRecord(quota)) return false;
const models = (quota as Partial<QuotaResult>).models;
return typeof quota.success === 'boolean' && Array.isArray(models);
}
/** Type guard: Check if quota result is from Codex provider */
export function isCodexQuotaResult(quota: UnifiedQuotaResult): quota is CodexQuotaResult {
return (
'windows' in quota && 'planType' in quota && Array.isArray((quota as CodexQuotaResult).windows)
if (!isRecord(quota)) return false;
const candidate = quota as Partial<CodexQuotaResult>;
if (typeof candidate.success !== 'boolean') return false;
if (!Array.isArray(candidate.windows)) return false;
if (!('planType' in candidate)) return false;
return candidate.windows.every(
(window) =>
isRecord(window) &&
typeof window.label === 'string' &&
isFiniteNumber(window.usedPercent) &&
isFiniteNumber(window.remainingPercent)
);
}
/** Type guard: Check if quota result is from Claude provider */
export function isClaudeQuotaResult(quota: UnifiedQuotaResult): quota is ClaudeQuotaResult {
return (
'windows' in quota &&
!('planType' in quota) &&
Array.isArray((quota as ClaudeQuotaResult).windows)
if (!isRecord(quota)) return false;
const candidate = quota as Partial<ClaudeQuotaResult>;
if (typeof candidate.success !== 'boolean') return false;
if (!Array.isArray(candidate.windows)) return false;
if ('planType' in candidate) return false;
return candidate.windows.every(
(window) =>
isRecord(window) &&
typeof window.rateLimitType === 'string' &&
isFiniteNumber(window.remainingPercent) &&
typeof window.status === 'string'
);
}
/** Type guard: Check if quota result is from Gemini CLI provider */
export function isGeminiQuotaResult(quota: UnifiedQuotaResult): quota is GeminiCliQuotaResult {
return 'buckets' in quota && Array.isArray((quota as GeminiCliQuotaResult).buckets);
if (!isRecord(quota)) return false;
const candidate = quota as Partial<GeminiCliQuotaResult>;
if (typeof candidate.success !== 'boolean') return false;
if (!Array.isArray(candidate.buckets)) return false;
return candidate.buckets.every(
(bucket) =>
isRecord(bucket) &&
typeof bucket.id === 'string' &&
isFiniteNumber(bucket.remainingFraction) &&
isFiniteNumber(bucket.remainingPercent) &&
Array.isArray(bucket.modelIds)
);
}
/** Type guard: Check if quota result is from GitHub Copilot (ghcp) provider */
export function isGhcpQuotaResult(quota: UnifiedQuotaResult): quota is GhcpQuotaResult {
const candidate = quota as GhcpQuotaResult;
const snapshots = candidate.snapshots as Record<string, unknown> | null | undefined;
if (!isRecord(quota)) return false;
return (
'snapshots' in quota &&
typeof snapshots === 'object' &&
snapshots !== null &&
'premiumInteractions' in snapshots &&
'chat' in snapshots &&
'completions' in snapshots
);
const candidate = quota as Partial<GhcpQuotaResult>;
const snapshots = candidate.snapshots as Record<string, unknown> | null | undefined;
if (typeof candidate.success !== 'boolean') return false;
if (!isRecord(snapshots)) return false;
const snapshotKeys: Array<keyof GhcpQuotaResult['snapshots']> = [
'premiumInteractions',
'chat',
'completions',
];
return snapshotKeys.every((key) => {
const snapshot = snapshots[key] as Record<string, unknown> | undefined;
return (
isRecord(snapshot) &&
isFiniteNumber(snapshot.percentRemaining) &&
isFiniteNumber(snapshot.percentUsed)
);
});
}
// ==================== Unified Quota Helpers ====================
+52
View File
@@ -15,6 +15,7 @@ import {
getProviderMinQuota,
getProviderResetTime,
isAgyQuotaResult,
isClaudeQuotaResult,
isCodexQuotaResult,
isGeminiQuotaResult,
isGhcpQuotaResult,
@@ -22,6 +23,7 @@ import {
import type {
CodexQuotaWindow,
CodexQuotaResult,
ClaudeQuotaResult,
GeminiCliBucket,
GeminiCliQuotaResult,
GhcpQuotaResult,
@@ -1002,6 +1004,56 @@ describe('isCodexQuotaResult', () => {
});
});
describe('isClaudeQuotaResult', () => {
it('returns true for valid Claude quota result', () => {
const quota: ClaudeQuotaResult = {
success: true,
windows: [
{
rateLimitType: 'five_hour',
label: 'Session limit',
status: 'allowed',
utilization: 0.5,
usedPercent: 50,
remainingPercent: 50,
resetAt: '2026-01-30T12:00:00Z',
},
],
coreUsage: {
fiveHour: {
rateLimitType: 'five_hour',
label: 'Session limit',
remainingPercent: 50,
resetAt: '2026-01-30T12:00:00Z',
status: 'allowed',
},
weekly: null,
},
lastUpdated: Date.now(),
};
expect(isClaudeQuotaResult(quota)).toBe(true);
});
it('returns false for Codex quota result', () => {
const quota: CodexQuotaResult = {
success: true,
windows: [],
planType: 'free',
lastUpdated: Date.now(),
};
expect(isClaudeQuotaResult(quota as unknown as ClaudeQuotaResult)).toBe(false);
});
it('returns false when Claude windows are malformed', () => {
const malformed = {
success: true,
windows: [{ rateLimitType: 'five_hour', label: 'Session limit' }],
lastUpdated: Date.now(),
};
expect(isClaudeQuotaResult(malformed as unknown as ClaudeQuotaResult)).toBe(false);
});
});
describe('isGeminiQuotaResult', () => {
it('returns true for valid Gemini quota result', () => {
const quota: GeminiCliQuotaResult = {