Add projects

This commit is contained in:
Alois 2026-08-24 00:10:41 +02:00
commit 8b607dd700
Signed by: alois
SSH key fingerprint: SHA256:GBzT2DXvAuGV9XIV5W3WrzVpjU54FThmxHXdbz95J24
1802 changed files with 503346 additions and 2 deletions

View file

@ -0,0 +1,21 @@
import { describe, expect, test } from 'vitest';
import { getNextAntigravityCountdownUpdateDelay } from '@/features/quota/providers/antigravity/countdown';
const MINUTE_MS = 60_000;
describe('Antigravity quota countdown scheduling', () => {
test('updates at the next rounded-minute boundary', () => {
expect(getNextAntigravityCountdownUpdateDelay([10.5 * MINUTE_MS], 0)).toBe(30_000);
expect(getNextAntigravityCountdownUpdateDelay([10 * MINUTE_MS], 0)).toBe(MINUTE_MS);
});
test('uses the earliest boundary across quota buckets', () => {
expect(getNextAntigravityCountdownUpdateDelay([10.75 * MINUTE_MS, 3.25 * MINUTE_MS], 0)).toBe(
15_000
);
});
test('stops scheduling after all reset times expire', () => {
expect(getNextAntigravityCountdownUpdateDelay([0, Number.NaN], MINUTE_MS)).toBeNull();
});
});

View file

@ -0,0 +1,51 @@
import { describe, expect, test } from 'vitest';
import { parseApiErrorResponse } from '../src/services/api/apiError';
describe('Management API error parsing', () => {
test('prefers the human-readable message and preserves the API error code', () => {
const result = parseApiErrorResponse(
{
error: 'plugin_install_failed',
message: 'download plugin archive: 404 Not Found',
},
'Request failed with status code 502'
);
expect(result).toEqual({
message: 'download plugin archive: 404 Not Found',
apiCode: 'plugin_install_failed',
});
});
test('falls back to a string error used by legacy endpoints', () => {
expect(parseApiErrorResponse({ error: 'invalid body' }, 'Bad Request')).toEqual({
message: 'invalid body',
apiCode: 'invalid body',
});
});
test('supports nested error messages and codes', () => {
expect(
parseApiErrorResponse(
{ error: { code: 'invalid_config', message: 'plugins-dir is invalid' } },
'Bad Request'
)
).toEqual({
message: 'plugins-dir is invalid',
apiCode: 'invalid_config',
});
});
test('uses a text response body before the transport fallback', () => {
expect(parseApiErrorResponse('upstream unavailable', 'Network Error')).toEqual({
message: 'upstream unavailable',
});
});
test('uses the transport message for an unknown response shape', () => {
expect(parseApiErrorResponse({ error: null }, 'Network Error')).toEqual({
message: 'Network Error',
apiCode: undefined,
});
});
});

View file

@ -0,0 +1,17 @@
import { describe, expect, test } from 'vitest';
import { generateSecureApiKey } from '../src/utils/apiKey';
describe('API key generation', () => {
test('generates a 51-character key with the expected prefix and charset', () => {
const apiKey = generateSecureApiKey();
expect(apiKey).toHaveLength(51);
expect(apiKey).toMatch(/^sk-[A-Za-z0-9]{48}$/);
});
test('generates distinct keys', () => {
const apiKeys = Array.from({ length: 100 }, () => generateSecureApiKey());
expect(new Set(apiKeys).size).toBe(apiKeys.length);
});
});

View file

@ -0,0 +1,82 @@
import { describe, expect, test } from 'vitest';
import { generateSecureApiKey } from '../src/utils/apiKey';
import {
API_KEY_STRENGTH_SEGMENTS,
evaluateApiKeyStrength,
type ApiKeyStrengthTier,
} from '../src/utils/apiKeyStrength';
const TIER_ORDER: ApiKeyStrengthTier[] = ['weak', 'fair', 'good', 'strong'];
describe('API key strength', () => {
test('empty input lights no segment', () => {
for (const value of ['', ' ']) {
expect(evaluateApiKeyStrength(value)).toEqual({ tier: 'weak', segments: 0, bits: 0 });
}
});
test('segments track the tier index', () => {
const samples = ['a', 'Tr0ub4dor', 'Xk7#mQ2vLp9$Rn4wZt6c', generateSecureApiKey()];
for (const sample of samples) {
const { tier, segments } = evaluateApiKeyStrength(sample);
expect(segments).toBe(TIER_ORDER.indexOf(tier) + 1);
expect(segments).toBeLessThanOrEqual(API_KEY_STRENGTH_SEGMENTS);
}
});
test('generated keys always reach the top tier', () => {
for (let i = 0; i < 50; i += 1) {
const { tier, segments } = evaluateApiKeyStrength(generateSecureApiKey());
expect(tier).toBe('strong');
expect(segments).toBe(API_KEY_STRENGTH_SEGMENTS);
}
});
test('short keys are capped regardless of charset richness', () => {
// 7 位就算四类字符齐全也只能是最弱档
expect(evaluateApiKeyStrength('aA1!bB2').tier).toBe('weak');
// 15 位封顶在第二档
expect(evaluateApiKeyStrength('aA1!bB2@cC3#dD4').tier).toBe('fair');
// 23 位封顶在第三档
expect(evaluateApiKeyStrength('aA1!bB2@cC3#dD4$eE5%fG').tier).toBe('good');
});
test('repeated and sequential runs are discounted', () => {
const repeated = evaluateApiKeyStrength('a'.repeat(48));
expect(repeated.tier).toBe('weak');
const sequential = evaluateApiKeyStrength('abcdefghijklmnopqrstuvwxyz0123456789');
const shuffled = evaluateApiKeyStrength('qzmXe4Rk9BtLw7Ncy2VsJp5Ghd8FaU3Zmr6Q');
expect(sequential.bits).toBeLessThan(shuffled.bits);
});
test('periodic keys score like a single period', () => {
// 32 位却只有 8 位的猜测成本
expect(evaluateApiKeyStrength('deadbeefdeadbeefdeadbeefdeadbeef').tier).toBe('fair');
expect(evaluateApiKeyStrength('abababababababababababababab').tier).toBe('weak');
// 尾部不完整的周期同样识别
const periodic = evaluateApiKeyStrength('myproxy2024myproxy2024myproxy');
const aperiodic = evaluateApiKeyStrength('myproxy2024ZtRv4Ns8Lc3Bd7hQwK');
expect(periodic.bits).toBeLessThan(aperiodic.bits);
});
test('guessable tokens drag the score down', () => {
const withToken = evaluateApiKeyStrength('sk-password-9fKw2mQx7ZtRv4Ns8Lc3Bd');
const withoutToken = evaluateApiKeyStrength('sk-hRvnqtwj-9fKw2mQx7ZtRv4Ns8Lc3Bd');
expect(withToken.bits).toBeLessThan(withoutToken.bits);
expect(TIER_ORDER.indexOf(withToken.tier)).toBeLessThan(TIER_ORDER.indexOf(withoutToken.tier));
});
test('longer keys never score below their prefix', () => {
const base = 'Xk7#mQ2vLp9$Rn4wZt6cHj8&Bd5xVy3';
let previous = 0;
for (let length = 1; length <= base.length; length += 1) {
const { bits } = evaluateApiKeyStrength(base.slice(0, length));
expect(bits).toBeGreaterThanOrEqual(previous);
previous = bits;
}
});
});

View file

@ -0,0 +1,66 @@
import { describe, expect, test } from 'vitest';
import { createElement } from 'react';
import { renderToStaticMarkup } from 'react-dom/server';
import i18n from '@/i18n';
import { ApiKeyStrengthMeter } from '@/features/config/components/blocks/ApiKeyStrengthMeter';
import { SEGMENT_STAGGER_MS, segmentFillDelayMs } from '@/features/config/components/blocks/shared';
import { generateSecureApiKey } from '@/utils/apiKey';
const LOCALES = ['en', 'zh-CN', 'zh-TW', 'ru'];
describe('ApiKeyStrengthMeter', () => {
test('exposes the tier through the progressbar', () => {
const markup = renderToStaticMarkup(
createElement(ApiKeyStrengthMeter, { value: generateSecureApiKey() })
);
expect(markup).toContain('aria-valuenow="4"');
expect(markup).toContain('aria-valuemax="4"');
expect(markup).toContain(
`aria-valuetext="${i18n.t('config_management.visual.api_keys.strength.strong')}"`
);
expect(markup.match(/data-filled="true"/g)).toHaveLength(4);
});
test('empty input lights nothing and shows a placeholder label', () => {
const markup = renderToStaticMarkup(createElement(ApiKeyStrengthMeter, { value: '' }));
expect(markup).toContain('aria-valuenow="0"');
expect(markup).not.toContain('data-filled="true"');
expect(markup).toContain('—');
});
test('segments cascade only over the newly lit ones', () => {
const delays = (segments: number, previous: number) =>
[0, 1, 2, 3].map((index) => segmentFillDelayMs(index, segments, previous));
// 0 → 4点「生成」四段依次起跑
expect(delays(4, 0)).toEqual([
0,
SEGMENT_STAGGER_MS,
SEGMENT_STAGGER_MS * 2,
SEGMENT_STAGGER_MS * 3,
]);
// 2 → 3键入一个字符新增的那段立刻亮不为它的下标排队
expect(delays(3, 2)).toEqual([0, 0, 0, 0]);
// 1 → 3只有新增的两段排队
expect(delays(3, 1)).toEqual([0, 0, SEGMENT_STAGGER_MS, 0]);
// 4 → 2删字符熄灭立即发生
expect(delays(2, 4)).toEqual([0, 0, 0, 0]);
});
test('every tier label is translated in all locales', async () => {
const original = i18n.language;
for (const locale of LOCALES) {
await i18n.changeLanguage(locale);
for (const key of ['label', 'empty', 'weak', 'fair', 'good', 'strong']) {
const path = `config_management.visual.api_keys.strength.${key}`;
expect(i18n.exists(path)).toBe(true);
expect(i18n.t(path)).not.toBe(path);
}
}
await i18n.changeLanguage(original);
});
});

View file

@ -0,0 +1,186 @@
import { describe, expect, test } from 'vitest';
import { deriveAuthFileIdentity, stripJsonExtension } from '../src/features/authFiles/identity';
import type { AuthFileItem } from '../src/types';
const authFile = (overrides: Partial<AuthFileItem> = {}): AuthFileItem => ({
name: 'credential.json',
type: 'codex',
...overrides,
});
describe('stripJsonExtension', () => {
test('strips the extension from a real codex file name', () => {
expect(stripJsonExtension('codex-abc12345-user@example.com-team.json')).toBe(
'codex-abc12345-user@example.com-team'
);
});
test('matches the extension case-insensitively', () => {
expect(stripJsonExtension('Antigravity-User@Example.com.JSON')).toBe(
'Antigravity-User@Example.com'
);
});
test('leaves an extension-less runtime-only id untouched', () => {
expect(stripJsonExtension('aistudio-channel-a')).toBe('aistudio-channel-a');
});
test('never strips down to an empty string', () => {
expect(stripJsonExtension('.json')).toBe('.json');
});
test('trims surrounding whitespace', () => {
expect(stripJsonExtension(' kimi-1712345678901.json ')).toBe('kimi-1712345678901');
});
});
describe('deriveAuthFileIdentity', () => {
test('leads with the account email and keeps the full name as the secondary row', () => {
expect(
deriveAuthFileIdentity(
authFile({
name: 'codex-abc12345-user@example.com-team.json',
email: 'user@example.com',
})
)
).toEqual({
primary: 'user@example.com',
kind: 'email',
secondary: 'codex-abc12345-user@example.com-team',
fullName: 'codex-abc12345-user@example.com-team.json',
});
});
test('treats a whitespace-only email as absent', () => {
const identity = deriveAuthFileIdentity(
authFile({ name: 'kimi-1712345678901.json', email: ' ' })
);
expect(identity.kind).toBe('fileName');
expect(identity.primary).toBe('kimi-1712345678901');
});
test('treats a non-string email as absent (index signature guard)', () => {
const identity = deriveAuthFileIdentity(
authFile({ name: 'kimi-1712345678901.json', email: 123 as unknown as string })
);
expect(identity.kind).toBe('fileName');
expect(identity.primary).toBe('kimi-1712345678901');
});
test('falls back to the file name and drops the duplicate secondary row', () => {
expect(
deriveAuthFileIdentity(authFile({ name: 'kimi-1712345678901.json', type: 'kimi' }))
).toEqual({
primary: 'kimi-1712345678901',
kind: 'fileName',
secondary: null,
fullName: 'kimi-1712345678901.json',
});
});
test('uses the project id when there is no email', () => {
expect(
deriveAuthFileIdentity(
authFile({ name: 'vertex-my-proj.json', type: 'vertex', projectId: 'my-proj' })
)
).toEqual({
primary: 'my-proj',
kind: 'projectId',
secondary: 'vertex-my-proj',
fullName: 'vertex-my-proj.json',
});
});
test('prefers the email over the project id', () => {
const identity = deriveAuthFileIdentity(
authFile({
name: 'vertex-my-proj.json',
email: 'sa@project.iam.gserviceaccount.com',
projectId: 'my-proj',
})
);
expect(identity.kind).toBe('email');
expect(identity.primary).toBe('sa@project.iam.gserviceaccount.com');
});
test('never surfaces the account field — it can be a raw API key', () => {
const identity = deriveAuthFileIdentity(
authFile({
name: 'gemini-apikey.json',
type: 'gemini',
account: 'sk-live-abcd1234',
account_type: 'api_key',
})
);
expect(identity.kind).toBe('fileName');
expect(identity.primary).toBe('gemini-apikey');
expect(JSON.stringify(identity)).not.toContain('sk-live');
});
test('ignores an oauth account even when it looks like an email (do not add it back to the chain)', () => {
const identity = deriveAuthFileIdentity(
authFile({
name: 'codex-abc12345-user@example.com-team.json',
account: 'user@example.com',
account_type: 'oauth',
})
);
expect(identity.kind).toBe('fileName');
expect(identity.primary).toBe('codex-abc12345-user@example.com-team');
expect(identity.secondary).toBeNull();
});
test('suppresses the secondary row for runtime-only entries whose name is the account', () => {
expect(
deriveAuthFileIdentity(
authFile({
name: 'aistudio-channel-a',
type: 'aistudio',
email: 'aistudio-channel-a',
runtimeOnly: true,
})
).secondary
).toBeNull();
});
test('the duplicate guard is case-insensitive', () => {
expect(
deriveAuthFileIdentity(authFile({ name: 'USER@X.COM.json', email: 'user@x.com' })).secondary
).toBeNull();
});
test('keeps the disambiguating secondary row for two credentials sharing one email', () => {
const team = deriveAuthFileIdentity(
authFile({ name: 'codex-abc12345-user@example.com-team.json', email: 'user@example.com' })
);
const plus = deriveAuthFileIdentity(
authFile({ name: 'codex-abc12345-user@example.com-plus.json', email: 'user@example.com' })
);
expect(team.primary).toBe(plus.primary);
expect(team.secondary).not.toBeNull();
expect(team.secondary).not.toBe(plus.secondary);
});
test('does not strip a provider prefix from the secondary row', () => {
expect(
deriveAuthFileIdentity(
authFile({ name: '-abc12345-user@example.com-team.json', email: 'user@example.com' })
).secondary
).toBe('-abc12345-user@example.com-team');
});
test('handles an empty file name without inventing a placeholder', () => {
expect(deriveAuthFileIdentity(authFile({ name: '', email: 'user@x.com' }))).toEqual({
primary: 'user@x.com',
kind: 'email',
secondary: null,
fullName: '',
});
expect(deriveAuthFileIdentity(authFile({ name: '' }))).toEqual({
primary: '',
kind: 'fileName',
secondary: null,
fullName: '',
});
});
});

View file

@ -0,0 +1,44 @@
import { describe, expect, test } from 'vitest';
import { isProblemAuthFile } from '../src/features/authFiles/constants';
import type { AuthFileItem } from '../src/types';
const authFile = (overrides: Partial<AuthFileItem> = {}): AuthFileItem => ({
name: 'credential.json',
type: 'codex',
...overrides,
});
describe('auth file problem status', () => {
test('does not classify a deliberately disabled credential as a problem', () => {
expect(
isProblemAuthFile(
authFile({
disabled: true,
status: 'disabled',
statusMessage: 'disabled via management API',
})
)
).toBe(false);
});
test('also respects the backend disabled status when the boolean is absent', () => {
expect(
isProblemAuthFile(
authFile({
status: ' DISABLED ',
statusMessage: 'disabled via management API',
})
)
).toBe(false);
});
test('ignores healthy status messages', () => {
expect(isProblemAuthFile(authFile({ status: 'active', statusMessage: 'ok' }))).toBe(false);
});
test('detects warning messages, unavailable credentials, and error status', () => {
expect(isProblemAuthFile(authFile({ statusMessage: 'quota exhausted' }))).toBe(true);
expect(isProblemAuthFile(authFile({ unavailable: true }))).toBe(true);
expect(isProblemAuthFile(authFile({ status: 'error' }))).toBe(true);
});
});

View file

@ -0,0 +1,154 @@
import { describe, expect, test } from 'vitest';
import {
buildAuthFileFieldsPatch,
type PrefixProxyEditorState,
} from '../src/features/authFiles/hooks/useAuthFilesPrefixProxyEditor';
import { readAuthFileDisableCooling } from '../src/features/authFiles/constants';
const makeEditor = (json: Record<string, unknown>, weight: string): PrefixProxyEditorState => ({
fileName: 'credential.json',
fileInfoText: '',
loading: false,
saving: false,
error: null,
originalText: JSON.stringify(json),
rawText: JSON.stringify(json),
invalidContentPreview: '',
json,
providerKey: 'codex',
prefix: '',
proxyUrl: '',
priority: '',
weight,
weightError: null,
disableCooling: false,
disableCoolingTouched: false,
websockets: false,
websocketsTouched: false,
usingApi: false,
usingApiTouched: false,
note: '',
noteTouched: false,
excludedModelsText: '',
excludedModelsTouched: false,
headersText: '',
headersTouched: false,
headersError: null,
});
const resolveError = (key: string) => key;
describe('auth-file credential weight patch', () => {
test('writes numeric weight and uses null to restore the default', () => {
expect(buildAuthFileFieldsPatch(makeEditor({}, '0'), resolveError)).toEqual({ weight: 0 });
expect(buildAuthFileFieldsPatch(makeEditor({ weight: 5 }, ''), resolveError)).toEqual({
weight: null,
});
});
test('recognizes a numeric string in an existing auth file', () => {
expect(buildAuthFileFieldsPatch(makeEditor({ weight: '7' }, '7'), resolveError)).toEqual({});
expect(buildAuthFileFieldsPatch(makeEditor({ weight: '7' }, ''), resolveError)).toEqual({
weight: null,
});
});
test('rejects invalid and oversized values before PATCH', () => {
expect(() => buildAuthFileFieldsPatch(makeEditor({}, '1.5'), resolveError)).toThrow(
'auth_files.weight_invalid_integer'
);
expect(() => buildAuthFileFieldsPatch(makeEditor({}, '1000001'), resolveError)).toThrow(
'auth_files.weight_invalid_max'
);
});
});
describe('auth-file disable cooling patch', () => {
test('reads canonical and legacy boolean-compatible metadata', () => {
expect(readAuthFileDisableCooling({ disable_cooling: 'true' })).toBe(true);
expect(readAuthFileDisableCooling({ 'disable-cooling': 1 })).toBe(true);
expect(
readAuthFileDisableCooling({ disable_cooling: 'invalid', 'disable-cooling': true })
).toBe(true);
expect(readAuthFileDisableCooling({ disable_cooling: false, 'disable-cooling': true })).toBe(
false
);
});
test('writes the canonical field when enabling the per-credential override', () => {
const editor = {
...makeEditor({}, ''),
disableCooling: true,
disableCoolingTouched: true,
};
expect(buildAuthFileFieldsPatch(editor, resolveError)).toEqual({ disable_cooling: true });
});
test('preserves the legacy field name and writes false when disabling it', () => {
const editor = {
...makeEditor({ 'disable-cooling': true }, ''),
disableCooling: false,
disableCoolingTouched: true,
};
expect(buildAuthFileFieldsPatch(editor, resolveError)).toEqual({ 'disable-cooling': false });
});
test('does not patch an untouched or unchanged override', () => {
expect(buildAuthFileFieldsPatch(makeEditor({ disable_cooling: true }, ''), resolveError)).toEqual(
{}
);
expect(
buildAuthFileFieldsPatch(
{
...makeEditor({ disable_cooling: 'true' }, ''),
disableCooling: true,
disableCoolingTouched: true,
},
resolveError
)
).toEqual({});
});
});
describe('auth-file excluded models patch', () => {
test('writes normalized model patterns to the canonical field', () => {
const editor = {
...makeEditor({}, ''),
excludedModelsText: ' gpt-5-*\nGPT-5-*\nclaude-opus ',
excludedModelsTouched: true,
};
expect(buildAuthFileFieldsPatch(editor, resolveError)).toEqual({
excluded_models: ['gpt-5-*', 'claude-opus'],
});
});
test('preserves the legacy hyphenated field name', () => {
const editor = {
...makeEditor({ 'excluded-models': ['old-model'] }, ''),
excludedModelsText: 'new-model',
excludedModelsTouched: true,
};
expect(buildAuthFileFieldsPatch(editor, resolveError)).toEqual({
'excluded-models': ['new-model'],
});
});
test('clears exclusions with an empty array and ignores untouched values', () => {
const original = { excluded_models: ['gpt-5-*'] };
expect(buildAuthFileFieldsPatch(makeEditor(original, ''), resolveError)).toEqual({});
expect(
buildAuthFileFieldsPatch(
{
...makeEditor(original, ''),
excludedModelsText: '',
excludedModelsTouched: true,
},
resolveError
)
).toEqual({ excluded_models: [] });
});
});

View file

@ -0,0 +1,158 @@
import { describe, expect, test } from 'vitest';
import {
buildWildcardSearch,
matchesAuthFileSearch,
sortAuthFiles,
} from '../src/features/authFiles/logic';
import type { AuthFileItem } from '../src/types';
const authFile = (overrides: Partial<AuthFileItem> = {}): AuthFileItem => ({
name: 'credential.json',
type: 'codex',
...overrides,
});
const search = (file: AuthFileItem, term: string) =>
matchesAuthFileSearch(file, term, buildWildcardSearch(term));
describe('buildWildcardSearch', () => {
test('returns null when the term has no wildcard', () => {
expect(buildWildcardSearch('user@example.com')).toBeNull();
});
test('stays unanchored — segments may match anywhere in the value', () => {
expect(buildWildcardSearch('a*b')?.test('zzzaqqbzzz')).toBe(true);
});
test('escapes regex metacharacters in the segments', () => {
const pattern = buildWildcardSearch('u+1*');
expect(pattern?.test('u+1@x.com')).toBe(true);
expect(pattern?.test('u1@x.com')).toBe(false);
});
test('a lone wildcard matches everything', () => {
expect(buildWildcardSearch('*')?.test('anything')).toBe(true);
});
});
describe('matchesAuthFileSearch', () => {
test('an empty term matches every file', () => {
expect(matchesAuthFileSearch(authFile(), '', null)).toBe(true);
});
test('matches the file name case-insensitively', () => {
expect(search(authFile({ name: 'codex-a.json' }), 'CODEX')).toBe(true);
});
test('matches the type and the provider', () => {
expect(search(authFile({ type: 'antigravity' }), 'antigrav')).toBe(true);
expect(search(authFile({ type: undefined, provider: 'gemini' }), 'gemi')).toBe(true);
});
test('finds a kimi credential by account email even though its name carries none', () => {
expect(
search(
authFile({ name: 'kimi-1712345678901.json', type: 'kimi', email: 'user@example.com' }),
'user@example'
)
).toBe(true);
});
test('matches the project id', () => {
expect(search(authFile({ name: 'vertex-x.json', projectId: 'my-proj' }), 'my-proj')).toBe(true);
});
test('searches the email on the wildcard path too', () => {
expect(
search(authFile({ name: 'kimi-1712345678901.json', email: 'user@example.com' }), 'user@*com')
).toBe(true);
});
test('never matches the account field — it can be a raw API key', () => {
expect(
search(
authFile({ name: 'gemini-apikey.json', account: 'sk-live-abcd', account_type: 'api_key' }),
'sk-live'
)
).toBe(false);
});
test('tolerates missing fields', () => {
expect(search({ name: 'bare.json' }, 'zzz')).toBe(false);
});
});
describe('sortAuthFiles', () => {
test("'default' orders by provider then name", () => {
const files = [
authFile({ name: 'b.json', provider: 'kimi' }),
authFile({ name: 'c.json', type: 'codex', provider: undefined }),
authFile({ name: 'a.json', provider: 'kimi' }),
];
expect(sortAuthFiles(files, 'default').map((file) => file.name)).toEqual([
'c.json',
'a.json',
'b.json',
]);
});
test("'az' orders by the displayed primary row, not by the file name", () => {
const files = [
authFile({ name: 'zzz.json', email: 'aaa@example.com' }),
authFile({ name: 'aaa.json', email: 'zzz@example.com' }),
];
expect(sortAuthFiles(files, 'az').map((file) => file.name)).toEqual(['zzz.json', 'aaa.json']);
});
test("'az' falls back to the file name when primaries are equal", () => {
const files = [
authFile({ name: 'codex-abc-user@x.com-team.json', email: 'user@x.com' }),
authFile({ name: 'codex-abc-user@x.com-plus.json', email: 'user@x.com' }),
];
expect(sortAuthFiles(files, 'az').map((file) => file.name)).toEqual([
'codex-abc-user@x.com-plus.json',
'codex-abc-user@x.com-team.json',
]);
});
test("'az' mixes account primaries and file-name fallbacks coherently", () => {
const files = [
authFile({ name: 'kimi-9.json', type: 'kimi' }),
authFile({ name: 'zzz.json', email: 'aaa@example.com' }),
];
expect(sortAuthFiles(files, 'az').map((file) => file.name)).toEqual([
'zzz.json',
'kimi-9.json',
]);
});
test("'priority' orders descending, treats missing values as 0 and stays stable on ties", () => {
const files = [
authFile({ name: 'a.json' }),
authFile({ name: 'b.json', priority: 5 }),
authFile({ name: 'c.json' }),
authFile({ name: 'd.json', priority: 9 }),
];
expect(sortAuthFiles(files, 'priority').map((file) => file.name)).toEqual([
'd.json',
'b.json',
'a.json',
'c.json',
]);
});
test('returns a new array and leaves the input untouched', () => {
const files = [authFile({ name: 'b.json' }), authFile({ name: 'a.json' })];
const result = sortAuthFiles(files, 'az');
expect(result).not.toBe(files);
expect(files.map((file) => file.name)).toEqual(['b.json', 'a.json']);
});
test('an unknown mode returns an unsorted copy', () => {
const files = [authFile({ name: 'b.json' }), authFile({ name: 'a.json' })];
expect(sortAuthFiles(files, 'nope' as unknown as 'az').map((file) => file.name)).toEqual([
'b.json',
'a.json',
]);
});
});

