feat(auth-files): redesign credential management as a row-based list

- Display credentials, plans, status, request statistics, quotas, and actions
- Add pagination, enable/disable switches, expandable details, and compact quota views
- Preserve model cooldown details, countdowns, and reset controls
- Support responsive layouts, light/dark themes, and localization
- Add component and UI regression tests
This commit is contained in:
lzt404 committed 2026-10-04 17:31:52 +08:00
1 parent d29a1af69b
commit cc24f8419f
35 files changed
+1504 -116

No files matched your search

+152
View File
@@ -0,0 +1,152 @@
const { chromium } = require(process.env.PLAYWRIGHT_MODULE || 'playwright');
const assert = require('node:assert/strict');
const path = require('node:path');
const fs = require('node:fs/promises');
(async () => {
const { createServer } = await import('vite');
const react = (await import('@vitejs/plugin-react')).default;
const server = await createServer({ configFile: false, root: path.resolve(__dirname, '..'), plugins: [react()],
optimizeDeps: { entries: ['tests/fixtures/auth-file-cooldown.html'] }, logLevel: 'error',
server: { host: '127.0.0.1', port: 0, watch: null } });
let browser;
const screenshotDir = path.resolve('bin-work/auth-file-cooldown-qa');
try {
await server.listen();
const base = `http://127.0.0.1:${server.httpServer.address().port}`;
browser = await chromium.launch({ channel: 'msedge', headless: true, args: ['--no-proxy-server'] });
const page = await browser.newPage({ viewport: { width: 1280, height: 900 } });
page.setDefaultTimeout(10000);
const errors = [];
page.on('pageerror', error => errors.push(String(error)));
await page.route('**/*', route => route.request().url().startsWith(`${base}/`) ? route.continue() : route.abort());
await page.clock.install({ time: new Date('2026-10-03T04:00:00Z') });
const card = name => page.locator('.auth-file-card').filter({ has: page.locator('.auth-card-identity strong', { hasText: name }) });
const primary = () => card('01-primary.json');
const other = () => card('02-other.json');
const clear = target => target.getByRole('button', { name: '清除冷却', exact: true });
const confirmation = () => page.getByRole('alertdialog', { name: '清除凭证冷却?', exact: true });
const requests = () => page.evaluate(() => window.cooldownFixture.resetRequests);
const open = async query => {
await page.goto(`${base}/tests/fixtures/auth-file-cooldown.html?${query || ''}`);
await primary().waitFor();
};
const releaseReset = () => page.evaluate(() => window.cooldownFixture.releaseReset());
const confirmReset = async target => {
await clear(target).click();
await confirmation().waitFor();
await confirmation().getByRole('button', { name: '清除冷却', exact: true }).click();
await page.waitForFunction(() => Boolean(window.cooldownFixture.releaseReset));
};
await fs.mkdir(screenshotDir, { recursive: true });
await open();
assert.equal(await primary().locator('details').getAttribute('open'), null);
assert.equal(await clear(primary()).isVisible(), true, 'Reset is discoverable while details are collapsed');
for (const name of ['05-empty.json', '06-null.json', '07-absent.json', '08-no-index.json']) {
assert.equal(await clear(card(name)).count(), 0, `${name} must not offer an unsafe or inapplicable reset`);
}
await primary().locator('.auth-health-summary').click();
const details = primary().locator('.auth-health-body');
for (const text of ['整个凭证', 'gpt-fictional', 'claude-fictional', '额度或速率限制', '上游服务暂时异常', 'HTTP 429', 'HTTP 503', '退避等级 0']) {
assert.ok((await details.innerText()).includes(text), `Cooldown detail contains ${text}`);
}
assert.equal(await details.locator('time').count(), 4, 'Every cooldown and its observation timestamp are available');
const beforeTick = await primary().locator('.auth-health-countdown').innerText();
await page.clock.runFor(3100);
assert.notEqual(await primary().locator('.auth-health-countdown').innerText(), beforeTick, 'Countdown advances without a list reload');
assert.equal(await page.evaluate(() => window.cooldownFixture.reads), 1);
assert.ok((await card('04-expired-soon.json').innerText()).includes('冷却计时已到期'));
assert.equal(await clear(card('04-expired-soon.json')).isEnabled(), true, 'Elapsed records remain explicitly resettable');
assert.ok((await card('03-disabled-runtime.json').innerText()).includes('运行时'));
assert.ok((await card('03-disabled-runtime.json').locator('.auth-file-health').innerText()).includes('已停用'));
assert.equal(await clear(card('03-disabled-runtime.json')).isEnabled(), true, 'Runtime and disabled credentials can clear existing cooldown records');
await page.screenshot({ path: path.join(screenshotDir, 'details-light-desktop.png'), fullPage: true });
await clear(primary()).click();
await confirmation().waitFor();
assert.ok((await confirmation().innerText()).includes('01-primary.json'));
assert.deepEqual(await requests(), [], 'Opening confirmation does not submit a reset');
await confirmation().getByRole('button', { name: '取消', exact: true }).click();
assert.deepEqual(await requests(), [], 'Cancel leaves the backend untouched');
assert.equal(await primary().locator('.auth-health-records li').count(), 3);
const otherDeadline = await other().locator('.auth-health-records time').getAttribute('datetime');
const otherBefore = await other().locator('.auth-health-countdown').innerText();
await confirmReset(primary());
const pending = primary().locator('.auth-file-health button');
assert.equal(await pending.isDisabled(), true, 'Pending reset prevents duplicate submission');
await pending.evaluate(button => button.click());
const sent = await requests();
assert.equal(sent.length, 1);
assert.equal(sent[0].method, 'POST');
assert.equal(sent[0].path, '/routing/cooldown/reset');
assert.deepEqual(sent[0].body, { auth_index: 'auth-primary-123' }, 'Only the selected auth_index is sent; no all/model scope');
assert.equal(await primary().locator('.auth-health-records li').count(), 3, 'Pending request retains existing cooldown details');
await page.clock.runFor(3000);
await releaseReset();
await page.waitForFunction(() => window.cooldownFixture.reads === 2);
await primary().locator('.auth-file-health').waitFor({ state: 'detached' });
assert.equal(await other().locator('.auth-health-records li').count(), 1);
assert.equal(await other().locator('.auth-health-records time').getAttribute('datetime'), otherDeadline);
assert.notEqual(await other().locator('.auth-health-countdown').innerText(), otherBefore, 'A sibling timer continues instead of restarting or clearing');
assert.ok((await page.locator('.app-notice-stack').innerText()).includes('01-primary.json'), 'Success identifies the cleared credential');
await open('resetError=1');
await primary().locator('.auth-health-summary').click();
await confirmReset(primary());
await releaseReset();
await page.getByText(/Fixture cooldown reset failed/).waitFor();
assert.equal(await primary().locator('.auth-health-records li').count(), 3, 'A rejected reset retains the displayed records');
assert.equal(await clear(primary()).isEnabled(), true, 'Failure allows a deliberate retry');
assert.equal(await page.evaluate(() => window.cooldownFixture.reads), 1, 'Failure does not pretend a refreshed successful state');
await page.screenshot({ path: path.join(screenshotDir, 'reset-failure.png'), fullPage: true });
await open();
await confirmReset(card('03-disabled-runtime.json'));
await releaseReset();
await page.waitForFunction(() => window.cooldownFixture.reads === 2);
await clear(card('03-disabled-runtime.json')).waitFor({ state: 'detached' });
assert.ok((await card('03-disabled-runtime.json').innerText()).includes('运行时'));
assert.ok((await card('03-disabled-runtime.json').locator('.auth-file-health').innerText()).includes('已停用'), 'Clearing cooldown preserves disabled state');
await open();
await confirmReset(primary());
await page.evaluate(() => { window.cooldownFixture.holdNextRead = true; });
await page.getByRole('button', { name: '刷新', exact: true }).click();
await page.waitForFunction(() => Boolean(window.cooldownFixture.releaseRead));
await releaseReset();
await page.waitForFunction(() => window.cooldownFixture.reads === 3 && window.cooldownFixture.completedReads === 2);
await primary().waitFor();
assert.equal(await primary().locator('.auth-file-health').count(), 0);
await page.evaluate(() => window.cooldownFixture.releaseRead());
await page.waitForFunction(() => window.cooldownFixture.completedReads === 3);
await page.clock.runFor(100);
assert.equal(await primary().locator('.auth-file-health').count(), 0, 'An older list response cannot resurrect cleared cooldowns');
for (const theme of ['light', 'dark']) {
for (const width of [1280, 390]) {
await page.setViewportSize({ width, height: 900 });
await open(`theme=${theme}`);
await primary().locator('.auth-health-summary').click();
await other().locator('.auth-health-summary').click();
const sizes = await page.locator('.auth-file-card, .auth-file-health').evaluateAll(nodes => nodes.map(node => ({
className: node.className, width: node.clientWidth, scroll: node.scrollWidth,
})));
assert.ok(sizes.every(size => size.scroll <= size.width + 1), `${theme} ${width}: ${JSON.stringify(sizes)}`);
assert.equal(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth + 1), true, `No page overflow at ${theme} ${width}`);
await page.screenshot({ path: path.join(screenshotDir, `details-${theme}-${width}.png`), fullPage: true });
await primary().screenshot({ path: path.join(screenshotDir, `card-${theme}-${width}.png`) });
await clear(primary()).click();
await confirmation().waitFor();
assert.equal(await confirmation().evaluate(node => node.scrollWidth <= node.clientWidth + 1), true, 'Confirmation fits viewport');
await page.screenshot({ path: path.join(screenshotDir, `confirmation-${theme}-${width}.png`), fullPage: true });
await confirmation().screenshot({ path: path.join(screenshotDir, `dialog-${theme}-${width}.png`) });
await confirmation().getByRole('button', { name: '取消', exact: true }).click();
}
}
assert.deepEqual(errors, []);
console.log('PASS: cooldown models/reasons/countdowns, confirmation and cancel, single auth_index POST, pending duplicate protection, isolated success refresh, failure preservation, stale response protection, unknown/empty/missing-index guards, elapsed records, runtime/disabled state, light/dark desktop/mobile layout.');
console.log(`Screenshots: ${screenshotDir}`);
} finally { await browser?.close(); await server.close(); }
})().catch(error => { console.error(error); process.exitCode = 1; });
+146
View File
@@ -0,0 +1,146 @@
const { chromium } = require(process.env.PLAYWRIGHT_MODULE || 'playwright');
const assert = require('node:assert/strict');
const path = require('node:path');
const fs = require('node:fs/promises');
(async () => {
const { createServer } = await import('vite');
const react = (await import('@vitejs/plugin-react')).default;
const server = await createServer({ configFile: false, root: path.resolve(__dirname, '..'), plugins: [react()],
optimizeDeps: { entries: ['tests/fixtures/auth-file-list.html'] }, logLevel: 'error',
server: { host: '127.0.0.1', port: 0, watch: null } });
let browser;
const screenshotDir = path.resolve('bin-work/auth-file-list-qa');
try {
await server.listen();
const base = `http://127.0.0.1:${server.httpServer.address().port}`;
browser = await chromium.launch({ channel: 'msedge', headless: true, args: ['--no-proxy-server'] });
const page = await browser.newPage({ viewport: { width: 1800, height: 1000 } });
page.setDefaultTimeout(10000);
const errors = [], externalRequests = [];
page.on('pageerror', error => errors.push(String(error)));
await page.route('**/*', route => {
if (route.request().url().startsWith(`${base}/`)) return route.continue();
externalRequests.push(route.request().url());
return route.abort();
});
await page.clock.install({ time: new Date('2026-10-03T04:00:00Z') });
const cards = () => page.locator('.auth-file-card');
const card = number => cards().filter({ has: page.locator(`.auth-card-filename[title="${String(number).padStart(2, '0')}-account.json"]`) });
const pagination = () => page.locator('.auth-list-pagination');
const next = () => pagination().getByRole('button', { name: '下一页', exact: true });
const previous = () => pagination().getByRole('button', { name: '上一页', exact: true });
const open = async (query = '') => {
await page.goto(`${base}/tests/fixtures/auth-file-list.html?${query}`);
await card(1).waitFor();
await page.waitForFunction(() => window.authFileListFixture.reads === 1);
};
const assertFits = async label => {
const dimensions = await cards().evaluateAll(nodes => nodes.map(node => ({ width: node.clientWidth, scroll: node.scrollWidth })));
assert.ok(dimensions.every(size => size.scroll <= size.width + 1), `${label}: cards overflow ${JSON.stringify(dimensions)}`);
assert.equal(await page.evaluate(() => document.documentElement.scrollWidth <= innerWidth + 1), true, `${label}: page overflow`);
};
await fs.mkdir(screenshotDir, { recursive: true });
await open();
assert.equal(await cards().count(), 10, 'Default page contains ten of twelve credentials');
assert.equal(await previous().isDisabled(), true);
assert.ok((await pagination().innerText()).includes('1 / 2'));
assert.equal(await page.locator('.auth-file-list-head > span').count(), 7);
const cells = await card(1).locator('.auth-credential-row > .auth-list-cell').evaluateAll(nodes => nodes.map(node => {
const bounds = node.getBoundingClientRect(); return { left: bounds.left, right: bounds.right };
}));
assert.equal(cells.length, 7, 'Desktop row includes all seven columns');
assert.ok(cells.slice(1).every((cell, index) => cell.left >= cells[index].right - 1), 'Desktop columns stay in one horizontal row');
await next().click();
await card(11).waitFor();
assert.equal(await cards().count(), 2);
assert.equal(await next().isDisabled(), true);
assert.ok((await pagination().innerText()).includes('2 / 2'));
await page.locator('.auth-files-toolbar input').fill('account');
await card(1).waitFor();
assert.equal(await cards().count(), 10, 'Search resets pagination even when all credentials match');
assert.equal(await previous().isDisabled(), true);
await next().click();
await page.locator('.auth-files-toolbar select').first().selectOption({ label: 'Codex' });
await card(1).waitFor();
assert.equal(await previous().isDisabled(), true, 'Provider filter resets to the first page');
await page.locator('.auth-files-toolbar select').first().selectOption('all');
await next().click();
await page.locator('.auth-files-toolbar select').last().selectOption('disabled');
await card(3).waitFor();
assert.equal(await cards().count(), 2, 'Disabled filter retains matching rows');
assert.equal(await previous().isDisabled(), true);
await page.locator('.auth-files-toolbar select').last().selectOption('all');
await pagination().locator('select').selectOption('20');
await card(12).waitFor();
assert.equal(await cards().count(), 12, 'Page size twenty shows all twelve credentials');
assert.equal(await next().isDisabled(), true);
const detailsButton = card(1).locator('.auth-list-details-button');
assert.equal(await card(1).locator('.auth-list-details').isVisible(), false);
await detailsButton.click();
assert.equal(await detailsButton.getAttribute('aria-expanded'), 'true');
assert.equal(await card(1).locator('.auth-list-details').isVisible(), true);
assert.ok((await card(1).locator('.auth-list-details').innerText()).includes('Fictional account note'));
await detailsButton.click();
assert.equal(await card(1).locator('.auth-list-details').isVisible(), false);
const extraQuota = card(4).locator('.credential-quota-more');
assert.equal(await extraQuota.getAttribute('open'), null);
assert.equal(await extraQuota.getByText('Additional model B', { exact: true }).isVisible(), false);
await extraQuota.locator('summary').click();
assert.equal(await extraQuota.getByText('Additional model B', { exact: true }).isVisible(), true, 'All additional quotas remain accessible');
assert.ok((await extraQuota.innerText()).includes('No quota limit supplied'));
assert.equal(await card(1).locator('.auth-file-request-block').count(), 20);
await card(1).locator('.auth-file-request-block').nth(18).focus();
assert.equal(await card(1).getByRole('tooltip').isVisible(), true, 'Recent request details are keyboard accessible');
await page.keyboard.press('Escape');
await card(1).locator('.auth-file-request-block').nth(18).blur();
assert.equal(await card(2).getByRole('button', { name: '清除冷却', exact: true }).isVisible(), true);
await card(2).locator('.auth-health-summary').click();
assert.ok((await card(2).locator('.auth-health-body').innerText()).includes('gpt-fictional-layout'), 'Model cooldown details remain available');
await card(1).getByRole('switch').click();
await page.waitForFunction(() => window.authFileListFixture.reads === 2);
assert.equal(await card(1).getByRole('switch').getAttribute('aria-checked'), 'false');
let updates = await page.evaluate(() => window.authFileListFixture.requests.filter(request => request.method === 'PATCH'));
assert.equal(updates.length, 1);
assert.equal(updates[0].path, '/credentials/status');
assert.deepEqual(updates[0].body, { name: '01-account.json', disabled: true });
await card(1).getByRole('switch').click();
await page.waitForFunction(() => window.authFileListFixture.reads === 3);
updates = await page.evaluate(() => window.authFileListFixture.requests.filter(request => request.method === 'PATCH'));
assert.deepEqual(updates[1].body, { name: '01-account.json', disabled: false });
assert.equal(await card(1).getByRole('switch').getAttribute('aria-checked'), 'true');
assert.equal(await card(3).getByRole('switch').getAttribute('aria-checked'), 'false', 'Sibling status stays unchanged');
await card(12).getByRole('button', { name: '删除', exact: true }).click();
const deleteDialog = page.getByRole('alertdialog', { name: '删除', exact: true });
await deleteDialog.waitFor();
await deleteDialog.getByRole('button', { name: '删除', exact: true }).click();
await card(12).waitFor({ state: 'detached' });
assert.equal(await cards().count(), 11);
assert.deepEqual(await page.evaluate(() => window.authFileListFixture.requests.find(request => request.method === 'DELETE')?.query), { name: '12-account.json' });
assert.deepEqual(await page.evaluate(() => window.authFileListFixture.unhandled), []);
const cases = ['light', 'dark'].flatMap(theme => [1800, 1280, 390].map(width => ({ theme, width, locale: 'zh-CN' })));
cases.push({ theme: 'light', width: 1800, locale: 'en' }, { theme: 'light', width: 390, locale: 'en' });
for (const { theme, width, locale } of cases) {
await page.setViewportSize({ width, height: 1000 });
await open(`theme=${theme}&locale=${locale}`);
await assertFits(`${locale} ${theme} ${width}`);
await page.screenshot({ path: path.join(screenshotDir, `list-${locale}-${theme}-${width}.png`), fullPage: true });
await card(1).locator('.auth-list-details-button').click();
await card(2).locator('.auth-health-summary').click();
await card(4).locator('.credential-quota-more > summary').click();
await assertFits(`${locale} ${theme} ${width} expanded`);
await card(4).screenshot({ path: path.join(screenshotDir, `expanded-quota-${locale}-${theme}-${width}.png`) });
assert.deepEqual(await page.evaluate(() => window.authFileListFixture.unhandled), []);
}
assert.deepEqual(errors, []);
assert.deepEqual(externalRequests, []);
console.log('PASS: offline credential list, seven desktop columns, ten/two pagination, search/provider/status reset, page size twenty, details, all quota rows, request keyboard tooltip, cooldown controls, exact status PATCH, deletion, light/dark 1800/1280/390 and English desktop/mobile without page or card overflow.');
console.log(`Screenshots: ${screenshotDir}`);
} finally { await browser?.close(); await server.close(); }
})().catch(error => { console.error(error); process.exitCode = 1; });
+79
View File
@@ -0,0 +1,79 @@
import { describe, expect, it } from 'bun:test';
import { renderToStaticMarkup } from 'react-dom/server';
import { I18nProvider } from '../src/i18n';
import { AuthFileQuotaPanel } from '../src/components/AuthFileQuotaPanel';
import { AuthFileRequestStatus } from '../src/components/AuthFileRequestStatus';
import type { QuotaState } from '../src/services/quotaService';
const renderQuota = (quota: QuotaState, compact = true) => renderToStaticMarkup(
<I18nProvider><AuthFileQuotaPanel file={{ name: 'test.json', provider: 'codex' }} quota={quota}
disabled={false} onRefresh={() => {}} onReset={() => {}} compact={compact} /></I18nProvider>,
);
const renderRequests = (file: Record<string, unknown>, compact = true) => renderToStaticMarkup(
<I18nProvider><AuthFileRequestStatus file={file} compact={compact} /></I18nProvider>,
);
describe('compact credential panels', () => {
it('keeps extra quota rows accessible through native details and preserves reset information', () => {
const quota: QuotaState = {
status: 'success', plan: 'Plan only shown in the plan column', resetCredits: 2, resetCreditsApplicable: 0,
rows: [
{ label: 'Five hour', remainingPercent: 24 },
{ label: 'Weekly', remainingPercent: 68 },
{ label: 'Extra model', remainingPercent: 0, detail: 'Additional quota detail' },
],
};
const compact = renderQuota(quota);
const beforeDetails = compact.split('<details')[0];
expect(beforeDetails).toContain('Five hour');
expect(beforeDetails).toContain('Weekly');
expect(beforeDetails).not.toContain('Extra model');
expect(compact).toMatch(/<details[^>]*><summary>Details \(\+1\)<\/summary>.*Extra model.*Additional quota detail.*<\/details>/);
expect(compact).toContain('title="Reset Quota"');
expect(compact).toContain('2 manual resets');
expect(compact).toContain('Currently applicable: 0');
expect(compact).not.toContain('credential-quota-refresh');
expect(compact).not.toContain(quota.plan!);
const full = renderQuota(quota, false);
expect(full).not.toContain('<details');
expect(full).toContain(quota.plan!);
expect(full).toContain('credential-quota-refresh');
});
it('preserves quota loading and error feedback in compact mode', () => {
expect(renderQuota({ status: 'loading', rows: [], pendingAction: 'reset' })).toContain('aria-busy="true"');
expect(renderQuota({ status: 'error', rows: [], error: 'Quota server unavailable' })).toContain('Quota server unavailable');
});
it('uses the latest nonempty request bucket time while retaining all twenty interactive buckets', () => {
const file = {
success: 91, failed: 9, modified: 'Unrelated modification time',
recent_requests: [
{ time: '09:00–09:10', success: 4, failed: 0 },
{ time: '09:10–09:20', success: 0, failed: 1 },
{ time: '09:20–09:30', success: 0, failed: 0 },
],
};
const compact = renderRequests(file);
expect(compact).toMatch(/auth-file-requests-latest[^>]*>09:10–09:20<\/div>/);
expect(compact.match(/class="auth-file-request-block"/g)).toHaveLength(20);
expect(compact).toContain('aria-label="09:10–09:20');
expect(compact).not.toContain('Unrelated modification time');
expect(compact).not.toContain('auth-file-requests-rate');
expect(compact).not.toContain('auth-file-requests-counts');
expect(renderRequests(file, false)).toContain('auth-file-requests-counts');
});
it('does not invent a recent request time for empty, unavailable or untimed statistics', () => {
const empty = renderRequests({ recent_requests: [{ time: '10:00–10:10', success: 0, failed: 0 }] });
const unavailable = renderRequests({});
const untimed = renderRequests({ recent_requests: [{ success: 1, failed: 0 }] });
for (const html of [empty, unavailable, untimed]) {
expect(html).toMatch(/auth-file-requests-latest[^>]*>—<\/div>/);
}
expect(empty).toContain('No requests');
expect(unavailable).toContain('The core did not provide recent request statistics');
});
});
+135
View File
@@ -0,0 +1,135 @@
import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
import { clearMocks, mockIPC } from '@tauri-apps/api/mocks';
import { authFileCooldownResetIndex, resetAuthFileCooldown } from '../src/services/authFiles';
import {
cooldownReasonKey,
cooldownRemainingSeconds,
normalizeAuthFileCooldowns,
summarizeAuthFileCooldowns,
} from '../src/services/authFileHealth';
describe('credential cooldown reset', () => {
let originalWindow: PropertyDescriptor | undefined;
beforeEach(() => {
originalWindow = Object.getOwnPropertyDescriptor(globalThis, 'window');
Object.defineProperty(globalThis, 'window', { value: {}, writable: true, configurable: true });
});
afterEach(() => {
clearMocks();
if (originalWindow) Object.defineProperty(globalThis, 'window', originalWindow);
else Reflect.deleteProperty(globalThis, 'window');
});
it('uses only an auth index and accepts runtime and disabled credentials', () => {
expect(authFileCooldownResetIndex({ auth_index: ' auth-one ', disabled: true })).toBe('auth-one');
expect(authFileCooldownResetIndex({ authIndex: 0, runtime_only: true })).toBe('0');
expect(authFileCooldownResetIndex({ auth_index: 'runtime-key', account_type: 'api_key' })).toBe('runtime-key');
expect(authFileCooldownResetIndex({ auth_index: null, authIndex: 'alias' })).toBe('alias');
expect(authFileCooldownResetIndex({ name: 'file.json', id: 'auth-id' })).toBeUndefined();
for (const auth_index of [undefined, null, '', ' ', {}, [], true, NaN, Infinity, -1, 1.5]) {
expect(authFileCooldownResetIndex({ auth_index })).toBeUndefined();
}
});
it('posts a raw auth_index operation through the v8 management bridge', async () => {
const requests: unknown[] = [];
mockIPC((command, args) => {
expect(command).toBe('management_request');
requests.push(args?.request);
return { status: 'ok', auth_index: 'auth-one', models: ['model-a', 'model-b'] };
});
expect(await resetAuthFileCooldown(' auth-one ')).toEqual({
status: 'ok', auth_index: 'auth-one', models: ['model-a', 'model-b'],
});
expect(requests).toEqual([{
method: 'POST', path: '/routing/cooldown/reset', body: { auth_index: 'auth-one' },
query: undefined, timeoutMs: undefined,
}]);
});
it('rejects a missing index before making a request', async () => {
let called = false;
const api = { post: async () => { called = true; return {}; } };
await expect(resetAuthFileCooldown(' ', api)).rejects.toThrow();
expect(called).toBe(false);
});
it('requires a successful acknowledgement for the selected credential', async () => {
for (const response of [undefined, null, [], {},
{ status: 'error', auth_index: 'auth-one', error: 'operation failed' },
{ status: 'ok', auth_index: 'another-index' },
{ status: 'ok' },
{ status: 'ok', auth_index: 'auth-one', models: [null] },
{ status: 'ok', auth_index: 'auth-one', models: 'model-a' },
]) {
await expect(resetAuthFileCooldown('auth-one', { post: async () => response })).rejects.toThrow();
}
for (const models of [[], null, undefined]) {
expect(await resetAuthFileCooldown('auth-one', {
post: async () => ({ status: 'ok', auth_index: 'auth-one', models }),
})).toEqual({ status: 'ok', auth_index: 'auth-one', models: [] });
}
});
it('preserves backend and transport failures without reporting successful reset', async () => {
for (const message of [
'Management API error (404): auth not found',
'Management API error (404): 404 page not found',
'Management API error (401): unauthorized',
'Management API error (500): failed to reset quota',
'Management API request failed: connection refused',
]) {
const failure = new Error(message);
mockIPC(() => { throw failure; });
await expect(resetAuthFileCooldown('auth-one')).rejects.toBe(failure);
}
});
});
describe('cooldown snapshot and timer invariants', () => {
const receivedAtMs = 10_000;
const model = {
scope: 'model', model_key: 'model-a', reason: 'quota',
retry_at: '2040-01-01T00:00:32Z', remaining_seconds: 32,
};
it('counts each model once and does not infer timers from diagnostic backoff', () => {
const snapshot = normalizeAuthFileCooldowns([
model, { ...model, remaining_seconds: 10, backoff_level: 100 },
], receivedAtMs)!;
const record = snapshot.records![1];
expect(cooldownRemainingSeconds(record, receivedAtMs, receivedAtMs - 5000)).toBe(10);
expect(cooldownRemainingSeconds(record, receivedAtMs, receivedAtMs + 1001)).toBe(9);
expect(cooldownRemainingSeconds(record, receivedAtMs, receivedAtMs + 10_000)).toBe(0);
expect(summarizeAuthFileCooldowns(snapshot, receivedAtMs)).toMatchObject({ modelCount: 1, earliestSeconds: 10 });
const elapsed = summarizeAuthFileCooldowns(snapshot, receivedAtMs + 40_000);
expect(elapsed.rows).toHaveLength(2);
expect(elapsed).toMatchObject({ elapsed: true, modelCount: 0, earliestSeconds: 0 });
});
it('treats absent, unknown, fresh empty and expired snapshots as separate states', () => {
expect(normalizeAuthFileCooldowns(undefined, receivedAtMs)).toBeUndefined();
expect(normalizeAuthFileCooldowns(null, receivedAtMs)?.records).toBeNull();
expect(normalizeAuthFileCooldowns([], receivedAtMs)?.records).toEqual([]);
const expired = normalizeAuthFileCooldowns([model], receivedAtMs)!;
expect(summarizeAuthFileCooldowns(expired, receivedAtMs + 33_000).elapsed).toBe(true);
const refreshed = normalizeAuthFileCooldowns([], receivedAtMs + 33_000)!;
expect(summarizeAuthFileCooldowns(refreshed, receivedAtMs + 33_000).elapsed).toBe(false);
expect(refreshed.records).toEqual([]);
});
it('normalizes diagnostics without displaying arbitrary reason values or invalid dates', () => {
for (const reason of [null, {}, 429, true, '']) {
const snapshot = normalizeAuthFileCooldowns([{ ...model, reason }], receivedAtMs, 'invalid');
expect(snapshot?.observedAt).toBeUndefined();
expect(snapshot?.records?.[0].reason).toBe('unknown');
}
for (const reason of ['future_reason', '__proto__', 'constructor', '<script>']) {
const record = normalizeAuthFileCooldowns([{ ...model, reason }], receivedAtMs)!.records![0];
expect(cooldownReasonKey(record.reason)).toBe('authFiles.health.reason.unknown');
}
expect(normalizeAuthFileCooldowns([
{ ...model, remaining_seconds: Number.MAX_SAFE_INTEGER },
], receivedAtMs)?.records).toBeNull();
});
});
+49
View File
@@ -0,0 +1,49 @@
import { describe, expect, it } from 'bun:test';
import { renderToStaticMarkup } from 'react-dom/server';
import { AuthFileHealthStatus } from '../src/components/AuthFileHealthStatus';
import { I18nProvider } from '../src/i18n';
const receivedAtMs = Date.now();
const cooldown = {
scope: 'model', model_key: 'model-a', reason: 'quota',
retry_at: '2026-01-01T10:00:32Z', remaining_seconds: 32, http_status: 429,
};
const render = (file: Record<string, unknown>, options: {
receivedAtMs?: number; resetting?: boolean; resetDisabled?: boolean; onReset?: () => void;
} = {}) => renderToStaticMarkup(<I18nProvider>
<AuthFileHealthStatus file={file} receivedAtMs={receivedAtMs} onReset={() => {}} {...options} />
</I18nProvider>);
describe('credential cooldown actions', () => {
it('makes model cooldowns and the clear action visible without opening details', () => {
const html = render({ status: 'error', cooldowns: [cooldown] });
const summary = html.slice(0, html.indexOf('</summary>'));
expect(summary).toContain('<strong>1 models cooling down</strong>');
expect(summary).toContain('Quota or rate limit');
expect(html.slice(html.indexOf('</details>'))).toContain('Clear cooldown</button>');
expect(html).toContain('HTTP 429');
});
it.each([undefined, null, [], {}, [null], [{ ...cooldown, remaining_seconds: 0 }]].map((cooldowns) => ({ cooldowns })))(
'does not expose clear for absent, empty or unknown cooldowns: %j', ({ cooldowns }) => {
expect(render({ status: 'error', cooldowns })).not.toContain('Clear cooldown</button>');
},
);
it('keeps elapsed cooldowns resettable without reporting restored availability', () => {
const html = render({ status: 'active', cooldowns: [cooldown] }, { receivedAtMs: receivedAtMs - 60_000 });
expect(html).toContain('Cooldown timer elapsed');
expect(html).toContain('Elapsed · confirmation needed');
expect(html).toContain('Clear cooldown</button>');
expect(html).not.toContain('Available');
});
it('honors unavailable and pending reset actions', () => {
const file = { status: 'error', cooldowns: [cooldown] };
expect(render(file, { onReset: undefined })).not.toContain('<button');
expect(render(file, { resetDisabled: true })).toMatch(/<button[^>]*disabled=""/);
const pending = render(file, { resetting: true });
expect(pending).toMatch(/<button[^>]*disabled=""[^>]*aria-busy="true"/);
expect(pending).toContain('Clearing…</button>');
});
});
+38
View File
@@ -0,0 +1,38 @@
import { describe, expect, it } from 'bun:test';
import { renderToStaticMarkup } from 'react-dom/server';
import { AuthFileUsageSummary } from '../src/components/AuthFileUsageSummary';
import { I18nProvider } from '../src/i18n';
const renderSummary = (file: Record<string, unknown>) => renderToStaticMarkup(
<I18nProvider><AuthFileUsageSummary file={file} /></I18nProvider>,
);
describe('credential cumulative usage', () => {
it('uses cumulative counters even when recent requests have a different success rate', () => {
const html = renderSummary({ success: 1900, failed: 100, recent_requests: [{ success: 0, failed: 20 }] });
expect(html).toContain('aria-label="Total Requests: 2,000"');
expect(html).toContain('aria-label="Success rate 95%"');
expect(html).toContain('aria-label="Success 1,900"');
expect(html).toContain('aria-label="Failed 100"');
expect(html).toContain('Totals since the core started; reset on restart.');
expect(html).toContain('Core session totals');
expect(html).not.toContain('tokens');
});
it('preserves unavailable counters instead of treating a recent-only window or partial count as totals', () => {
const recentOnly = renderSummary({ recent_requests: [{ success: 12, failed: 0 }] });
expect(recentOnly).toContain('aria-label="Total Requests: —"');
expect(recentOnly).toContain('aria-label="Success rate —"');
const partial = renderSummary({ success: 12 });
expect(partial).toContain('aria-label="Success 12"');
expect(partial).toContain('aria-label="Failed —"');
expect(partial).toContain('aria-label="Total Requests: —"');
expect(partial).toContain('aria-label="Success rate —"');
});
it('shows zero requests without inventing a success percentage', () => {
const html = renderSummary({ success: 0, failed: 0 });
expect(html).toContain('aria-label="Total Requests: 0"');
expect(html).toContain('aria-label="Success rate —"');
});
});
+2
View File
@@ -0,0 +1,2 @@
<!doctype html>
<html><head><meta name="viewport" content="width=device-width, initial-scale=1.0" /></head><body><div id="root"></div><script type="module" src="/tests/fixtures/auth-file-cooldown.tsx"></script></body></html>
+93
View File
@@ -0,0 +1,93 @@
import React from 'react';
import { createRoot } from 'react-dom/client';
import { mockIPC } from '@tauri-apps/api/mocks';
import { I18nProvider } from '../../src/i18n';
import { AuthFileManagementPage } from '../../src/pages/AuthFileManagementPage';
import '../../src/styles/index.css';
// Fictional metadata only. All management IPC is handled locally by this fixture.
const params = new URLSearchParams(location.search);
localStorage.setItem('easy-cli-proxy-api.locale', params.get('locale') || 'zh-CN');
document.documentElement.dataset.theme = params.get('theme') || 'light';
const startedAt = Date.now();
type MockFile = Record<string, unknown> & { name: string; cooldowns?: Record<string, unknown>[] | null };
type Request = { path: string; method: string; body?: Record<string, unknown> };
const timer = (seconds: number, model?: string, reason = 'quota', status = 429) => ({
scope: model ? 'model' : 'credential', reason, model_key: model,
retry_at: new Date(startedAt + seconds * 1000).toISOString(), remaining_seconds: seconds,
http_status: status, backoff_level: 0,
});
const files: MockFile[] = [
{ name: '01-primary.json', auth_index: 'auth-primary-123', status: 'error', unavailable: true,
status_message: 'quota exhausted', cooldowns: [timer(55), timer(55, 'gpt-fictional'), timer(180, 'claude-fictional', 'transient_error', 503)] },
{ name: '02-other.json', auth_index: 'auth-other-456', status: 'error',
cooldowns: [timer(45, 'model-with-a-deliberately-long-name-for-responsive-wrapping-verification', 'model_not_supported', 404)] },
{ name: '03-disabled-runtime.json', auth_index: 'auth-runtime-789', status: 'disabled', disabled: true,
runtime_only: true, cooldowns: [timer(120, 'runtime-model')] },
{ name: '04-expired-soon.json', auth_index: 'auth-expired-321', status: 'error', cooldowns: [timer(2, 'expired-model')] },
{ name: '05-empty.json', auth_index: 'auth-empty', status: 'active', cooldowns: [] },
{ name: '06-null.json', auth_index: 'auth-null', status: 'error', cooldowns: null },
{ name: '07-absent.json', auth_index: 'auth-absent', status: 'error' },
{ name: '08-no-index.json', status: 'error', cooldowns: [timer(120, 'missing-index-model')] },
];
const state = {
files,
reads: 0,
resetRequests: [] as Request[],
failReset: params.has('resetError'),
releaseReset: undefined as (() => void) | undefined,
holdNextRead: false,
releaseRead: undefined as (() => void) | undefined,
completedReads: 0,
};
(window as typeof window & { cooldownFixture: typeof state }).cooldownFixture = state;
mockIPC(async (cmd, args) => {
if (cmd === 'set_app_locale') return null;
if (cmd !== 'management_request') throw new Error(`Unhandled fixture command: ${cmd}`);
const request = args?.request as Request;
if (request.path === '/credentials' && request.method === 'GET') {
state.reads += 1;
const elapsed = Math.floor((Date.now() - startedAt) / 1000);
const snapshot = {
observed_at: new Date().toISOString(),
files: state.files.map(file => ({
provider: 'codex', source: 'file', size: 1024, updated_at: '2026-10-01T00:00:00Z',
success: 20, failed: 2, ...file,
...(Array.isArray(file.cooldowns) ? { cooldowns: file.cooldowns.map(record => ({
...record, remaining_seconds: Math.max(1, Number(record.remaining_seconds) - elapsed),
})) } : {}),
})),
};
if (state.holdNextRead) {
state.holdNextRead = false;
await new Promise<void>(resolve => { state.releaseRead = resolve; });
state.releaseRead = undefined;
}
state.completedReads += 1;
return snapshot;
}
if (request.path === '/routing/cooldown/reset' && request.method === 'POST') {
state.resetRequests.push(structuredClone(request));
await new Promise<void>(resolve => { state.releaseReset = resolve; });
state.releaseReset = undefined;
if (state.failReset) throw new Error('Fixture cooldown reset failed');
const target = state.files.find(file => file.auth_index === request.body?.auth_index);
if (!target) throw new Error('Fixture reset requires one known auth_index');
target.cooldowns = [];
target.unavailable = false;
target.status_message = '';
if (!target.disabled) target.status = 'active';
return { status: 'ok', auth_index: target.auth_index, models: ['gpt-fictional', 'claude-fictional'] };
}
throw new Error(`Unhandled fixture request: ${request.method} ${request.path}`);
});
createRoot(document.getElementById('root')!).render(
<I18nProvider>
<div className="app-shell">
<aside className="sidebar" aria-hidden="true" />
<div className="workspace"><main className="content"><AuthFileManagementPage /></main></div>
</div>
</I18nProvider>,
);
+2
View File
@@ -0,0 +1,2 @@
<!doctype html>
<html><head><meta name="viewport" content="width=device-width, initial-scale=1.0" /></head><body><div id="root"></div><script type="module" src="/tests/fixtures/auth-file-list.tsx"></script></body></html>
+89
View File
@@ -0,0 +1,89 @@
import React from 'react';
import { createRoot } from 'react-dom/client';
import { mockIPC } from '@tauri-apps/api/mocks';
import { I18nProvider } from '../../src/i18n';
import { AuthFileManagementPage } from '../../src/pages/AuthFileManagementPage';
import { updateQuotaCache } from '../../src/services/quotaCache';
import { quotaKey, type QuotaState } from '../../src/services/quotaService';
import '../../src/styles/index.css';
// Fictional credentials and local IPC only; this fixture never connects to an upstream service.
const params = new URLSearchParams(location.search);
localStorage.setItem('easy-cli-proxy-api.locale', params.get('locale') || 'zh-CN');
document.documentElement.dataset.theme = params.get('theme') || 'light';
const startedAt = Date.now();
type Request = { path: string; method: string; body?: Record<string, unknown>; query?: Record<string, string> };
const files: Record<string, unknown>[] = Array.from({ length: 12 }, (_, index) => ({
name: `${String(index + 1).padStart(2, '0')}-account.json`,
email: index === 0 ? 'long-fictional-account-for-layout@example.test' : `account-${index + 1}@example.test`,
auth_index: `fixture-account-${index + 1}`, provider: index === 3 ? 'claude' : index === 4 ? 'xai' : 'codex',
source: 'file', size: 1800 + index * 350, updated_at: '2026-10-01T00:00:00Z',
disabled: index === 2 || index === 6, status: index === 2 || index === 6 ? 'disabled' : 'active',
priority: index % 3, plan_type: index % 3 === 0 ? 'Pro 20x' : 'Team',
note: index === 0 ? 'Fictional account note, visible after opening credential details.' : '',
success: 1200 + index * 2450, failed: 3 + index * 97,
recent_requests: Array.from({ length: 20 }, (_, bucket) => ({
time: `${String(8 + Math.floor(bucket / 6)).padStart(2, '0')}:${String(bucket % 6 * 10).padStart(2, '0')}`,
success: bucket === 19 ? 0 : (bucket + index) % 7 + 1,
failed: bucket === 19 || (bucket + index) % 5 ? 0 : 2,
})),
cooldowns: index === 1 ? [{ scope: 'model', model_key: 'gpt-fictional-layout', reason: 'quota',
retry_at: new Date(startedAt + 180000).toISOString(), remaining_seconds: 180, http_status: 429, backoff_level: 0 }] : [],
}));
updateQuotaCache(Object.fromEntries(files.map((file, index) => {
const quota: QuotaState = {
status: 'success', plan: String(file.plan_type),
subscriptionActiveUntil: index === 0 ? '2026-11-01T00:00:00Z' : undefined,
resetCredits: index === 0 ? 2 : undefined,
resetCreditsApplicable: index === 0 ? 0 : undefined,
rows: index === 4 ? [{ label: 'Paid API account', remainingPercent: null, detail: 'Upstream does not report remaining quota.' }]
: [
{ label: '5h', remainingPercent: index === 1 ? 0 : index === 2 ? 8 : 72 - index * 3, resetAtMs: startedAt + 3 * 3600000 },
{ label: 'Weekly', remainingPercent: 83 - index * 5, resetAtMs: startedAt + 5 * 86400000 },
...(index === 3 ? [
{ label: 'Additional model A', remainingPercent: 34, resetAtMs: startedAt + 3600000 },
{ label: 'Additional model B', remainingPercent: null, detail: 'No quota limit supplied for this model.' },
] : []),
],
};
return [quotaKey(file), quota];
})));
const state = { files, requests: [] as Request[], unhandled: [] as string[], reads: 0 };
(window as typeof window & { authFileListFixture: typeof state }).authFileListFixture = state;
mockIPC(async (cmd, args) => {
if (cmd === 'set_app_locale') return null;
if (cmd !== 'management_request') {
state.unhandled.push(cmd);
throw new Error(`Unhandled fixture command: ${cmd}`);
}
const request = args?.request as Request;
state.requests.push(structuredClone(request));
if (request.path === '/credentials' && request.method === 'GET') {
state.reads += 1;
return { observed_at: new Date().toISOString(), files: structuredClone(state.files) };
}
if (request.path === '/credentials/status' && request.method === 'PATCH') {
const target = state.files.find(file => file.name === request.body?.name);
if (!target || typeof request.body?.disabled !== 'boolean') throw new Error('Fixture status update needs a known name and boolean');
target.disabled = request.body.disabled;
target.status = target.disabled ? 'disabled' : 'active';
return { status: 'ok' };
}
if (request.path === '/credentials' && request.method === 'DELETE') {
const index = state.files.findIndex(file => file.name === request.query?.name);
if (index < 0) throw new Error('Fixture deletion requires a known filename');
state.files.splice(index, 1);
return { status: 'ok' };
}
state.unhandled.push(`${request.method} ${request.path}`);
throw new Error(`Unhandled fixture request: ${request.method} ${request.path}`);
});
createRoot(document.getElementById('root')!).render(
<I18nProvider><div className="app-shell">
<aside className="sidebar" aria-hidden="true" />
<div className="workspace"><main className="content"><AuthFileManagementPage /></main></div>
</div></I18nProvider>,
);
+3 -3
View File
@@ -23,16 +23,16 @@ describe('简易模式、首页、配置与版本管理导航', () => {
test('内核运行后解锁其他功能页', () => {
expect(canOpenAppPage('config', true)).toBe(true);
expect(canOpenAppPage('agents', true)).toBe(true);
expect(canOpenAppPage('quota', true)).toBe(true);
});
});
describe('OAuth 子页面导航', () => {
test('认证文件和额度查询收纳在 OAuth 页面内', () => {
expect(oauthSubpages.map((page) => page.id)).toEqual(['login', 'authFiles', 'quota']);
test('OAuth 页面保留登录和认证文件,额度查询独立导航', () => {
expect(oauthSubpages.map((page) => page.id)).toEqual(['login', 'authFiles']);
expect(oauthSubpages.map((page) => page.labelKey)).toEqual([
'oauth.title',
'authFiles.title',
'quota.title',
]);
});
});
+1 -1
View File
@@ -58,7 +58,7 @@ describe('控件自身反馈', () => {
expect(attributes?.find((attribute) => attribute.name.getText(source) === 'className')?.initializer?.getText(source))
.toBe("{`${disabled ? 'primary-button' : 'secondary-button'} compact-button auth-card-toggle`}");
expect(attributes?.find((attribute) => attribute.name.getText(source) === 'disabled')?.initializer?.getText(source))
.toBe('{busy || !isOAuthCredentialFile(file)}');
.toBe('{busy || resettingCooldown || !isOAuthCredentialFile(file)}');
const styles = readFileSync(new URL('../src/styles.css', import.meta.url), 'utf8');
expect(styles).toMatch(/\.real-auth-file-row\.disabled\s*>\s*:not\(\.auth-file-actions\)\s*\{\s*opacity:\s*0\.68;/);
expect(styles).not.toMatch(/\.real-auth-file-row\.disabled\s*\{[^}]*opacity:/);
+6 -2
View File
@@ -189,12 +189,16 @@ const viewports = [
await assertReachable(page.locator('.provider-row-actions button').first(), `${label} provider action`);
await open('OAuth', '.oauth-subpage-tabs');
assert.equal(await page.locator('#oauth-subpage-tab-quota').count(), 0, `${label}: quota has a separate sidebar entry`);
await tab('oauth-subpage-tab-login', '.oauth-card');
await assertNoPageOverflow(`${label} OAuth login`);
await tab('oauth-subpage-tab-authFiles', '.auth-file-card');
await assertNoPageOverflow(`${label} OAuth credentials`);
await tab('oauth-subpage-tab-quota', '.real-quota-card');
await assertNoPageOverflow(`${label} OAuth quota`);
await open('额度查询', '.quota-page .real-quota-card');
assert.equal(await page.locator('.nav-section button.active').innerText(), '额度查询', `${label}: quota activates its sidebar entry`);
assert.equal(await page.locator('.quota-page h1').innerText(), '额度查询', `${label}: standalone quota page has its own heading`);
assert.equal(await page.locator('.oauth-subpage-tabs').count(), 0, `${label}: standalone quota page does not contain OAuth navigation`);
await assertNoPageOverflow(`${label} quota`);
await open('智能体配置', '.agent-config-panel');
await page.waitForFunction(() => !document.querySelector('.agent-client-list-heading button')?.disabled);