diff --git a/src/cliproxy/quota/quota-label-sanitizer.ts b/src/cliproxy/quota/quota-label-sanitizer.ts index 438527ea..b7279eb1 100644 --- a/src/cliproxy/quota/quota-label-sanitizer.ts +++ b/src/cliproxy/quota/quota-label-sanitizer.ts @@ -11,8 +11,8 @@ const TERMINAL_CONTROL_CHARS_REGEX = /[\u0000-\u001f\u007f-\u009f]/g; * 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; +export function sanitizeCodexFeatureLabelOrNull(value: unknown): string | null { + if (typeof value !== 'string') return null; const sanitized = value .replace(TERMINAL_ESCAPE_SEQUENCE_REGEX, '') @@ -21,5 +21,9 @@ export function sanitizeCodexFeatureLabel(value: unknown): string { .slice(0, CODEX_FEATURE_LABEL_MAX_LENGTH) .trimEnd(); - return sanitized.length > 0 ? sanitized : CODEX_FEATURE_LABEL_FALLBACK; + return sanitized.length > 0 ? sanitized : null; +} + +export function sanitizeCodexFeatureLabel(value: unknown): string { + return sanitizeCodexFeatureLabelOrNull(value) ?? CODEX_FEATURE_LABEL_FALLBACK; } diff --git a/src/commands/cliproxy/quota-subcommand.ts b/src/commands/cliproxy/quota-subcommand.ts index 2efdbd05..428d0f65 100644 --- a/src/commands/cliproxy/quota-subcommand.ts +++ b/src/commands/cliproxy/quota-subcommand.ts @@ -18,7 +18,10 @@ 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 { + sanitizeCodexFeatureLabel, + sanitizeCodexFeatureLabelOrNull, +} 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'; @@ -280,9 +283,12 @@ 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: unknown): string { - const trimmed = sanitizeCodexFeatureLabel(featureLabel); - if (!trimmed) return 'Additional'; +function prettifyCodexFeatureLabel(featureLabel: unknown, fallbackLabel?: unknown): string { + const trimmed = + sanitizeCodexFeatureLabelOrNull(featureLabel) ?? + (fallbackLabel === undefined + ? sanitizeCodexFeatureLabel(featureLabel) + : sanitizeCodexFeatureLabel(fallbackLabel)); const stripped = trimmed.replace(/^GPT-[\d.]+-Codex-/i, ''); if (stripped !== trimmed && stripped.length > 0) { return `Codex ${stripped}`; @@ -303,7 +309,7 @@ function getCodexWindowDisplayLabel( } if (window.category === 'additional') { - const pretty = prettifyCodexFeatureLabel(window.featureLabel ?? window.label); + const pretty = prettifyCodexFeatureLabel(window.featureLabel, window.label); if (window.cadence === '5h') return `${pretty} (5h)`; if (window.cadence === 'weekly') return `${pretty} (weekly)`; return pretty; diff --git a/tests/unit/commands/cliproxy-quota-subcommand.test.ts b/tests/unit/commands/cliproxy-quota-subcommand.test.ts index 5c30fc95..955ea422 100644 --- a/tests/unit/commands/cliproxy-quota-subcommand.test.ts +++ b/tests/unit/commands/cliproxy-quota-subcommand.test.ts @@ -86,18 +86,31 @@ describe('cliproxy quota subcommand failure formatting', () => { }); describe('cliproxy quota subcommand Codex label formatting', () => { - it('falls back for non-string cached Codex feature labels', async () => { + it('falls back to the cached window label for invalid Codex feature labels', async () => { const { getCodexWindowDisplayLabel } = await loadQuotaCommandTestExports(); - const label = getCodexWindowDisplayLabel({ - label: 'ignored', - resetAfterSeconds: 3600, - category: 'additional', - cadence: '5h', - featureLabel: { unexpected: true }, - } as never); + 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; - expect(label).toBe('Additional (5h)'); + 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 () => {