View file

@ -0,0 +1,108 @@
import { describe, expect, test } from 'vitest';
import { normalizeAuthFilesResponse } from '../src/services/api/authFiles';
import type { AuthFilesResponse } from '../src/types/authFile';
const responseWithRawFiles = (files: Array<Record<string, unknown>>): AuthFilesResponse =>
({ files }) as unknown as AuthFilesResponse;
describe('auth-files response normalization', () => {
test('normalizes WRR weights while preserving zero and negative values', () => {
const result = normalizeAuthFilesResponse(
responseWithRawFiles([
{ name: 'positive.json', weight: 5 },
{ name: 'string.json', weight: '7' },
{ name: 'zero.json', weight: 0 },
{ name: 'negative.json', weight: -2 },
])
);
expect(result.files.map((file) => file.weight)).toEqual([-2, 5, 7, 0]);
});
test('omits missing, fractional, and unsafe WRR weights from the normalized field', () => {
const result = normalizeAuthFilesResponse(
responseWithRawFiles([
{ name: 'missing.json' },
{ name: 'fractional.json', weight: '1.5' },
{ name: 'unsafe.json', weight: Number.MAX_SAFE_INTEGER + 1 },
])
);
expect(result.files.map((file) => file.weight)).toEqual([undefined, undefined, undefined]);
});
test('applies the same safe-integer normalization to priority', () => {
const result = normalizeAuthFilesResponse(
responseWithRawFiles([
{ name: 'valid.json', priority: '-3' },
{ name: 'invalid.json', priority: '3.5' },
])
);
expect(result.files.map((file) => file.priority)).toEqual([undefined, -3]);
});
test('surfaces the trimmed account email', () => {
const result = normalizeAuthFilesResponse(
responseWithRawFiles([{ name: 'codex-a.json', email: ' user@example.com ' }])
);
expect(result.files[0]?.email).toBe('user@example.com');
});
test('leaves an empty backend email as the raw empty string', () => {
const result = normalizeAuthFilesResponse(
responseWithRawFiles([{ name: 'kimi-1.json', email: '' }])
);
expect(result.files[0]?.email).toBe('');
});
test('normalizes project_id to projectId while keeping the raw key', () => {
const result = normalizeAuthFilesResponse(
responseWithRawFiles([{ name: 'vertex-a.json', project_id: ' my-proj ' }])
);
expect(result.files[0]?.projectId).toBe('my-proj');
expect(result.files[0]?.project_id).toBe(' my-proj ');
});
test('recovers a non-empty email from the lower-priority duplicate entry', () => {
const result = normalizeAuthFilesResponse(
responseWithRawFiles([
{ name: 'codex-a.json', source: 'file', path: '/auths/codex-a.json', email: '' },
{ name: 'codex-a.json', source: 'memory', email: 'user@example.com' },
])
);
expect(result.files).toHaveLength(1);
expect(result.files[0]?.email).toBe('user@example.com');
});
test('prefers the higher-scored entry when both emails are non-empty and differ', () => {
const result = normalizeAuthFilesResponse(
responseWithRawFiles([
{ name: 'codex-a.json', source: 'memory', email: 'stale@example.com' },
{
name: 'codex-a.json',
source: 'file',
path: '/auths/codex-a.json',
email: 'fresh@example.com',
},
])
);
expect(result.files[0]?.email).toBe('fresh@example.com');
});
test('passes account through raw without deriving a camelCase field', () => {
const result = normalizeAuthFilesResponse(
responseWithRawFiles([
{ name: 'gemini-apikey.json', account: 'sk-live-abcd', account_type: 'api_key' },
])
);
expect(result.files[0]?.account).toBe('sk-live-abcd');
expect(result.files[0]?.accountType).toBeUndefined();
});
});

View file

@ -0,0 +1,189 @@
import { describe, expect, test } from 'vitest';
import type { TFunction } from 'i18next';
import { buildClaudeQuotaWindows } from '@/features/quota/providers/claude/data';
import type { ClaudeUsagePayload } from '@/types';
import { formatQuotaResetTime } from '@/utils/quota';
const t = ((key: string) => key) as TFunction;
const modernReset = '2026-07-27T10:00:00.000000+00:00';
const legacyReset = '2026-07-28T10:00:00.000000+00:00';
describe('Claude Fable quota', () => {
test('builds a Fable window from the modern scoped limits payload', () => {
const windows = buildClaudeQuotaWindows(
{
limits: [
{
kind: 'weekly_scoped',
group: 'weekly',
percent: 64,
resets_at: modernReset,
is_active: true,
scope: { model: { id: null, display_name: 'Fable' } },
},
],
},
t
);
expect(windows).toEqual([
{
id: 'seven-day-fable',
label: 'claude_quota.seven_day_fable',
labelKey: 'claude_quota.seven_day_fable',
usedPercent: 64,
resetLabel: formatQuotaResetTime(modernReset),
resetAtMs: Date.parse(modernReset),
periodHours: 24 * 7,
},
]);
});
test('falls back to the legacy Fable field', () => {
const windows = buildClaudeQuotaWindows(
{
iguana_necktie: {
utilization: 41,
resets_at: legacyReset,
},
},
t
);
expect(windows).toEqual([
{
id: 'seven-day-fable',
label: 'claude_quota.seven_day_fable',
labelKey: 'claude_quota.seven_day_fable',
usedPercent: 41,
resetLabel: formatQuotaResetTime(legacyReset),
resetAtMs: Date.parse(legacyReset),
periodHours: 24 * 7,
},
]);
});
test('falls back to the legacy field when the modern percent is invalid', () => {
const windows = buildClaudeQuotaWindows(
{
iguana_necktie: {
utilization: 41,
resets_at: legacyReset,
},
limits: [
{
kind: 'weekly_scoped',
percent: null,
resets_at: modernReset,
is_active: true,
scope: { model: { display_name: 'Fable' } },
},
],
},
t
);
expect(windows).toEqual([
{
id: 'seven-day-fable',
label: 'claude_quota.seven_day_fable',
labelKey: 'claude_quota.seven_day_fable',
usedPercent: 41,
resetLabel: formatQuotaResetTime(legacyReset),
resetAtMs: Date.parse(legacyReset),
periodHours: 24 * 7,
},
]);
});
test('prefers the active modern field without rendering a duplicate', () => {
const windows = buildClaudeQuotaWindows(
{
iguana_necktie: {
utilization: 41,
resets_at: legacyReset,
},
limits: [
{
kind: 'weekly_scoped',
percent: 12,
resets_at: legacyReset,
is_active: false,
scope: { model: { display_name: 'Fable 5' } },
},
{
kind: 'weekly_scoped',
percent: 64,
resets_at: modernReset,
is_active: true,
scope: { model: { display_name: 'Fable' } },
},
],
},
t
);
expect(windows).toHaveLength(1);
expect(windows[0]).toMatchObject({
id: 'seven-day-fable',
usedPercent: 64,
resetLabel: formatQuotaResetTime(modernReset),
});
});
test('uses a valid modern candidate when the preferred candidate is invalid', () => {
const windows = buildClaudeQuotaWindows(
{
limits: [
{
kind: 'weekly_scoped',
percent: null,
resets_at: legacyReset,
is_active: true,
scope: { model: { display_name: 'Fable' } },
},
{
kind: 'weekly_scoped',
percent: 64,
resets_at: modernReset,
is_active: false,
scope: { model: { display_name: 'Fable' } },
},
],
},
t
);
expect(windows).toEqual([
{
id: 'seven-day-fable',
label: 'claude_quota.seven_day_fable',
labelKey: 'claude_quota.seven_day_fable',
usedPercent: 64,
resetLabel: formatQuotaResetTime(modernReset),
resetAtMs: Date.parse(modernReset),
periodHours: 24 * 7,
},
]);
});
test('ignores malformed and unrelated limits while preserving standard windows', () => {
const payload = {
five_hour: { utilization: 10, resets_at: null },
seven_day: { utilization: 20, resets_at: legacyReset },
limits: [
null,
{ kind: 'weekly_scoped', percent: 35, scope: { model: { display_name: 'Sonnet' } } },
{ kind: 'session', percent: 50, scope: { model: { display_name: 'Fable' } } },
{ kind: 'weekly_scoped', percent: null, scope: { model: { display_name: 'Fable' } } },
],
} as unknown as ClaudeUsagePayload;
const windows = buildClaudeQuotaWindows(payload, t);
expect(windows.map(({ id, usedPercent }) => ({ id, usedPercent }))).toEqual([
{ id: 'five-hour', usedPercent: 10 },
{ id: 'seven-day', usedPercent: 20 },
]);
});
});

View file

@ -0,0 +1,89 @@
import { describe, expect, test } from 'vitest';
import type { TFunction } from 'i18next';
import { CODEX_CONFIG, buildCodexQuotaWindows } from '@/features/quota/providers/codex/data';
import type { CodexQuotaState, CodexUsagePayload } from '@/types';
import { normalizeCodexResetCreditsPayload, parseCodexUsagePayload } from '@/utils/quota';
const t = ((key: string) => key) as TFunction;
const CURRENT_CODEX_USAGE_PAYLOAD: CodexUsagePayload = {
plan_type: 'pro',
rate_limit: {
allowed: true,
limit_reached: false,
primary_window: {
used_percent: 1,
limit_window_seconds: 604800,
reset_after_seconds: 601888,
reset_at: 1785902974,
},
secondary_window: null,
},
code_review_rate_limit: null,
additional_rate_limits: [
{
limit_name: 'GPT-5.3-Codex-Spark',
metered_feature: 'codex_bengalfox',
rate_limit: {
allowed: true,
limit_reached: false,
primary_window: {
used_percent: 0,
limit_window_seconds: 604800,
reset_after_seconds: 602111,
reset_at: 1785903197,
},
secondary_window: null,
},
},
],
rate_limit_reset_credits: {
available_count: 1,
applicable_available_count: 0,
},
};
describe('Codex current usage payload', () => {
test('parses the proxied JSON body and classifies both primary weekly windows', () => {
const payload = parseCodexUsagePayload(JSON.stringify(CURRENT_CODEX_USAGE_PAYLOAD));
expect(payload).not.toBeNull();
const windows = buildCodexQuotaWindows(payload!, t);
expect(windows.map(({ id }) => id)).toEqual(['weekly', 'gpt-5-3-codex-spark-weekly-0']);
expect(windows.map(({ labelKey }) => labelKey)).toEqual([
'codex_quota.secondary_window',
'codex_quota.additional_secondary_window',
]);
expect(windows.map(({ usedPercent }) => usedPercent)).toEqual([1, 0]);
expect(windows[1]?.labelParams).toEqual({ name: 'GPT-5.3-Codex-Spark' });
});
test('shows reset support when total credits remain but none currently apply', () => {
const summary = normalizeCodexResetCreditsPayload(
CURRENT_CODEX_USAGE_PAYLOAD.rate_limit_reset_credits
);
expect(summary.invalidPayload).toBe(false);
expect(summary.availableCount).toBe(1);
expect(summary.applicableAvailableCount).toBe(0);
const quota: CodexQuotaState = {
status: 'success',
windows: [],
rateLimitResetCreditsAvailableCount: summary.availableCount,
rateLimitResetCreditsApplicableAvailableCount: summary.applicableAvailableCount,
};
expect(CODEX_CONFIG.canResetQuota?.(quota)).toBe(true);
});
test('keeps reset support for legacy payloads without applicable count', () => {
const quota: CodexQuotaState = {
status: 'success',
windows: [],
rateLimitResetCreditsAvailableCount: 1,
};
expect(CODEX_CONFIG.canResetQuota?.(quota)).toBe(true);
});
});

View file

@ -0,0 +1,132 @@
// 配置项零遗漏守卫:三方对账
// ① 搜索索引searchIndex.ts唯一的机器可读字段清单
// ② 值键映射constants.ts FIELD_VALUE_KEYS ↔ VisualConfigValues 叶值键)
// ③ 分区 JSX 里实际渲染的 <FieldAnchor fieldId="…"> 锚点(源码扫描)
// 任何一方增删字段而漏改其余两方,本套件即红。
import { readFileSync, readdirSync } from 'node:fs';
import { join } from 'node:path';
import { fileURLToPath } from 'node:url';
import { describe, expect, test } from 'vitest';
import {
COMMON_FIELD_IDS,
CONFIG_SECTION_IDS,
CONFIG_TAB_IDS,
FIELD_VALUE_KEYS,
SECTION_VALIDATION_FIELDS,
} from '@/features/config/constants';
import { CONFIG_FIELD_SEARCH_INDEX } from '@/features/config/searchIndex';
import { getVisualConfigValidationErrors } from '@/hooks/useVisualConfig';
import { DEFAULT_VISUAL_VALUES } from '@/types/visualConfig';
const INDEX_FIELD_IDS = CONFIG_FIELD_SEARCH_INDEX.map((entry) => entry.fieldId);
const INDEX_FIELD_ID_SET = new Set(INDEX_FIELD_IDS);
/** VisualConfigValues 的叶值键:顶层标量 + streaming 展开为点号叶(= dirtyFields 的键域)。 */
const LEAF_VALUE_KEYS = new Set(
Object.keys(DEFAULT_VISUAL_VALUES).flatMap((key) =>
key === 'streaming'
? Object.keys(DEFAULT_VISUAL_VALUES.streaming).map((leaf) => `streaming.${leaf}`)
: [key]
)
);
const sorted = (values: Iterable<string>) => [...values].sort();
describe('search index integrity', () => {
test('field ids are unique', () => {
expect(INDEX_FIELD_ID_SET.size).toBe(INDEX_FIELD_IDS.length);
});
test('every entry belongs to a canonical section', () => {
const sectionIds = new Set<string>(CONFIG_SECTION_IDS);
for (const entry of CONFIG_FIELD_SEARCH_INDEX) {
expect(sectionIds.has(entry.sectionId)).toBe(true);
}
});
test('label / qualifier / hint keys resolve to strings in en.json', async () => {
const json = JSON.parse(readFileSync('src/i18n/locales/en.json', 'utf8')) as Record<
string,
unknown
>;
const resolveKey = (path: string): unknown =>
path.split('.').reduce<unknown>((node, part) => {
if (node && typeof node === 'object') return (node as Record<string, unknown>)[part];
return undefined;
}, json);
for (const entry of CONFIG_FIELD_SEARCH_INDEX) {
expect(typeof resolveKey(entry.labelKey)).toBe('string');
if (entry.qualifierKey) expect(typeof resolveKey(entry.qualifierKey)).toBe('string');
if (entry.hintKey) expect(typeof resolveKey(entry.hintKey)).toBe('string');
}
});
});
describe('value-key coverage (index ↔ VisualConfigValues)', () => {
test('FIELD_VALUE_KEYS keys are exactly the index field ids', () => {
expect(sorted(Object.keys(FIELD_VALUE_KEYS))).toEqual(sorted(INDEX_FIELD_ID_SET));
});
test('the union of mapped value keys is exactly the VisualConfigValues leaf keys', () => {
const mapped = new Set(Object.values(FIELD_VALUE_KEYS).flat());
// 双向:漏映射的表单键 / 指向不存在键的映射,都在这里现形
expect(sorted(mapped)).toEqual(sorted(LEAF_VALUE_KEYS));
});
});
describe('JSX anchor parity (source scan)', () => {
test('every index entry is rendered by exactly the section JSX, and vice versa', () => {
const componentsDir = fileURLToPath(
new URL('../src/features/config/components', import.meta.url)
);
const sectionsDir = join(componentsDir, 'sections');
const scannedFiles = [
...readdirSync(sectionsDir)
.filter((name) => name.endsWith('.tsx'))
.map((name) => join(sectionsDir, name)),
join(componentsDir, 'fields/sharedFields.tsx'),
];
const renderedFieldIds = new Set<string>();
for (const filePath of scannedFiles) {
const source = readFileSync(filePath, 'utf8');
for (const match of source.matchAll(/fieldId="([^"]+)"/g)) {
renderedFieldIds.add(match[1]);
}
}
const missingFromJsx = sorted(INDEX_FIELD_IDS).filter((id) => !renderedFieldIds.has(id));
const unknownInJsx = sorted(renderedFieldIds).filter((id) => !INDEX_FIELD_ID_SET.has(id));
// 分区 JSX 静默丢字段 → missingFromJsx 非空;新增字段没进索引 → unknownInJsx 非空
expect(missingFromJsx).toEqual([]);
expect(unknownInJsx).toEqual([]);
});
});
describe('registry consistency', () => {
test('tab id registry is common + the seven canonical sections', () => {
expect([...CONFIG_TAB_IDS]).toEqual(['common', ...CONFIG_SECTION_IDS]);
});
test('every validation field path lives in exactly one section bucket', () => {
const allPaths = Object.keys(getVisualConfigValidationErrors(DEFAULT_VISUAL_VALUES)).sort();
const bucketed = Object.values(SECTION_VALIDATION_FIELDS).flat();
expect(new Set(bucketed).size).toBe(bucketed.length); // 不允许一个字段进两个桶
expect(sorted(bucketed)).toEqual(allPaths);
});
test('validation field paths are real value keys', () => {
for (const path of Object.values(SECTION_VALIDATION_FIELDS).flat()) {
expect(LEAF_VALUE_KEYS.has(path)).toBe(true);
}
});
test('common tab fields are a subset of the index', () => {
for (const fieldId of COMMON_FIELD_IDS) {
expect(INDEX_FIELD_ID_SET.has(fieldId)).toBe(true);
}
});
});

View file

@ -0,0 +1,27 @@
import { describe, expect, test } from 'vitest';
import { createElement } from 'react';
import { renderToStaticMarkup } from 'react-dom/server';
import i18n from '@/i18n';
import { ConfigTabs } from '@/features/config/components/ConfigTabs';
const noop = () => {};
describe('ConfigTabs accessibility', () => {
test('announces validation errors and unsaved changes in the tab name', () => {
const markup = renderToStaticMarkup(
createElement(ConfigTabs, {
active: 'common',
errorCounts: { streaming: 2 },
dirtyTabs: new Set(['streaming']),
onChange: noop,
})
);
const accessibleLabel = [
i18n.t('config_management.visual.sections.streaming.title'),
i18n.t('config_management.meta_errors', { count: 2 }),
i18n.t('config_management.status_dirty_short'),
].join(', ');
expect(markup).toContain(`aria-label="${accessibleLabel}"`);
});
});

View file

@ -0,0 +1,223 @@
import { readFileSync } from 'node:fs';
import { describe, expect, test } from 'vitest';
import {
buildHeaderMeta,
countSectionErrors,
countTotalErrors,
readSavedMode,
readSavedSection,
resolveDirtyTabs,
resolveStatus,
type ConfigStatusInput,
} from '@/features/config/uiState';
import type { VisualConfigValidationErrors } from '@/types/visualConfig';
const statusInput = (overrides: Partial<ConfigStatusInput> = {}): ConfigStatusInput => ({
disconnected: false,
loading: false,
loadFailed: false,
yamlError: false,
validationBlocked: false,
saving: false,
dirty: false,
...overrides,
});
describe('resolveStatus', () => {
test('follows the legacy precedence chain top-down', () => {
// disconnected > loading > load_failed > yaml_error > validation_blocked > saving > dirty > synced
expect(resolveStatus(statusInput({ disconnected: true, loading: true })).key).toBe(
'disconnected'
);
expect(resolveStatus(statusInput({ loading: true, loadFailed: true })).key).toBe('loading');
expect(resolveStatus(statusInput({ loadFailed: true, yamlError: true })).key).toBe(
'load_failed'
);
expect(resolveStatus(statusInput({ yamlError: true, validationBlocked: true })).key).toBe(
'yaml_error'
);
expect(resolveStatus(statusInput({ validationBlocked: true, saving: true })).key).toBe(
'validation_blocked'
);
expect(resolveStatus(statusInput({ saving: true, dirty: true })).key).toBe('saving');
expect(resolveStatus(statusInput({ dirty: true })).key).toBe('dirty');
expect(resolveStatus(statusInput()).key).toBe('synced');
});
test('validation_blocked short key lives at config_management top level (regression: old .visual. path bug)', () => {
const status = resolveStatus(statusInput({ validationBlocked: true }));
expect(status.shortLabelKey).toBe('config_management.validation_blocked_short');
expect(status.labelKey).toBe('config_management.visual.validation.validation_blocked');
expect(status.tone).toBe('error');
});
test('every status resolves label keys that exist in all four locales', async () => {
const locales = ['en', 'zh-CN', 'zh-TW', 'ru'];
const inputs: Partial<ConfigStatusInput>[] = [
{ disconnected: true },
{ loading: true },
{ loadFailed: true },
{ yamlError: true },
{ validationBlocked: true },
{ saving: true },
{ dirty: true },
{},
];
for (const locale of locales) {
const json = JSON.parse(
readFileSync(`src/i18n/locales/${locale}.json`, 'utf8')
) as Record<string, unknown>;
const resolveKey = (path: string): unknown =>
path.split('.').reduce<unknown>((node, part) => {
if (node && typeof node === 'object') return (node as Record<string, unknown>)[part];
return undefined;
}, json);
for (const overrides of inputs) {
const status = resolveStatus(statusInput(overrides));
expect(typeof resolveKey(status.labelKey)).toBe('string');
expect(typeof resolveKey(status.shortLabelKey)).toBe('string');
}
}
});
});
describe('countSectionErrors', () => {
test('buckets field errors by section and mirrors common-tab fields', () => {
const errors: VisualConfigValidationErrors = {
port: 'port_range',
requestRetry: 'non_negative_integer',
'streaming.keepaliveSeconds': 'non_negative_integer',
};
const counts = countSectionErrors(errors, false);
expect(counts.connectivity).toBe(1);
expect(counts.network).toBe(1);
expect(counts.streaming).toBe(1);
expect(counts.logging).toBe(0);
expect(counts.quota).toBe(0);
expect(counts.advanced).toBe(0);
expect(counts.payload).toBe(0);
// port 由常用 tab 渲染,同一错误在两个 tab 都要可见
expect(counts.common).toBe(1);
});
test('payload flag adds one to the payload tab only', () => {
const counts = countSectionErrors(undefined, true);
expect(counts.payload).toBe(1);
expect(counts.common).toBe(0);
expect(counts.connectivity).toBe(0);
});
test('undefined error entries do not count', () => {
const errors: VisualConfigValidationErrors = { port: undefined };
const counts = countSectionErrors(errors, false);
expect(counts.connectivity).toBe(0);
expect(counts.common).toBe(0);
});
});
describe('countTotalErrors', () => {
test('sums field errors plus the payload flag', () => {
const errors: VisualConfigValidationErrors = {
port: 'port_range',
maxRetryInterval: 'non_negative_integer',
logsMaxTotalSizeMb: undefined,
};
expect(countTotalErrors(errors, false)).toBe(2);
expect(countTotalErrors(errors, true)).toBe(3);
expect(countTotalErrors(undefined, false)).toBe(0);
});
});
describe('resolveDirtyTabs', () => {
test('maps dirty value keys to their canonical sections', () => {
const tabs = resolveDirtyTabs(new Set(['rmSecretKey', 'streaming.bootstrapRetries']));
expect(tabs.has('connectivity')).toBe(true);
expect(tabs.has('streaming')).toBe(true);
expect(tabs.has('common')).toBe(false);
expect(tabs.size).toBe(2);
});
test('a common field lights both the common tab and its canonical section', () => {
const tabs = resolveDirtyTabs(new Set(['apiKeysText']));
expect(tabs.has('common')).toBe(true);
expect(tabs.has('connectivity')).toBe(true);
expect(tabs.size).toBe(2);
const quotaTabs = resolveDirtyTabs(new Set(['quotaSwitchProject']));
expect(quotaTabs.has('common')).toBe(true);
expect(quotaTabs.has('quota')).toBe(true);
});
test('unknown keys are ignored instead of crashing', () => {
const tabs = resolveDirtyTabs(new Set(['not-a-real-key']));
expect(tabs.size).toBe(0);
});
});
describe('buildHeaderMeta', () => {
const base = {
fieldCount: 58,
status: resolveStatus(statusInput()),
dirtyCount: 0,
sourceDirty: false,
errorCount: 0,
};
test('blocking statuses come directly from the page status machine', () => {
for (const key of ['disconnected', 'loading', 'load_failed'] as const) {
const status = resolveStatus(
statusInput({
disconnected: key === 'disconnected',
loading: key === 'loading',
loadFailed: key === 'load_failed',
})
);
const meta = buildHeaderMeta({ ...base, status, dirtyCount: 3 });
expect(meta.map((segment) => segment.key)).toEqual(['fields', key]);
}
});
test('clean state ends with a synced segment', () => {
const meta = buildHeaderMeta(base);
expect(meta.map((segment) => segment.key)).toEqual(['fields', 'synced']);
expect(meta[0].count).toBe(58);
});
test('dirty and errors stack after the field count', () => {
const status = resolveStatus(statusInput({ validationBlocked: true, dirty: true }));
const meta = buildHeaderMeta({ ...base, status, dirtyCount: 3, errorCount: 2 });
expect(meta.map((segment) => segment.key)).toEqual(['fields', 'dirty', 'errors']);
expect(meta[1].count).toBe(3);
expect(meta[1].tone).toBe('warning');
expect(meta[2].count).toBe(2);
expect(meta[2].tone).toBe('error');
});
test('source dirty supersedes the visual dirty count', () => {
const status = resolveStatus(statusInput({ dirty: true }));
const meta = buildHeaderMeta({ ...base, status, dirtyCount: 3, sourceDirty: true });
expect(meta.map((segment) => segment.key)).toEqual(['fields', 'dirty_source']);
});
test('yaml error shows without a synced tail', () => {
const status = resolveStatus(statusInput({ yamlError: true }));
const meta = buildHeaderMeta({ ...base, status });
expect(meta.map((segment) => segment.key)).toEqual(['fields', 'yaml_error']);
});
});
describe('localStorage readers', () => {
test('readSavedMode falls back to visual on unknown values', () => {
expect(readSavedMode('source')).toBe('source');
expect(readSavedMode('visual')).toBe('visual');
expect(readSavedMode('full')).toBe('visual'); // 旧「简单/完整」值域不再合法
expect(readSavedMode(null)).toBe('visual');
});
test('readSavedSection falls back to common on stale values', () => {
expect(readSavedSection('payload')).toBe('payload');
expect(readSavedSection('common')).toBe('common');
expect(readSavedSection('server')).toBe('common'); // 历史分区 id 不再存在
expect(readSavedSection(null)).toBe('common');
});
});

