Files
ccs/tests/unit/commands/cliproxy-quota-subcommand.test.ts
T

373 lines
12 KiB
TypeScript

import { describe, expect, it, spyOn } from 'bun:test';
import { mkdtempSync, rmSync } from 'node:fs';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { displayClaudeQuotaSection } from '../../../src/commands/cliproxy/quota-subcommand/sections/claude';
import { displayCodexQuotaSection } from '../../../src/commands/cliproxy/quota-subcommand/sections/codex';
import { displayGeminiCliQuotaSection } from '../../../src/commands/cliproxy/quota-subcommand/sections/gemini-cli';
import { formatQuotaBar } from '../../../src/commands/cliproxy/quota-subcommand/format-helpers';
import { handleDoctor } from '../../../src/commands/cliproxy/quota-subcommand/handlers';
import type { AccountInfo } from '../../../src/cliproxy/accounts/types';
import type { AllAccountsQuotaResult } from '../../../src/cliproxy/quota/quota-fetcher';
import * as quotaFetcher from '../../../src/cliproxy/quota/quota-fetcher';
import * as accountManager from '../../../src/cliproxy/accounts/account-manager';
async function loadQuotaCommandTestExports() {
const moduleId = Date.now() + Math.random();
const mod = await import(
`../../../src/commands/cliproxy/quota-subcommand?cliproxy-quota-subcommand=${moduleId}`
);
return mod.__testExports;
}
describe('cliproxy quota subcommand failure formatting', () => {
it('renders Claude usage-probe 429 as a warning with an inference-safe hint', () => {
const output: string[] = [];
const originalLog = console.log;
console.log = (...args: unknown[]) => output.push(args.map(String).join(' '));
try {
displayClaudeQuotaSection([
{
account: 'healthy@example.com',
quota: {
success: false,
windows: [],
coreUsage: { fiveHour: null, weekly: null },
lastUpdated: 1,
accountId: 'healthy@example.com',
error: 'Claude usage status temporarily unavailable',
errorCode: 'usage_probe_unavailable',
actionHint:
'Inference may still be available. Retry the Claude quota status check later.',
httpStatus: 429,
errorDetail: 'retry-after:0',
retryable: true,
},
},
]);
} finally {
console.log = originalLog;
}
expect(output.join('\n')).toContain('[!] healthy@example.com');
expect(output.join('\n')).not.toContain('[X] healthy@example.com');
expect(output.join('\n')).toContain('Claude usage status temporarily unavailable');
expect(output.join('\n')).toContain('Inference may still be available');
expect(output.join('\n')).toContain('HTTP 429 | Code: usage_probe_unavailable | Retryable');
expect(output.join('\n')).toContain('Detail: retry-after:0');
});
it('builds Gemini failure lines with the remediation hint, code, and detail', async () => {
const { getQuotaFailureDisplayEntries } = await loadQuotaCommandTestExports();
const entries = getQuotaFailureDisplayEntries({
error: 'Google requires you to verify this account before using Gemini CLI quota.',
actionHint:
'Complete the Google account verification mentioned above, then retry quota refresh.',
httpStatus: 403,
errorCode: 'PERMISSION_DENIED',
errorDetail: 'ACCOUNT_VERIFICATION_REQUIRED',
retryable: false,
});
expect(entries).toEqual([
{
tone: 'error',
text: 'Google requires you to verify this account before using Gemini CLI quota.',
},
{
tone: 'info',
text: 'Complete the Google account verification mentioned above, then retry quota refresh.',
},
{
tone: 'dim',
text: 'HTTP 403 | Code: PERMISSION_DENIED',
},
{
tone: 'dim',
text: 'Detail: ACCOUNT_VERIFICATION_REQUIRED',
},
]);
});
it('marks retryable failures in the CLI diagnostics line', async () => {
const { getQuotaFailureDisplayEntries } = await loadQuotaCommandTestExports();
const entries = getQuotaFailureDisplayEntries({
error: 'Gemini quota service unavailable (HTTP 503)',
actionHint: 'Retry later. This looks like a temporary Google upstream problem.',
httpStatus: 503,
errorCode: 'provider_unavailable',
errorDetail: 'Service temporarily unavailable',
retryable: true,
});
expect(entries[2]).toEqual({
tone: 'dim',
text: 'HTTP 503 | Code: provider_unavailable | Retryable',
});
});
it('suppresses duplicate error detail lines', async () => {
const { getQuotaFailureDisplayEntries } = await loadQuotaCommandTestExports();
const entries = getQuotaFailureDisplayEntries({
error: 'Internal Server Error',
errorDetail: 'Internal Server Error',
});
expect(entries).toEqual([
{
tone: 'error',
text: 'Internal Server Error',
},
]);
});
it('prefers live quota tier over stale account tier', async () => {
const { resolveDisplayedTier } = await loadQuotaCommandTestExports();
expect(resolveDisplayedTier('unknown', 'pro')).toBe('pro');
expect(resolveDisplayedTier('pro', 'ultra')).toBe('ultra');
expect(resolveDisplayedTier('pro', 'unknown')).toBe('pro');
});
});
describe('cliproxy quota subcommand Codex label formatting', () => {
it('falls back to the cached window label for invalid Codex feature labels', async () => {
const { getCodexWindowDisplayLabel } = await loadQuotaCommandTestExports();
const cases = [
{ featureLabel: '', cadence: '5h', expected: 'Codex Spark (5h)' },
{ featureLabel: ' ', cadence: 'weekly', expected: 'Codex Spark (weekly)' },
{
featureLabel: '\u001b[2J\u001b]52;c;payload\u0007',
cadence: '5h',
expected: 'Codex Spark (5h)',
},
{ featureLabel: { unexpected: true }, cadence: '5h', expected: 'Codex Spark (5h)' },
] as const;
for (const { featureLabel, cadence, expected } of cases) {
const label = getCodexWindowDisplayLabel({
label: 'GPT-5.3-Codex-Spark',
resetAfterSeconds: 3600,
category: 'additional',
cadence,
featureLabel,
} as never);
expect(label).toBe(expected);
}
});
it('removes terminal control characters from cached Codex feature labels', async () => {
const { getCodexWindowDisplayLabel } = await loadQuotaCommandTestExports();
const label = getCodexWindowDisplayLabel({
label: 'ignored',
resetAfterSeconds: 3600,
category: 'additional',
cadence: 'weekly',
featureLabel: '\u001b[2JGPT-5.3-Codex-Spark\u001b]52;c;payload\u0007',
});
expect(label).toBe('Codex Spark (weekly)');
expect(label).not.toContain('\u001b');
expect(label).not.toContain('\u0007');
});
});
function captureConsoleLog(fn: () => void): string {
const output: string[] = [];
const originalLog = console.log;
console.log = (...args: unknown[]) => output.push(args.map(String).join(' '));
try {
fn();
} finally {
console.log = originalLog;
}
return output.join('\n');
}
describe('cliproxy quota remaining percent labels and ASCII compliance', () => {
it('renders formatQuotaBar with pure ASCII characters', () => {
const bar100 = formatQuotaBar(100);
const bar75 = formatQuotaBar(75);
const bar50 = formatQuotaBar(50);
const bar25 = formatQuotaBar(25);
const bar5 = formatQuotaBar(5);
const bar0 = formatQuotaBar(0);
expect(bar100).toBe(`[${'#'.repeat(20)}]`);
expect(bar75).toBe(`[${'#'.repeat(15)}${' '.repeat(5)}]`);
expect(bar50).toBe(`[${'+'.repeat(10)}${' '.repeat(10)}]`);
expect(bar25).toBe(`[${'+'.repeat(5)}${' '.repeat(15)}]`);
expect(bar5).toBe(`[${'-'.repeat(1)}${' '.repeat(19)}]`);
expect(bar0).toBe(`[${' '.repeat(20)}]`);
// Strict ASCII verification
expect(bar100).toMatch(/^[\x00-\x7F]+$/);
expect(bar75).toMatch(/^[\x00-\x7F]+$/);
expect(bar50).toMatch(/^[\x00-\x7F]+$/);
expect(bar25).toMatch(/^[\x00-\x7F]+$/);
expect(bar5).toMatch(/^[\x00-\x7F]+$/);
expect(bar0).toMatch(/^[\x00-\x7F]+$/);
});
it('renders Claude remaining percent with a remaining suffix and pure ASCII', () => {
const text = captureConsoleLog(() => {
displayClaudeQuotaSection([
{
account: 'claude@example.com',
quota: {
success: true,
windows: [
{
rateLimitType: 'five_hour',
label: '5h usage limit',
status: 'allowed',
utilization: 0.28,
usedPercent: 28,
remainingPercent: 72,
resetAt: null,
},
],
coreUsage: { fiveHour: null, weekly: null },
lastUpdated: 1,
accountId: 'claude@example.com',
},
},
]);
});
expect(text).toContain('72% remaining');
expect(text).toMatch(/^[\x00-\x7F\n\r]+$/);
});
it('renders Codex remaining percent with a remaining suffix and pure ASCII', () => {
const text = captureConsoleLog(() => {
displayCodexQuotaSection([
{
account: 'codex@example.com',
quota: {
success: true,
windows: [
{
label: 'Primary',
usedPercent: 28,
remainingPercent: 72,
resetAfterSeconds: null,
resetAt: null,
category: 'usage',
cadence: '5h',
},
],
planType: 'pro',
lastUpdated: 1,
accountId: 'codex@example.com',
},
},
]);
});
expect(text).toContain('72% remaining');
expect(text).toMatch(/^[\x00-\x7F\n\r]+$/);
});
it('renders Gemini CLI remaining percent with a remaining suffix and pure ASCII', () => {
const text = captureConsoleLog(() => {
displayGeminiCliQuotaSection([
{
account: 'gemini@example.com',
quota: {
success: true,
buckets: [
{
id: 'gemini-flash-series',
label: 'Gemini Flash Series',
tokenType: null,
remainingFraction: 0.72,
remainingPercent: 72,
resetTime: null,
modelIds: ['gemini-flash'],
},
],
projectId: null,
lastUpdated: 1,
accountId: 'gemini@example.com',
},
},
]);
});
expect(text).toContain('72% remaining');
expect(text).toMatch(/^[\x00-\x7F\n\r]+$/);
});
it('renders Doctor Antigravity quota with remaining suffix and pure ASCII', async () => {
const tempHome = mkdtempSync(join(tmpdir(), 'ccs-doctor-test-'));
const originalCcsHome = process.env.CCS_HOME;
process.env.CCS_HOME = tempHome;
const mockAccount: AccountInfo = {
id: 'agy@example.com',
email: 'agy@example.com',
provider: 'agy',
isDefault: true,
tokenFile: 'agy-token.json',
createdAt: new Date().toISOString(),
};
const mockQuotaResult: AllAccountsQuotaResult = {
accounts: [
{
account: mockAccount,
quota: {
success: true,
models: [
{
name: 'gemini-2.5-flash',
percentage: 85,
resetTime: null,
},
],
projectId: 'proj-123',
lastUpdated: 1,
accountId: 'agy@example.com',
},
},
],
projectGroups: {},
};
const accountsSpy = spyOn(accountManager, 'getProviderAccounts').mockReturnValue([mockAccount]);
const fetchSpy = spyOn(quotaFetcher, 'fetchAllProviderQuotas').mockResolvedValue(
mockQuotaResult
);
try {
const output: string[] = [];
const originalLog = console.log;
console.log = (...args: unknown[]) => output.push(args.map(String).join(' '));
try {
await handleDoctor(false);
} finally {
console.log = originalLog;
}
const text = output.join('\n');
expect(text).toContain('gemini-2.5-flash');
expect(text).toContain('85% remaining');
expect(text).toMatch(/^[\x00-\x7F\n\r]+$/);
} finally {
accountsSpy.mockRestore();
fetchSpy.mockRestore();
if (originalCcsHome !== undefined) {
process.env.CCS_HOME = originalCcsHome;
} else {
delete process.env.CCS_HOME;
}
rmSync(tempHome, { recursive: true, force: true });
}
});
});