diff --git a/src/pages/AuthFileManagementPage.tsx b/src/pages/AuthFileManagementPage.tsx index 1bab8bd..aa6d95b 100644 --- a/src/pages/AuthFileManagementPage.tsx +++ b/src/pages/AuthFileManagementPage.tsx @@ -69,6 +69,7 @@ import { parseAuthFilePriority, resetAuthFileCooldown, setOAuthCredentialFileDisabled, + sortAuthFilesByPriority, } from '../services/authFiles'; import { authFileHealth, normalizeAuthFileCooldowns } from '../services/authFileHealth'; import { codexMetadataFor } from '../services/quotaMetadata'; @@ -175,7 +176,7 @@ export function AuthFileManagementPage() { try { const payload = await managementApi.get('/credentials'); if (!mountedRef.current || requestId !== fileRequestRef.current) return; - const nextFiles = dedupeAuthFiles(responseList(payload, 'files')); + const nextFiles = sortAuthFilesByPriority(dedupeAuthFiles(responseList(payload, 'files'))); setFileSnapshot({ files: nextFiles, receivedAtMs: Date.now(), observedAt: readString(payload, 'observed_at') }); const validQuotaKeys = new Set(nextFiles.map(quotaKey)); pruneQuotaCache(validQuotaKeys); diff --git a/src/pages/QuotaPage.tsx b/src/pages/QuotaPage.tsx index 281ec70..f54a5bd 100644 --- a/src/pages/QuotaPage.tsx +++ b/src/pages/QuotaPage.tsx @@ -34,7 +34,7 @@ import { updateQuotaCache, useQuotaCache, } from '../services/quotaCache'; -import { dedupeAuthFiles } from '../services/authFiles'; +import { dedupeAuthFiles, parseAuthFilePriority, sortAuthFilesByPriority } from '../services/authFiles'; import { useI18n } from '../i18n'; import './QuotaPage.css'; @@ -65,7 +65,7 @@ export function QuotaPage() { setError(''); try { const payload = await managementApi.get('/credentials'); - const allFiles = dedupeAuthFiles(responseList(payload, 'files')); + const allFiles = sortAuthFilesByPriority(dedupeAuthFiles(responseList(payload, 'files'))); const nextFiles = allFiles.filter((file) => !readBoolean(file, 'disabled') && providerForFile(file)); setFiles(nextFiles); const validQuotaKeys = new Set(allFiles.map(quotaKey)); @@ -188,6 +188,7 @@ export function QuotaCard({ file, quota, onRefresh, onReset }: { file: AuthFile; const provider = providerForFile(file); const name = fileName(file); const disabled = readBoolean(file, 'disabled'); + const priority = parseAuthFilePriority(file.priority) ?? 0; return (
@@ -196,7 +197,7 @@ export function QuotaCard({ file, quota, onRefresh, onReset }: { file: AuthFile;
{name}{provider ? providerMeta[provider].label : t('quota.unknownProvider')}
{quota.plan || '—'}{quota.subscriptionActiveUntil ? t('quota.expiresAt', { time: formatQuotaTimestamp(quota.subscriptionActiveUntil, locale) }) : ''}
-
{quota.status === 'success' ? t('common.enabled') : quota.status === 'loading' ? t('quota.querying') : quota.status === 'error' ? t('common.unavailable') : t('quota.notFetched')}{t('authFiles.settings.priority')} 0
+
{quota.status === 'success' ? t('common.enabled') : quota.status === 'loading' ? t('quota.querying') : quota.status === 'error' ? t('common.unavailable') : t('quota.notFetched')}{t('authFiles.settings.priority')} {priority}
diff --git a/src/services/authFiles.ts b/src/services/authFiles.ts index 9e2542c..6629b24 100644 --- a/src/services/authFiles.ts +++ b/src/services/authFiles.ts @@ -37,6 +37,19 @@ export const resetAuthFileCooldown = async ( export const authFileName = (file: AuthFileRecord) => readString(file, 'name') || translate(getCurrentLocale(), 'authFiles.unnamed'); +/** + * Present credentials in the same order used by routing: higher explicit + * priority first, then a deterministic case-insensitive filename order. + * Return a new array so callers do not mutate the API response or snapshots. + */ +export const sortAuthFilesByPriority = (files: File[]): File[] => + [...files].sort((left, right) => { + const priorityDelta = (parseAuthFilePriority(right.priority) ?? 0) + - (parseAuthFilePriority(left.priority) ?? 0); + if (priorityDelta !== 0) return priorityDelta; + return authFileName(left).localeCompare(authFileName(right), undefined, { sensitivity: 'base' }); + }); + export const isRuntimeOnlyAuthFile = (file: AuthFileRecord) => readBoolean(file, 'runtime_only', 'runtimeOnly'); diff --git a/tests/authFileQuotaPanel.test.tsx b/tests/authFileQuotaPanel.test.tsx index 0eb7a33..3af4fbb 100644 --- a/tests/authFileQuotaPanel.test.tsx +++ b/tests/authFileQuotaPanel.test.tsx @@ -25,6 +25,28 @@ it('shows every reset expiry in chronological order, including duplicates, on bo } }); +it('shows the OAuth credential priority on the quota card', () => { + const html = renderToStaticMarkup( {}} + />); + expect(html).toContain('Priority 7'); + expect(html).not.toContain('Priority 0'); +}); + +it('normalizes a string priority and keeps zero as the default', () => { + const stringPriority = renderToStaticMarkup( {}} + />); + const missingPriority = renderToStaticMarkup( {}} + />); + expect(stringPriority).toContain('Priority 3'); + expect(missingPriority).toContain('Priority 0'); +}); + it('renders real quota values with bounded bars and keeps unknown distinct from zero', () => { const html = render({ status: 'success', rows: [ { label: 'empty', remainingPercent: 0 }, { label: 'full', remainingPercent: 100 }, diff --git a/tests/authFiles.test.ts b/tests/authFiles.test.ts index 0e40b72..cad2f5e 100644 --- a/tests/authFiles.test.ts +++ b/tests/authFiles.test.ts @@ -9,6 +9,7 @@ import { oauthModelProvidersFromAuthFiles, parseAuthFilePriority, setOAuthCredentialFileDisabled, + sortAuthFilesByPriority, snapshotAuthFiles, } from '../src/services/authFiles'; @@ -181,6 +182,18 @@ describe('authentication file priority', () => { expect(normalizeAuthFilePriorityInput('1.5')).toBeNull(); }); + it('sorts credentials by descending priority, then filename', () => { + expect(sortAuthFilesByPriority([ + { name: 'zeta.json', priority: 2 }, + { name: 'Beta.json', priority: 5 }, + { name: 'alpha.json', priority: 5 }, + { name: 'default.json' }, + { name: 'negative.json', priority: -1 }, + ]).map((file) => file.name)).toEqual([ + 'alpha.json', 'Beta.json', 'zeta.json', 'default.json', 'negative.json', + ]); + }); + it('finds only credentials created or updated by the completed OAuth provider', () => { const before = snapshotAuthFiles([ { name: 'codex-old.json', provider: 'codex', modtime: 1, priority: 8 },