View file

@ -0,0 +1,32 @@
import { describe, expect, test } from 'vitest';
import {
MAX_CREDENTIAL_WEIGHT,
parseCredentialWeightText,
readCredentialWeight,
validateCredentialWeightText,
} from '../src/utils/credentialWeight';
describe('credential weight validation', () => {
test('accepts the default range and non-positive scheduling exclusions', () => {
expect(parseCredentialWeightText('')).toBeUndefined();
expect(parseCredentialWeightText('1')).toBe(1);
expect(parseCredentialWeightText('0')).toBe(0);
expect(parseCredentialWeightText('-2')).toBe(-2);
expect(parseCredentialWeightText(String(MAX_CREDENTIAL_WEIGHT))).toBe(MAX_CREDENTIAL_WEIGHT);
});
test('rejects non-integers and values above the backend maximum', () => {
expect(validateCredentialWeightText('1.5')).toBe('integer');
expect(validateCredentialWeightText('1e3')).toBe('integer');
expect(validateCredentialWeightText(String(MAX_CREDENTIAL_WEIGHT + 1))).toBe('max');
expect(parseCredentialWeightText('1.5')).toBeUndefined();
});
test('reads only valid numeric response fields', () => {
expect(readCredentialWeight(7)).toBe(7);
expect(readCredentialWeight(0)).toBe(0);
expect(readCredentialWeight(' 7 ')).toBe(7);
expect(readCredentialWeight('7.5')).toBeUndefined();
expect(readCredentialWeight(MAX_CREDENTIAL_WEIGHT + 1)).toBeUndefined();
});
});

View file

@ -0,0 +1,133 @@
import { describe, expect, test } from 'vitest';
import { formatCompactNumber, formatPercent } from '../src/utils/format';
import { getProviderKeyCounts } from '../src/features/dashboard/hooks/useDashboardOverview';
import {
axisMax,
niceCeil,
providerLabel,
splitWindowMinutes,
toneForSuccessRate,
} from '../src/features/dashboard/utils';
describe('formatCompactNumber', () => {
test('leaves values below one thousand alone', () => {
expect(formatCompactNumber(0)).toBe('0');
expect(formatCompactNumber(999)).toBe('999');
});
test('compacts with a single decimal and no redundant .0', () => {
expect(formatCompactNumber(1000)).toBe('1K');
expect(formatCompactNumber(1284)).toBe('1.3K');
expect(formatCompactNumber(12_900)).toBe('12.9K');
expect(formatCompactNumber(1_500_000)).toBe('1.5M');
});
test('carries into the next tier instead of rendering 1000K', () => {
expect(formatCompactNumber(999_999)).toBe('1M');
expect(formatCompactNumber(999_999_999)).toBe('1B');
});
test('keeps the sign and survives non-finite input', () => {
expect(formatCompactNumber(-1500)).toBe('-1.5K');
expect(formatCompactNumber(Number.NaN)).toBe('0');
});
});
describe('formatPercent', () => {
test('trims trailing zeros but keeps meaningful decimals', () => {
expect(formatPercent(100)).toBe('100%');
expect(formatPercent(99.5)).toBe('99.5%');
expect(formatPercent(0)).toBe('0%');
});
test('renders an em dash for non-finite rates', () => {
expect(formatPercent(Number.NaN)).toBe('—');
});
});
describe('niceCeil', () => {
test('rounds up onto the step ladder', () => {
expect(niceCeil(1)).toBe(1);
expect(niceCeil(7)).toBe(8);
expect(niceCeil(12)).toBe(15);
expect(niceCeil(48)).toBe(50);
expect(niceCeil(320)).toBe(400);
});
test('never returns zero, so bar heights cannot divide by zero', () => {
expect(niceCeil(0)).toBe(1);
expect(niceCeil(-5)).toBe(1);
});
});
describe('axisMax', () => {
test('lands every gridline on a whole number', () => {
// 峰值 112 → 上限 120刻度 0/30/60/90/120而不是浪费半张图的 200
expect(axisMax(112, 4)).toBe(120);
expect(axisMax(7, 4)).toBe(8);
expect(axisMax(1533, 4)).toBe(1600);
});
test('keeps the axis just above the peak', () => {
for (const peak of [1, 3, 9, 17, 64, 112, 250, 999, 4321]) {
const max = axisMax(peak, 4);
expect(max).toBeGreaterThanOrEqual(peak);
// 上限不应超过峰值的两倍,否则柱子被压得太矮。
// 峰值极小时受「每格至少 1」约束下限就是间隔数本身。
expect(max).toBeLessThanOrEqual(Math.max(4, peak * 2));
// 每格都必须是整数
expect(Number.isInteger(max / 4)).toBe(true);
}
});
test('degrades safely with no traffic', () => {
expect(axisMax(0, 4)).toBe(4);
expect(axisMax(-3, 4)).toBe(4);
});
});
describe('toneForSuccessRate', () => {
test('maps a rate onto a severity band', () => {
expect(toneForSuccessRate(null)).toBe('idle');
expect(toneForSuccessRate(100)).toBe('good');
expect(toneForSuccessRate(95)).toBe('good');
expect(toneForSuccessRate(94.9)).toBe('warning');
expect(toneForSuccessRate(80)).toBe('warning');
expect(toneForSuccessRate(79.9)).toBe('critical');
});
});
describe('splitWindowMinutes', () => {
test('splits the rolling window into hours and minutes', () => {
expect(splitWindowMinutes(200)).toEqual({ hours: 3, minutes: 20 });
expect(splitWindowMinutes(60)).toEqual({ hours: 1, minutes: 0 });
expect(splitWindowMinutes(40)).toEqual({ hours: 0, minutes: 40 });
});
});
describe('provider key counts', () => {
test('includes native Interactions API keys in the dashboard total inputs', () => {
const counts = getProviderKeyCounts({
geminiApiKeys: [{ apiKey: 'gemini-key' }],
interactionsApiKeys: [{ apiKey: 'interactions-1' }, { apiKey: 'interactions-2' }],
codexApiKeys: [{ apiKey: 'codex-key' }],
});
expect(counts.interactions).toBe(2);
expect(Object.values(counts).reduce((sum, count) => sum + count, 0)).toBe(4);
});
});
describe('providerLabel', () => {
test('uses the brand spelling for known providers', () => {
expect(providerLabel('xai', 'Unattributed')).toBe('xAI');
expect(providerLabel('aistudio', 'Unattributed')).toBe('AI Studio');
expect(providerLabel('gemini-interactions', 'Unattributed')).toBe('Interactions API');
});
test('falls back to a capitalised id, and localises unknown', () => {
expect(providerLabel('somenewbrand', 'Unattributed')).toBe('Somenewbrand');
expect(providerLabel('unknown', 'Unattributed')).toBe('Unattributed');
expect(providerLabel('', 'Unattributed')).toBe('Unattributed');
});
});

View file

@ -0,0 +1,148 @@
import { describe, expect, test } from 'vitest';
import {
getModelExclusionState,
isModelExcluded,
matchedModelsByRule,
summarizeExclusion,
} from '../src/components/excludedModels/excludedModelRules';
const CATALOG = ['gpt-5-codex', 'gpt-5-mini', 'gpt-5-pro', 'claude-opus', 'gemini-3-pro'];
describe('getModelExclusionState', () => {
test('included when no rule touches the model', () => {
expect(getModelExclusionState(['claude-opus'], 'gpt-5-mini')).toEqual({ state: 'included' });
});
test('exact when only a literal rule matches', () => {
expect(getModelExclusionState(['gpt-5-mini'], 'gpt-5-mini')).toEqual({
state: 'excluded',
by: 'exact',
});
});
test('wildcard carries the responsible rule so the row can explain itself', () => {
expect(getModelExclusionState(['gpt-5-*'], 'gpt-5-mini')).toEqual({
state: 'excluded',
by: 'wildcard',
rule: 'gpt-5-*',
});
});
test('both when a model is explicitly picked AND caught by a wildcard', () => {
expect(getModelExclusionState(['gpt-5-mini', 'gpt-5-*'], 'gpt-5-mini')).toEqual({
state: 'excluded',
by: 'both',
rule: 'gpt-5-*',
});
});
test('order of the rules does not change the resolved state', () => {
expect(getModelExclusionState(['gpt-5-*', 'gpt-5-mini'], 'gpt-5-mini')).toEqual({
state: 'excluded',
by: 'both',
rule: 'gpt-5-*',
});
});
test('reports the first matching wildcard when several apply', () => {
expect(getModelExclusionState(['gpt-*', 'gpt-5-*'], 'gpt-5-mini')).toEqual({
state: 'excluded',
by: 'wildcard',
rule: 'gpt-*',
});
});
test('matching is case-insensitive in both directions', () => {
expect(getModelExclusionState(['GPT-5-MINI'], 'gpt-5-mini')).toEqual({
state: 'excluded',
by: 'exact',
});
expect(getModelExclusionState(['gpt-5-mini'], 'GPT-5-MINI')).toEqual({
state: 'excluded',
by: 'exact',
});
});
test('a blank model id is never excluded', () => {
expect(getModelExclusionState(['*-mini'], ' ')).toEqual({ state: 'included' });
});
test('isModelExcluded collapses all three excluded variants', () => {
expect(isModelExcluded(['gpt-5-mini'], 'gpt-5-mini')).toBe(true);
expect(isModelExcluded(['gpt-5-*'], 'gpt-5-mini')).toBe(true);
expect(isModelExcluded(['gpt-5-mini', 'gpt-5-*'], 'gpt-5-mini')).toBe(true);
expect(isModelExcluded(['claude-opus'], 'gpt-5-mini')).toBe(false);
});
});
describe('matchedModelsByRule', () => {
test('reports what each rule actually catches, in catalog order', () => {
expect(matchedModelsByRule(['gpt-5-*'], CATALOG)).toEqual([
{ rule: 'gpt-5-*', matched: ['gpt-5-codex', 'gpt-5-mini', 'gpt-5-pro'], matchCount: 3 },
]);
});
test('a rule matching nothing is reported with a zero count, not omitted', () => {
expect(matchedModelsByRule(['retired-*'], CATALOG)).toEqual([
{ rule: 'retired-*', matched: [], matchCount: 0 },
]);
});
test('an exact rule matches exactly its own model', () => {
expect(matchedModelsByRule(['claude-opus'], CATALOG)).toEqual([
{ rule: 'claude-opus', matched: ['claude-opus'], matchCount: 1 },
]);
});
test('overlapping rules each report the full set they catch', () => {
expect(matchedModelsByRule(['gpt-*', 'gpt-5-pro'], CATALOG)).toEqual([
{ rule: 'gpt-*', matched: ['gpt-5-codex', 'gpt-5-mini', 'gpt-5-pro'], matchCount: 3 },
{ rule: 'gpt-5-pro', matched: ['gpt-5-pro'], matchCount: 1 },
]);
});
test('preserves the given rule order and length', () => {
expect(matchedModelsByRule(['b-*', 'a-*'], CATALOG).map((s) => s.rule)).toEqual(['b-*', 'a-*']);
});
});
describe('summarizeExclusion', () => {
test('counts catalog models hit by any rule, not the rules themselves', () => {
// One rule, three models — a rule count would say 1 and the meter would lie.
expect(summarizeExclusion(['gpt-5-*'], CATALOG)).toEqual({
total: 5,
excluded: 3,
available: 2,
});
});
test('a model caught by both an exact and a wildcard rule counts once', () => {
expect(summarizeExclusion(['gpt-5-mini', 'gpt-5-*'], CATALOG)).toEqual({
total: 5,
excluded: 3,
available: 2,
});
});
test('rules that match nothing in the catalog do not inflate the count', () => {
expect(summarizeExclusion(['retired-model', 'gone-*'], CATALOG)).toEqual({
total: 5,
excluded: 0,
available: 5,
});
});
test('available is always total minus excluded', () => {
const stats = summarizeExclusion(['gpt-*', 'claude-opus'], CATALOG);
expect(stats.available).toBe(stats.total - stats.excluded);
expect(stats).toEqual({ total: 5, excluded: 4, available: 1 });
});
test('an empty catalog yields all zeroes rather than NaN', () => {
expect(summarizeExclusion(['gpt-5-*'], [])).toEqual({ total: 0, excluded: 0, available: 0 });
});
test('no rules means nothing excluded', () => {
expect(summarizeExclusion([], CATALOG)).toEqual({ total: 5, excluded: 0, available: 5 });
});
});

View file

@ -0,0 +1,210 @@
import { describe, expect, test } from 'vitest';
import {
formatExcludedRulesText,
hasExcludedRule,
isMatchedByWildcardRule,
isWildcardRule,
matchesExcludedRule,
normalizeExcludedRules,
parseExcludedRulesText,
replaceCustomExcludedRules,
splitExcludedRules,
toggleExcludedRule,
} from '../src/components/excludedModels/excludedModelRules';
describe('normalizeExcludedRules / parseExcludedRulesText', () => {
test('trims, drops blanks, and removes case-insensitive duplicates', () => {
expect(normalizeExcludedRules([' gpt-* ', 'GPT-*', '', 'claude-3'])).toEqual([
'gpt-*',
'claude-3',
]);
});
test('keeps the first spelling of a case-insensitive duplicate', () => {
expect(normalizeExcludedRules(['GPT-4o', 'gpt-4O'])).toEqual(['GPT-4o']);
});
test('parsing text is the same operation as normalizing its lines', () => {
const text = ' GPT-5-*\ngpt-5-*\nclaude-opus ';
expect(parseExcludedRulesText(text)).toEqual(['GPT-5-*', 'claude-opus']);
expect(parseExcludedRulesText(text)).toEqual(normalizeExcludedRules(text.split(/\r?\n/)));
});
test('handles CRLF line endings', () => {
expect(parseExcludedRulesText('a-*\r\nb-model\r\n')).toEqual(['a-*', 'b-model']);
});
test('round-trips through formatExcludedRulesText', () => {
const rules = ['GPT-5-*', 'claude-opus'];
expect(parseExcludedRulesText(formatExcludedRulesText(rules))).toEqual(rules);
});
/**
* excluded_models `JSON.stringify` ****
* diffuseAuthFilesPrefixProxyEditor.ts:327picker
*
*/
test('parse→format is a fixed point for already-normalized input (order preserved)', () => {
const fromBackend = ['GPT-5-Codex', 'gpt-5-*', 'retired-model'];
const text = fromBackend.join('\n');
expect(formatExcludedRulesText(parseExcludedRulesText(text))).toBe(text);
// 再跑一轮仍是同一个不动点。
expect(parseExcludedRulesText(formatExcludedRulesText(parseExcludedRulesText(text)))).toEqual(
fromBackend
);
});
test('normalization never reorders surviving rules', () => {
expect(normalizeExcludedRules(['z-model', 'a-model', 'm-*'])).toEqual([
'z-model',
'a-model',
'm-*',
]);
});
});
describe('matchesExcludedRule', () => {
test('matches backend wildcard semantics case-insensitively', () => {
expect(matchesExcludedRule('gpt-5-*', 'GPT-5-Codex')).toBe(true);
expect(matchesExcludedRule('*-preview', 'gemini-3-pro-preview')).toBe(true);
expect(matchesExcludedRule('gpt-5-*', 'gpt-4.1')).toBe(false);
});
test('treats regex metacharacters in the rule as literals', () => {
// The `.` must be a literal dot, not "any character".
expect(matchesExcludedRule('gpt-4.1', 'gpt-4.1')).toBe(true);
expect(matchesExcludedRule('gpt-4.1', 'gpt-4x1')).toBe(false);
expect(matchesExcludedRule('gpt-4.*', 'gpt-4.1-mini')).toBe(true);
expect(matchesExcludedRule('gpt-4.*', 'gpt-4x1-mini')).toBe(false);
});
test('anchors at both ends', () => {
expect(matchesExcludedRule('gpt-5', 'gpt-5-codex')).toBe(false);
expect(matchesExcludedRule('gpt-5*', 'gpt-5-codex')).toBe(true);
});
test('a blank rule or model never matches', () => {
expect(matchesExcludedRule('', 'gpt-5')).toBe(false);
expect(matchesExcludedRule('gpt-5', ' ')).toBe(false);
});
test('isWildcardRule / isMatchedByWildcardRule ignore exact rules', () => {
expect(isWildcardRule('gpt-5-*')).toBe(true);
expect(isWildcardRule('gpt-5-codex')).toBe(false);
expect(isMatchedByWildcardRule(['gpt-5-*'], 'gpt-5-mini')).toBe(true);
// An exact rule matching the model is not a *wildcard* match.
expect(isMatchedByWildcardRule(['gpt-5-mini'], 'gpt-5-mini')).toBe(false);
});
});
describe('hasExcludedRule', () => {
test('compares literally and case-insensitively, without wildcard expansion', () => {
expect(hasExcludedRule(['GPT-4o'], 'gpt-4O')).toBe(true);
expect(hasExcludedRule(['gpt-5-*'], 'gpt-5-codex')).toBe(false);
expect(hasExcludedRule(['gpt-5-*'], 'GPT-5-*')).toBe(true);
expect(hasExcludedRule(['gpt-4o'], ' ')).toBe(false);
});
});
describe('toggleExcludedRule', () => {
test('adds and removes exact rules without touching wildcard rules', () => {
const added = toggleExcludedRule(['gpt-5-*'], 'claude-opus', true);
expect(added).toEqual(['gpt-5-*', 'claude-opus']);
expect(toggleExcludedRule(added, 'CLAUDE-OPUS', false)).toEqual(['gpt-5-*']);
});
test('removes a wildcard rule by name (the old auth-file helper refused to)', () => {
expect(toggleExcludedRule(['gpt-5-*', 'claude-opus'], 'GPT-5-*', false)).toEqual([
'claude-opus',
]);
});
test('adding an existing rule moves it to the end rather than duplicating', () => {
expect(toggleExcludedRule(['a', 'b'], 'A', true)).toEqual(['b', 'A']);
});
test('a blank candidate is a no-op add', () => {
expect(toggleExcludedRule(['a'], ' ', true)).toEqual(['a']);
});
test('trims the candidate when removing, down to an empty list', () => {
expect(toggleExcludedRule(['GPT-4o'], ' gpt-4O ', false)).toEqual([]);
});
test('trims the candidate when adding', () => {
expect(toggleExcludedRule(['gpt-4o'], ' gpt-* ', true)).toEqual(['gpt-4o', 'gpt-*']);
});
});
describe('splitExcludedRules', () => {
test('partitions into exact / wildcard / unknown', () => {
expect(
splitExcludedRules(
['GPT-5-Codex', 'gpt-5-*', 'unlisted-model'],
['gpt-5-codex', 'claude-opus']
)
).toEqual({
exactRules: ['gpt-5-codex'],
wildcardRules: ['gpt-5-*'],
unknownRules: ['unlisted-model'],
customRules: ['gpt-5-*', 'unlisted-model'],
});
});
test('rewrites exact rules to the catalog spelling', () => {
expect(splitExcludedRules(['GPT-5-CODEX'], ['gpt-5-codex']).exactRules).toEqual([
'gpt-5-codex',
]);
});
test('preserves the configured spelling for wildcard and unknown rules', () => {
const { wildcardRules, unknownRules } = splitExcludedRules(
['GPT-5-*', 'Retired-Model'],
['gpt-5-codex']
);
expect(wildcardRules).toEqual(['GPT-5-*']);
expect(unknownRules).toEqual(['Retired-Model']);
});
test('customRules keeps the original interleaved order, not bucket order', () => {
// `unlisted` appears before `a-*`; concatenating the buckets would reverse them.
expect(splitExcludedRules(['unlisted', 'a-*'], ['gpt-5-codex']).customRules).toEqual([
'unlisted',
'a-*',
]);
});
test('an empty catalog makes every exact rule unknown', () => {
expect(splitExcludedRules(['a', 'b-*'], [])).toEqual({
exactRules: [],
wildcardRules: ['b-*'],
unknownRules: ['a'],
customRules: ['a', 'b-*'],
});
});
});
describe('replaceCustomExcludedRules', () => {
test('swaps the custom half while retaining exact selections', () => {
expect(
replaceCustomExcludedRules(
['gpt-5-codex', 'old-*'],
['gpt-5-codex', 'claude-opus'],
'new-*\nlegacy-model'
)
).toEqual(['gpt-5-codex', 'new-*', 'legacy-model']);
});
test('clearing the text leaves only the exact selections', () => {
expect(replaceCustomExcludedRules(['gpt-5-codex', 'old-*'], ['gpt-5-codex'], '')).toEqual([
'gpt-5-codex',
]);
});
test('a custom rule duplicating an exact selection does not double it', () => {
expect(replaceCustomExcludedRules(['gpt-5-codex'], ['gpt-5-codex'], 'GPT-5-CODEX')).toEqual([
'gpt-5-codex',
]);
});
});

View file

@ -0,0 +1,26 @@
import { describe, expect, test } from 'vitest';
import {
buildFennoAIRaw,
FENNO_AI_CODEX_BASE_URL,
FENNO_AI_PROVIDER_NAME,
} from '../src/features/providers/fennoAI';
import { getSponsorProviderDefinition } from '../src/features/providers/sponsorDefinitions';
describe('FennoAI provider aggregation', () => {
test('does not claim OpenAI configs that its form cannot display', () => {
const raw = buildFennoAIRaw({
openaiCompatibility: [
{
name: FENNO_AI_PROVIDER_NAME,
baseUrl: FENNO_AI_CODEX_BASE_URL,
apiKeyEntries: [{ apiKey: 'openai-key' }],
},
],
codexApiKeys: [{ apiKey: 'codex-key', baseUrl: FENNO_AI_CODEX_BASE_URL }],
});
expect(getSponsorProviderDefinition('fennoAI').protocols).toEqual(['codex', 'claude']);
expect(raw.openai).toEqual([]);
expect(raw.codex.map((item) => item.index)).toEqual([0]);
});
});

View file

@ -0,0 +1,102 @@
import { describe, expect, test } from 'vitest';
import { infistarToResource } from '../src/features/providers/adapters';
import { PROVIDER_LOGOS } from '../src/features/providers/brandLogos';
import { PROVIDER_BRAND_ORDER } from '../src/features/providers/descriptors';
import {
INFISTAR_AFFILIATE_URL,
INFISTAR_BASE_URL_OPTIONS,
INFISTAR_DOMESTIC_BASE_URL,
INFISTAR_DOMESTIC_ROOT_URL,
INFISTAR_GLOBAL_BASE_URL,
INFISTAR_GLOBAL_ROOT_URL,
buildInfistarRaw,
getInfistarProtocolUrls,
resolveInfistarBaseUrl,
} from '../src/features/providers/infistar';
import { getSponsorProviderDefinition } from '../src/features/providers/sponsorDefinitions';
const allProtocolConfig = {
openaiCompatibility: [
{
name: 'infistar',
baseUrl: INFISTAR_DOMESTIC_BASE_URL,
apiKeyEntries: [{ apiKey: 'openai-key' }],
},
],
claudeApiKeys: [{ apiKey: 'claude-key', baseUrl: INFISTAR_DOMESTIC_ROOT_URL }],
codexApiKeys: [{ apiKey: 'codex-key', baseUrl: INFISTAR_DOMESTIC_BASE_URL }],
geminiApiKeys: [{ apiKey: 'gemini-key', baseUrl: INFISTAR_DOMESTIC_ROOT_URL }],
interactionsApiKeys: [{ apiKey: 'interactions-key', baseUrl: INFISTAR_DOMESTIC_BASE_URL }],
};
describe('Infistar sponsor provider', () => {
test('offers the requested mainland China and global URLs', () => {
expect(INFISTAR_AFFILIATE_URL).toBe(
'https://infistar.ai/register?aff=FQKC6J6R&ref_source=link'
);
expect(
INFISTAR_BASE_URL_OPTIONS.map(({ id, baseUrl }) => ({
id,
baseUrl,
}))
).toEqual([
{ id: 'mainlandChina', baseUrl: 'https://coneverse.com/v1' },
{ id: 'global', baseUrl: 'https://infistar.ai/v1' },
]);
expect(resolveInfistarBaseUrl(undefined)).toBe(INFISTAR_DOMESTIC_BASE_URL);
expect(resolveInfistarBaseUrl(INFISTAR_GLOBAL_ROOT_URL)).toBe(INFISTAR_GLOBAL_BASE_URL);
});
test('maps both choices to all four supported protocol endpoints', () => {
expect(getInfistarProtocolUrls(undefined)).toEqual({
openai: 'https://coneverse.com/v1',
codex: 'https://coneverse.com/v1',
anthropic: 'https://coneverse.com',
gemini: 'https://coneverse.com',
});
expect(getInfistarProtocolUrls(INFISTAR_GLOBAL_BASE_URL)).toEqual({
openai: 'https://infistar.ai/v1',
codex: 'https://infistar.ai/v1',
anthropic: 'https://infistar.ai',
gemini: 'https://infistar.ai',
});
const definition = getSponsorProviderDefinition('infistar');
expect(definition.protocols).toEqual(['openai', 'claude', 'gemini', 'codex']);
expect(definition.protocols).not.toContain('interactions');
});
test('aggregates four protocol configs without claiming Interactions API', () => {
const raw = buildInfistarRaw(allProtocolConfig);
expect(raw.openai.map((item) => item.index)).toEqual([0]);
expect(raw.claude.map((item) => item.index)).toEqual([0]);
expect(raw.codex.map((item) => item.index)).toEqual([0]);
expect(raw.gemini.map((item) => item.index)).toEqual([0]);
const resource = infistarToResource(raw);
expect(resource?.brand).toBe('infistar');
expect(resource?.name).toBe('无限星河');
expect(resource?.flags.protocols).toEqual(['openai', 'anthropic', 'gemini', 'codexResponses']);
});
test('keeps custom endpoints outside the Infistar sponsor group', () => {
const raw = buildInfistarRaw({
openaiCompatibility: [
{
name: 'infistar',
baseUrl: 'https://gateway.example.com/v1',
apiKeyEntries: [{ apiKey: 'custom-key' }],
},
],
});
expect(raw.openai).toEqual([]);
});
test('is appended to the provider catalog with the supplied logo', () => {
expect(PROVIDER_BRAND_ORDER.at(-1)).toBe('infistar');
expect(PROVIDER_LOGOS.infistar.src).toContain('infistar.png');
expect(PROVIDER_LOGOS.infistar.transparent).toBe(true);
});
});

