feat(mock): expand browser mock coverage for providers and edge cases

Add realistic quota, credential, usage, OAuth, and error scenarios.
Support in-memory mutations for imports, resets, cooldowns, and deletions.
Add browser UI and runtime regression tests with mock usage documentation.
This commit is contained in:
lzt404 committed 2026-10-05 16:10:37 +08:00
1 parent 9f451f8299
commit 46a9d3d449
7 files changed
+416 -59

No files matched your search

+1
View File
@@ -13,6 +13,7 @@
"test:ui:api-access-layout": "node tests/api-access-layout-ui.cjs",
"test:ui:responsive": "node tests/responsive-layout-ui.cjs",
"test:ui:settings": "node tests/config-settings-ui.cjs",
"test:ui:browser-mock": "node tests/browser-mock-ui.cjs",
"screenshots": "node scripts/capture-readme-screenshots.cjs",
"tauri": "tauri"
},
+52
View File
@@ -0,0 +1,52 @@
# Browser preview mocks
Run `bun run dev` and open the Vite URL in a regular browser. Mock IPC is installed
only in browser development builds; Tauri and production builds use the real backend.
No backend or upstream account is required. Data is fictional and mutations are
in memory, isolated per page load. Reload to reset changes.
Use the bottom toolbar or query parameters:
| URL option | Scenario |
| --- | --- |
| `?mock=running` | Running core, configured providers, usage records and mixed credential states |
| `?mock=stopped` | Installed but stopped core; start it to explore the populated pages |
| `?mock=empty` | Uninstalled core with empty credentials, providers and usage; install/start it to inspect empty states |
| `?mock=error` | Main backend reads fail so error and retry UI can be inspected |
| `?mock=off` | Disable mock IPC |
| `?mock=running&mockDelay=500` | Delay each IPC call by 500 ms (maximum 5000 ms) |
The selected scenario is remembered locally; an explicit `mock` query overrides it.
## Credential and quota coverage
The running dataset includes more than ten OAuth files, plus a runtime credential
which the authentication-file list excludes. It covers Codex, Claude, Antigravity,
Kimi, xAI and Devin. Examples include long account names and quota labels, multiple
quota windows, a disabled account, an exhausted account with model cooldown,
an expired OAuth token, an empty upstream quota response, unknown remaining quota,
a paid API account and an account with no recent requests.
Quota replies pass through the normal application parsers. Codex and Claude reset
credits are tracked per account: consuming one decreases its balance and refreshes
its usage windows. Credential edits, enable/disable, cooldown reset, deletion,
JSON import/replacement and OAuth cancellation update the same in-memory state.
Malformed imports and missing credentials fail explicitly. Usage event filters
(time, model, provider, source, API key, failure/cancellation) run before pagination.
Unknown IPC commands, management routes and upstream URLs fail explicitly instead
of returning fake success. Add new contracts to `browserMockRuntime.ts`,
`quotaMock.ts` or `pluginMock.ts` when implementing new backend-facing UI.
Other desktop operations may still return preview-only results; the mock does not
launch real apps, write credentials to disk or make upstream API requests.
## Verification
```sh
bun test tests/browserMockRuntime.test.ts tests/browserMockQuota.test.ts
node tests/browser-mock-ui.cjs
```
The browser test uses Playwright (and Microsoft Edge on Windows), opens the real
app, and checks quota rendering, pagination and credential mutations while blocking
external HTTP requests. Its screenshot is saved under `bin-work/`.
+80 -59
View File
@@ -1,5 +1,6 @@
import { applyTemplateChanges, readTemplatePath, sameTemplateValue, type TemplateConfigChange } from '../services/templateConfig';
import { createPluginMock } from './pluginMock';
import { createQuotaMock, createQuotaMockFiles } from './quotaMock';
export type BrowserMockScenario = 'running' | 'stopped' | 'empty' | 'error';
export type BrowserMockMode = BrowserMockScenario | 'off';
@@ -47,7 +48,7 @@ const SHARED_PROVIDER_FIELDS = new Set([
const clone = <Value,>(value: Value): Value => structuredClone(value);
const sleep = (delayMs: number) => delayMs > 0
? new Promise<void>((resolve) => window.setTimeout(resolve, delayMs))
? new Promise<void>((resolve) => globalThis.setTimeout(resolve, delayMs))
: Promise.resolve();
const asObject = (value: unknown): JsonObject => (
@@ -169,10 +170,10 @@ function createUsageEvents() {
source: index % 2 === 0 ? 'codex' : 'claude-code',
source_display: index % 2 === 0 ? 'Codex' : 'Claude Code',
failed,
canceled: false,
canceled: index === 9,
failure_status: failed ? 429 : 0,
failure_body: failed ? 'Mock rate limit exceeded' : '',
provider: index % 2 === 0 ? 'OpenAI OAuth' : 'Claude OAuth',
provider: index % 2 === 0 ? 'openai-oauth' : 'claude-oauth',
model: models[index % models.length],
alias: '',
reasoning_effort: index % 2 === 0 ? 'high' : 'medium',
@@ -303,6 +304,7 @@ function createState(scenario: BrowserMockScenario) {
modtime: Date.now() - 45 * 60_000,
excluded_models: ['claude-legacy-*'],
},
...createQuotaMockFiles(),
{
name: 'gemini-runtime',
provider: 'gemini',
@@ -498,9 +500,10 @@ function createState(scenario: BrowserMockScenario) {
versionSource: { source: 'github', gitcodeAvailable: true, customMirrors: ['https://gh-proxy.example.com/'] },
// Native groups are authoritative; legacy routes only adapt their own view.
providerConfig: Object.fromEntries(PROVIDER_SECTIONS.map((section) => [
section, groupProviderRecords(section, legacyProviderConfig[section]),
section, scenario === 'empty' ? [] : groupProviderRecords(section, legacyProviderConfig[section]),
])) as Record<ProviderSection, JsonObject[]>,
authFiles,
authFiles: scenario === 'empty' ? [] : authFiles,
quotaApiCall: createQuotaMock(),
oauthExcludedModels: { codex: [], claude: ['claude-legacy-*'], gemini: [] } as Record<string, string[]>,
oauthSessions: new Map<string, string>(),
analysis,
@@ -631,7 +634,9 @@ function mockModelDefinitions() {
};
}
function mockApiCall(body: JsonObject) {
function mockApiCall(state: BrowserMockState, body: JsonObject) {
const quota = state.quotaApiCall(body);
if (quota) return quota;
const url = readString(body.url).toLowerCase();
if (url.includes('/models')) {
return {
@@ -645,47 +650,8 @@ function mockApiCall(body: JsonObject) {
},
};
}
if (url.includes('/api/oauth/profile')) {
return { status_code: 200, body: { account: { has_claude_pro: true, has_claude_max: false } } };
}
if (url.includes('wham/usage')) {
return {
status_code: 200,
body: {
rate_limit: {
primary_window: { used_percent: 28, reset_after_seconds: 7_200, limit_window_seconds: 18_000 },
secondary_window: { used_percent: 42, reset_after_seconds: 345_600, limit_window_seconds: 604_800 },
},
},
};
}
if (url.includes('anthropic.com/api/oauth/usage')) {
return {
status_code: 200,
body: {
five_hour: { utilization: 24, resets_at: isoHoursAgo(-3) },
seven_day: { utilization: 41, resets_at: isoHoursAgo(-72) },
},
};
}
if (url.includes('retrieveuserquotasummary')) {
return {
status_code: 200,
body: {
groups: [{ display_name: 'Gemini Pro', buckets: [{ window: '5h', remaining_fraction: 0.74, reset_time: isoHoursAgo(-3) }] }],
},
};
}
if (url.includes('billing')) {
return {
status_code: 200,
body: {
periodType: 'weekly',
usagePercent: 36,
periodEnd: isoHoursAgo(-48),
productUsage: [{ product: 'Grok Code', usagePercent: 31 }],
},
};
if (!/\/(chat\/completions|responses|messages)(\?|$)/.test(url)) {
throw new Error(`Browser Mock upstream request not implemented: ${readString(body.method)} ${readString(body.url)}`);
}
return {
status_code: 200,
@@ -717,6 +683,18 @@ function managementResponse(state: BrowserMockState, payload: JsonObject) {
])),
};
}
if (method === 'GET' && path === '/config/api-keys') {
return Object.fromEntries(PROVIDER_SECTIONS.map(section => [V8_PROVIDER_BY_SECTION[section], clone(state.providerConfig[section])]));
}
const legacySection = PROVIDER_SECTIONS.find(section => path === `/${section}`);
if (method === 'GET' && legacySection) {
return state.providerConfig[legacySection].flatMap(group => {
const { keys, ...shared } = group;
return legacySection === 'openai-compatibility'
? [{ ...clone(shared), 'api-key-entries': clone(keys) }]
: asArray(keys).map(key => ({ ...clone(shared), ...clone(asObject(key)) }));
});
}
if (v8Provider) {
const section = v8Provider[0] as ProviderSection;
if (method === 'GET') return clone(state.providerConfig[section]);
@@ -730,7 +708,8 @@ function managementResponse(state: BrowserMockState, payload: JsonObject) {
}
if (method === 'GET' && path === '/credentials/download') {
const file = findAuthFile(state, readString(query.name));
return clone(file ?? { name: query.name, excluded_models: [] });
if (!file) throw new Error(`Browser Mock credential not found: ${readString(query.name)}`);
return clone(file);
}
if (method === 'GET' && path === '/credentials/models') return mockModelDefinitions();
if (method === 'GET' && path.startsWith('/routing/model-definitions/')) return mockModelDefinitions();
@@ -744,17 +723,30 @@ function managementResponse(state: BrowserMockState, payload: JsonObject) {
if (method === 'PATCH' && (path === '/credentials/fields' || path === '/credentials/status')) {
const patch = asObject(body);
const file = findAuthFile(state, readString(patch.name));
if (file) Object.assign(file, patch);
return clone(file ?? null);
if (!file) throw new Error(`Browser Mock credential not found: ${readString(patch.name)}`);
Object.assign(file, clone(patch));
return { status: 'ok' };
}
if (method === 'POST' && path === '/routing/cooldown/reset') {
const authIndex = readString(asObject(body).auth_index);
const file = state.authFiles.find(file => file.auth_index === authIndex);
if (!file) throw new Error(`Browser Mock credential not found: ${authIndex}`);
const models = asArray(file.cooldowns).map(item => readString(asObject(item).model_key)).filter(Boolean);
file.cooldowns = [];
return { status: 'ok', auth_index: authIndex, models };
}
if (method === 'DELETE' && path === '/credentials') {
const index = state.authFiles.findIndex((file) => file.name === query.name);
if (index >= 0) state.authFiles.splice(index, 1);
if (index < 0) throw new Error(`Browser Mock credential not found: ${readString(query.name)}`);
state.authFiles.splice(index, 1);
return null;
}
if (method === 'POST' && path === '/requests/api-call') return mockApiCall(asObject(body));
if (method === 'DELETE' && path === '/oauth/session') return null;
return {};
if (method === 'POST' && path === '/requests/api-call') return mockApiCall(state, asObject(body));
if (method === 'DELETE' && path === '/oauth/session') {
state.oauthSessions.delete(readString(query.state));
return null;
}
throw new Error(`Browser Mock management request not implemented: ${method} ${path}`);
}
const CORE_CONFIG_PATHS: Record<string, string> = {
@@ -810,6 +802,17 @@ export function createBrowserMockRuntime(
delayMs = 0,
): BrowserMockRuntime {
const state = createState(scenario);
if (scenario === 'empty') {
state.usageEvents = [];
state.usagePrices = [];
Object.assign(state.usageOverview, Object.fromEntries(Object.entries(state.usageOverview)
.filter(([, value]) => typeof value === 'number').map(([key]) => [key, 0])));
state.usageOverview.timeline = [];
state.analysis = { models: [], providers: [], sources: [], apiKeys: [] };
state.usageStorage.databaseSizeBytes = 0;
state.usageStorage.totalRecords = 0;
state.coreConfig.apiKeys = [];
}
const pluginMock = createPluginMock(state);
const emit = (event: string, payload: unknown) => emitEvent(event, clone(payload));
@@ -1018,7 +1021,13 @@ export function createBrowserMockRuntime(
case 'management_request': return clone(managementResponse(state, payload));
case 'upload_auth_file': {
const name = readString(payload.name) || 'uploaded-mock.json';
state.authFiles.push({ name, provider: 'codex', type: 'codex', source: 'file', auth_index: `mock-upload-${state.authFiles.length + 1}`, disabled: false, priority: 0 });
const parsed: unknown = JSON.parse(new TextDecoder().decode(Uint8Array.from(asArray(payload.data) as number[])));
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) throw new Error('Browser Mock: credential must be a JSON object');
const content = asObject(parsed);
const existing = findAuthFile(state, name);
const file = { ...clone(content), name, provider: readString(content.provider || content.type) || 'codex', source: 'file',
auth_index: existing?.auth_index ?? `mock-upload-${crypto.randomUUID()}`, modtime: Date.now() };
if (existing) state.authFiles[state.authFiles.indexOf(existing)] = file; else state.authFiles.push(file);
return { ok: true, name };
}
case 'resolve_api_access_remarks': {
@@ -1049,6 +1058,7 @@ export function createBrowserMockRuntime(
case 'get_oauth_status': {
const session = readString(payload.state);
const provider = state.oauthSessions.get(session);
if (!provider) return { status: 'error', error: 'Unknown or canceled mock OAuth session' };
if (provider && !state.authFiles.some((file) => file.name === `${provider}-mock-login.json`)) {
state.authFiles.push({
name: `${provider}-mock-login.json`, provider, type: provider, source: 'file',
@@ -1071,13 +1081,24 @@ export function createBrowserMockRuntime(
const query = asObject(payload.query);
const page = Math.max(1, readNumber(query.page, 1));
const pageSize = Math.max(1, readNumber(query.page_size, 50));
const events = state.usageEvents.filter(event => {
for (const key of ['model', 'provider', 'source', 'api_key_hash'] as const) {
if (query[key] && event[key] !== query[key]) return false;
}
for (const key of ['failed', 'canceled'] as const) {
if (typeof query[key] === 'boolean' && event[key] !== query[key]) return false;
}
const timestamp = Date.parse(event.timestamp);
return (!query.start || timestamp >= Date.parse(String(query.start)))
&& (!query.end || timestamp <= Date.parse(String(query.end)));
});
const start = (page - 1) * pageSize;
return {
items: clone(state.usageEvents.slice(start, start + pageSize)),
total: state.usageEvents.length,
items: clone(events.slice(start, start + pageSize)),
total: events.length,
page,
pageSize,
totalPages: Math.max(1, Math.ceil(state.usageEvents.length / pageSize)),
totalPages: Math.max(1, Math.ceil(events.length / pageSize)),
};
}
case 'get_usage_pricing': {
@@ -1092,7 +1113,7 @@ export function createBrowserMockRuntime(
estimatedCost: index === 0 ? 1.92 : 1.71,
price: clone(price),
}));
return { rows, totalCost: 3.63, totalRequests: 278, pricedRequests: 278, savedPrices: state.usagePrices.length };
return { rows, totalCost: rows.length ? 3.63 : 0, totalRequests: rows.length ? 278 : 0, pricedRequests: rows.length ? 278 : 0, savedPrices: state.usagePrices.length };
}
case 'get_usage_storage_settings': return clone(state.usageStorage);
case 'save_usage_storage_settings': {
+105
View File
@@ -0,0 +1,105 @@
type RecordValue = Record<string, unknown>;
const future = (hours: number) => new Date(Date.now() + hours * 3_600_000).toISOString();
/** Fictional accounts exercise pagination, long labels and independently failing quotas. */
export function createQuotaMockFiles(): RecordValue[] {
const seeds: Array<[string, string, RecordValue]> = [
['antigravity', 'team-with-a-long-account-name', {}],
['kimi', 'personal', {}],
['xai', 'subscription', { user_id: 'mock-xai-user' }],
['devin', 'team', {}],
['codex', 'disabled', { disabled: true }],
['codex', 'exhausted', { cooldowns: [{ scope: 'model', model_key: 'gpt-5.2-codex', reason: 'quota', http_status: 429, retry_at: future(1), remaining_seconds: 3600 }] }],
['claude', 'expired', { status: 'error', status_message: 'Mock OAuth token has expired; sign in again.' }],
['xai', 'paid-api', { using_api: true, prefix: 'paid' }],
['antigravity', 'empty-quota', {}],
['codex', 'unknown-quota', {}],
['codex', 'idle', {}],
];
return seeds.map(([provider, label, extra], index) => ({
name: `${provider}-${label}.json`, provider, type: provider, source: 'file',
auth_index: `mock-${provider}-${label}`, email: `${label}@example.test`,
account_id: `mock-account-${index}`, project_id: 'mock-project',
plan_type: 'pro', subscription_active_until: future(24 * 30),
disabled: false, status: 'active', priority: index % 3,
size: 1500 + index * 100, modtime: Date.now() - index * 60_000,
note: 'Fictional browser preview account. Changes stay in memory until reload.',
success: label === 'idle' ? 0 : 250 + index * 97, failed: label === 'idle' ? 0 : index,
recent_requests: label === 'idle' ? [] : Array.from({ length: 20 }, (_, bucket) => ({
time: new Date(Date.now() - (19 - bucket) * 600_000).toISOString(),
success: bucket % 4 + 1, failed: bucket % 7 === 0 ? 1 : 0,
})),
...extra,
}));
}
/** Each runtime owns its own balances; refreshing or mutating one account cannot affect another. */
export function createQuotaMock() {
const spent = new Map<string, number>();
const redeemed = new Set<string>();
return (body: RecordValue) => {
const url = new URL(String(body.url));
const path = url.pathname.toLowerCase();
const auth = String(body.authIndex ?? '');
const used = spent.get(auth) ?? 0;
const remaining = Math.max(0, 2 - used);
const success = (payload: unknown) => ({ status_code: 200, header: { Date: new Date().toUTCString() }, body: payload });
if (auth.endsWith('-expired')) return { status_code: 401, body: { error: { message: 'Mock OAuth token expired' } } };
const consume = () => {
const data = JSON.parse(String(body.data || '{}')) as RecordValue;
const id = String(data.redeem_request_id ?? data.request_id ?? '');
const key = `${auth}:${id}`;
if (id && redeemed.has(key)) return true;
if (!remaining) return false;
spent.set(auth, used + 1);
if (id) redeemed.add(key);
return true;
};
if (path.endsWith('/rate-limit-reset-credits/consume')) {
if (body.method !== 'POST') throw new Error('Browser Mock: credit consumption requires POST');
return consume() ? success({ status: 'ok' }) : { status_code: 409, body: { error: 'No reset credits remaining' } };
}
if (path.endsWith('/rate-limit-reset-credits')) return success({
available_count: remaining, applicable_available_count: remaining,
credits: Array.from({ length: remaining }, (_, index) => ({ id: `mock-credit-${index + used}`, reset_type: 'codex_rate_limits', status: 'available', expires_at: future(24 * (index + 1)) })),
});
if (path.endsWith('/wham/usage')) return success({
plan_type: 'pro', credits: { balance: '12.50', unlimited: false },
rate_limit_reset_credits: { available_count: remaining },
rate_limit: {
primary_window: { used_percent: auth.endsWith('-unknown-quota') ? null : used ? 0 : auth.endsWith('-exhausted') ? 100 : 28, reset_after_seconds: 7200, limit_window_seconds: 18000 },
secondary_window: { used_percent: used ? 0 : 42, reset_after_seconds: 345600, limit_window_seconds: 604800 },
},
});
if (path.endsWith('/api/oauth/profile')) return success({ account: { has_claude_pro: true }, organization: { uuid: '00000000-0000-4000-8000-000000000001' } });
if (path.endsWith('/reset_rate_limits')) {
if (body.method !== 'POST') throw new Error('Browser Mock: quota reset requires POST');
return success({ result: consume() ? 'reset' : 'already_used' });
}
if (path.endsWith('/api/oauth/usage')) return success({
five_hour: { utilization: used ? 0 : 24, resets_at: future(3) },
seven_day: { utilization: used ? 0 : 41, resets_at: future(72) },
cedar_ember: { eligible: true, at_limit: false, next_grant_id: remaining ? 'mock-grant' : null,
grants: [{ id: 'mock-grant', label: 'Preview quota resets', resets_total: 2, resets_left: remaining, ends_at: future(72), clears: ['five_hour', 'seven_day'], usable_now: remaining > 0, use_requires_limit: false }] },
});
if (path.endsWith(':loadcodeassist')) return success({ currentTier: { id: 'g1-pro-tier', name: 'Pro' } });
if (path.endsWith(':retrieveuserquotasummary')) return success({ groups: auth.endsWith('-empty-quota') ? [] : ['Gemini Models', 'Claude Models'].map((displayName, index) => ({
displayName, description: 'Models within this group share quota. The limit fully resets at the time shown above.',
buckets: ['Five Hour Limit Remaining', 'Weekly Limit Remaining'].map((window, bucket) => ({
window, remainingFraction: index ? 0.06 : bucket ? 0.84 : 0.75,
resetTime: future(bucket ? 72 : 3),
})),
})) });
if (url.hostname === 'api.kimi.com' && path.endsWith('/usages')) return success({
usage: { used: 64, limit: 100 },
limits: [{ window: { duration: 5, timeUnit: 'TIME_UNIT_HOUR' }, detail: { limit: 100, remaining: 80, reset_in: 3600 } }],
});
if (path.endsWith('/getuserstatus')) return success({ userStatus: { planStatus: {
planInfo: { planName: 'Devin Pro' }, dailyQuotaRemainingPercent: 63, weeklyQuotaRemainingPercent: 42,
dailyQuotaResetAtUnix: Math.floor(Date.now() / 1000) + 3600, weeklyQuotaResetAtUnix: Math.floor(Date.now() / 1000) + 259200,
planEnd: future(24 * 30),
} } });
if (path.endsWith('/billing')) return success({ currentPeriod: { type: 'weekly', start: future(-120), end: future(48) }, creditUsagePercent: 36, productUsage: [{ product: 'Grok Code', usagePercent: 31 }] });
return undefined;
};
}
+50
View File
@@ -0,0 +1,50 @@
const { chromium } = require(process.env.PLAYWRIGHT_MODULE || 'playwright');
const assert = require('node:assert/strict');
const path = require('node:path');
(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()],
logLevel: 'error', server: { host: '127.0.0.1', port: 0, watch: null } });
let browser;
try {
await server.listen();
const base = `http://127.0.0.1:${server.httpServer.address().port}`;
browser = await chromium.launch({ channel: process.platform === 'win32' ? 'msedge' : undefined, headless: true, args: ['--no-proxy-server'] });
const page = await browser.newPage({ viewport: { width: 1505, 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.addInitScript(() => localStorage.setItem('easy-cli-proxy-api.locale', 'en'));
await page.goto(`${base}/?mock=running&mockDelay=5`);
await page.locator('.app-shell').waitFor();
await page.locator('.nav-section button').nth(2).click();
await page.getByRole('tab', { name: 'Authentication Files', exact: true }).click();
await page.locator('.auth-file-card').first().waitFor();
assert.equal(await page.locator('.auth-file-card').count(), 10);
await page.locator('.auth-list-pagination select').selectOption('20');
await page.waitForFunction(() => document.querySelectorAll('.auth-file-card').length > 10);
await page.locator('.auth-files-heading-actions').getByRole('button', { name: 'Refresh quota', exact: true }).click();
const card = name => page.locator('.auth-file-card').filter({ has: page.locator(`.auth-card-filename[title="${name}"]`) });
for (const name of ['codex-personal.json', 'claude-team.json', 'kimi-personal.json', 'devin-team.json', 'xai-subscription.json', 'antigravity-team-with-a-long-account-name.json']) {
await card(name).locator('[role="progressbar"]').first().waitFor();
}
assert.equal(await card('codex-disabled.json').getByRole('switch').getAttribute('aria-checked'), 'false');
await card('codex-disabled.json').getByRole('switch').click();
await page.waitForFunction(() => document.querySelector('.auth-card-filename[title="codex-disabled.json"]')?.closest('.auth-file-card')?.querySelector('[role="switch"]')?.getAttribute('aria-checked') === 'true');
const cooling = card('codex-exhausted.json');
await cooling.getByRole('button', { name: 'Clear cooldown', exact: true }).click();
await page.getByRole('alertdialog').getByRole('button', { name: 'Clear cooldown', exact: true }).click();
await cooling.locator('.auth-file-health-compact').waitFor({ state: 'hidden' });
await page.screenshot({ path: 'bin-work/browser-mock-credentials.png', fullPage: true });
assert.deepEqual(errors, []);
assert.deepEqual(externalRequests, []);
console.log('PASS: real app browser mock, six provider quotas, pagination, credential toggle and cooldown reset without external requests.');
} finally { await browser?.close(); await server.close(); }
})().catch(error => { console.error(error); process.exitCode = 1; });
+82
View File
@@ -0,0 +1,82 @@
import { afterEach, beforeEach, expect, spyOn, test } from 'bun:test';
import { createBrowserMockRuntime } from '../src/mocks/browserMockRuntime';
import { managementApi } from '../src/services/managementApi';
import { consumeClaudeResetCredit, consumeCodexResetCredit, loadQuota, type AuthFile } from '../src/services/quotaService';
import { resetAuthFileCooldown } from '../src/services/authFiles';
let runtime: ReturnType<typeof createBrowserMockRuntime>;
let get: ReturnType<typeof spyOn>;
let post: ReturnType<typeof spyOn>;
const request = (method: string, path: string, body?: unknown, query?: unknown) =>
runtime.invoke('management_request', { request: { method, path, body, query } });
const files = async () => (await request('GET', '/credentials') as { files: AuthFile[] }).files;
beforeEach(() => {
runtime = createBrowserMockRuntime('running');
get = spyOn(managementApi, 'get').mockImplementation(async (path, query) => await request('GET', path, undefined, query) as never);
post = spyOn(managementApi, 'post').mockImplementation(async (path, body) => await request('POST', path, body) as never);
});
afterEach(() => { get.mockRestore(); post.mockRestore(); });
test('all supported providers parse through the actual quota service', async () => {
const accounts = await files();
expect(accounts.filter(file => file.source === 'file').length).toBeGreaterThan(10);
for (const provider of ['codex', 'claude', 'antigravity', 'kimi', 'xai', 'devin']) {
const file = accounts.find(file => file.provider === provider)!;
const quota = await loadQuota(file);
expect(quota.status).toBe('success');
expect(quota.rows.length).toBeGreaterThanOrEqual(2);
expect(quota.resetCreditsError).toBeUndefined();
expect(quota.rows.every(row => row.remainingPercent !== null), JSON.stringify({ provider, rows: quota.rows })).toBe(true);
}
});
test('edge accounts expose exhausted, unknown, disabled and upstream-error states', async () => {
const accounts = await files();
const quota = (name: string) => loadQuota(accounts.find(file => file.name === name)!);
expect((await quota('codex-exhausted.json')).rows[0].remainingPercent).toBe(0);
expect((await quota('codex-unknown-quota.json')).rows[0].remainingPercent).toBeNull();
expect((await quota('antigravity-empty-quota.json')).status).toBe('error');
expect((await quota('xai-paid-api.json')).rows[0].remainingPercent).toBeNull();
expect((await quota('codex-disabled.json')).status).toBe('error');
expect(await quota('claude-expired.json')).toMatchObject({ status: 'error', error: 'Mock OAuth token expired' });
});
test('Codex and Claude resets consume credits and refresh only that account', async () => {
const accounts = await files();
for (const provider of ['codex', 'claude']) {
const account = accounts.find(file => file.provider === provider)!;
const reset = provider === 'codex' ? consumeCodexResetCredit : consumeClaudeResetCredit;
expect((await loadQuota(account)).resetCredits).toBe(2);
const result = await reset(account);
expect(result.status).toBe('success');
expect(result.resetCredits).toBe(1);
expect(result.rows[0].remainingPercent).toBe(100);
expect((await reset(account)).resetCredits).toBe(0);
}
expect((await loadQuota(accounts.find(file => file.name === 'codex-idle.json')!)).resetCredits).toBe(2);
});
test('cooldown reset acknowledges the correct credential and preserves disabled state', async () => {
const account = (await files()).find(file => file.name === 'codex-exhausted.json')!;
await request('PATCH', '/credentials/status', { name: account.name, disabled: true });
expect(await resetAuthFileCooldown(String(account.auth_index))).toMatchObject({ status: 'ok', auth_index: account.auth_index, models: ['gpt-5.2-codex'] });
expect((await files()).find(file => file.name === account.name)).toMatchObject({ disabled: true, cooldowns: [] });
});
test('imports JSON contents, replaces the same filename, and deletes without mutating other accounts', async () => {
const originalCount = (await files()).length;
const upload = (content: string) => runtime.invoke('upload_auth_file', { name: 'imported.json', data: Array.from(new TextEncoder().encode(content)) });
await upload(JSON.stringify({ type: 'kimi', note: 'Imported example', priority: 9 }));
const first = (await files()).find(file => file.name === 'imported.json')!;
expect(first).toMatchObject({ provider: 'kimi', note: 'Imported example', priority: 9 });
await upload(JSON.stringify({ type: 'claude', note: 'Replaced' }));
expect((await files()).length).toBe(originalCount + 1);
expect((await files()).find(file => file.name === 'imported.json')?.priority).toBeUndefined();
expect(await request('GET', '/credentials/download', undefined, { name: 'imported.json' })).toMatchObject({ provider: 'claude', note: 'Replaced', auth_index: first.auth_index });
await expect(upload('{broken')).rejects.toThrow();
await expect(upload('[]')).rejects.toThrow('JSON object');
await request('DELETE', '/credentials', undefined, { name: 'imported.json' });
expect((await files()).length).toBe(originalCount);
await expect(request('GET', '/credentials/download', undefined, { name: 'imported.json' })).rejects.toThrow('not found');
});
+46
View File
@@ -19,6 +19,52 @@ describe('browser mock options', () => {
});
describe('browser mock runtime', () => {
test('empty scenario exposes empty credentials, providers and usage after installing the core', async () => {
const runtime = createBrowserMockRuntime('empty');
await runtime.invoke('install_bundled_core');
await runtime.invoke('start_core_process');
expect(await runtime.invoke('management_request', { request: { method: 'GET', path: '/credentials' } })).toMatchObject({ files: [] });
const providers = await runtime.invoke('management_request', { request: { method: 'GET', path: '/config/api-keys' } }) as Record<string, unknown[]>;
expect(Object.values(providers).every(groups => groups.length === 0)).toBe(true);
expect(await runtime.invoke('get_usage_overview')).toMatchObject({ totalRequests: 0, totalTokens: 0, timeline: [] });
expect(await runtime.invoke('get_usage_events')).toMatchObject({ items: [], total: 0 });
expect(await runtime.invoke('get_usage_analysis')).toEqual({ models: [], providers: [], sources: [], apiKeys: [] });
expect(await runtime.invoke('get_usage_pricing')).toMatchObject({ rows: [], totalCost: 0 });
});
test('filters usage before pagination, including boolean and date filters', async () => {
const runtime = createBrowserMockRuntime('running');
const first = await runtime.invoke('get_usage_events', { query: { failed: true, page_size: 1 } }) as { items: Array<Record<string, unknown>>; total: number; totalPages: number };
expect(first).toMatchObject({ total: 2, totalPages: 2 });
expect(first.items).toHaveLength(1);
expect(first.items[0].failed).toBe(true);
const next = await runtime.invoke('get_usage_events', { query: { failed: true, page_size: 1, page: 2 } }) as typeof first;
expect(next.items[0].id).not.toBe(first.items[0].id);
const exact = await runtime.invoke('get_usage_events', { query: {
start: first.items[0].timestamp, end: first.items[0].timestamp,
provider: first.items[0].provider, model: first.items[0].model, source: first.items[0].source,
api_key_hash: first.items[0].api_key_hash, failed: true, canceled: false,
} }) as typeof first;
expect(exact.items).toEqual(first.items);
expect(await runtime.invoke('get_usage_events', { query: { model: 'absent-model' } })).toMatchObject({ items: [], total: 0 });
expect(await runtime.invoke('get_usage_events', { query: { canceled: true } })).toMatchObject({ total: 1 });
});
test('unknown routes fail explicitly and canceled OAuth sessions cannot create credentials', async () => {
const runtime = createBrowserMockRuntime('running');
await expect(runtime.invoke('management_request', { request: { method: 'GET', path: '/missing-route' } })).rejects.toThrow('not implemented');
await expect(runtime.invoke('management_request', { request: { method: 'POST', path: '/requests/api-call', body: { url: 'https://example.test/missing', method: 'GET' } } })).rejects.toThrow('not implemented');
const session = await runtime.invoke('start_oauth_login', { provider: 'kimi' }) as { state: string };
await runtime.invoke('management_request', { request: { method: 'DELETE', path: '/oauth/session', query: { state: session.state } } });
expect(await runtime.invoke('get_oauth_status', { state: session.state })).toMatchObject({ status: 'error' });
const credentials = await runtime.invoke('management_request', { request: { method: 'GET', path: '/credentials' } }) as { files: Array<{ name: string }> };
expect(credentials.files.some(file => file.name === 'kimi-mock-login.json')).toBe(false);
});
test('latency simulation also works outside a browser', async () => {
expect(await createBrowserMockRuntime('running', undefined, 1).invoke('get_core_status')).toMatchObject({ ready: true });
});
for (const scenario of ['running', 'empty'] as const) {
test(`installs the bundled version independently of the latest release (${scenario})`, async () => {
const runtime = createBrowserMockRuntime(scenario);