mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 14:16:43 +00:00
fix(cliproxy): sanitize Codex quota labels
This commit is contained in:
@@ -382,6 +382,44 @@ describe('Codex Quota Fetcher', () => {
|
||||
expect(windows[0].featureLabel).toBe('Custom-Feature');
|
||||
expect(windows[0].usedPercent).toBe(50);
|
||||
});
|
||||
|
||||
it('should remove terminal control characters from additional limit labels', () => {
|
||||
const response = {
|
||||
additional_rate_limits: [
|
||||
{
|
||||
limit_name: '\u001b[2JGPT-5.3-Codex-Spark\u001b]52;c;payload\u0007',
|
||||
rate_limit: {
|
||||
primary_window: { used_percent: 25, reset_after_seconds: 3600 },
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const windows = buildCodexQuotaWindows(response);
|
||||
|
||||
expect(windows).toHaveLength(1);
|
||||
expect(windows[0].featureLabel).toBe('GPT-5.3-Codex-Spark');
|
||||
expect(windows[0].label).toBe('GPT-5.3-Codex-Spark (Primary)');
|
||||
});
|
||||
|
||||
it('should bound additional limit labels before storing them', () => {
|
||||
const response = {
|
||||
additional_rate_limits: [
|
||||
{
|
||||
limit_name: `Feature-${'x'.repeat(120)}`,
|
||||
rate_limit: {
|
||||
primary_window: { used_percent: 25, reset_after_seconds: 3600 },
|
||||
},
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const windows = buildCodexQuotaWindows(response);
|
||||
|
||||
expect(windows).toHaveLength(1);
|
||||
expect(windows[0].featureLabel).toHaveLength(80);
|
||||
expect(windows[0].label).toBe(`${windows[0].featureLabel} (Primary)`);
|
||||
});
|
||||
});
|
||||
|
||||
describe('buildCodexCoreUsageSummary', () => {
|
||||
|
||||
@@ -11,6 +11,7 @@ import { getAuthDir } from '../config/config-generator';
|
||||
import { getAccount, getProviderAccounts, getPausedDir } from '../accounts/account-manager';
|
||||
import { sanitizeEmail, isTokenExpired } from '../auth/auth-utils';
|
||||
import type { CodexQuotaResult, CodexQuotaWindow, CodexCoreUsageSummary } from './quota-types';
|
||||
import { sanitizeCodexFeatureLabel } from './quota-label-sanitizer';
|
||||
import { extractCanonicalEmailFromAccountId } from '../accounts/email-account-identity';
|
||||
|
||||
/** ChatGPT backend API base URL */
|
||||
@@ -58,8 +59,8 @@ interface CodexRateLimitWindow {
|
||||
* Each entry surfaces its own primary/secondary windows under a feature-specific limit name.
|
||||
*/
|
||||
interface CodexAdditionalRateLimit {
|
||||
limit_name?: string;
|
||||
limitName?: string;
|
||||
limit_name?: unknown;
|
||||
limitName?: unknown;
|
||||
metered_feature?: string;
|
||||
meteredFeature?: string;
|
||||
rate_limit?: CodexRateLimitWindow;
|
||||
@@ -380,11 +381,7 @@ function buildCodexQuotaWindows(payload: CodexUsageResponse): CodexQuotaWindow[]
|
||||
const entryRateLimit = entry.rate_limit || entry.rateLimit;
|
||||
if (!entryRateLimit) continue;
|
||||
|
||||
const rawFeatureLabel = entry.limit_name ?? entry.limitName;
|
||||
const featureLabel =
|
||||
typeof rawFeatureLabel === 'string' && rawFeatureLabel.trim().length > 0
|
||||
? rawFeatureLabel.trim()
|
||||
: 'Additional';
|
||||
const featureLabel = sanitizeCodexFeatureLabel(entry.limit_name ?? entry.limitName);
|
||||
addWindow(
|
||||
`${featureLabel} (Primary)`,
|
||||
entryRateLimit.primary_window || entryRateLimit.primaryWindow,
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
const CODEX_FEATURE_LABEL_FALLBACK = 'Additional';
|
||||
const CODEX_FEATURE_LABEL_MAX_LENGTH = 80;
|
||||
const TERMINAL_ESCAPE_SEQUENCE_REGEX =
|
||||
/\u001b\][^\u0007\u001b]*(?:\u0007|\u001b\\)?|\u001b\[[0-?]*[ -/]*[@-~]/g;
|
||||
const TERMINAL_CONTROL_CHARS_REGEX = /[\u0000-\u001f\u007f-\u009f]/g;
|
||||
|
||||
/**
|
||||
* Sanitize upstream Codex feature labels before storing or rendering them.
|
||||
*
|
||||
* The quota API is not schema-validated at runtime, so additional-rate-limit
|
||||
* labels must be constrained to safe, printable strings before they reach the
|
||||
* terminal.
|
||||
*/
|
||||
export function sanitizeCodexFeatureLabel(value: unknown): string {
|
||||
if (typeof value !== 'string') return CODEX_FEATURE_LABEL_FALLBACK;
|
||||
|
||||
const sanitized = value
|
||||
.replace(TERMINAL_ESCAPE_SEQUENCE_REGEX, '')
|
||||
.replace(TERMINAL_CONTROL_CHARS_REGEX, '')
|
||||
.trim()
|
||||
.slice(0, CODEX_FEATURE_LABEL_MAX_LENGTH)
|
||||
.trimEnd();
|
||||
|
||||
return sanitized.length > 0 ? sanitized : CODEX_FEATURE_LABEL_FALLBACK;
|
||||
}
|
||||
@@ -18,6 +18,7 @@ import {
|
||||
} from '../../cliproxy/accounts/account-manager';
|
||||
import { fetchAllProviderQuotas } from '../../cliproxy/quota/quota-fetcher';
|
||||
import { fetchAllCodexQuotas } from '../../cliproxy/quota/quota-fetcher-codex';
|
||||
import { sanitizeCodexFeatureLabel } from '../../cliproxy/quota/quota-label-sanitizer';
|
||||
import { fetchAllClaudeQuotas } from '../../cliproxy/quota/quota-fetcher-claude';
|
||||
import { pickMostRestrictiveClaudeWeeklyWindow } from '../../cliproxy/quota/quota-fetcher-claude-normalizer';
|
||||
import { fetchAllGeminiCliQuotas } from '../../cliproxy/quota/quota-fetcher-gemini-cli';
|
||||
@@ -279,8 +280,8 @@ function inferCodeReviewCadence(
|
||||
* Strip a leading "GPT-X.Y-Codex-" prefix from a feature label and turn the
|
||||
* remainder into a Codex-prefixed display name. Other labels pass through unchanged.
|
||||
*/
|
||||
function prettifyCodexFeatureLabel(featureLabel: string): string {
|
||||
const trimmed = featureLabel.trim();
|
||||
function prettifyCodexFeatureLabel(featureLabel: unknown): string {
|
||||
const trimmed = sanitizeCodexFeatureLabel(featureLabel);
|
||||
if (!trimmed) return 'Additional';
|
||||
const stripped = trimmed.replace(/^GPT-[\d.]+-Codex-/i, '');
|
||||
if (stripped !== trimmed && stripped.length > 0) {
|
||||
@@ -302,7 +303,7 @@ function getCodexWindowDisplayLabel(
|
||||
}
|
||||
|
||||
if (window.category === 'additional') {
|
||||
const pretty = prettifyCodexFeatureLabel(window.featureLabel || window.label || 'Additional');
|
||||
const pretty = prettifyCodexFeatureLabel(window.featureLabel ?? window.label);
|
||||
if (window.cadence === '5h') return `${pretty} (5h)`;
|
||||
if (window.cadence === 'weekly') return `${pretty} (weekly)`;
|
||||
return pretty;
|
||||
@@ -850,7 +851,9 @@ const QUOTA_PROVIDER_RUNTIME: Record<QuotaSupportedProvider, QuotaProviderRuntim
|
||||
};
|
||||
|
||||
export const __testExports = {
|
||||
getCodexWindowDisplayLabel,
|
||||
getQuotaFailureDisplayEntries,
|
||||
prettifyCodexFeatureLabel,
|
||||
resolveDisplayedTier,
|
||||
};
|
||||
|
||||
|
||||
@@ -84,3 +84,35 @@ describe('cliproxy quota subcommand failure formatting', () => {
|
||||
expect(resolveDisplayedTier('pro', 'unknown')).toBe('pro');
|
||||
});
|
||||
});
|
||||
|
||||
describe('cliproxy quota subcommand Codex label formatting', () => {
|
||||
it('falls back for non-string cached Codex feature labels', async () => {
|
||||
const { getCodexWindowDisplayLabel } = await loadQuotaCommandTestExports();
|
||||
|
||||
const label = getCodexWindowDisplayLabel({
|
||||
label: 'ignored',
|
||||
resetAfterSeconds: 3600,
|
||||
category: 'additional',
|
||||
cadence: '5h',
|
||||
featureLabel: { unexpected: true },
|
||||
} as never);
|
||||
|
||||
expect(label).toBe('Additional (5h)');
|
||||
});
|
||||
|
||||
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');
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user