View file

@ -0,0 +1,249 @@
import { afterEach, describe, expect, test } from 'vitest';
import {
buildInteractionsEndpoint,
buildInteractionsProbePayload,
getProviderUsageKey,
INTERACTIONS_API_REVISION,
} from '../src/components/providers/utils';
import { interactionsToResource } from '../src/features/providers/adapters';
import { PROVIDER_BRAND_ORDER, PROVIDER_DESCRIPTORS } from '../src/features/providers/descriptors';
import { MODEL_DISCOVERY_BRANDS } from '../src/features/providers/sheets/forms/useModelDiscovery';
import { apiClient } from '../src/services/api/client';
import { providersApi } from '../src/services/api/providers';
import { normalizeConfigResponse } from '../src/services/api/transformers';
const originalGet = apiClient.get;
const originalPut = apiClient.put;
const originalDelete = apiClient.delete;
afterEach(() => {
apiClient.get = originalGet;
apiClient.put = originalPut;
apiClient.delete = originalDelete;
});
describe('Interactions API key provider', () => {
test('normalizes the backend contract and exposes a dedicated workbench resource', () => {
const config = normalizeConfigResponse({
'interactions-api-key': [
{
'api-key': 'interactions-secret',
priority: 8,
weight: 3,
prefix: 'native',
'base-url': 'https://generativelanguage.googleapis.com',
'proxy-url': 'direct',
headers: { 'X-Custom': 'value' },
models: [
{
name: 'gemini-3.1-flash-lite',
alias: 'native-flash',
thinking: { levels: ['low', 'medium', 'high'] },
},
],
'excluded-models': ['gemini-2.5-*'],
'disable-cooling': true,
'auth-index': 'gemini-interactions:apikey:1',
},
],
});
expect(config.interactionsApiKeys).toEqual([
{
apiKey: 'interactions-secret',
priority: 8,
weight: 3,
prefix: 'native',
baseUrl: 'https://generativelanguage.googleapis.com',
proxyUrl: 'direct',
headers: { 'X-Custom': 'value' },
models: [
{
name: 'gemini-3.1-flash-lite',
alias: 'native-flash',
thinking: { levels: ['low', 'medium', 'high'] },
},
],
excludedModels: ['gemini-2.5-*'],
disableCooling: true,
authIndex: 'gemini-interactions:apikey:1',
},
]);
const resource = interactionsToResource(config.interactionsApiKeys![0], 0);
expect(resource.brand).toBe('interactions');
expect(resource.models).toEqual(['gemini-3.1-flash-lite']);
expect(resource.selector).toEqual({
brand: 'interactions',
apiKey: 'interactions-secret',
baseUrl: 'https://generativelanguage.googleapis.com',
index: 0,
});
expect(PROVIDER_DESCRIPTORS.interactions.baseUrlRequired).toBe(false);
expect(PROVIDER_DESCRIPTORS.interactions.supportsTestModel).toBe(true);
expect(PROVIDER_BRAND_ORDER.indexOf('interactions')).toBe(
PROVIDER_BRAND_ORDER.indexOf('gemini') + 1
);
expect(MODEL_DISCOVERY_BRANDS).toContain('interactions');
});
test('builds the native interactions endpoint from supported base URL forms', () => {
expect(buildInteractionsEndpoint('')).toBe(
'https://generativelanguage.googleapis.com/v1beta/interactions'
);
expect(buildInteractionsEndpoint('https://generativelanguage.googleapis.com')).toBe(
'https://generativelanguage.googleapis.com/v1beta/interactions'
);
expect(buildInteractionsEndpoint('https://example.com/v1beta')).toBe(
'https://example.com/v1beta/interactions'
);
expect(buildInteractionsEndpoint('https://example.com/v1beta/interactions')).toBe(
'https://example.com/v1beta/interactions'
);
});
test('uses the documented revision and minimal non-streaming probe body', () => {
expect(INTERACTIONS_API_REVISION).toBe('2026-05-20');
expect(buildInteractionsProbePayload('gemini-3.6-flash')).toEqual({
model: 'gemini-3.6-flash',
input: 'Hi',
});
});
test('maps the UI brand to the backend runtime usage provider', () => {
expect(getProviderUsageKey('interactions')).toBe('gemini-interactions');
expect(getProviderUsageKey('gemini')).toBe('gemini');
expect(getProviderUsageKey('claudeApi')).toBe('claude');
});
test('updates only the matching key and base URL while preserving unknown fields', async () => {
let putData: unknown;
apiClient.get = (async () => ({
'interactions-api-key': [
{
'api-key': 'shared-key',
'base-url': 'https://first.example.com',
'future-field': 'first',
},
{
'api-key': 'shared-key',
'base-url': 'https://second.example.com',
'proxy-url': 'direct',
headers: { 'X-Old': 'value' },
'excluded-models': ['old-model'],
'disable-cooling': true,
'future-field': 'preserved',
'auth-index': 'response-only',
},
],
})) as typeof apiClient.get;
apiClient.put = (async (_url: string, data?: unknown) => {
putData = data;
return undefined;
}) as typeof apiClient.put;
await providersApi.updateInteractionsKey('shared-key', 'https://second.example.com', {
apiKey: 'shared-key',
baseUrl: 'https://updated.example.com',
models: [{ name: 'gemini-3.1-flash-lite', alias: 'native-flash' }],
});
expect(putData).toEqual([
{
'api-key': 'shared-key',
'base-url': 'https://first.example.com',
'future-field': 'first',
},
{
'future-field': 'preserved',
'api-key': 'shared-key',
'base-url': 'https://updated.example.com',
models: [{ name: 'gemini-3.1-flash-lite', alias: 'native-flash' }],
},
]);
});
test('creates and deletes keys through the interactions management endpoints', async () => {
const calls: Array<{ method: string; url: string; data?: unknown }> = [];
apiClient.get = (async (url: string) => {
calls.push({ method: 'GET', url });
return {
'interactions-api-key': [
{
'api-key': 'existing',
'base-url': 'https://generativelanguage.googleapis.com',
'future-field': 'preserved',
},
],
};
}) as typeof apiClient.get;
apiClient.put = (async (url: string, data?: unknown) => {
calls.push({ method: 'PUT', url, data });
return undefined;
}) as typeof apiClient.put;
apiClient.delete = (async (url: string) => {
calls.push({ method: 'DELETE', url });
return undefined;
}) as typeof apiClient.delete;
await providersApi.createInteractionsKey({
apiKey: 'interactions-new',
priority: 4,
weight: 2,
prefix: 'native',
baseUrl: 'https://generativelanguage.googleapis.com',
proxyUrl: 'direct',
headers: { 'X-Custom': 'value' },
models: [
{
name: 'gemini-3.1-flash-lite',
alias: 'native-flash',
thinking: { min: 128, max: 8192, dynamic_allowed: true },
},
],
excludedModels: ['gemini-2.5-*'],
disableCooling: true,
});
await providersApi.deleteInteractionsKey(
'interactions-new',
'https://generativelanguage.googleapis.com'
);
expect(calls).toEqual([
{ method: 'GET', url: '/config' },
{
method: 'PUT',
url: '/interactions-api-key',
data: [
{
'api-key': 'existing',
'base-url': 'https://generativelanguage.googleapis.com',
'future-field': 'preserved',
},
{
'api-key': 'interactions-new',
priority: 4,
weight: 2,
prefix: 'native',
'base-url': 'https://generativelanguage.googleapis.com',
'proxy-url': 'direct',
'disable-cooling': true,
headers: { 'X-Custom': 'value' },
models: [
{
name: 'gemini-3.1-flash-lite',
alias: 'native-flash',
thinking: { min: 128, max: 8192, dynamic_allowed: true },
},
],
'excluded-models': ['gemini-2.5-*'],
},
],
},
{
method: 'DELETE',
url: '/interactions-api-key?api-key=interactions-new&base-url=https%3A%2F%2Fgenerativelanguage.googleapis.com',
},
]);
});
});

View file

@ -0,0 +1,152 @@
import { afterEach, describe, expect, test } from 'vitest';
import { buildOpenAIChatCompletionsEndpoint } from '../src/components/providers/utils';
import {
KIMI_ANTHROPIC_BASE_URL,
KIMI_CHINESE_AFFILIATE_URL,
KIMI_DOMESTIC_ANTHROPIC_BASE_URL,
KIMI_DOMESTIC_BASE_URL,
KIMI_DOMESTIC_OPENAI_BASE_URL,
KIMI_INTERNATIONAL_AFFILIATE_URL,
KIMI_LEGACY_OPENAI_BASE_URL,
KIMI_OPENAI_BASE_URL,
buildKimiRaw,
getKimiAffiliateUrl,
getKimiProtocolUrls,
isKimiClaudeProvider,
isKimiOpenAIProvider,
resolveKimiBaseUrl,
} from '../src/features/providers/kimi';
import { PROVIDER_LOGOS } from '../src/features/providers/brandLogos';
import { PROVIDER_BRAND_ORDER } from '../src/features/providers/descriptors';
import { getSponsorProviderDefinition } from '../src/features/providers/sponsorDefinitions';
import { apiCallApi } from '../src/services/api/apiCall';
import { modelsApi } from '../src/services/api/models';
const originalApiCallRequest = apiCallApi.request;
afterEach(() => {
apiCallApi.request = originalApiCallRequest;
});
describe('Kimi provider', () => {
test('defaults to the domestic OpenAI-compatible and Claude protocol endpoints', () => {
expect(getKimiProtocolUrls(undefined)).toEqual({
openai: 'https://api.moonshot.cn/v1',
anthropic: 'https://api.moonshot.cn/anthropic',
codex: '',
gemini: '',
});
expect(getKimiProtocolUrls(KIMI_OPENAI_BASE_URL)).toEqual({
openai: 'https://api.moonshot.ai/v1',
anthropic: 'https://api.moonshot.ai/anthropic',
codex: '',
gemini: '',
});
expect(buildOpenAIChatCompletionsEndpoint(KIMI_OPENAI_BASE_URL)).toBe(
'https://api.moonshot.ai/v1/chat/completions'
);
expect(getSponsorProviderDefinition('kimi').protocols).toEqual(['openai', 'claude']);
});
test('offers overseas and domestic URLs and maps the domestic protocol endpoints', () => {
expect(
getSponsorProviderDefinition('kimi').baseUrlOptions.map(({ id, baseUrl }) => ({
id,
baseUrl,
}))
).toEqual([
{ id: 'domestic', baseUrl: KIMI_DOMESTIC_OPENAI_BASE_URL },
{ id: 'overseas', baseUrl: KIMI_OPENAI_BASE_URL },
]);
expect(resolveKimiBaseUrl(undefined)).toBe(KIMI_DOMESTIC_OPENAI_BASE_URL);
expect(resolveKimiBaseUrl(KIMI_DOMESTIC_BASE_URL)).toBe(KIMI_DOMESTIC_OPENAI_BASE_URL);
expect(resolveKimiBaseUrl(KIMI_LEGACY_OPENAI_BASE_URL)).toBe(KIMI_OPENAI_BASE_URL);
expect(resolveKimiBaseUrl(KIMI_DOMESTIC_ANTHROPIC_BASE_URL)).toBe(
KIMI_DOMESTIC_OPENAI_BASE_URL
);
expect(getKimiProtocolUrls(KIMI_DOMESTIC_OPENAI_BASE_URL)).toEqual({
openai: 'https://api.moonshot.cn/v1',
anthropic: 'https://api.moonshot.cn/anthropic',
codex: '',
gemini: '',
});
});
test('discovers models through the versioned OpenAI endpoint', async () => {
let requestedUrl = '';
apiCallApi.request = (async (payload) => {
requestedUrl = payload.url;
return { statusCode: 200, header: {}, bodyText: '', body: { data: [] } };
}) as typeof apiCallApi.request;
await modelsApi.fetchModelsViaApiCall(KIMI_OPENAI_BASE_URL, 'test-key');
expect(requestedUrl).toBe('https://api.moonshot.ai/v1/models');
});
test('uses the domestic registration link for Chinese and the international link otherwise', () => {
expect(getKimiAffiliateUrl('zh-CN')).toBe(KIMI_CHINESE_AFFILIATE_URL);
expect(getKimiAffiliateUrl('zh-TW')).toBe(KIMI_CHINESE_AFFILIATE_URL);
expect(getKimiAffiliateUrl('en')).toBe(KIMI_INTERNATIONAL_AFFILIATE_URL);
expect(getKimiAffiliateUrl('ru')).toBe(KIMI_INTERNATIONAL_AFFILIATE_URL);
});
test('uses the OAuth-style theme surface for its provider icon', () => {
expect(PROVIDER_LOGOS.kimi.themeSurface).toBe(true);
});
test('is the first provider in the catalog', () => {
expect(PROVIDER_BRAND_ORDER[0]).toBe('kimi');
});
test('recognizes Kimi configs only by supported protocol endpoint', () => {
expect(
isKimiOpenAIProvider({
name: 'Kimi',
baseUrl: 'https://custom.example.com',
})
).toBe(false);
expect(
isKimiOpenAIProvider({
name: 'moonshot',
baseUrl: `${KIMI_OPENAI_BASE_URL}/`,
})
).toBe(true);
expect(
isKimiOpenAIProvider({
name: 'legacy-moonshot',
baseUrl: KIMI_LEGACY_OPENAI_BASE_URL,
})
).toBe(true);
expect(
isKimiOpenAIProvider({
name: 'domestic-moonshot',
baseUrl: KIMI_DOMESTIC_OPENAI_BASE_URL,
})
).toBe(true);
expect(
isKimiClaudeProvider({ apiKey: 'sk-test', baseUrl: KIMI_ANTHROPIC_BASE_URL })
).toBe(true);
expect(
isKimiClaudeProvider({ apiKey: 'sk-test', baseUrl: KIMI_DOMESTIC_ANTHROPIC_BASE_URL })
).toBe(true);
});
test('aggregates only the Kimi OpenAI-compatible and Claude configs', () => {
const raw = buildKimiRaw({
openaiCompatibility: [
{ name: 'kimi', baseUrl: KIMI_OPENAI_BASE_URL },
{ name: 'other', baseUrl: 'https://example.com' },
],
claudeApiKeys: [
{ apiKey: 'kimi-key', baseUrl: KIMI_ANTHROPIC_BASE_URL },
{ apiKey: 'other-key', baseUrl: 'https://api.anthropic.com' },
],
});
expect(raw.openai.map((item) => item.index)).toEqual([0]);
expect(raw.claude.map((item) => item.index)).toEqual([0]);
expect(raw.codex).toEqual([]);
expect(raw.gemini).toEqual([]);
});
});

View file

@ -0,0 +1,74 @@
import { describe, expect, test } from 'vitest';
import { buildTimelineLane } from '@/features/quota/quotaTimelineModel';
import { buildKimiQuotaRows } from '@/utils/quota';
describe('Kimi quota ordering', () => {
test('shows the 5-hour limit before the weekly limit and exposes it to the timeline', () => {
const rows = buildKimiQuotaRows({
usage: {
used: '1',
limit: '100',
remaining: '99',
resetTime: '2099-08-06T13:59:23.136523Z',
},
limits: [
{
detail: {
used: '2',
limit: '100',
remaining: '98',
resetTime: '2099-07-31T06:59:23.136523Z',
},
window: {
duration: 300,
timeUnit: 'TIME_UNIT_MINUTE',
},
},
],
});
expect(rows.map(({ id }) => id)).toEqual(['limit-0', 'summary']);
expect(rows[0]?.labelKey).toBe('kimi_quota.limit_window');
expect(rows[0]?.labelParams).toEqual({ duration: '5h' });
expect(rows[0]?.periodHours).toBe(5);
expect(rows[1]?.labelKey).toBe('kimi_quota.weekly_limit');
expect(rows[1]?.periodHours).toBe(168);
const lane = buildTimelineLane({
name: 'kimi.json',
displayName: 'Kimi',
provider: 'kimi',
quota: { status: 'success', rows },
maxPeriodHours: 5,
});
expect(lane.anchorMs).toBe(rows[0]?.resetAtMs);
expect(lane.periodHours).toBe(5);
});
});
describe('Kimi quota reset formatting', () => {
const getResetHint = (resetIn: number): string | undefined => {
const rows = buildKimiQuotaRows({
usage: {
used: 200,
limit: 1000,
resetIn,
},
});
return rows[0]?.resetHint;
};
test('converts reset durations longer than a day to days and hours', () => {
expect(getResetHint(132 * 3600)).toBe('5d 12h');
expect(getResetHint(168 * 3600)).toBe('7d 0h');
});
test('keeps hour and minute formatting for durations shorter than a day', () => {
expect(getResetHint(5 * 3600 + 30 * 60)).toBe('5h 30m');
});
test('shows less than one minute for short positive durations', () => {
expect(getResetHint(59)).toBe('<1m');
});
});

View file

@ -0,0 +1,78 @@
import { describe, expect, test } from 'vitest';
import { lmuAIToResource } from '../src/features/providers/adapters';
import { PROVIDER_LOGOS } from '../src/features/providers/brandLogos';
import { PROVIDER_BRAND_ORDER } from '../src/features/providers/descriptors';
import {
LMU_AI_AFFILIATE_URL,
LMU_AI_BASE_URL,
LMU_AI_OPENAI_BASE_URL,
buildLmuAIRaw,
getLmuAIProtocolUrls,
} from '../src/features/providers/lmuAI';
import { getSponsorProviderDefinition } from '../src/features/providers/sponsorDefinitions';
const allProtocolConfig = {
openaiCompatibility: [
{
name: 'lmuAI',
baseUrl: LMU_AI_OPENAI_BASE_URL,
apiKeyEntries: [{ apiKey: 'openai-key' }],
},
],
claudeApiKeys: [{ apiKey: 'claude-key', baseUrl: LMU_AI_BASE_URL }],
codexApiKeys: [{ apiKey: 'codex-key', baseUrl: LMU_AI_OPENAI_BASE_URL }],
geminiApiKeys: [{ apiKey: 'gemini-key', baseUrl: LMU_AI_BASE_URL }],
interactionsApiKeys: [{ apiKey: 'interactions-key', baseUrl: LMU_AI_BASE_URL }],
};
describe('LMU AI provider', () => {
test('uses the official URL for all four supported protocols', () => {
expect(LMU_AI_AFFILIATE_URL).toBe('https://api.lmuai.com/register?ref=yJ6Kwg9g');
expect(getLmuAIProtocolUrls(undefined)).toEqual({
openai: 'https://api.lmuai.com/v1',
codex: 'https://api.lmuai.com/v1',
anthropic: 'https://api.lmuai.com',
gemini: 'https://api.lmuai.com',
});
const definition = getSponsorProviderDefinition('lmuAI');
expect(definition.protocols).toEqual(['openai', 'claude', 'gemini', 'codex']);
expect(definition.protocols).not.toContain('interactions');
});
test('aggregates the four protocol configs without claiming Interactions API', () => {
const raw = buildLmuAIRaw(allProtocolConfig);
expect(raw.openai.map((item) => item.index)).toEqual([0]);
expect(raw.claude.map((item) => item.index)).toEqual([0]);
expect(raw.codex.map((item) => item.index)).toEqual([0]);
expect(raw.gemini.map((item) => item.index)).toEqual([0]);
const resource = lmuAIToResource(raw);
expect(resource?.brand).toBe('lmuAI');
expect(resource?.name).toBe('LMU AI灵眸AI');
expect(resource?.flags.protocols).toEqual(['openai', 'anthropic', 'gemini', 'codexResponses']);
});
test('keeps custom endpoints outside the LMU AI sponsor group', () => {
const raw = buildLmuAIRaw({
openaiCompatibility: [
{
name: 'lmuAI',
baseUrl: 'https://gateway.example.com/v1',
apiKeyEntries: [{ apiKey: 'custom-key' }],
},
],
});
expect(raw.openai).toEqual([]);
});
test('remains in the provider catalog with the sponsor logo', () => {
expect(PROVIDER_BRAND_ORDER).toContain('lmuAI');
expect(PROVIDER_BRAND_ORDER.indexOf('lmuAI')).toBeLessThan(
PROVIDER_BRAND_ORDER.indexOf('infistar')
);
expect(PROVIDER_LOGOS.lmuAI.src).toContain('lmu-ai.png');
});
});

View file

@ -0,0 +1,10 @@
import { describe, expect, test } from 'vitest';
import { hasModelAliasConflict } from '../src/components/modelAlias/aliasValidation';
describe('model alias validation', () => {
test('checks aliases case-insensitively while excluding the renamed node', () => {
expect(hasModelAliasConflict(['Foo'], ' foo ')).toBe(true);
expect(hasModelAliasConflict(['Foo'], 'foo', 'Foo')).toBe(false);
expect(hasModelAliasConflict(['Foo', 'Bar'], 'FOO', 'Bar')).toBe(true);
});
});

View file

@ -0,0 +1,53 @@
import { describe, expect, test } from 'vitest';
import { createElement } from 'react';
import { renderToStaticMarkup } from 'react-dom/server';
import '../src/i18n/index';
import { OAuthExcludedCard } from '../src/features/authFiles/components/OAuthExcludedCard';
import { OAuthModelAliasCard } from '../src/features/authFiles/components/OAuthModelAliasCard';
const noop = () => {};
const noopAsync = async () => {};
describe('OAuth configuration load guards', () => {
test('disables excluded-model writes and exposes retry after a load failure', () => {
const markup = renderToStaticMarkup(
createElement(OAuthExcludedCard, {
disableControls: false,
excludedError: 'load',
excluded: {},
onRetry: noop,
onAdd: noop,
onEdit: noop,
onDelete: noop,
})
);
expect(markup).toContain('disabled=""');
expect(markup).toContain('empty-action');
});
test('disables model-alias writes and exposes retry after a load failure', () => {
const markup = renderToStaticMarkup(
createElement(OAuthModelAliasCard, {
disableControls: false,
viewMode: 'list',
onViewModeChange: noop,
onRetry: noop,
onAdd: noop,
onEditProvider: noop,
onDeleteProvider: noop,
modelAliasError: 'load',
modelAlias: {},
allProviderModels: {},
onUpdate: noopAsync,
onDeleteLink: noop,
onToggleFork: noopAsync,
onRenameAlias: noopAsync,
onDeleteAlias: noop,
})
);
expect(markup).toContain('disabled=""');
expect(markup).toContain('empty-action');
});
});

View file

@ -0,0 +1,27 @@
import { describe, expect, test } from 'vitest';
import {
getModelAliasDraftSignature,
getStringSetSignature,
isOAuthEditorDirty,
} from '../src/features/authFiles/oauthEditorState';
describe('OAuth editor dirty state', () => {
test('compares model selections independent of order', () => {
expect(getStringSetSignature(['b', 'a'])).toBe(getStringSetSignature(['a', 'b']));
});
test('ignores generated row ids but preserves partial alias edits', () => {
expect(getModelAliasDraftSignature([{ id: 'one', name: '', alias: '', fork: true }])).toBe(
getModelAliasDraftSignature([])
);
expect(
getModelAliasDraftSignature([{ id: 'one', name: 'partial', alias: '', fork: true }])
).not.toBe(getModelAliasDraftSignature([]));
});
test('marks provider or content changes as dirty', () => {
expect(isOAuthEditorDirty('codex', 'codex', 'same', 'same')).toBe(false);
expect(isOAuthEditorDirty('codex', 'claude', 'same', 'same')).toBe(true);
expect(isOAuthEditorDirty('codex', 'codex', 'before', 'after')).toBe(true);
});
});

View file

@ -0,0 +1,27 @@
import { describe, expect, test } from 'vitest';
import {
normalizeOauthModelAlias,
serializeOauthModelAliases,
} from '../src/services/api/authFiles';
describe('OAuth model alias force mapping', () => {
test('normalizes and serializes force-mapping without dropping it', () => {
const normalized = normalizeOauthModelAlias({
'oauth-model-alias': {
codex: [
{ name: 'gpt-source', alias: 'gpt-alias', 'force-mapping': true },
{ name: 'gpt-source-2', alias: 'gpt-alias-2', forceMapping: false },
],
},
});
expect(normalized.codex).toEqual([
{ name: 'gpt-source', alias: 'gpt-alias', forceMapping: true },
{ name: 'gpt-source-2', alias: 'gpt-alias-2', forceMapping: false },
]);
expect(serializeOauthModelAliases(normalized.codex)).toEqual([
{ name: 'gpt-source', alias: 'gpt-alias', 'force-mapping': true },
{ name: 'gpt-source-2', alias: 'gpt-alias-2', 'force-mapping': false },
]);
});
});

View file

@ -0,0 +1,93 @@
import { describe, expect, test } from 'vitest';
import {
buildPluginConfigDraft,
buildPluginConfigPatch,
} from '../src/features/plugins/pluginConfigDraft';
import type { PluginConfigField } from '../src/types';
const fields: PluginConfigField[] = [
{ name: 'mixed', type: 'array', enumValues: [], description: '' },
{ name: 'optional', type: 'boolean', enumValues: [], description: '' },
{ name: 'label', type: 'string', enumValues: [], description: '' },
];
const t = (key: string) => key;
describe('plugin config draft', () => {
test('represents arrays as JSON without marking missing booleans as touched', () => {
const draft = buildPluginConfigDraft(
{ enabled: true, configFields: fields },
{ priority: 3, mixed: [1, true, { x: 1 }] }
);
expect(draft.values.mixed).toBe('[\n 1,\n true,\n {\n "x": 1\n }\n]');
expect(draft.values.optional).toBe(false);
expect(draft.touchedFields).toEqual({});
});
test('only patches touched base fields and preserves untouched plugin values', () => {
const draft = buildPluginConfigDraft(
{ enabled: true, configFields: fields },
{ priority: 3, mixed: [1, true, { x: 1 }] }
);
draft.priority = '5';
draft.priorityTouched = true;
expect(buildPluginConfigPatch(draft, fields, t)).toEqual({
patch: { priority: 5 },
errors: {},
});
});
test('parses touched array JSON without coercing item types', () => {
const draft = buildPluginConfigDraft(
{ enabled: true, configFields: fields },
{ mixed: ['old'] }
);
draft.values.mixed = '[1, false, {"next": 2}]';
draft.touchedFields.mixed = true;
expect(buildPluginConfigPatch(draft, fields, t)).toEqual({
patch: { mixed: [1, false, { next: 2 }] },
errors: {},
});
});
test('writes a missing boolean only after the user touches it', () => {
const draft = buildPluginConfigDraft({ enabled: true, configFields: fields }, {});
draft.touchedFields.optional = true;
expect(buildPluginConfigPatch(draft, fields, t)).toEqual({
patch: { optional: false },
errors: {},
});
});
test('deletes a cleared touched field with null', () => {
const draft = buildPluginConfigDraft(
{ enabled: true, configFields: fields },
{ label: 'keep me' }
);
draft.values.label = ' ';
draft.touchedFields.label = true;
expect(buildPluginConfigPatch(draft, fields, t)).toEqual({
patch: { label: null },
errors: {},
});
});
test('rejects non-array JSON for an array field', () => {
const draft = buildPluginConfigDraft(
{ enabled: true, configFields: fields },
{ mixed: ['old'] }
);
draft.values.mixed = '{"not":"an array"}';
draft.touchedFields.mixed = true;
expect(buildPluginConfigPatch(draft, fields, t)).toEqual({
patch: {},
errors: { mixed: 'plugin_management.expected_array' },
});
});
});

