- Add plugin store, version selection, configuration, and OAuth login - Support Muse (Meta) OAuth device authorization - Redesign home dashboard and add batch model health checks - Support per-credential provider editing, removal, and reordering - Improve usage charts and track actual response models - Preserve installed plugins and runtime data during portable rebuilds - Update translations, documentation, and related tests
168 lines
8.5 KiB
TypeScript
168 lines
8.5 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
|
|
import { clearMocks, mockIPC } from '@tauri-apps/api/mocks';
|
|
import {
|
|
countHomeProviderKeys,
|
|
loadHomeOverview,
|
|
normalizeHomeModels,
|
|
summarizeHomeCredentials,
|
|
summarizeHomeUsage,
|
|
} from '../src/services/homeOverview';
|
|
|
|
describe('home overview aggregation', () => {
|
|
it('measures success over completed requests and leaves empty/canceled-only windows unmeasured', () => {
|
|
expect(summarizeHomeUsage({ totalRequests: 12, successCount: 9, failureCount: 1, canceledCount: 2 }).successRate).toBe(90);
|
|
expect(summarizeHomeUsage({ totalRequests: 0, successCount: 0, failureCount: 0, canceledCount: 0 }).successRate).toBeNull();
|
|
expect(summarizeHomeUsage({ totalRequests: 5, successCount: 0, failureCount: 0, canceledCount: 5 }).successRate).toBeNull();
|
|
expect(summarizeHomeUsage({ totalRequests: 2, successCount: 0, failureCount: 2, canceledCount: 0 }).successRate).toBe(0);
|
|
expect(() => summarizeHomeUsage({ totalRequests: 0 })).toThrow();
|
|
expect(() => summarizeHomeUsage({ totalRequests: 1, successCount: 2, failureCount: 0, canceledCount: 0 })).toThrow();
|
|
});
|
|
|
|
it('deduplicates OAuth files and excludes runtime/API key records', () => {
|
|
expect(summarizeHomeCredentials({ files: [
|
|
{ name: 'meta-user.json', source: 'file', status: 'active', cooldowns: [] },
|
|
{ name: 'meta-user.json', runtime_only: true, status: 'active' },
|
|
{ name: 'codex-user.json', source: 'file', status: 'ready' },
|
|
{ name: 'disabled.json', disabled: true },
|
|
{ name: 'runtime.json', runtime_only: true, status: 'active' },
|
|
{ name: 'key.json', account_type: 'api_key', status: 'active' },
|
|
{ name: 'plain-key', status: 'active' },
|
|
] })).toEqual({ total: 3, available: 2, unavailable: 1, unknown: 0 });
|
|
});
|
|
|
|
it('does not call missing, pending, model-only error, or malformed cooldown states available', () => {
|
|
const cooldown = { scope: 'credential', remaining_seconds: 60, retry_at: '2026-10-03T12:00:00Z' };
|
|
expect(summarizeHomeCredentials({ files: [
|
|
{ name: 'quota.json', status: 'active', cooldowns: [cooldown] },
|
|
{ name: 'expired.json', status: 'error', unavailable: true },
|
|
{ name: 'disabled.json', status: 'disabled' },
|
|
{ name: 'missing.json' },
|
|
{ name: 'pending.json', status: 'pending' },
|
|
{ name: 'refreshing.json', status: 'refreshing' },
|
|
{ name: 'model-only.json', status: 'error', unavailable: false,
|
|
cooldowns: [{ ...cooldown, scope: 'model', model_key: 'model-a' }] },
|
|
{ name: 'unknown-cooldown.json', status: 'active', cooldowns: null },
|
|
{ name: 'broken-cooldown.json', status: 'ready', cooldowns: [{}] },
|
|
] })).toEqual({ total: 9, available: 0, unavailable: 3, unknown: 6 });
|
|
expect(() => summarizeHomeCredentials({})).toThrow();
|
|
expect(() => summarizeHomeCredentials({ files: [null] })).toThrow();
|
|
expect(summarizeHomeCredentials({ files: [] })).toEqual({ total: 0, available: 0, unavailable: 0, unknown: 0 });
|
|
});
|
|
|
|
it('counts actual provider keys across groups, including disabled keys and future providers', () => {
|
|
expect(countHomeProviderKeys({
|
|
gemini: [{ name: 'one', keys: [{ 'api-key': 'gemini-1' }, { 'api-key': 'gemini-2', disabled: true }] }],
|
|
'openai-compatibility': [{ name: 'compat', keys: [{ 'api-key': 'compat-1' }, { 'api-key': 'compat-2' }] }],
|
|
meta: [{ keys: [{ 'api-key': 'meta-1' }] }],
|
|
'future-provider': [{ keys: [{ 'api-key': 'next-key' }, { 'api-key': ' ' }, {}] }],
|
|
claude: [{ keys: [] }, { name: 'keyless' }],
|
|
})).toBe(6);
|
|
expect(countHomeProviderKeys({})).toBe(0);
|
|
expect(() => countHomeProviderKeys(['client-key'])).toThrow();
|
|
expect(() => countHomeProviderKeys({ codex: [{ keys: 'invalid' }] })).toThrow();
|
|
});
|
|
|
|
it('preserves exposed aliases and case-sensitive model IDs while removing duplicate IDs', () => {
|
|
expect(normalizeHomeModels([
|
|
{ name: ' prefix/Model-A ', displayName: 'Model A', isAlias: true },
|
|
{ name: 'prefix/Model-A' },
|
|
{ name: 'prefix/model-a' },
|
|
{ name: 'custom-alias', displayName: '' },
|
|
])).toEqual([
|
|
{ name: 'prefix/Model-A', displayName: 'Model A' },
|
|
{ name: 'prefix/model-a' },
|
|
{ name: 'custom-alias' },
|
|
]);
|
|
expect(normalizeHomeModels([])).toEqual([]);
|
|
expect(() => normalizeHomeModels(null)).toThrow();
|
|
expect(() => normalizeHomeModels([{ name: '' }])).toThrow();
|
|
});
|
|
});
|
|
|
|
describe('home overview loading', () => {
|
|
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');
|
|
});
|
|
|
|
const usage = { totalRequests: 5, successCount: 4, failureCount: 1, canceledCount: 0 };
|
|
|
|
it('loads a rolling 24 hour window locally without contacting a stopped core', async () => {
|
|
const calls: string[] = [];
|
|
mockIPC((command, args) => {
|
|
calls.push(command);
|
|
expect(command).toBe('get_usage_overview');
|
|
expect(args?.query).toEqual({ start: '2026-10-02T08:30:00.000Z', end: '2026-10-03T08:30:00.000Z' });
|
|
return usage;
|
|
});
|
|
expect(await loadHomeOverview(false, Date.parse('2026-10-03T08:30:00Z'))).toEqual({
|
|
usage: { ...usage, successRate: 80 }, credentials: null, providerKeys: null, models: null, errors: {},
|
|
});
|
|
expect(calls).toEqual(['get_usage_overview']);
|
|
});
|
|
|
|
it('uses the credential and raw provider config endpoints plus the dedicated core model command', async () => {
|
|
const requests: string[] = [];
|
|
mockIPC((command, args) => {
|
|
if (command === 'get_usage_overview') return usage;
|
|
if (command === 'get_core_models') return [{ name: 'exposed-alias' }];
|
|
expect(command).toBe('management_request');
|
|
const request = args?.request as { method: string; path: string };
|
|
expect(request.method).toBe('GET');
|
|
requests.push(request.path);
|
|
if (request.path === '/credentials') return { files: [{ name: 'muse.json', status: 'ready' }] };
|
|
if (request.path === '/config/api-keys') return { meta: [{ keys: [{ 'api-key': 'provider-key' }] }] };
|
|
throw new Error(`Unexpected path ${request.path}`);
|
|
});
|
|
expect(await loadHomeOverview(true)).toEqual({
|
|
usage: { ...usage, successRate: 80 }, credentials: { total: 1, available: 1, unavailable: 0, unknown: 0 },
|
|
providerKeys: 1, models: [{ name: 'exposed-alias' }], errors: {},
|
|
});
|
|
expect(requests.sort()).toEqual(['/config/api-keys', '/credentials']);
|
|
});
|
|
|
|
it('keeps successful metrics when independent requests fail and reports failed values as unknown', async () => {
|
|
mockIPC((command, args) => {
|
|
if (command === 'get_usage_overview') return usage;
|
|
if (command === 'get_core_models') throw 'Model list unavailable';
|
|
const request = args?.request as { path: string };
|
|
if (request.path === '/credentials') return { files: [] };
|
|
throw new Error('Management API error (401): unauthorized');
|
|
});
|
|
expect(await loadHomeOverview(true)).toEqual({
|
|
usage: { ...usage, successRate: 80 }, credentials: { total: 0, available: 0, unavailable: 0, unknown: 0 },
|
|
providerKeys: null, models: null,
|
|
errors: { providerKeys: 'Management API error (401): unauthorized', models: 'Model list unavailable' },
|
|
});
|
|
});
|
|
|
|
it('treats only an explicitly absent provider config node as an empty configuration', async () => {
|
|
for (const message of ['Management API error (404): not_found', 'Management API error (404): 404 page not found']) {
|
|
mockIPC((command, args) => {
|
|
if (command === 'get_usage_overview') throw new Error('Usage database unavailable');
|
|
if (command === 'get_core_models') return [];
|
|
const request = args?.request as { path: string };
|
|
if (request.path === '/credentials') return { files: [] };
|
|
throw message;
|
|
});
|
|
const overview = await loadHomeOverview(true);
|
|
expect(overview.usage).toBeNull();
|
|
expect(overview.errors.usage).toBe('Usage database unavailable');
|
|
expect(overview.models).toEqual([]);
|
|
if (message.endsWith(': not_found')) {
|
|
expect(overview.providerKeys).toBe(0);
|
|
expect(overview.errors.providerKeys).toBeUndefined();
|
|
} else {
|
|
expect(overview.providerKeys).toBeNull();
|
|
expect(overview.errors.providerKeys).toBe(message);
|
|
}
|
|
}
|
|
});
|
|
});
|