View file

@ -0,0 +1,16 @@
import { describe, expect, test } from 'vitest';
import { isOfficialPlugin } from '../src/features/plugins/pluginResources';
import type { PluginStoreEntry } from '../src/types';
const pluginEntry = (sourceId: string, repository: string) =>
({ sourceId, repository }) as PluginStoreEntry;
describe('plugin store trust', () => {
test('trusts only the official source with an official repository', () => {
expect(isOfficialPlugin(pluginEntry('official', 'router-for-me/example-plugin'))).toBe(true);
expect(isOfficialPlugin(pluginEntry('third-party', 'router-for-me/example-plugin'))).toBe(
false
);
expect(isOfficialPlugin(pluginEntry('official', 'someone-else/example-plugin'))).toBe(false);
});
});

View file

@ -0,0 +1,11 @@
import { describe, expect, test } from 'vitest';
import { supportsPluginVersionSelection } from '../src/features/plugins/pluginReleaseVersions';
describe('plugin version selection', () => {
test('allows custom versions only for GitHub release installs', () => {
expect(supportsPluginVersionSelection('github-release')).toBe(true);
expect(supportsPluginVersionSelection(' GitHub-Release ')).toBe(true);
expect(supportsPluginVersionSelection('direct')).toBe(false);
expect(supportsPluginVersionSelection('')).toBe(false);
});
});

View file

@ -0,0 +1,44 @@
import { describe, expect, test } from 'vitest';
import {
appendLatestProviderRecord,
replaceLatestProviderRecord,
} from '../src/services/api/providers';
const mergeRecord = (raw: unknown, payload: Record<string, unknown>) => ({
...(raw as Record<string, unknown> | undefined),
...payload,
});
describe('provider list concurrency', () => {
test('preserves concurrent additions while appending a provider', () => {
const latest = [
{ 'api-key': 'existing', custom: 'keep' },
{ 'api-key': 'concurrent', custom: 'also-keep' },
];
expect(appendLatestProviderRecord(latest, { 'api-key': 'created' }, mergeRecord)).toEqual([
{ 'api-key': 'existing', custom: 'keep' },
{ 'api-key': 'concurrent', custom: 'also-keep' },
{ 'api-key': 'created' },
]);
});
test('replaces only the selected provider in the latest list', () => {
const latest = [
{ 'api-key': 'existing', custom: 'keep' },
{ 'api-key': 'concurrent', custom: 'also-keep' },
];
expect(
replaceLatestProviderRecord(
latest,
(record) => record['api-key'] === 'existing',
{ 'api-key': 'updated' },
mergeRecord
)
).toEqual([
{ 'api-key': 'updated', custom: 'keep' },
{ 'api-key': 'concurrent', custom: 'also-keep' },
]);
});
});

View file

@ -0,0 +1,40 @@
import { describe, expect, test } from 'vitest';
import { buildExcludedModels } from '../src/features/providers/useProviderWorkbench';
/**
* `excluded-models: ['*']` provider
* `disabled` flag flag
*
* textarea ExcludedModelsPicker
*
*/
describe('buildExcludedModels — the "*" disable-rule invariant', () => {
test('appends "*" when disabled', () => {
expect(buildExcludedModels('a\nb', true, 'gemini')).toEqual(['a', 'b', '*']);
});
test('omits "*" when not disabled', () => {
expect(buildExcludedModels('a\nb', false, 'gemini')).toEqual(['a', 'b']);
});
test('a hand-typed "*" never duplicates the disable rule', () => {
expect(buildExcludedModels('a\n*\nb', true, 'gemini')).toEqual(['a', 'b', '*']);
});
test('a hand-typed "*" never switches the provider to disabled', () => {
expect(buildExcludedModels('a\n*\nb', false, 'gemini')).toEqual(['a', 'b']);
});
test('disabled with no rules yields exactly the disable rule', () => {
expect(buildExcludedModels('', true, 'gemini')).toEqual(['*']);
});
test('no rules and not disabled yields undefined, not an empty array', () => {
expect(buildExcludedModels('', false, 'gemini')).toBeUndefined();
});
test('openaiCompatibility never receives the disable rule', () => {
expect(buildExcludedModels('a', true, 'openaiCompatibility')).toEqual(['a']);
expect(buildExcludedModels('', true, 'openaiCompatibility')).toBeUndefined();
});
});

View file

@ -0,0 +1,31 @@
import { describe, expect, test } from 'vitest';
import { createProviderRecentRequestsCacheController } from '../src/components/providers/hooks/useProviderRecentRequests';
describe('provider recent request cache isolation', () => {
test('creates a fresh cache when the backend or management key changes', () => {
const controller = createProviderRecentRequestsCacheController();
const serverA = controller.forScope('https://server-a.example', 'key-a');
serverA.cachedUsageByProvider = new Map([['provider-a', new Map()]]);
serverA.cachedAt = Date.now();
serverA.inFlightRequest = Promise.resolve(serverA.cachedUsageByProvider);
const serverB = controller.forScope('https://server-b.example', 'key-b');
expect(serverB).not.toBe(serverA);
expect(serverB.cachedUsageByProvider.size).toBe(0);
expect(serverB.cachedAt).toBe(0);
expect(serverB.inFlightRequest).toBeNull();
serverA.cachedUsageByProvider = new Map([['late-provider-a', new Map()]]);
expect(controller.forScope('https://server-b.example', 'key-b')).toBe(serverB);
expect(serverB.cachedUsageByProvider.size).toBe(0);
});
test('reuses the cache only within the same connection scope', () => {
const controller = createProviderRecentRequestsCacheController();
const first = controller.forScope('https://server.example', 'key-a');
expect(controller.forScope('https://server.example', 'key-a')).toBe(first);
expect(controller.forScope('https://server.example', 'key-b')).not.toBe(first);
});
});

View file

@ -0,0 +1,100 @@
import { afterEach, describe, expect, test } from 'vitest';
import { apiClient } from '../src/services/api/client';
import { providersApi } from '../src/services/api/providers';
const originalGet = apiClient.get;
const originalPut = apiClient.put;
afterEach(() => {
apiClient.get = originalGet;
apiClient.put = originalPut;
});
describe('provider model thinking config', () => {
test('serializes thinking overrides for Vertex models', async () => {
let putData: unknown;
apiClient.get = (async () => ({ 'vertex-api-key': [] })) as typeof apiClient.get;
apiClient.put = (async (_url: string, data?: unknown) => {
putData = data;
return undefined;
}) as typeof apiClient.put;
await providersApi.createVertexConfig({
apiKey: 'vertex-key',
models: [
{
name: 'gemini-3-pro',
alias: 'vertex-pro',
thinking: {
min: 128,
max: 32768,
zero_allowed: true,
dynamic_allowed: true,
},
},
],
});
expect(putData).toEqual([
{
'api-key': 'vertex-key',
models: [
{
name: 'gemini-3-pro',
alias: 'vertex-pro',
thinking: {
min: 128,
max: 32768,
zero_allowed: true,
dynamic_allowed: true,
},
},
],
},
]);
});
test('can clear thinking while preserving unknown model fields', async () => {
let putData: unknown;
apiClient.get = (async () => ({
'codex-api-key': [
{
'api-key': 'codex-key',
'base-url': 'https://example.com',
models: [
{
name: 'gpt-5-codex',
alias: 'codex-latest',
thinking: { levels: ['low', 'high'] },
'future-field': 'preserved',
},
],
},
],
})) as typeof apiClient.get;
apiClient.put = (async (_url: string, data?: unknown) => {
putData = data;
return undefined;
}) as typeof apiClient.put;
await providersApi.updateCodexConfig('codex-key', 'https://example.com', {
apiKey: 'codex-key',
baseUrl: 'https://example.com',
models: [{ name: 'gpt-5-codex', alias: 'codex-latest' }],
});
expect(putData).toEqual([
{
'api-key': 'codex-key',
'base-url': 'https://example.com',
models: [
{
'future-field': 'preserved',
name: 'gpt-5-codex',
alias: 'codex-latest',
},
],
},
]);
});
});

View file

@ -0,0 +1,106 @@
import { afterEach, describe, expect, test } from 'vitest';
import { apiClient } from '../src/services/api/client';
import { providersApi } from '../src/services/api/providers';
import {
normalizeGeminiKeyConfig,
normalizeOpenAIProvider,
normalizeProviderKeyConfig,
} from '../src/services/api/transformers';
const originalGet = apiClient.get;
const originalPut = apiClient.put;
afterEach(() => {
apiClient.get = originalGet;
apiClient.put = originalPut;
});
describe('provider credential weight normalization', () => {
test('reads weight for direct API key credentials', () => {
expect(normalizeGeminiKeyConfig({ 'api-key': 'gemini-key', weight: 5 })?.weight).toBe(5);
expect(normalizeProviderKeyConfig({ 'api-key': 'provider-key', weight: 0 })?.weight).toBe(0);
});
test('reads per-key weight for OpenAI-compatible providers', () => {
const provider = normalizeOpenAIProvider({
name: 'example',
'base-url': 'https://example.com/v1',
'api-key-entries': [{ 'api-key': 'key-a', weight: 3 }, { 'api-key': 'key-b' }],
});
expect(provider?.apiKeyEntries[0]?.weight).toBe(3);
expect(provider?.apiKeyEntries[1]?.weight).toBeUndefined();
});
test('removes a cleared Vertex weight while preserving unknown fields', async () => {
let written: unknown;
apiClient.get = (async () => ({
'vertex-api-key': [
{
'api-key': 'vertex-key',
'base-url': 'https://vertex.example',
weight: 9,
'future-field': 'keep',
},
],
})) as typeof apiClient.get;
apiClient.put = (async (_url: string, data?: unknown) => {
written = data;
return undefined;
}) as typeof apiClient.put;
await providersApi.updateVertexConfig('vertex-key', 'https://vertex.example', {
apiKey: 'vertex-key',
baseUrl: 'https://vertex.example',
weight: undefined,
});
expect(written).toEqual([
{
'api-key': 'vertex-key',
'base-url': 'https://vertex.example',
'future-field': 'keep',
},
]);
});
test('writes and clears nested OpenAI-compatible key weights', async () => {
let written: unknown;
apiClient.get = (async () => ({
'openai-compatibility': [
{
name: 'example',
'base-url': 'https://example.com/v1',
'api-key-entries': [
{ 'api-key': 'key-a', weight: 8, custom: 'keep-a' },
{ 'api-key': 'key-b', custom: 'keep-b' },
],
},
],
})) as typeof apiClient.get;
apiClient.put = (async (_url: string, data?: unknown) => {
written = data;
return undefined;
}) as typeof apiClient.put;
await providersApi.updateOpenAIProvider('example', 0, {
name: 'example',
baseUrl: 'https://example.com/v1',
apiKeyEntries: [
{ apiKey: 'key-a', weight: undefined },
{ apiKey: 'key-b', weight: 4 },
],
});
expect(written).toEqual([
{
name: 'example',
'base-url': 'https://example.com/v1',
'api-key-entries': [
{ 'api-key': 'key-a', custom: 'keep-a' },
{ 'api-key': 'key-b', custom: 'keep-b', weight: 4 },
],
},
]);
});
});

View file

@ -0,0 +1,199 @@
/**
* Provider bodies rendered end-to-end.
*
* Bodies receive their class map as a prop and import no stylesheet, so unlike
* QuotaCard they can be rendered directly here which is the only place the
* "absolute plus countdown" pairing is checked as actual markup rather than as
* a formatter's return value.
*/
import { beforeAll, describe, expect, test } from 'vitest';
import { createElement } from 'react';
import { renderToStaticMarkup } from 'react-dom/server';
import i18n from '@/i18n';
import { CodexQuotaBody } from '@/features/quota/providers/codex/CodexQuotaBody';
import { ClaudeQuotaBody } from '@/features/quota/providers/claude/ClaudeQuotaBody';
import { KimiQuotaBody } from '@/features/quota/providers/kimi/KimiQuotaBody';
import { QUOTA_CLASS_KEYS, bindQuotaClasses } from '@/features/quota/types';
import { formatInstantShort } from '@/utils/quota';
import { DAY_MS, HOUR_MS } from '@/utils/time/durations';
import type { ClaudeQuotaState, CodexQuotaState, KimiQuotaState } from '@/types';
const classes = bindQuotaClasses(
Object.fromEntries(QUOTA_CLASS_KEYS.map((key) => [key, key])),
'test-host'
);
/**
* useNow() freezes to module-load time under renderToStaticMarkup (it reads
* getServerSnapshot), so instants are placed relative to the real clock.
*/
const now = Date.now();
// The i18n fallback is zh-CN; pin English so the countdown assertions read.
beforeAll(async () => {
await i18n.changeLanguage('en');
});
describe('CodexQuotaBody', () => {
const quota: CodexQuotaState = {
status: 'success',
planType: 'pro',
windows: [
{
id: 'primary',
label: '5-hour limit',
usedPercent: 38,
resetLabel: '08-02 18:00',
resetAtMs: now + 3 * HOUR_MS,
periodHours: 5,
},
],
rateLimitResetCredits: [
{
id: 'credit-1',
status: 'available',
grantedAt: new Date(now - DAY_MS).toISOString(),
expiresAt: new Date(now + 11 * DAY_MS).toISOString(),
},
],
rateLimitResetCreditsAvailableCount: 1,
};
test('renders a window reset as absolute plus countdown', () => {
const markup = renderToStaticMarkup(createElement(CodexQuotaBody, { quota, classes }));
expect(markup).toContain('08-02 18:00');
expect(markup).toContain('quotaResetRelative');
expect(markup).toMatch(/3 hours/);
});
test('renders reset-credit expiry in local time with a countdown', () => {
const markup = renderToStaticMarkup(createElement(CodexQuotaBody, { quota, classes }));
expect(markup).toContain(formatInstantShort(now + 11 * DAY_MS));
expect(markup).toMatch(/11 days/);
});
test('highlights a credit expiring within the final hour', () => {
const creditFirst: CodexQuotaState = {
...quota,
windows: [{ ...quota.windows[0], resetAtMs: now + 5 * DAY_MS }],
rateLimitResetCredits: [
{
id: 'credit-1',
status: 'available',
grantedAt: new Date(now - DAY_MS).toISOString(),
expiresAt: new Date(now + 30 * 60_000).toISOString(),
},
],
};
const markup = renderToStaticMarkup(
createElement(CodexQuotaBody, { quota: creditFirst, classes })
);
expect(markup).toContain('codexResetCreditRowSoon');
expect(markup).not.toContain('quotaRowSoon');
});
test('does not emphasize a reset countdown more than one hour away', () => {
const markup = renderToStaticMarkup(createElement(CodexQuotaBody, { quota, classes }));
expect(markup).not.toContain('quotaRowSoon');
expect(markup).not.toContain('quotaResetRelativeSoon');
expect(markup).not.toContain('codexResetCreditRowSoon');
});
test('emphasizes a reset countdown within the final hour', () => {
const urgent: CodexQuotaState = {
...quota,
windows: [{ ...quota.windows[0], resetAtMs: now + 30 * 60_000 }],
};
const markup = renderToStaticMarkup(createElement(CodexQuotaBody, { quota: urgent, classes }));
expect(markup).toContain('quotaResetRelativeSoon');
});
test('highlights nothing once every instant is in the past', () => {
const stale: CodexQuotaState = {
...quota,
windows: [{ ...quota.windows[0], resetAtMs: now - HOUR_MS }],
rateLimitResetCredits: [],
rateLimitResetCreditsAvailableCount: null,
};
const markup = renderToStaticMarkup(createElement(CodexQuotaBody, { quota: stale, classes }));
expect(markup).not.toContain('Soon');
});
test('keeps the baked label alone when the store entry predates resetAtMs', () => {
const stale: CodexQuotaState = {
...quota,
windows: [{ ...quota.windows[0], resetAtMs: undefined, periodHours: undefined }],
rateLimitResetCredits: [],
rateLimitResetCreditsAvailableCount: null,
};
const markup = renderToStaticMarkup(createElement(CodexQuotaBody, { quota: stale, classes }));
expect(markup).toContain('08-02 18:00');
expect(markup).not.toContain('quotaResetRelative');
});
});
describe('KimiQuotaBody', () => {
test('renders the concrete reset time alongside its countdown', () => {
const resetAtMs = now + 3 * HOUR_MS;
const quota: KimiQuotaState = {
status: 'success',
rows: [
{
id: 'summary',
label: 'Weekly limit',
used: 34,
limit: 100,
resetHint: '3h',
resetAtMs,
periodHours: 168,
},
],
};
const markup = renderToStaticMarkup(createElement(KimiQuotaBody, { quota, classes }));
expect(markup).toContain(formatInstantShort(resetAtMs));
expect(markup).toContain('quotaResetRelative');
expect(markup).toMatch(/3 hours/);
expect(markup).not.toContain('resets in 3h');
});
});
describe('ClaudeQuotaBody', () => {
test('pairs each window reset with a countdown', () => {
const quota: ClaudeQuotaState = {
status: 'success',
windows: [
{
id: 'five_hour',
label: '5-hour',
usedPercent: 12,
resetLabel: '08-02 17:00',
resetAtMs: now + 2 * HOUR_MS,
periodHours: 5,
},
{
id: 'seven_day',
label: '7-day',
usedPercent: 60,
resetLabel: '08-06 04:00',
resetAtMs: now + 4 * DAY_MS,
periodHours: 168,
},
],
};
const markup = renderToStaticMarkup(createElement(ClaudeQuotaBody, { quota, classes }));
expect(markup).toContain('08-02 17:00');
expect(markup).toContain('08-06 04:00');
expect(markup).toMatch(/2 hours/);
expect(markup).toMatch(/4 days/);
});
});

View file

@ -0,0 +1,49 @@
/**
* Guard for the QuotaClassMap SCSS contract.
*
* `bindQuotaClasses` throws at *module initialization*, and both hosts bind at
* import time so a contract key missing from either stylesheet does not
* degrade one row, it white-screens the whole application. This converts that
* into a failing test.
*
* The check is textual rather than a module import so it validates the source
* selectors instead of Vite's generated class-name object.
*/
import { readFile } from 'node:fs/promises';
import { describe, expect, test } from 'vitest';
import { QUOTA_CLASS_KEYS, bindQuotaClasses } from '@/features/quota/types';
const HOSTS = {
'QuotaBody.module.scss': 'src/features/quota/components/QuotaBody.module.scss',
'AuthFileQuota.module.scss': 'src/features/authFiles/components/AuthFileQuota.module.scss',
} as const;
const readHost = (path: string) => readFile(new URL(`../${path}`, import.meta.url), 'utf8');
describe('quota class contract', () => {
for (const [host, path] of Object.entries(HOSTS)) {
test(`${host} defines every QuotaClassMap key`, async () => {
const css = await readHost(path);
const missing = QUOTA_CLASS_KEYS.filter(
(key) => !new RegExp(`^\\s*\\.${key}\\b`, 'm').test(css)
);
expect(missing).toEqual([]);
});
}
test('bindQuotaClasses reports every missing key rather than the first', () => {
const partial = Object.fromEntries(QUOTA_CLASS_KEYS.map((key) => [key, `_${key}`]));
delete partial.quotaReset;
delete partial.quotaResetRelative;
expect(() => bindQuotaClasses(partial, 'test-host')).toThrow(/quotaReset.*quotaResetRelative/);
});
test('bindQuotaClasses returns exactly the contract keys', () => {
const full = Object.fromEntries(QUOTA_CLASS_KEYS.map((key) => [key, `_${key}`]));
const bound = bindQuotaClasses({ ...full, strayKey: '_stray' }, 'test-host');
expect(Object.keys(bound).sort()).toEqual([...QUOTA_CLASS_KEYS].sort());
});
});

View file

@ -0,0 +1,187 @@
import { describe, expect, test } from 'vitest';
import { QUOTA_PAGE_SIZE } from '@/features/quota/constants';
import {
buildTabCounts,
classifyQuotaFiles,
filterEntriesByTab,
isQuotaRefreshDisabled,
paginate,
resolveQuotaProviderType,
sortQuotaEntries,
type QuotaFileEntry,
} from '@/features/quota/logic';
import type { AuthFileItem } from '@/types';
const file = (name: string, provider: string, extra: Partial<AuthFileItem> = {}): AuthFileItem =>
({ name, provider, ...extra }) as AuthFileItem;
const FILES: AuthFileItem[] = [
file('codex-a.json', 'codex'),
file('claude-a.json', 'claude'),
file('kimi-a.json', 'kimi'),
file('codex-b.json', 'codex'),
file('grok-a.json', 'grok'), // 别名归一到 xai
file('gemini-a.json', 'gemini'), // 不支持额度
file('claude-off.json', 'claude', { disabled: true }), // 停用
];
describe('resolveQuotaProviderType', () => {
test('maps provider aliases and rejects unsupported or disabled files', () => {
expect(resolveQuotaProviderType(file('a', 'grok'))).toBe('xai');
expect(resolveQuotaProviderType(file('a', 'antigravity'))).toBe('antigravity');
expect(resolveQuotaProviderType(file('a', 'gemini'))).toBeNull();
expect(resolveQuotaProviderType(file('a', 'claude', { disabled: true }))).toBeNull();
});
});
describe('classifyQuotaFiles', () => {
test('drops unsupported and disabled files', () => {
const entries = classifyQuotaFiles(FILES);
expect(entries.map((entry) => entry.file.name)).not.toContain('gemini-a.json');
expect(entries.map((entry) => entry.file.name)).not.toContain('claude-off.json');
expect(entries).toHaveLength(5);
});
test('orders entries by provider tab order', () => {
const entries = classifyQuotaFiles(FILES);
expect(entries.map((entry) => entry.type)).toEqual(['claude', 'codex', 'codex', 'xai', 'kimi']);
});
});
describe('buildTabCounts', () => {
test('counts per provider plus an all total, zero-filling empty tabs', () => {
expect(buildTabCounts(classifyQuotaFiles(FILES))).toEqual({
all: 5,
claude: 1,
antigravity: 0,
codex: 2,
xai: 1,
kimi: 1,
});
});
});
describe('filterEntriesByTab', () => {
const entries = classifyQuotaFiles(FILES);
test("passes everything through on the 'all' tab", () => {
expect(filterEntriesByTab(entries, 'all')).toHaveLength(5);
});
test('filters to a single provider', () => {
expect(filterEntriesByTab(entries, 'codex').map((entry) => entry.file.name)).toEqual([
'codex-a.json',
'codex-b.json',
]);
expect(filterEntriesByTab(entries, 'antigravity')).toEqual([]);
});
});
describe('isQuotaRefreshDisabled', () => {
test('blocks a single-card refresh while the same quota is resetting', () => {
expect(isQuotaRefreshDisabled(true, false, true)).toBe(true);
expect(isQuotaRefreshDisabled(true, false, false)).toBe(false);
});
});
describe('paginate', () => {
const items = Array.from({ length: 45 }, (_, index) => index);
test('uses the configured 20-item page size', () => {
expect(QUOTA_PAGE_SIZE).toBe(20);
expect(paginate(items, 2, QUOTA_PAGE_SIZE)).toEqual({
pageItems: items.slice(20, 40),
currentPage: 2,
totalPages: 3,
});
});
test('clamps an out-of-range page instead of returning an empty slice', () => {
expect(paginate(items, 9, QUOTA_PAGE_SIZE).currentPage).toBe(3);
expect(paginate(items, 9, QUOTA_PAGE_SIZE).pageItems).toEqual(items.slice(40));
expect(paginate(items, 0, QUOTA_PAGE_SIZE).currentPage).toBe(1);
});
test('keeps at least one page when the list is empty', () => {
expect(paginate([], 1, QUOTA_PAGE_SIZE)).toEqual({
pageItems: [],
currentPage: 1,
totalPages: 1,
});
});
});
describe('sortQuotaEntries', () => {
const entries = classifyQuotaFiles(FILES);
const byName = (list: QuotaFileEntry[]) => list.map((entry) => entry.file.name);
/** Recovery instants keyed by file name; anything absent resolves to null. */
const resolver = (instants: Record<string, number>) => (entry: QuotaFileEntry) =>
instants[entry.file.name] ?? null;
test('default mode preserves order but returns a new array', () => {
const sorted = sortQuotaEntries(entries, 'default', () => 1);
expect(byName(sorted)).toEqual(byName(entries));
expect(sorted).not.toBe(entries);
});
test('orders loaded credentials by how soon they recover, across providers', () => {
const sorted = sortQuotaEntries(
entries,
'soonest',
resolver({
'codex-a.json': 300,
'claude-a.json': 100,
'kimi-a.json': 200,
'codex-b.json': 400,
'grok-a.json': 50,
})
);
expect(byName(sorted)).toEqual([
'grok-a.json',
'claude-a.json',
'kimi-a.json',
'codex-a.json',
'codex-b.json',
]);
});
test('sinks credentials with no instant, keeping their provider-grouped order', () => {
// Loading is click-to-fetch, so an unloaded tail is the normal case.
const sorted = sortQuotaEntries(
entries,
'soonest',
resolver({ 'codex-b.json': 200, 'kimi-a.json': 100 })
);
expect(byName(sorted)).toEqual([
'kimi-a.json',
'codex-b.json',
// unresolved tail, in the order classifyQuotaFiles produced
'claude-a.json',
'codex-a.json',
'grok-a.json',
]);
});
test('leaves the order untouched when nothing has loaded', () => {
expect(byName(sortQuotaEntries(entries, 'soonest', () => null))).toEqual(byName(entries));
});
test('breaks ties on the original position, so equal instants stay stable', () => {
const sorted = sortQuotaEntries(entries, 'soonest', () => 500);
expect(byName(sorted)).toEqual(byName(entries));
});
test('does not mutate the input', () => {
const input = [...entries];
sortQuotaEntries(input, 'soonest', resolver({ 'codex-b.json': 1 }));
expect(input).toEqual(entries);
});
test('sorts before paginating, so the globally soonest lands on page one', () => {
// Last in the default order, first to recover.
const last = entries[entries.length - 1].file.name;
const sorted = sortQuotaEntries(entries, 'soonest', resolver({ [last]: 1 }));
expect(paginate(sorted, 1, 2).pageItems[0].file.name).toBe(last);
});
});

View file

@ -0,0 +1,41 @@
import { describe, expect, test } from 'vitest';
import {
ELITE_CODEX_PLAN_TYPE,
PREMIUM_CODEX_PLAN_TYPES,
resolvePlanTier,
} from '@/utils/quota';
describe('resolvePlanTier', () => {
test("elite wins for 'pro' even though it is also in the premium set (order contract)", () => {
// 顺序契约回归:'pro' 同时命中 PREMIUM_CODEX_PLAN_TYPES
// 一旦 premium 判断先行Pro 20x 会静默退回金卡。
expect(PREMIUM_CODEX_PLAN_TYPES.has(ELITE_CODEX_PLAN_TYPE)).toBe(true);
expect(resolvePlanTier('pro')).toBe('elite');
});
test('normalizes case and whitespace before matching', () => {
expect(resolvePlanTier('PRO')).toBe('elite');
expect(resolvePlanTier(' Pro ')).toBe('elite');
expect(resolvePlanTier('Pro-Lite')).toBe('premium');
});
test('maps every pro-lite spelling to premium', () => {
expect(resolvePlanTier('prolite')).toBe('premium');
expect(resolvePlanTier('pro-lite')).toBe('premium');
expect(resolvePlanTier('pro_lite')).toBe('premium');
});
test('maps ordinary and unknown plans to plain', () => {
expect(resolvePlanTier('plus')).toBe('plain');
expect(resolvePlanTier('team')).toBe('plain');
expect(resolvePlanTier('free')).toBe('plain');
expect(resolvePlanTier('enterprise')).toBe('plain');
});
test('maps missing values to plain', () => {
expect(resolvePlanTier(null)).toBe('plain');
expect(resolvePlanTier(undefined)).toBe('plain');
expect(resolvePlanTier('')).toBe('plain');
expect(resolvePlanTier(' ')).toBe('plain');
});
});

View file

@ -0,0 +1,127 @@
/**
* Relative-time formatting for quota cards.
*/
import { describe, expect, test } from 'vitest';
import {
buildResetDisplay,
formatInstantShort,
formatQuotaResetTime,
formatRelativeInstant,
relativeTimeParts,
} from '@/utils/quota';
import { DAY_MS, HOUR_MS, MINUTE_MS } from '@/utils/time/durations';
const NOW = new Date(2026, 7, 2, 12, 0, 0).getTime();
describe('relativeTimeParts', () => {
test('picks the coarsest unit that still describes the gap', () => {
expect(relativeTimeParts(NOW + DAY_MS, NOW)).toEqual({ value: 1, unit: 'day' });
expect(relativeTimeParts(NOW + HOUR_MS, NOW)).toEqual({ value: 1, unit: 'hour' });
expect(relativeTimeParts(NOW + 30_000, NOW)).toEqual({ value: 1, unit: 'minute' });
});
test('never renders a magnitude that should have been the next unit up', () => {
// Rounding up here would produce "24 hours" and "60 minutes".
expect(relativeTimeParts(NOW + DAY_MS - 1, NOW)).toEqual({ value: 23, unit: 'hour' });
expect(relativeTimeParts(NOW + HOUR_MS - 1, NOW)).toEqual({ value: 59, unit: 'minute' });
});
test('truncates, so a deadline never appears further off than it is', () => {
expect(relativeTimeParts(NOW + 11 * DAY_MS + 1, NOW)).toEqual({ value: 11, unit: 'day' });
expect(relativeTimeParts(NOW + 11 * DAY_MS + 23 * HOUR_MS, NOW)).toEqual({
value: 11,
unit: 'day',
});
expect(relativeTimeParts(NOW + 90 * MINUTE_MS, NOW)).toEqual({ value: 1, unit: 'hour' });
});
test('past instants are negative rather than clamped to zero', () => {
// The timeline-local predecessor clamped with Math.max(0, …), so an expired
// credit read "in 1 minute".
expect(relativeTimeParts(NOW - 3 * DAY_MS, NOW)).toEqual({ value: -3, unit: 'day' });
expect(relativeTimeParts(NOW - 2 * HOUR_MS, NOW)).toEqual({ value: -2, unit: 'hour' });
});
test('sub-minute magnitudes floor to 1 in both directions', () => {
expect(relativeTimeParts(NOW + 1, NOW)).toEqual({ value: 1, unit: 'minute' });
expect(relativeTimeParts(NOW, NOW)).toEqual({ value: 1, unit: 'minute' });
expect(relativeTimeParts(NOW - 1, NOW)).toEqual({ value: -1, unit: 'minute' });
});
});
describe('formatRelativeInstant', () => {
test('renders in the requested locale', () => {
expect(formatRelativeInstant(NOW + 11 * DAY_MS, NOW, 'en')).toContain('days');
expect(formatRelativeInstant(NOW + 11 * DAY_MS, NOW, 'zh-CN')).toContain('天');
expect(formatRelativeInstant(NOW + 11 * DAY_MS, NOW, 'zh-TW')).toContain('天');
expect(formatRelativeInstant(NOW + 11 * DAY_MS, NOW, 'ru')).toBeTruthy();
});
test('distinguishes past from future', () => {
const future = formatRelativeInstant(NOW + 3 * DAY_MS, NOW, 'en');
const past = formatRelativeInstant(NOW - 3 * DAY_MS, NOW, 'en');
expect(future).not.toBe(past);
expect(past).toContain('ago');
});
test('falls back instead of throwing on an unusable locale tag', () => {
expect(() => formatRelativeInstant(NOW + DAY_MS, NOW, 'not-a-locale!!')).not.toThrow();
expect(formatRelativeInstant(NOW + DAY_MS, NOW, 'not-a-locale!!')).toBeTruthy();
});
});
describe('formatInstantShort', () => {
test('matches the shape the baked reset labels already use', () => {
const iso = new Date(2026, 7, 13, 14, 30).toISOString();
expect(formatInstantShort(new Date(iso).getTime())).toBe(formatQuotaResetTime(iso));
});
test('degrades to a dash rather than "Invalid Date"', () => {
expect(formatInstantShort(Number.NaN)).toBe('-');
});
});
describe('buildResetDisplay', () => {
test('pairs a baked absolute label with a computed relative one', () => {
const display = buildResetDisplay('08-13 14:30', NOW + 11 * DAY_MS, NOW, 'en');
expect(display).not.toBeNull();
expect(display?.absolute).toBe('08-13 14:30');
expect(display?.relative).toContain('11 days');
});
test('keeps the baked label alone when the instant is missing', () => {
// A store entry cached by an older build carries resetLabel but no resetAtMs.
expect(buildResetDisplay('08-13 14:30', null, NOW, 'en')).toEqual({
absolute: '08-13 14:30',
relative: null,
});
expect(buildResetDisplay('08-13 14:30', undefined, NOW, 'en')?.relative).toBeNull();
});
test('derives the absolute half from the instant when no label was baked', () => {
const at = NOW + 2 * HOUR_MS;
const display = buildResetDisplay(undefined, at, NOW, 'en');
expect(display?.absolute).toBe(formatInstantShort(at));
expect(display?.relative).toContain('2 hours');
});
test('returns null when there is nothing to render', () => {
expect(buildResetDisplay(undefined, null, NOW, 'en')).toBeNull();
expect(buildResetDisplay('', null, NOW, 'en')).toBeNull();
expect(buildResetDisplay(' ', null, NOW, 'en')).toBeNull();
// '-' is the providers' own placeholder for "no reset known".
expect(buildResetDisplay('-', null, NOW, 'en')).toBeNull();
});
test('treats a placeholder label with a real instant as renderable', () => {
const display = buildResetDisplay('-', NOW + DAY_MS, NOW, 'en');
expect(display?.absolute).toBe(formatInstantShort(NOW + DAY_MS));
expect(display?.relative).toContain('1 day');
});
test('rejects a non-finite instant', () => {
expect(buildResetDisplay(undefined, Number.NaN, NOW, 'en')).toBeNull();
expect(buildResetDisplay('08-13 14:30', Number.POSITIVE_INFINITY, NOW, 'en')?.relative).toBeNull();
});
});

View file

@ -0,0 +1,101 @@
import { describe, expect, test } from 'vitest';
import {
claudePeriodHours,
parseIsoToMs,
parseOffsetSecondsToMs,
parseUnixToMs,
periodHoursFromSeconds,
resolveResetMs,
} from '../src/utils/quota/resetInstants';
describe('parseIsoToMs', () => {
test('parses a plain ISO timestamp', () => {
expect(parseIsoToMs('2026-07-29T14:59:00Z')).toBe(Date.UTC(2026, 6, 29, 14, 59));
});
test('tolerates over-precise fractional seconds', () => {
// Some providers emit nanoseconds, which Date rejects on some engines.
expect(parseIsoToMs('2026-07-29T14:59:00.123456789Z')).toBe(
Date.UTC(2026, 6, 29, 14, 59, 0, 123)
);
});
test('rejects non-strings, blanks and unparseable text', () => {
expect(parseIsoToMs(undefined)).toBeNull();
expect(parseIsoToMs(' ')).toBeNull();
expect(parseIsoToMs('not a date')).toBeNull();
expect(parseIsoToMs(1754000000)).toBeNull();
});
});
describe('parseUnixToMs', () => {
test('treats small magnitudes as seconds and large as milliseconds', () => {
expect(parseUnixToMs(1_754_000_000)).toBe(1_754_000_000_000);
expect(parseUnixToMs(1_754_000_000_000)).toBe(1_754_000_000_000);
});
test('accepts numeric strings', () => {
expect(parseUnixToMs('1754000000')).toBe(1_754_000_000_000);
});
test('rejects zero, negatives and junk', () => {
expect(parseUnixToMs(0)).toBeNull();
expect(parseUnixToMs(-5)).toBeNull();
expect(parseUnixToMs('soon')).toBeNull();
expect(parseUnixToMs(null)).toBeNull();
});
});
describe('parseOffsetSecondsToMs', () => {
test('projects a countdown forward from now', () => {
const now = 1_754_000_000_000;
expect(parseOffsetSecondsToMs(3600, now)).toBe(now + 3_600_000);
});
test('rejects a non-positive or unparseable offset', () => {
expect(parseOffsetSecondsToMs(0, 1000)).toBeNull();
expect(parseOffsetSecondsToMs(-60, 1000)).toBeNull();
expect(parseOffsetSecondsToMs('nope', 1000)).toBeNull();
});
});
describe('resolveResetMs', () => {
test('takes the first parseable candidate, ISO or Unix', () => {
expect(resolveResetMs([undefined, null, '2026-07-29T14:59:00Z'])).toBe(
Date.UTC(2026, 6, 29, 14, 59)
);
expect(resolveResetMs([undefined, 1_754_000_000])).toBe(1_754_000_000_000);
});
test('prefers an earlier candidate over a later one in the list', () => {
// Order is priority, not chronology — callers list their preferred key first.
expect(resolveResetMs(['2026-01-01T00:00:00Z', 1_754_000_000])).toBe(
Date.UTC(2026, 0, 1)
);
});
test('returns null when nothing parses', () => {
expect(resolveResetMs([])).toBeNull();
expect(resolveResetMs([undefined, null, '', 'later'])).toBeNull();
});
});
describe('period derivation', () => {
test('converts a window length in seconds to hours', () => {
expect(periodHoursFromSeconds(18_000)).toBe(5);
expect(periodHoursFromSeconds(604_800)).toBe(168);
expect(periodHoursFromSeconds('18000')).toBe(5);
});
test('rejects an absent or non-positive window length', () => {
expect(periodHoursFromSeconds(0)).toBeNull();
expect(periodHoursFromSeconds(undefined)).toBeNull();
});
test('derives the Claude period from its window key', () => {
// Claude states the period nowhere in the payload — only the key implies it.
expect(claudePeriodHours('five_hour')).toBe(5);
expect(claudePeriodHours('seven_day')).toBe(168);
expect(claudePeriodHours('seven_day_opus')).toBe(168);
});
});

View file

@ -0,0 +1,239 @@
/**
* Which row on a quota card recovers first, per provider.
*/
import { describe, expect, test } from 'vitest';
import {
XAI_WEEKLY_ROW_ID,
collectQuotaRowInstants,
nextRecoveryMs,
pickSoonestRowId,
pickUrgentRowId,
resetCreditRowId,
} from '@/features/quota/resetSchedule';
import { DAY_MS, HOUR_MS } from '@/utils/time/durations';
const NOW = new Date(2026, 7, 2, 12).getTime();
const iso = (ms: number) => new Date(ms).toISOString();
const claudeQuota = {
status: 'success',
windows: [
{ id: 'five_hour', resetAtMs: NOW + 3 * HOUR_MS },
{ id: 'seven_day', resetAtMs: NOW + 4 * DAY_MS },
],
};
const codexQuota = {
status: 'success',
windows: [
{ id: 'primary', resetAtMs: NOW + 3 * HOUR_MS },
{ id: 'secondary', resetAtMs: NOW + 6 * DAY_MS },
],
rateLimitResetCredits: [
{ id: 'credit-a', status: 'available', expiresAt: iso(NOW + 11 * DAY_MS) },
{ id: 'credit-b', status: 'available', expiresAt: iso(NOW + 2 * DAY_MS) },
],
};
describe('collectQuotaRowInstants', () => {
test('collects every Claude window', () => {
expect(collectQuotaRowInstants('claude', claudeQuota)).toEqual([
{ rowId: 'five_hour', atMs: NOW + 3 * HOUR_MS, kind: 'window' },
{ rowId: 'seven_day', atMs: NOW + 4 * DAY_MS, kind: 'window' },
]);
});
test('collects Codex windows and available reset credits together', () => {
const instants = collectQuotaRowInstants('codex', codexQuota);
expect(instants).toHaveLength(4);
expect(instants.filter((i) => i.kind === 'credit').map((i) => i.rowId)).toEqual([
'credit-a',
'credit-b',
]);
});
test('ignores reset credits that are not available', () => {
const consumed = {
...codexQuota,
rateLimitResetCredits: [
{ id: 'used', status: 'consumed', expiresAt: iso(NOW + HOUR_MS) },
{ id: 'live', status: 'available', expiresAt: iso(NOW + 2 * HOUR_MS) },
],
};
expect(
collectQuotaRowInstants('codex', consumed)
.filter((i) => i.kind === 'credit')
.map((i) => i.rowId)
).toEqual(['live']);
});
test('collects the xAI weekly window', () => {
const quota = { status: 'success', billing: { periodType: 'weekly', resetAtMs: NOW + DAY_MS } };
expect(collectQuotaRowInstants('xai', quota)).toEqual([
{ rowId: XAI_WEEKLY_ROW_ID, atMs: NOW + DAY_MS, kind: 'window' },
]);
});
test('ignores an xAI monthly summary — a billing cycle is not capacity returning', () => {
const quota = {
status: 'success',
billing: { periodType: 'monthly', resetAtMs: NOW + DAY_MS },
};
expect(collectQuotaRowInstants('xai', quota)).toEqual([]);
});
test('flattens Antigravity buckets across groups', () => {
const quota = {
status: 'success',
groups: [
{ id: 'g1', buckets: [{ id: 'b1', resetAtMs: NOW + HOUR_MS }] },
{ id: 'g2', buckets: [{ id: 'b2', resetAtMs: NOW + 2 * HOUR_MS }] },
],
};
expect(collectQuotaRowInstants('antigravity', quota).map((i) => i.rowId)).toEqual(['b1', 'b2']);
});
test('collects Kimi rows', () => {
const quota = { status: 'success', rows: [{ id: 'r1', resetAtMs: NOW + HOUR_MS }] };
expect(collectQuotaRowInstants('kimi', quota).map((i) => i.rowId)).toEqual(['r1']);
});
test('returns nothing unless the credential loaded successfully', () => {
for (const status of ['idle', 'loading', 'error']) {
expect(collectQuotaRowInstants('claude', { ...claudeQuota, status })).toEqual([]);
}
expect(collectQuotaRowInstants('claude', undefined)).toEqual([]);
});
test('drops rows with no usable instant rather than emitting NaN', () => {
const quota = {
status: 'success',
windows: [
{ id: 'ok', resetAtMs: NOW + HOUR_MS },
{ id: 'missing' },
{ id: 'null', resetAtMs: null },
{ id: 'nan', resetAtMs: Number.NaN },
],
};
expect(collectQuotaRowInstants('claude', quota).map((i) => i.rowId)).toEqual(['ok']);
});
test('drops a reset credit whose expiry will not parse', () => {
const quota = {
status: 'success',
windows: [],
rateLimitResetCredits: [{ id: 'bad', status: 'available', expiresAt: 'not a date' }],
};
expect(collectQuotaRowInstants('codex', quota)).toEqual([]);
});
});
describe('pickSoonestRowId', () => {
test('picks the nearest upcoming instant', () => {
expect(pickSoonestRowId(collectQuotaRowInstants('claude', claudeQuota), NOW)).toBe('five_hour');
});
test('a credit expiring before every window wins the emphasis', () => {
const quota = {
...codexQuota,
windows: [{ id: 'primary', resetAtMs: NOW + 5 * DAY_MS }],
};
expect(pickSoonestRowId(collectQuotaRowInstants('codex', quota), NOW)).toBe('credit-b');
});
test('a window resetting before every credit wins the emphasis', () => {
expect(pickSoonestRowId(collectQuotaRowInstants('codex', codexQuota), NOW)).toBe('primary');
});
test('skips instants that have already passed', () => {
const instants = [
{ rowId: 'past', atMs: NOW - HOUR_MS, kind: 'window' as const },
{ rowId: 'exactly-now', atMs: NOW, kind: 'window' as const },
{ rowId: 'future', atMs: NOW + HOUR_MS, kind: 'window' as const },
];
expect(pickSoonestRowId(instants, NOW)).toBe('future');
});
test('returns null when nothing is pending', () => {
expect(pickSoonestRowId([], NOW)).toBeNull();
expect(pickSoonestRowId([{ rowId: 'past', atMs: NOW - 1, kind: 'window' }], NOW)).toBeNull();
});
test('breaks ties deterministically on row id', () => {
const a = [
{ rowId: 'b', atMs: NOW + HOUR_MS, kind: 'window' as const },
{ rowId: 'a', atMs: NOW + HOUR_MS, kind: 'window' as const },
];
expect(pickSoonestRowId(a, NOW)).toBe('a');
expect(pickSoonestRowId([...a].reverse(), NOW)).toBe('a');
});
});
describe('pickUrgentRowId', () => {
test('highlights only the nearest reset strictly inside the final hour', () => {
const instants = [
{ rowId: 'later-urgent', atMs: NOW + 45 * 60_000, kind: 'window' as const },
{ rowId: 'nearest-urgent', atMs: NOW + 30 * 60_000, kind: 'window' as const },
{ rowId: 'past', atMs: NOW - 1, kind: 'window' as const },
];
expect(pickUrgentRowId(instants, NOW)).toBe('nearest-urgent');
});
test('does not highlight at exactly one hour or beyond', () => {
expect(
pickUrgentRowId(
[
{ rowId: 'exactly-one-hour', atMs: NOW + HOUR_MS, kind: 'window' },
{ rowId: 'later', atMs: NOW + 2 * HOUR_MS, kind: 'window' },
],
NOW
)
).toBeNull();
});
});
describe('resetCreditRowId', () => {
test('prefers the credit id', () => {
expect(resetCreditRowId({ id: 'credit-a', expiresAt: 'x' }, 3)).toBe('credit-a');
});
test('falls back to expiry and index when the payload carries no id', () => {
// Must stay byte-identical to CodexQuotaBody's React key.
expect(resetCreditRowId({ id: '', expiresAt: '2026-08-13T00:00:00Z' }, 2)).toBe(
'2026-08-13T00:00:00Z-2'
);
expect(resetCreditRowId({ expiresAt: '2026-08-13T00:00:00Z' }, 0)).toBe(
'2026-08-13T00:00:00Z-0'
);
});
});
describe('nextRecoveryMs', () => {
test('returns the soonest upcoming instant across windows and credits', () => {
expect(nextRecoveryMs('codex', codexQuota, NOW)).toBe(NOW + 3 * HOUR_MS);
});
test('ignores instants already in the past', () => {
const quota = {
status: 'success',
windows: [
{ id: 'stale', resetAtMs: NOW - DAY_MS },
{ id: 'live', resetAtMs: NOW + DAY_MS },
],
};
expect(nextRecoveryMs('claude', quota, NOW)).toBe(NOW + DAY_MS);
});
test('is null for an unloaded credential, so sorting can sink it', () => {
expect(nextRecoveryMs('claude', undefined, NOW)).toBeNull();
expect(nextRecoveryMs('claude', { status: 'idle' }, NOW)).toBeNull();
expect(nextRecoveryMs('claude', { status: 'error' }, NOW)).toBeNull();
});
test('is null when every known instant has passed', () => {
const quota = { status: 'success', windows: [{ id: 'stale', resetAtMs: NOW - 1 }] };
expect(nextRecoveryMs('claude', quota, NOW)).toBeNull();
});
});

View file

@ -0,0 +1,70 @@
import { beforeEach, describe, expect, test } from 'vitest';
import { invalidateAuthFileDerivedCaches } from '../src/features/authFiles/cacheInvalidation';
import {
captureQuotaCacheGeneration,
commitIfQuotaCacheCurrent,
useQuotaStore,
} from '../src/stores/useQuotaStore';
describe('quota cache session isolation', () => {
beforeEach(() => {
useQuotaStore.getState().clearQuotaCache();
});
test('prevents an earlier connection from committing after the cache is cleared', () => {
const previousConnection = captureQuotaCacheGeneration();
let committed = false;
useQuotaStore.getState().clearQuotaCache();
expect(
commitIfQuotaCacheCurrent(previousConnection, () => {
committed = true;
})
).toBe(false);
expect(committed).toBe(false);
});
test('allows the current connection generation to commit', () => {
const currentConnection = captureQuotaCacheGeneration();
let committed = false;
expect(
commitIfQuotaCacheCurrent(currentConnection, () => {
committed = true;
})
).toBe(true);
expect(committed).toBe(true);
});
test('clears same-name quota and rejects an in-flight commit after auth mutation', () => {
const fileName = 'shared-codex.json';
useQuotaStore.getState().setCodexQuota({
[fileName]: {
status: 'success',
windows: [],
planType: 'account-a',
},
});
const accountARequest = captureQuotaCacheGeneration();
let invalidatedNames: string[] | undefined;
invalidateAuthFileDerivedCaches(
(names) => {
invalidatedNames = names;
},
[fileName]
);
expect(invalidatedNames).toEqual([fileName]);
expect(useQuotaStore.getState().codexQuota[fileName]).toBeUndefined();
let committed = false;
expect(
commitIfQuotaCacheCurrent(accountARequest, () => {
committed = true;
})
).toBe(false);
expect(committed).toBe(false);
});
});

View file

@ -0,0 +1,522 @@
import { describe, expect, test } from 'vitest';
import {
DAY_MS,
HOUR_MS,
buildTimelineLane,
laneHasWindow,
pickLaneWindow,
projectLane,
projectResetCredits,
startOfDay,
startOfWeek,
timelineSpan,
windowsIn,
} from '../src/features/quota/quotaTimelineModel';
import type { TimelineLane } from '../src/features/quota/quotaTimelineModel';
const at = (y: number, m: number, d: number, h = 0, min = 0) => new Date(y, m, d, h, min).getTime();
describe('windowsIn', () => {
test('projects backwards and forwards from the anchor', () => {
// Anchor is a known reset; the span opens before the current window did.
const anchor = at(2026, 6, 29, 12);
const from = anchor - 2.5 * DAY_MS;
const to = anchor + 1.5 * DAY_MS;
const windows = windowsIn(anchor, DAY_MS, from, to);
// A 4-day span of daily windows needs 5 bars: the span edges fall mid-window,
// so there's a partial window at each end.
expect(windows.length).toBe(5);
// Every boundary sits on a whole period from the anchor.
for (const window of windows) {
// `+ 0` normalizes JS's -0 from a negative remainder (windows before the anchor).
expect(((window.endMs - anchor) % DAY_MS) + 0).toBe(0);
expect(window.endMs - window.startMs).toBe(DAY_MS);
}
expect(windows.some((w) => w.startMs <= anchor && w.endMs >= anchor)).toBe(true);
// Fully covers the requested range.
expect(windows[0].startMs).toBeLessThanOrEqual(from);
expect(windows[windows.length - 1].endMs).toBeGreaterThanOrEqual(to);
});
test('covers the whole span with no gaps or overlaps', () => {
const windows = windowsIn(at(2026, 6, 29), 5 * HOUR_MS, at(2026, 6, 28), at(2026, 6, 30));
for (let i = 1; i < windows.length; i += 1) {
expect(windows[i].startMs).toBe(windows[i - 1].endMs);
}
});
test('rejects a degenerate period, span or anchor', () => {
expect(windowsIn(1000, 0, 0, 5000)).toEqual([]);
expect(windowsIn(1000, -5, 0, 5000)).toEqual([]);
expect(windowsIn(NaN, 1000, 0, 5000)).toEqual([]);
expect(windowsIn(1000, 1000, 5000, 5000)).toEqual([]);
});
test('bails out rather than looping forever on an absurd period', () => {
// A bad payload could give a 1ms period over a fortnight.
expect(windowsIn(0, 1, 0, 14 * DAY_MS)).toEqual([]);
});
});
describe('span boundaries', () => {
test('startOfDay and startOfWeek land on local midnight', () => {
const mid = at(2026, 6, 29, 14, 37);
expect(new Date(startOfDay(mid)).getHours()).toBe(0);
expect(new Date(startOfWeek(mid)).getDay()).toBe(0);
expect(new Date(startOfWeek(mid)).getHours()).toBe(0);
});
test('weekly span is a fortnight from the containing Sunday', () => {
const now = at(2026, 6, 29, 14, 0); // a Wednesday
const span = timelineSpan('weekly', 0, now);
expect(new Date(span.startMs).getDay()).toBe(0);
expect(span.days).toBe(14);
expect(span.startMs).toBeLessThanOrEqual(now);
expect(span.endMs).toBeGreaterThan(now);
});
test('offsets step a week in weekly mode and a day in session mode', () => {
const now = at(2026, 6, 29, 14, 0);
const weekly = timelineSpan('weekly', 0, now);
const weeklyNext = timelineSpan('weekly', 1, now);
expect(Math.round((weeklyNext.startMs - weekly.startMs) / DAY_MS)).toBe(7);
const session = timelineSpan('session', 0, now);
const sessionNext = timelineSpan('session', 1, now);
expect(Math.round((sessionNext.startMs - session.startMs) / DAY_MS)).toBe(1);
expect(session.days).toBe(3);
});
test('spans a whole number of days even across a DST transition', () => {
// US DST springs forward 2026-03-08; a fixed +14*DAY_MS would land at 23:00.
const span = timelineSpan('weekly', 0, at(2026, 2, 10, 12));
expect(new Date(span.startMs).getHours()).toBe(0);
expect(new Date(span.endMs).getHours()).toBe(0);
});
});
describe('projectLane', () => {
const lane = (over: Partial<TimelineLane> = {}): TimelineLane => ({
name: 'a.json',
displayName: 'Alice',
provider: 'claude',
anchorMs: at(2026, 6, 29, 20),
periodHours: 24 * 7,
remaining: 40,
limits: [],
resetCredits: [],
...over,
});
const span = timelineSpan('weekly', 0, at(2026, 6, 29, 12));
test('classifies past, live and next windows against now', () => {
const now = at(2026, 6, 29, 12);
const windows = projectLane(lane(), span.startMs, span.endMs, now, 'weekly');
expect(windows.length).toBeGreaterThan(0);
const live = windows.filter((w) => w.state === 'live');
expect(live.length).toBe(1);
expect(live[0].startMs).toBeLessThanOrEqual(now);
expect(live[0].endMs).toBeGreaterThan(now);
expect(live[0].remaining).toBe(40);
expect(windows.filter((w) => w.state === 'past').every((w) => w.endMs <= now)).toBe(true);
expect(windows.filter((w) => w.state === 'next').every((w) => w.startMs > now)).toBe(true);
});
test('does not carry stale remaining usage past the reported reset', () => {
const nowAfterReset = at(2026, 6, 30, 12);
const windows = projectLane(lane(), span.startMs, span.endMs, nowAfterReset, 'weekly');
const live = windows.find((window) => window.state === 'live');
expect(live).toBeDefined();
expect(live?.startMs).toBe(at(2026, 6, 29, 20));
expect(live?.remaining).toBeNull();
});
test('clips bars to the visible span', () => {
const windows = projectLane(lane(), span.startMs, span.endMs, at(2026, 6, 29, 12), 'weekly');
for (const window of windows) {
expect(window.leftPercent).toBeGreaterThanOrEqual(0);
expect(window.widthPercent).toBeGreaterThan(0);
expect(window.leftPercent + window.widthPercent).toBeLessThanOrEqual(100.0001);
}
});
test('returns nothing when the lane has no anchor or period', () => {
const now = at(2026, 6, 29, 12);
expect(projectLane(lane({ anchorMs: null }), span.startMs, span.endMs, now, 'weekly')).toEqual(
[]
);
expect(
projectLane(lane({ periodHours: null }), span.startMs, span.endMs, now, 'weekly')
).toEqual([]);
});
test('session mode projects only real 5-hour windows', () => {
const now = at(2026, 6, 29, 12);
const sessionSpan = timelineSpan('session', 0, now);
expect(projectLane(lane(), sessionSpan.startMs, sessionSpan.endMs, now, 'session')).toEqual([]);
const windows = projectLane(
lane({ periodHours: 5 }),
sessionSpan.startMs,
sessionSpan.endMs,
now,
'session'
);
expect(windows.some((w) => w.endMs - w.startMs === 5 * HOUR_MS)).toBe(true);
});
test('projects only unexpired reset credits inside the visible span', () => {
const now = at(2026, 6, 29, 12);
const visibleExpiry = at(2026, 7, 2, 12);
const marks = projectResetCredits(
lane({
resetCredits: [
{ id: 'expired', grantedAtMs: null, expiresAtMs: now - HOUR_MS },
{ id: 'visible', grantedAtMs: now - DAY_MS, expiresAtMs: visibleExpiry },
{ id: 'outside', grantedAtMs: now, expiresAtMs: span.endMs + HOUR_MS },
],
}),
span.startMs,
span.endMs,
now
);
expect(marks).toHaveLength(1);
expect(marks[0].id).toBe('visible');
expect(marks[0].leftPercent).toBe(
((visibleExpiry - span.startMs) / (span.endMs - span.startMs)) * 100
);
});
});
describe('pickLaneWindow', () => {
test('ignores windows with no reset instant', () => {
const chosen = pickLaneWindow([
{ resetAtMs: null, periodHours: 168 },
{ resetAtMs: 1000, periodHours: 5 },
{ resetAtMs: undefined, periodHours: 168 },
]);
expect(chosen?.resetAtMs).toBe(1000);
});
/**
* The bug this rule exists for: across a fortnight, the 5-hour window always
* resets soonest, so "pick the soonest" drew ~67 slivers per lane instead of
* two readable weekly bars.
*/
test('prefers the longest window that fits the span, not the soonest reset', () => {
const fiveHour = { resetAtMs: 1_000, periodHours: 5 };
const weekly = { resetAtMs: 9_000, periodHours: 168 };
expect(pickLaneWindow([fiveHour, weekly], 14 * 24)).toBe(weekly);
// A three-day span can't fit a weekly window, so the short one wins.
expect(pickLaneWindow([fiveHour, weekly], 3 * 24)).toBe(fiveHour);
});
test('breaks a period tie on the soonest reset', () => {
const later = { resetAtMs: 9_000, periodHours: 168 };
const sooner = { resetAtMs: 5_000, periodHours: 168 };
expect(pickLaneWindow([later, sooner], 14 * 24)).toBe(sooner);
});
test('falls back to the shortest available rather than drawing nothing', () => {
// Every window is longer than the span — still better to draw one.
const monthly = { resetAtMs: 5_000, periodHours: 720 };
expect(pickLaneWindow([monthly], 3 * 24)).toBe(monthly);
});
test('returns null when nothing qualifies', () => {
expect(pickLaneWindow([{ resetAtMs: null }])).toBeNull();
expect(pickLaneWindow([])).toBeNull();
});
});
describe('buildTimelineLane', () => {
const base = { name: 'a.json', displayName: 'Alice' };
test('claude/codex: anchors on the span-appropriate window, remaining from used', () => {
const soon = at(2026, 6, 29, 20);
const later = at(2026, 7, 1, 20);
const quota = {
status: 'success',
windows: [
{ label: '7-day', usedPercent: 93, resetAtMs: later, periodHours: 168 },
{ label: '5-hour', usedPercent: 20, resetAtMs: soon, periodHours: 5 },
],
};
// Fortnight view: the weekly window, even though the 5-hour resets sooner.
const weekly = buildTimelineLane({
...base,
provider: 'claude',
quota,
maxPeriodHours: 14 * 24,
});
expect(weekly.anchorMs).toBe(later);
expect(weekly.periodHours).toBe(168);
expect(weekly.remaining).toBe(7); // stored USED
// Three-day view: the weekly window doesn't fit, so the short one is used.
const session = buildTimelineLane({
...base,
provider: 'claude',
quota,
maxPeriodHours: 3 * 24,
});
expect(session.anchorMs).toBe(soon);
expect(session.periodHours).toBe(5);
expect(session.remaining).toBe(80);
// Every limit is still summarized in the lane head regardless of the pick.
expect(weekly.limits).toEqual([
{ label: '7-day', remaining: 7 },
{ label: '5-hour', remaining: 80 },
]);
});
test('codex: keeps the weekly lane on the account quota instead of Spark quota', () => {
const accountReset = at(2026, 7, 1, 20);
const sparkReset = at(2026, 6, 29, 20);
const lane = buildTimelineLane({
...base,
provider: 'codex',
quota: {
status: 'success',
windows: [
{
id: 'weekly',
label: 'Weekly limit',
usedPercent: 70,
resetAtMs: accountReset,
periodHours: 168,
},
{
id: 'gpt-5-3-codex-spark-weekly-0',
label: 'GPT-5.3-Codex-Spark weekly limit',
usedPercent: 2,
resetAtMs: sparkReset,
periodHours: 168,
},
],
},
maxPeriodHours: 14 * 24,
});
expect(lane.anchorMs).toBe(accountReset);
expect(lane.periodHours).toBe(168);
expect(lane.remaining).toBe(30);
});
test('codex: includes available reset credits with parseable expiry dates', () => {
const expiresAt = '2026-08-02T12:00:00Z';
const lane = buildTimelineLane({
...base,
provider: 'codex',
quota: {
status: 'success',
windows: [{ label: '7-day', usedPercent: 90, resetAtMs: 5000, periodHours: 168 }],
rateLimitResetCredits: [
{
id: 'credit-1',
status: 'available',
grantedAt: '2026-07-01T12:00:00Z',
expiresAt,
},
{
id: 'spent',
status: 'consumed',
grantedAt: '2026-07-01T12:00:00Z',
expiresAt,
},
{ id: 'invalid', status: 'available', grantedAt: '', expiresAt: 'not-a-date' },
],
},
});
expect(lane.resetCredits).toEqual([
{
id: 'credit-1',
grantedAtMs: new Date('2026-07-01T12:00:00Z').getTime(),
expiresAtMs: new Date(expiresAt).getTime(),
},
]);
});
test('kimi: derives remaining from raw used/limit counts', () => {
const lane = buildTimelineLane({
...base,
provider: 'kimi',
quota: {
status: 'success',
rows: [
{ label: 'Daily', used: 540, limit: 1000, resetAtMs: 5000, periodHours: 24 },
{ label: 'Monthly', used: 8100, limit: 30000, resetAtMs: 9000, periodHours: 720 },
],
},
// A fortnight fits the daily window but not the monthly one.
maxPeriodHours: 14 * 24,
});
expect(lane.anchorMs).toBe(5000);
expect(lane.remaining).toBe(46);
expect(lane.limits).toEqual([
{ label: 'Daily', remaining: 46 },
{ label: 'Monthly', remaining: 73 },
]);
});
test('antigravity anchors on its bucket reset, with remaining from the fraction', () => {
const lane = buildTimelineLane({
...base,
provider: 'antigravity',
quota: {
status: 'success',
groups: [
{
buckets: [
{ label: '5h', remainingFraction: 0.4, resetAtMs: 1000, periodHours: 5 },
{ label: 'Weekly', remainingFraction: 0.82, resetAtMs: 5000, periodHours: 168 },
],
},
],
},
maxPeriodHours: 336,
});
// Longest window that fits wins, exactly as it does for claude/codex.
expect(lane.anchorMs).toBe(5000);
expect(lane.periodHours).toBe(168);
// remainingFraction is REMAINING, so it is not inverted.
expect(lane.remaining).toBe(82);
expect(lane.limits).toEqual([
{ label: '5h', remaining: 40 },
{ label: 'Weekly', remaining: 82 },
]);
});
test('antigravity buckets without a parseable reset do not anchor the lane', () => {
const lane = buildTimelineLane({
...base,
provider: 'antigravity',
quota: {
status: 'success',
groups: [{ buckets: [{ label: '5h', remainingFraction: 0.4, resetAtMs: null }] }],
},
});
expect(lane.anchorMs).toBeNull();
expect(lane.limits).toEqual([]);
});
test('xai anchors on the weekly limit, with per-product limits', () => {
const lane = buildTimelineLane({
...base,
provider: 'xai',
quota: {
status: 'success',
billing: {
periodType: 'weekly',
usagePercent: 5,
resetAtMs: 9000,
periodHours: 168,
productUsage: [
{ product: 'GrokBuild', usagePercent: 5 },
{ product: 'GrokChat', usagePercent: null },
],
},
},
});
expect(lane.anchorMs).toBe(9000);
expect(lane.periodHours).toBe(168);
expect(lane.remaining).toBe(95);
// GrokChat has no percentage, so it is not summarized.
expect(lane.limits).toEqual([{ label: 'GrokBuild', remaining: 95 }]);
});
test('xai without a weekly limit produces no window at all', () => {
// A monthly summary carries periodEnd too, but that is a billing cycle.
for (const periodType of ['monthly', 'unknown'] as const) {
const lane = buildTimelineLane({
...base,
provider: 'xai',
quota: {
status: 'success',
billing: { periodType, usagePercent: 69, resetAtMs: 9000, periodHours: 720 },
},
});
expect(lane.anchorMs).toBeNull();
expect(laneHasWindow(lane)).toBe(false);
}
});
test('xai defaults to a 7-day period when the payload states no start', () => {
const lane = buildTimelineLane({
...base,
provider: 'xai',
quota: {
status: 'success',
billing: { periodType: 'weekly', usagePercent: 5, resetAtMs: 9000, periodHours: null },
},
});
expect(lane.periodHours).toBe(24 * 7);
});
test('laneHasWindow drops only lanes that can never draw a bar', () => {
const drawable = buildTimelineLane({
...base,
provider: 'claude',
quota: {
status: 'success',
windows: [{ label: '7-day', usedPercent: 10, resetAtMs: 5000, periodHours: 168 }],
},
});
expect(laneHasWindow(drawable)).toBe(true);
// Unloaded quota has nothing to show yet, so it takes no row.
expect(
laneHasWindow(buildTimelineLane({ ...base, provider: 'claude', quota: undefined }))
).toBe(false);
});
test('providers with no usable reset produce an empty lane, not a dropped one', () => {
// buildTimelineLane always returns a lane; dropping is the caller's job via
// laneHasWindow, so the two concerns stay separately testable.
for (const provider of ['antigravity', 'xai'] as const) {
const lane = buildTimelineLane({
...base,
provider,
quota: { status: 'success', groups: [] },
});
expect(lane.name).toBe('a.json');
expect(lane.anchorMs).toBeNull();
expect(lane.limits).toEqual([]);
}
});
test('unloaded or errored quota produces an empty lane', () => {
expect(
buildTimelineLane({ ...base, provider: 'claude', quota: undefined }).anchorMs
).toBeNull();
expect(
buildTimelineLane({ ...base, provider: 'claude', quota: { status: 'error' } }).anchorMs
).toBeNull();
});
test('windows without a reset instant do not anchor the lane', () => {
const lane = buildTimelineLane({
...base,
provider: 'claude',
quota: {
status: 'success',
windows: [{ label: '7-day', usedPercent: 50, resetAtMs: null, periodHours: 168 }],
},
});
expect(lane.anchorMs).toBeNull();
});
});

View file

@ -0,0 +1,164 @@
import { describe, expect, test } from 'vitest';
import { createElement } from 'react';
import { renderToStaticMarkup } from 'react-dom/server';
import '../src/i18n/index';
import { QuotaTimeline } from '../src/features/quota/components/QuotaTimeline';
import type { QuotaFileEntry } from '../src/features/quota/logic';
import { buildKimiQuotaRows } from '../src/utils/quota';
const entries: QuotaFileEntry[] = [
{
file: { name: 'weekly-only.json', type: 'claude' },
type: 'claude',
},
];
const baseProps = {
entries,
displayNameFor: (name: string) => name,
resolvedTheme: 'light' as const,
now: new Date(2026, 6, 29, 12).getTime(),
};
describe('QuotaTimeline rendering', () => {
test('shows the selected period date instead of always labelling it Today', () => {
const markup = renderToStaticMarkup(
createElement(QuotaTimeline, {
...baseProps,
initialOffset: 1,
quotaFor: () => ({
status: 'success',
windows: [
{
label: '7-day',
usedPercent: 25,
resetAtMs: new Date(2026, 7, 1, 12).getTime(),
periodHours: 168,
},
],
}),
})
);
// The next weekly period starts on Sunday 08/02. The button remains the
// shortcut back to Today (aria-label/title), but its visible label now
// reflects the period selected with the previous/next arrows.
expect(markup).toMatch(
/<button type="button" aria-label="[^"]+" title="[^"]+">08\/02<\/button>/
);
});
test('keeps the panel and controls visible when 5-hour mode has no matching lanes', () => {
const weeklyOnlyQuota = {
status: 'success' as const,
windows: [
{
label: '7-day',
usedPercent: 25,
resetAtMs: new Date(2026, 7, 1, 12).getTime(),
periodHours: 168,
},
],
};
const markup = renderToStaticMarkup(
createElement(QuotaTimeline, {
...baseProps,
initialMode: 'session',
quotaFor: () => weeklyOnlyQuota,
})
);
expect(markup).toContain('<section');
expect(markup).toContain('aria-pressed="true"');
expect(markup).toContain('role="status"');
});
test('renders a Kimi 5-hour lane from the protobuf-style time unit', () => {
const rows = buildKimiQuotaRows({
usage: {
used: '1',
limit: '100',
resetTime: '2099-08-06T13:59:23.136523Z',
},
limits: [
{
window: { duration: 300, timeUnit: 'TIME_UNIT_MINUTE' },
detail: {
used: '2',
limit: '100',
resetTime: '2099-07-31T06:59:23.136523Z',
},
},
],
});
const markup = renderToStaticMarkup(
createElement(QuotaTimeline, {
entries: [
{
file: { name: 'kimi-real-response.json', type: 'kimi' },
type: 'kimi',
},
],
displayNameFor: (name: string) => name,
resolvedTheme: 'light',
now: new Date('2099-07-31T04:40:00Z').getTime(),
initialMode: 'session',
quotaFor: () => ({ status: 'success', rows }),
})
);
expect(markup).toContain('kimi-real-response.json');
expect(markup).not.toContain('role="status"');
});
test('renders an unexpired Codex reset credit as an expiry tick', () => {
const markup = renderToStaticMarkup(
createElement(QuotaTimeline, {
entries: [
{
file: { name: 'codex-credit.json', type: 'codex' },
type: 'codex',
},
],
displayNameFor: (name: string) => name,
resolvedTheme: 'light',
now: new Date(2026, 6, 29, 12).getTime(),
quotaFor: () => ({
status: 'success',
windows: [
{
label: '7-day',
usedPercent: 90,
resetAtMs: new Date(2026, 7, 1, 12).getTime(),
periodHours: 168,
},
],
rateLimitResetCredits: [
{
id: 'credit-1',
status: 'available',
grantedAt: '2026-07-20T12:00:00Z',
expiresAt: '2026-08-03T12:00:00Z',
},
],
}),
})
);
expect(markup).toContain('role="img"');
expect(markup).toContain('08/03 12:00');
});
test('stays hidden before any credential exposes a usable quota window', () => {
const markup = renderToStaticMarkup(
createElement(QuotaTimeline, {
...baseProps,
quotaFor: () => undefined,
})
);
expect(markup).toBe('');
});
});

View file

@ -0,0 +1,75 @@
/**
* Session-scoped quota page preferences.
*
* The merge-on-write case is the one that matters: the tab strip and the sort
* control each write a single field and know nothing about the other, so a
* whole-object write would make changing a tab silently reset the sort.
*/
import { afterAll, beforeEach, describe, expect, test } from 'vitest';
import { readQuotaUiState, writeQuotaUiState } from '@/features/quota/uiState';
const KEY = 'quotaPage.uiState';
/** Test files share one process — leaving a fake `window` behind would leak. */
const originalWindow = (globalThis as { window?: unknown }).window;
/** The Node test environment has no sessionStorage; a Map-backed stub is enough here. */
function installSessionStorage() {
const store = new Map<string, string>();
const storage = {
getItem: (key: string) => store.get(key) ?? null,
setItem: (key: string, value: string) => void store.set(key, value),
removeItem: (key: string) => void store.delete(key),
clear: () => store.clear(),
key: (index: number) => [...store.keys()][index] ?? null,
get length() {
return store.size;
},
};
(globalThis as unknown as { window: unknown }).window = { sessionStorage: storage };
return storage;
}
let storage: ReturnType<typeof installSessionStorage>;
beforeEach(() => {
storage = installSessionStorage();
});
afterAll(() => {
if (originalWindow === undefined) {
delete (globalThis as { window?: unknown }).window;
} else {
(globalThis as { window?: unknown }).window = originalWindow;
}
});
describe('quota ui state', () => {
test('round-trips both preferences', () => {
writeQuotaUiState({ tab: 'codex', sortMode: 'soonest' });
expect(readQuotaUiState()).toEqual({ tab: 'codex', sortMode: 'soonest' });
});
test('writing one preference preserves the other', () => {
writeQuotaUiState({ sortMode: 'soonest' });
writeQuotaUiState({ tab: 'kimi' });
expect(readQuotaUiState()).toEqual({ tab: 'kimi', sortMode: 'soonest' });
});
test('rejects values that are not part of the current contract', () => {
storage.setItem(KEY, JSON.stringify({ tab: 'not-a-tab', sortMode: 'by-vibes' }));
expect(readQuotaUiState()).toEqual({ tab: undefined, sortMode: undefined });
});
test('survives absent, malformed, and non-object payloads', () => {
expect(readQuotaUiState()).toBeNull();
storage.setItem(KEY, '{not json');
expect(readQuotaUiState()).toBeNull();
storage.setItem(KEY, '"a string"');
expect(readQuotaUiState()).toBeNull();
});
});

View file

@ -0,0 +1,126 @@
/**
* Shared minute clock: snapshot stability and timer lifecycle.
*
* The stability case is the important one a `getSnapshot` that returns a
* fresh `Date.now()` makes React's useSyncExternalStore re-render forever.
*/
import { describe, expect, test } from 'vitest';
import { createSharedClock } from '@/utils/time/sharedClock';
/** Deterministic stand-in for setInterval: fires only when the test says so. */
function makeFakeTimers() {
const timers = new Map<number, () => void>();
let nextId = 1;
let created = 0;
let cleared = 0;
return {
created: () => created,
cleared: () => cleared,
active: () => timers.size,
fireAll: () => timers.forEach((fn) => fn()),
setTimer: (fn: () => void) => {
created += 1;
const id = nextId++;
timers.set(id, fn);
return id;
},
clearTimer: (id: unknown) => {
cleared += 1;
timers.delete(id as number);
},
};
}
function makeClock(startAt = 1_000_000) {
const timers = makeFakeTimers();
let current = startAt;
const clock = createSharedClock({
intervalMs: 60_000,
now: () => current,
setTimer: timers.setTimer,
clearTimer: timers.clearTimer,
});
return { clock, timers, advance: (ms: number) => (current += ms) };
}
describe('createSharedClock', () => {
test('getSnapshot is referentially stable until a tick fires', () => {
const { clock, timers, advance } = makeClock();
clock.subscribe(() => {});
const first = clock.getSnapshot();
advance(30_000);
// Wall time moved but no tick fired — the snapshot must not.
expect(clock.getSnapshot()).toBe(first);
expect(clock.getSnapshot()).toBe(first);
timers.fireAll();
expect(clock.getSnapshot()).toBe(first + 30_000);
});
test('notifies every subscriber on a tick', () => {
const { clock, timers, advance } = makeClock();
let a = 0;
let b = 0;
clock.subscribe(() => (a += 1));
clock.subscribe(() => (b += 1));
advance(60_000);
timers.fireAll();
expect(a).toBe(1);
expect(b).toBe(1);
});
test('three subscribers share exactly one timer', () => {
const { clock, timers } = makeClock();
clock.subscribe(() => {});
clock.subscribe(() => {});
clock.subscribe(() => {});
expect(clock.subscriberCount()).toBe(3);
expect(timers.created()).toBe(1);
expect(timers.active()).toBe(1);
});
test('clears the timer when the last subscriber leaves, and restarts after', () => {
const { clock, timers } = makeClock();
const off1 = clock.subscribe(() => {});
const off2 = clock.subscribe(() => {});
off1();
expect(timers.active()).toBe(1); // still one listener
expect(timers.cleared()).toBe(0);
off2();
expect(clock.subscriberCount()).toBe(0);
expect(timers.active()).toBe(0);
expect(timers.cleared()).toBe(1);
clock.subscribe(() => {});
expect(timers.created()).toBe(2);
expect(timers.active()).toBe(1);
});
test('resynchronizes on the first subscribe so an idle clock is not stale', () => {
const { clock, timers, advance } = makeClock();
advance(3_600_000); // an hour passes with nobody watching
clock.subscribe(() => {});
expect(clock.getSnapshot()).toBe(1_000_000 + 3_600_000);
expect(timers.created()).toBe(1);
});
test('unsubscribing twice does not clear a fresh timer', () => {
const { clock, timers } = makeClock();
const off = clock.subscribe(() => {});
off();
off();
clock.subscribe(() => {});
expect(timers.cleared()).toBe(1);
expect(timers.active()).toBe(1);
});
});

View file

@ -0,0 +1,55 @@
import { describe, expect, test } from 'vitest';
import { getSponsorAggregationConflict } from '../src/features/providers/sponsorDefinitions';
import type { SponsorProviderRaw } from '../src/features/providers/types';
const emptyRaw = (): SponsorProviderRaw => ({
openai: [],
claude: [],
codex: [],
gemini: [],
});
describe('sponsor aggregation safety', () => {
test('detects multiple configs for one protocol', () => {
const raw = emptyRaw();
raw.codex = [
{ index: 0, config: { apiKey: 'first' } },
{ index: 1, config: { apiKey: 'second' } },
];
expect(getSponsorAggregationConflict(raw)).toBe('multiple-configs');
});
test('detects multiple OpenAI API keys in one config', () => {
const raw = emptyRaw();
raw.openai = [
{
index: 0,
config: {
name: 'Sponsor',
baseUrl: 'https://example.com/v1',
apiKeyEntries: [{ apiKey: 'first' }, { apiKey: 'second' }],
},
},
];
expect(getSponsorAggregationConflict(raw)).toBe('multiple-openai-keys');
});
test('allows the supported one-config-per-protocol shape', () => {
const raw = emptyRaw();
raw.codex = [{ index: 0, config: { apiKey: 'codex' } }];
raw.openai = [
{
index: 0,
config: {
name: 'Sponsor',
baseUrl: 'https://example.com/v1',
apiKeyEntries: [{ apiKey: 'openai' }],
},
},
];
expect(getSponsorAggregationConflict(raw)).toBeNull();
});
});

View file

@ -0,0 +1,115 @@
import { describe, expect, test } from 'vitest';
import { openaiToResource } from '../src/features/providers/adapters';
import {
buildCode0Raw,
CODE0_OPENAI_BASE_URL,
CODE0_PROVIDER_NAME,
} from '../src/features/providers/code0';
import {
buildQiniuCloudRaw,
QINIU_CLOUD_BASE_URL_OPTIONS,
QINIU_CLOUD_PROVIDER_NAME,
} from '../src/features/providers/qiniuCloud';
import {
APIKEY_FUN_OPENAI_BASE_URL,
APIKEY_FUN_PROVIDER_NAME,
buildApiKeyFunRaw,
} from '../src/features/providers/sponsor';
import { normalizeConfigResponse } from '../src/services/api/transformers';
const openAIConfig = (name: string, baseUrl: string) => ({
openaiCompatibility: [
{
name,
baseUrl,
apiKeyEntries: [{ apiKey: 'test-key' }],
},
],
});
const customOpenAIConfig = (name: string) => openAIConfig(name, 'https://gateway.example.com/v1');
const mixedOpenAIConfig = (name: string, officialBaseUrl: string) => ({
openaiCompatibility: [
{
name,
baseUrl: officialBaseUrl,
apiKeyEntries: [{ apiKey: 'official-key' }],
},
{
name,
baseUrl: 'https://gateway.example.com/v1',
apiKeyEntries: [{ apiKey: 'custom-key' }],
},
],
});
describe('sponsor custom endpoint isolation', () => {
test('keeps APIKEY.FUN-named custom endpoints in the generic OpenAI group', () => {
expect(buildApiKeyFunRaw(customOpenAIConfig(APIKEY_FUN_PROVIDER_NAME)).openai).toEqual([]);
});
test('keeps Code0-named custom endpoints in the generic OpenAI group', () => {
expect(buildCode0Raw(customOpenAIConfig(CODE0_PROVIDER_NAME)).openai).toEqual([]);
});
test('keeps Qiniu-named custom endpoints in the generic OpenAI group', () => {
expect(buildQiniuCloudRaw(customOpenAIConfig(QINIU_CLOUD_PROVIDER_NAME)).openai).toEqual([]);
});
test('keeps same-name custom entries outside sponsor delete targets', () => {
expect(
buildApiKeyFunRaw(
mixedOpenAIConfig(APIKEY_FUN_PROVIDER_NAME, APIKEY_FUN_OPENAI_BASE_URL)
).openai.map((item) => item.index)
).toEqual([0]);
expect(
buildCode0Raw(mixedOpenAIConfig(CODE0_PROVIDER_NAME, CODE0_OPENAI_BASE_URL)).openai.map(
(item) => item.index
)
).toEqual([0]);
expect(
buildQiniuCloudRaw(
mixedOpenAIConfig(QINIU_CLOUD_PROVIDER_NAME, QINIU_CLOUD_BASE_URL_OPTIONS[0].openaiBaseUrl)
).openai.map((item) => item.index)
).toEqual([0]);
});
test('keeps backend indexes when normalization filters an unnamed item', () => {
const config = normalizeConfigResponse({
'openai-compatibility': [
{ 'base-url': 'https://invalid.example.com/v1' },
{
name: APIKEY_FUN_PROVIDER_NAME,
'base-url': APIKEY_FUN_OPENAI_BASE_URL,
'api-key-entries': [{ 'api-key': 'official-a' }],
},
{
name: APIKEY_FUN_PROVIDER_NAME,
'base-url': 'https://gateway.example.com/v1',
'api-key-entries': [{ 'api-key': 'custom-key' }],
},
{
name: APIKEY_FUN_PROVIDER_NAME,
'base-url': APIKEY_FUN_OPENAI_BASE_URL,
'api-key-entries': [{ 'api-key': 'official-b' }],
},
],
});
expect(config.openaiCompatibility?.map((item) => item.sourceIndex)).toEqual([1, 2, 3]);
expect(buildApiKeyFunRaw(config).openai.map((item) => item.index)).toEqual([1, 3]);
expect(openaiToResource(config.openaiCompatibility![1], 1).originalIndex).toBe(2);
});
test('still aggregates each sponsor official OpenAI endpoint', () => {
expect(
buildApiKeyFunRaw(openAIConfig('custom-name', APIKEY_FUN_OPENAI_BASE_URL)).openai.length
).toBe(1);
expect(buildCode0Raw(openAIConfig('custom-name', CODE0_OPENAI_BASE_URL)).openai.length).toBe(1);
expect(
buildQiniuCloudRaw(openAIConfig('custom-name', QINIU_CLOUD_BASE_URL_OPTIONS[0].openaiBaseUrl))
.openai.length
).toBe(1);
});
});

View file

@ -0,0 +1,40 @@
import { describe, expect, test, vi } from 'vitest';
import {
isSponsorPartialMutationError,
runSponsorMutationWithRecovery,
} from '../src/features/providers/sponsorMutationRecovery';
describe('sponsor mutation recovery', () => {
test('refreshes after a failed multi-endpoint mutation and preserves the original failure', async () => {
const originalError = new Error('Claude update failed');
const refresh = vi.fn(async () => {});
let caught: unknown;
try {
await runSponsorMutationWithRecovery(async () => {
throw originalError;
}, refresh);
} catch (error) {
caught = error;
}
expect(refresh).toHaveBeenCalledTimes(1);
expect(isSponsorPartialMutationError(caught)).toBe(true);
expect((caught as Error & { cause?: unknown }).cause).toBe(originalError);
});
test('does not let a refresh failure replace the original mutation failure', async () => {
const originalError = new Error('OpenAI update failed');
await expect(
runSponsorMutationWithRecovery(
async () => {
throw originalError;
},
async () => {
throw new Error('refresh failed');
}
)
).rejects.toMatchObject({ cause: originalError });
});
});

View file

@ -0,0 +1,38 @@
import { describe, expect, test } from 'vitest';
import {
buildThinkingFromLevels,
readThinkingLevels,
THINKING_LEVELS,
} from '../src/features/providers/thinkingLevels';
describe('standard thinking level selector', () => {
test('only exposes levels recognized by the backend', () => {
expect(THINKING_LEVELS).toEqual([
'none',
'minimal',
'low',
'medium',
'high',
'xhigh',
'max',
'auto',
]);
});
test('reads standard levels and legacy capability flags', () => {
expect(
readThinkingLevels({
levels: ['LOW', 'custom', 'high', 'none'],
zero_allowed: true,
dynamic_allowed: true,
})
).toEqual(['none', 'low', 'high', 'auto']);
});
test('writes canonical backend levels and omits an empty selection', () => {
expect(buildThinkingFromLevels([])).toBeUndefined();
expect(buildThinkingFromLevels(['auto', 'high', 'none', 'low'])).toEqual({
levels: ['low', 'high', 'none', 'auto'],
});
});
});

View file

@ -0,0 +1,71 @@
/**
* Timezone labelling, and the guard against the Asia/Shanghai hardcode
* returning.
*
* Codex reset-credit expiry was rendered in a fixed GMT+8 while every other
* timestamp on the same page used the browser's timezone so one credit
* appeared twice, in two timezones, on one screen.
*/
import { describe, expect, test } from 'vitest';
import i18n from '@/i18n';
import * as resetCredits from '@/utils/quota/resetCredits';
import { formatUtcOffsetLabel, resolveTimeZoneLabel } from '@/utils/time/timezone';
describe('formatUtcOffsetLabel', () => {
test('renders whole-hour offsets east and west of UTC', () => {
expect(formatUtcOffsetLabel(480)).toBe('GMT+8');
expect(formatUtcOffsetLabel(-300)).toBe('GMT-5');
expect(formatUtcOffsetLabel(720)).toBe('GMT+12');
expect(formatUtcOffsetLabel(-720)).toBe('GMT-12');
});
test('renders half- and quarter-hour offsets', () => {
expect(formatUtcOffsetLabel(330)).toBe('GMT+5:30');
expect(formatUtcOffsetLabel(-570)).toBe('GMT-9:30');
expect(formatUtcOffsetLabel(345)).toBe('GMT+5:45');
});
test('UTC itself carries no sign', () => {
expect(formatUtcOffsetLabel(0)).toBe('GMT');
expect(formatUtcOffsetLabel(-0)).toBe('GMT');
});
test('a non-finite offset degrades to a bare GMT rather than "GMT+NaN"', () => {
expect(formatUtcOffsetLabel(Number.NaN)).toBe('GMT');
});
});
describe('resolveTimeZoneLabel', () => {
test('produces a well-formed label whatever timezone the runner is in', () => {
expect(resolveTimeZoneLabel()).toMatch(/^GMT([+-]\d{1,2}(:\d{2})?)?$/);
});
test('reflects the offset of the instant it is given, not of "now"', () => {
// A DST-observing zone answers differently in January than in July, so the
// label has to follow the date the timestamps are being rendered for.
const january = new Date(2026, 0, 15, 12);
const july = new Date(2026, 6, 15, 12);
expect(resolveTimeZoneLabel(january)).toBe(
formatUtcOffsetLabel(-january.getTimezoneOffset())
);
expect(resolveTimeZoneLabel(july)).toBe(formatUtcOffsetLabel(-july.getTimezoneOffset()));
});
});
describe('Asia/Shanghai hardcode', () => {
test('is gone from the reset-credit module', () => {
expect('formatShanghaiDateTime' in resetCredits).toBe(false);
expect(resetCredits.normalizeCodexResetCreditsPayload).toBeDefined();
});
test('the expiry heading interpolates a timezone in all four locales', async () => {
for (const locale of ['en', 'zh-CN', 'zh-TW', 'ru']) {
await i18n.changeLanguage(locale);
const label = i18n.t('codex_quota.reset_credits_expiry_label', { timezone: 'GMT+8' });
expect(label).toContain('GMT+8');
expect(label).not.toContain('{{');
}
await i18n.changeLanguage('en');
});
});

View file

@ -0,0 +1,42 @@
import { describe, expect, test } from 'vitest';
import { createElement, useState } from 'react';
import { renderToStaticMarkup } from 'react-dom/server';
import { parse as parseYaml } from 'yaml';
import { useVisualConfig } from '../src/hooks/useVisualConfig';
describe('visual config concurrency', () => {
test('only applies dirty visual fields to the latest server YAML', () => {
function Harness() {
const visualConfig = useVisualConfig();
const [phase, setPhase] = useState(0);
if (phase === 0) {
visualConfig.loadVisualValuesFromYaml(
'debug: false\nproxy-url: http://old-proxy.example\n'
);
setPhase(1);
} else if (phase === 1) {
visualConfig.setVisualValues({ proxyUrl: 'http://localhost:8080' });
setPhase(2);
} else {
return createElement(
'pre',
null,
visualConfig.applyVisualChangesToYaml(
'debug: true\nproxy-url: http://old-proxy.example\n'
)
);
}
return null;
}
const markup = renderToStaticMarkup(createElement(Harness));
const merged = markup.slice('<pre>'.length, -'</pre>'.length);
expect(parseYaml(merged)).toEqual({
debug: true,
'proxy-url': 'http://localhost:8080',
});
});
});

View file

@ -0,0 +1,37 @@
import { describe, expect, test } from 'vitest';
import { createElement, useState } from 'react';
import { renderToStaticMarkup } from 'react-dom/server';
import { parse as parseYaml } from 'yaml';
import {
parseDisableImageGenerationMode,
useVisualConfig,
} from '../src/hooks/useVisualConfig';
describe('visual config disable-image-generation', () => {
test('loads and writes the passthrough mode', () => {
expect(parseDisableImageGenerationMode('passthrough')).toBe('passthrough');
function Harness() {
const visualConfig = useVisualConfig();
const [phase, setPhase] = useState(0);
if (phase === 0) {
visualConfig.setVisualValues({ disableImageGeneration: 'passthrough' });
setPhase(1);
} else {
return createElement(
'pre',
null,
visualConfig.applyVisualChangesToYaml('disable-image-generation: false\n')
);
}
return null;
}
const markup = renderToStaticMarkup(createElement(Harness));
const result = markup.slice('<pre>'.length, -'</pre>'.length);
expect(parseYaml(result)).toEqual({ 'disable-image-generation': 'passthrough' });
});
});

View file

@ -0,0 +1,42 @@
import { describe, expect, test } from 'vitest';
import { createElement, useState } from 'react';
import { renderToStaticMarkup } from 'react-dom/server';
import { parse as parseYaml } from 'yaml';
import { parseRoutingStrategy, useVisualConfig } from '../src/hooks/useVisualConfig';
describe('visual config weighted routing strategy', () => {
test('recognizes the weighted-round-robin backend value', () => {
expect(parseRoutingStrategy('weighted-round-robin')).toBe('weighted-round-robin');
expect(parseRoutingStrategy('weightedroundrobin')).toBe('weighted-round-robin');
expect(parseRoutingStrategy('wrr')).toBe('weighted-round-robin');
expect(parseRoutingStrategy('fill-first')).toBe('fill-first');
expect(parseRoutingStrategy('fillfirst')).toBe('fill-first');
expect(parseRoutingStrategy('ff')).toBe('fill-first');
expect(parseRoutingStrategy(undefined)).toBe('round-robin');
});
test('writes weighted-round-robin without coercing it to round-robin', () => {
function Harness() {
const visualConfig = useVisualConfig();
const [phase, setPhase] = useState(0);
if (phase === 0) {
visualConfig.setVisualValues({ routingStrategy: 'weighted-round-robin' });
setPhase(1);
} else {
return createElement(
'pre',
null,
visualConfig.applyVisualChangesToYaml('routing:\n strategy: round-robin\n')
);
}
return null;
}
const markup = renderToStaticMarkup(createElement(Harness));
const result = markup.slice('<pre>'.length, -'</pre>'.length);
expect(parseYaml(result)).toEqual({ routing: { strategy: 'weighted-round-robin' } });
});
});

View file

@ -0,0 +1,25 @@
import { describe, expect, test } from 'vitest';
import { getVisualConfigValidationErrors } from '../src/hooks/useVisualConfig';
import { DEFAULT_VISUAL_VALUES } from '../src/types/visualConfig';
describe('visual config validation', () => {
test('requires Redis usage retention to be empty or within 1..3600', () => {
const values = structuredClone(DEFAULT_VISUAL_VALUES);
values.redisUsageQueueRetentionSeconds = '';
expect(getVisualConfigValidationErrors(values).redisUsageQueueRetentionSeconds).toBeUndefined();
values.redisUsageQueueRetentionSeconds = '0';
expect(getVisualConfigValidationErrors(values).redisUsageQueueRetentionSeconds).toBe(
'integer_range_1_3600'
);
values.redisUsageQueueRetentionSeconds = '3601';
expect(getVisualConfigValidationErrors(values).redisUsageQueueRetentionSeconds).toBe(
'integer_range_1_3600'
);
values.redisUsageQueueRetentionSeconds = '3600';
expect(getVisualConfigValidationErrors(values).redisUsageQueueRetentionSeconds).toBeUndefined();
});
});

View file

@ -0,0 +1,149 @@
import { afterEach, describe, expect, test } from 'vitest';
import { xaiToResource } from '../src/features/providers/adapters';
import { PROVIDER_DESCRIPTORS } from '../src/features/providers/descriptors';
import { apiClient } from '../src/services/api/client';
import { providersApi } from '../src/services/api/providers';
import { normalizeConfigResponse } from '../src/services/api/transformers';
const originalGet = apiClient.get;
const originalPut = apiClient.put;
const originalDelete = apiClient.delete;
afterEach(() => {
apiClient.get = originalGet;
apiClient.put = originalPut;
apiClient.delete = originalDelete;
});
describe('xAI API key provider', () => {
test('normalizes the backend xai-api-key contract and exposes a workbench resource', () => {
const config = normalizeConfigResponse({
'xai-api-key': [
{
'api-key': 'xai-secret',
priority: 7,
prefix: 'team-xai',
'base-url': 'https://api.x.ai/v1',
websockets: true,
'proxy-url': 'http://proxy.local',
headers: { 'X-Custom': 'value' },
models: [{ name: 'grok-4.5', alias: 'grok-latest' }],
'excluded-models': ['grok-3-*'],
'disable-cooling': true,
'auth-index': 'xai:apikey:1',
},
],
});
expect(config.xaiApiKeys).toEqual([
{
apiKey: 'xai-secret',
priority: 7,
prefix: 'team-xai',
baseUrl: 'https://api.x.ai/v1',
websockets: true,
proxyUrl: 'http://proxy.local',
headers: { 'X-Custom': 'value' },
models: [{ name: 'grok-4.5', alias: 'grok-latest' }],
excludedModels: ['grok-3-*'],
disableCooling: true,
authIndex: 'xai:apikey:1',
},
]);
const resource = xaiToResource(config.xaiApiKeys![0], 0);
expect(resource.brand).toBe('xai');
expect(resource.baseUrl).toBe('https://api.x.ai/v1');
expect(resource.models).toEqual(['grok-4.5']);
expect(resource.flags.websockets).toBe(true);
expect(resource.selector).toEqual({
brand: 'xai',
apiKey: 'xai-secret',
baseUrl: 'https://api.x.ai/v1',
index: 0,
});
expect(PROVIDER_DESCRIPTORS.xai.baseUrlRequired).toBe(true);
expect(PROVIDER_DESCRIPTORS.xai.supportsWebsockets).toBe(true);
});
test('creates and deletes xAI keys through the backend management contract', async () => {
const calls: Array<{ method: string; url: string; data?: unknown }> = [];
apiClient.get = (async (url: string) => {
calls.push({ method: 'GET', url });
return {
'xai-api-key': [
{
'api-key': 'existing',
'base-url': 'https://api.x.ai/v1',
'future-field': 'preserved',
},
],
};
}) as typeof apiClient.get;
apiClient.put = (async (url: string, data?: unknown) => {
calls.push({ method: 'PUT', url, data });
return undefined;
}) as typeof apiClient.put;
apiClient.delete = (async (url: string) => {
calls.push({ method: 'DELETE', url });
return undefined;
}) as typeof apiClient.delete;
await providersApi.createXAIConfig({
apiKey: 'xai-new',
priority: 3,
prefix: 'xai',
baseUrl: 'https://api.x.ai/v1',
websockets: true,
proxyUrl: 'direct',
headers: { 'X-Custom': 'value' },
models: [
{
name: 'grok-4.5',
alias: 'grok-latest',
thinking: { levels: ['low', 'high', 'xhigh'] },
},
],
excludedModels: ['grok-3-*'],
disableCooling: true,
});
await providersApi.deleteXAIConfig('xai-new', 'https://api.x.ai/v1');
expect(calls).toEqual([
{ method: 'GET', url: '/config' },
{
method: 'PUT',
url: '/xai-api-key',
data: [
{
'api-key': 'existing',
'base-url': 'https://api.x.ai/v1',
'future-field': 'preserved',
},
{
'api-key': 'xai-new',
priority: 3,
prefix: 'xai',
'base-url': 'https://api.x.ai/v1',
websockets: true,
'proxy-url': 'direct',
headers: { 'X-Custom': 'value' },
models: [
{
name: 'grok-4.5',
alias: 'grok-latest',
thinking: { levels: ['low', 'high', 'xhigh'] },
},
],
'excluded-models': ['grok-3-*'],
'disable-cooling': true,
},
],
},
{
method: 'DELETE',
url: '/xai-api-key?api-key=xai-new&base-url=https%3A%2F%2Fapi.x.ai%2Fv1',
},
]);
});
});

View file

@ -0,0 +1,176 @@
import { afterEach, beforeEach, describe, expect, test } from 'vitest';
import type { TFunction } from 'i18next';
import { XAI_CONFIG } from '@/features/quota/providers/xai/data';
import { apiCallApi, type ApiCallRequest, type ApiCallResult } from '@/services/api';
import {
XAI_API_CHAT_URL,
XAI_API_ME_URL,
XAI_BILLING_MONTHLY_URL,
XAI_BILLING_WEEKLY_URL,
isPaidXaiAuthFile,
} from '@/utils/quota';
const t = ((key: string) => key) as unknown as TFunction;
const originalApiCallRequest = apiCallApi.request;
const result = (statusCode: number, body: unknown = null): ApiCallResult => ({
statusCode,
header: {},
bodyText: body === null ? '' : JSON.stringify(body),
body,
});
const encodeJwt = (payload: Record<string, unknown>): string => {
const encoded = btoa(JSON.stringify(payload))
.replace(/=/g, '')
.replace(/\+/g, '-')
.replace(/\//g, '_');
return `header.${encoded}.signature`;
};
describe('xAI paid OAuth quota fallback', () => {
let requests: ApiCallRequest[];
beforeEach(() => {
requests = [];
});
afterEach(() => {
apiCallApi.request = originalApiCallRequest;
});
test('recognizes paid credentials without trusting individual route hints', () => {
expect(isPaidXaiAuthFile({ name: 'paid-route.json', using_api: true, prefix: 'PAID' })).toBe(
true
);
expect(
isPaidXaiAuthFile({
name: 'tier.json',
metadata: { access_token: encodeJwt({ tier: 1 }) },
})
).toBe(true);
expect(isPaidXaiAuthFile({ name: 'using-api-only.json', using_api: true })).toBe(false);
expect(isPaidXaiAuthFile({ name: 'prefix-only.json', prefix: 'paid' })).toBe(false);
expect(isPaidXaiAuthFile({ name: 'free.json', using_api: false })).toBe(false);
expect(isPaidXaiAuthFile({ name: 'free-default.json', base_url: 'https://api.x.ai/v1' })).toBe(
false
);
});
test('skips free billing endpoints for a recognized paid list entry', async () => {
apiCallApi.request = async (payload) => {
requests.push(payload);
if (payload.url === XAI_API_ME_URL) {
return result(200, { user_id: 'user-1', team_id: 'team-1' });
}
return result(200, { choices: [] });
};
const summary = await XAI_CONFIG.fetchQuota(
{
name: 'paid.json',
type: 'xai',
auth_index: 'xai:1',
using_api: true,
prefix: 'paid',
},
t
);
expect(requests.map((request) => request.url)).toEqual([XAI_API_ME_URL, XAI_API_CHAT_URL]);
expect(JSON.parse(requests[1]?.data ?? '{}')).toMatchObject({
model: 'grok-4.5',
max_tokens: 1,
stream: false,
});
expect(summary).toMatchObject({
mode: 'paid-health',
source: 'api.x.ai-fallback',
planType: 'paid',
healthStatus: 'chat-ok',
userId: 'user-1',
teamId: 'team-1',
});
});
test('keeps existing billing behavior when only one route hint is present', async () => {
apiCallApi.request = async (payload) => {
requests.push(payload);
if (payload.url === XAI_BILLING_WEEKLY_URL) {
return result(200, {
config: {
currentPeriod: { type: 'weekly' },
creditUsagePercent: 25,
},
});
}
if (payload.url === XAI_BILLING_MONTHLY_URL) {
return result(200, {
config: {
monthlyLimit: { val: 10000 },
used: { val: 2500 },
billingPeriodStart: '2026-07-01T00:00:00Z',
billingPeriodEnd: '2026-08-01T00:00:00Z',
},
});
}
throw new Error(`Unexpected URL: ${payload.url}`);
};
const summary = await XAI_CONFIG.fetchQuota(
{ name: 'free.json', type: 'xai', auth_index: 'xai:3', using_api: true },
t
);
expect(requests.map((request) => request.url).sort()).toEqual(
[XAI_BILLING_WEEKLY_URL, XAI_BILLING_MONTHLY_URL].sort()
);
expect(summary).toMatchObject({
mode: 'billing',
source: 'cli-chat-proxy',
periodType: 'weekly',
usagePercent: 25,
monthlyLimitCents: 10000,
billingPeriodEnd: '2026-08-01T00:00:00Z',
resetAtMs: null,
periodHours: null,
});
expect(summary.periodStart).toBeUndefined();
expect(summary.periodEnd).toBeUndefined();
});
test('falls back to paid health after both free billing probes fail', async () => {
apiCallApi.request = async (payload) => {
requests.push(payload);
if (payload.url === XAI_BILLING_WEEKLY_URL || payload.url === XAI_BILLING_MONTHLY_URL) {
return result(403, { error: 'Access denied' });
}
if (payload.url === XAI_API_ME_URL) return result(200, { user_id: 'paid-user' });
return result(200, { choices: [] });
};
const summary = await XAI_CONFIG.fetchQuota(
{ name: 'unknown.json', type: 'xai', auth_index: 'xai:4' },
t
);
expect(requests.map((request) => request.url)).toEqual([
XAI_BILLING_WEEKLY_URL,
XAI_BILLING_MONTHLY_URL,
XAI_API_ME_URL,
XAI_API_CHAT_URL,
]);
expect(summary).toMatchObject({ mode: 'paid-health', userId: 'paid-user' });
});
test('preserves the billing error when the paid fallback also fails', async () => {
apiCallApi.request = async (payload) => {
if (payload.url === XAI_API_CHAT_URL) return result(401, { error: 'Invalid token' });
return result(403, { error: 'Access denied' });
};
await expect(
XAI_CONFIG.fetchQuota({ name: 'invalid.json', type: 'xai', auth_index: 'xai:5' }, t)
).rejects.toMatchObject({ status: 403 });
});
});

View file

@ -0,0 +1,32 @@
import { describe, expect, test } from 'vitest';
import {
applyAuthFileUsingApi,
readAuthFileUsingApi,
supportsAuthFileUsingApi,
} from '@/features/authFiles/constants';
describe('xAI auth-file using_api', () => {
test('is only exposed for xAI credentials', () => {
expect(supportsAuthFileUsingApi('xai')).toBe(true);
expect(supportsAuthFileUsingApi('grok')).toBe(true);
expect(supportsAuthFileUsingApi('codex')).toBe(false);
});
test('reads boolean-compatible values and defaults to chat-proxy mode', () => {
expect(readAuthFileUsingApi({ using_api: true })).toBe(true);
expect(readAuthFileUsingApi({ using_api: 'true' })).toBe(true);
expect(readAuthFileUsingApi({ using_api: false })).toBe(false);
expect(readAuthFileUsingApi({})).toBe(false);
});
test('writes an explicit using_api boolean', () => {
expect(applyAuthFileUsingApi({ type: 'xai' }, true)).toEqual({
type: 'xai',
using_api: true,
});
expect(applyAuthFileUsingApi({ type: 'xai', using_api: true }, false)).toEqual({
type: 'xai',
using_api: false,
});
});
});