feat(tests): add comprehensive tests for TokenRouter and related components

- Implement tests for TokenRouter downstream policy, covering allowedRouteIds and site weight multipliers.
- Add tests for TokenRouter patterns and model mapping, including regex pattern matching and model mapping precedence.
- Create tests for BrandIcon component to verify brand detection logic.
- Develop tests for Accounts page, focusing on rebind panel behavior and refresh action on error.
- Introduce fuzzy search utility tests to validate substring and subsequence matching.
- Implement navigation focus helpers tests for building and parsing focus paths.
- Add progressive render tests to ensure correct initial and next visible counts.
- Create route model candidates index tests to validate route candidate indexing and matching logic.
This commit is contained in:
cita
2026-03-02 13:33:35 +08:00
parent 99c9e145c3
commit f6389a999a
98 changed files with 11286 additions and 801 deletions
+22
View File
@@ -0,0 +1,22 @@
import { describe, expect, it } from 'vitest';
import { fuzzyMatch } from './fuzzySearch.js';
describe('fuzzyMatch', () => {
it('matches plain substring case-insensitively', () => {
expect(fuzzyMatch('claude-opus-4-6', 'OPUS')).toBe(true);
expect(fuzzyMatch('gpt-4o-mini', 'gpt')).toBe(true);
});
it('matches after removing separators', () => {
expect(fuzzyMatch('gpt-5.2-codex', 'gpt52')).toBe(true);
expect(fuzzyMatch('re:^claude-(opus|sonnet)-4-5$', 'claude45')).toBe(true);
});
it('supports subsequence fuzzy matching', () => {
expect(fuzzyMatch('cerebras-llama-3.1-8b', 'cll318')).toBe(true);
});
it('returns false when query cannot be matched', () => {
expect(fuzzyMatch('claude-opus-4-6', 'gemini')).toBe(false);
});
});
+32
View File
@@ -0,0 +1,32 @@
function normalize(value: string): string {
return (value || '').toLowerCase().trim();
}
function compact(value: string): string {
return normalize(value).replace(/[^a-z0-9]/g, '');
}
function isSubsequence(text: string, query: string): boolean {
if (!query) return true;
let qi = 0;
for (let i = 0; i < text.length; i += 1) {
if (text[i] === query[qi]) qi += 1;
if (qi >= query.length) return true;
}
return false;
}
export function fuzzyMatch(text: string, query: string): boolean {
const q = normalize(query);
if (!q) return true;
const t = normalize(text);
if (t.includes(q)) return true;
const compactText = compact(t);
const compactQuery = compact(q);
if (!compactQuery) return true;
if (compactText.includes(compactQuery)) return true;
return isSubsequence(compactText, compactQuery);
}
+6 -1
View File
@@ -125,7 +125,12 @@ const toNullableFiniteNumber = (value: unknown): number | null => {
return null;
};
const isExactModelPattern = (modelPattern: string): boolean => !/[\*\?\[]/.test(modelPattern);
const isExactModelPattern = (modelPattern: string): boolean => {
const normalized = modelPattern.trim();
if (!normalized) return false;
if (normalized.toLowerCase().startsWith('re:')) return false;
return !/[\*\?\[]/.test(normalized);
};
export const collectModelTesterModelNames = (
marketplace: { models?: Array<{ name?: unknown }>; } | null | undefined,
@@ -0,0 +1,73 @@
import { describe, expect, it } from 'vitest';
import {
buildAccountFocusPath,
buildEventNavigationPath,
buildSiteFocusPath,
clearFocusParams,
readFocusAccountIntent,
readFocusSiteId,
} from './navigationFocus.js';
describe('navigationFocus helpers', () => {
it('builds focus paths for site and account', () => {
expect(buildSiteFocusPath(12)).toBe('/sites?focusSiteId=12');
expect(buildSiteFocusPath(0)).toBe('/sites');
expect(buildAccountFocusPath(34)).toBe('/accounts?focusAccountId=34');
expect(buildAccountFocusPath(34, { openRebind: true })).toBe('/accounts?focusAccountId=34&openRebind=1');
expect(buildAccountFocusPath(-1)).toBe('/accounts');
});
it('parses focus params from query string', () => {
expect(readFocusSiteId('?focusSiteId=15')).toBe(15);
expect(readFocusSiteId('?focusSiteId=abc')).toBeNull();
expect(readFocusAccountIntent('?focusAccountId=22&openRebind=1')).toEqual({
accountId: 22,
openRebind: true,
});
expect(readFocusAccountIntent('?focusAccountId=22&openRebind=true')).toEqual({
accountId: 22,
openRebind: true,
});
expect(readFocusAccountIntent('?focusAccountId=22')).toEqual({
accountId: 22,
openRebind: false,
});
});
it('clears focus params but keeps other params', () => {
expect(clearFocusParams('?focusSiteId=12&q=abc')).toBe('?q=abc');
expect(clearFocusParams('?focusAccountId=2&openRebind=1&type=token')).toBe('?type=token');
expect(clearFocusParams('?focusAccountId=2')).toBe('');
});
it('builds event navigation path by related entity', () => {
expect(buildEventNavigationPath({
relatedType: 'account',
relatedId: 18,
type: 'token',
})).toBe('/accounts?focusAccountId=18&openRebind=1');
expect(buildEventNavigationPath({
relatedType: 'account',
relatedId: 18,
type: 'checkin',
})).toBe('/accounts?focusAccountId=18');
expect(buildEventNavigationPath({
relatedType: 'site',
relatedId: 9,
type: 'status',
})).toBe('/sites?focusSiteId=9');
expect(buildEventNavigationPath({
relatedType: 'route',
relatedId: null,
type: 'proxy',
})).toBe('/routes');
expect(buildEventNavigationPath({ type: 'proxy' })).toBe('/logs');
expect(buildEventNavigationPath({ type: 'checkin' })).toBe('/checkin');
expect(buildEventNavigationPath({})).toBe('/events');
});
});
+83
View File
@@ -0,0 +1,83 @@
const FOCUS_SITE_ID_KEY = 'focusSiteId';
const FOCUS_ACCOUNT_ID_KEY = 'focusAccountId';
const OPEN_REBIND_KEY = 'openRebind';
function normalizePositiveId(input: unknown): number | null {
const value = Number.parseInt(String(input ?? ''), 10);
if (!Number.isFinite(value) || value <= 0) return null;
return value;
}
function isTruthyFlag(value: string | null): boolean {
if (!value) return false;
const normalized = value.trim().toLowerCase();
return normalized === '1' || normalized === 'true' || normalized === 'yes';
}
export function buildSiteFocusPath(siteId: number): string {
const normalizedId = normalizePositiveId(siteId);
if (!normalizedId) return '/sites';
return `/sites?${FOCUS_SITE_ID_KEY}=${normalizedId}`;
}
export function buildAccountFocusPath(
accountId: number,
options?: { openRebind?: boolean },
): string {
const normalizedId = normalizePositiveId(accountId);
if (!normalizedId) return '/accounts';
const params = new URLSearchParams();
params.set(FOCUS_ACCOUNT_ID_KEY, String(normalizedId));
if (options?.openRebind) params.set(OPEN_REBIND_KEY, '1');
return `/accounts?${params.toString()}`;
}
export function readFocusSiteId(search: string): number | null {
const params = new URLSearchParams(search);
return normalizePositiveId(params.get(FOCUS_SITE_ID_KEY));
}
export function readFocusAccountIntent(search: string): { accountId: number | null; openRebind: boolean } {
const params = new URLSearchParams(search);
return {
accountId: normalizePositiveId(params.get(FOCUS_ACCOUNT_ID_KEY)),
openRebind: isTruthyFlag(params.get(OPEN_REBIND_KEY)),
};
}
export function clearFocusParams(search: string): string {
const params = new URLSearchParams(search);
params.delete(FOCUS_SITE_ID_KEY);
params.delete(FOCUS_ACCOUNT_ID_KEY);
params.delete(OPEN_REBIND_KEY);
const next = params.toString();
return next ? `?${next}` : '';
}
export function buildEventNavigationPath(event: {
relatedType?: string | null;
relatedId?: number | null;
type?: string | null;
}): string {
const relatedType = (event.relatedType || '').toLowerCase();
const relatedId = normalizePositiveId(event.relatedId);
const eventType = (event.type || '').toLowerCase();
if (relatedType === 'account' && relatedId) {
return buildAccountFocusPath(relatedId, { openRebind: eventType === 'token' });
}
if (relatedType === 'site' && relatedId) {
return buildSiteFocusPath(relatedId);
}
if (relatedType === 'route') {
return '/routes';
}
if (eventType === 'proxy') {
return '/logs';
}
if (eventType === 'checkin') {
return '/checkin';
}
return '/events';
}
@@ -0,0 +1,17 @@
import { describe, expect, it } from 'vitest';
import { getInitialVisibleCount, getNextVisibleCount } from './progressiveRender.js';
describe('progressiveRender', () => {
it('returns bounded initial visible count', () => {
expect(getInitialVisibleCount(0, 40)).toBe(0);
expect(getInitialVisibleCount(10, 40)).toBe(10);
expect(getInitialVisibleCount(120, 40)).toBe(40);
});
it('returns next visible count without exceeding total', () => {
expect(getNextVisibleCount(0, 120, 40)).toBe(40);
expect(getNextVisibleCount(40, 120, 40)).toBe(80);
expect(getNextVisibleCount(80, 120, 40)).toBe(120);
expect(getNextVisibleCount(200, 120, 40)).toBe(120);
});
});
@@ -0,0 +1,17 @@
export function getInitialVisibleCount(total: number, chunkSize: number): number {
if (!Number.isFinite(total) || total <= 0) return 0;
const normalizedChunk = Number.isFinite(chunkSize) && chunkSize > 0 ? Math.trunc(chunkSize) : 1;
return Math.min(Math.trunc(total), normalizedChunk);
}
export function getNextVisibleCount(currentVisible: number, total: number, chunkSize: number): number {
const normalizedTotal = Number.isFinite(total) && total > 0 ? Math.trunc(total) : 0;
if (normalizedTotal <= 0) return 0;
const normalizedCurrent = Number.isFinite(currentVisible) && currentVisible > 0
? Math.trunc(currentVisible)
: 0;
const normalizedChunk = Number.isFinite(chunkSize) && chunkSize > 0 ? Math.trunc(chunkSize) : 1;
return Math.min(normalizedTotal, normalizedCurrent + normalizedChunk);
}
@@ -0,0 +1,116 @@
import { describe, expect, it } from 'vitest';
import {
buildRouteModelCandidatesIndex,
type IndexedRouteModelCandidate,
type RouteModelCandidatesByModelName,
} from './routeModelCandidatesIndex.js';
function matchesModelPattern(model: string, pattern: string): boolean {
if (!pattern) return false;
if (pattern === '*') return true;
if (pattern.endsWith('*')) return model.startsWith(pattern.slice(0, -1));
return model === pattern;
}
describe('buildRouteModelCandidatesIndex', () => {
it('precomputes route candidates, account options and token options by account', () => {
const routes = [
{ id: 1, modelPattern: 'claude-*' },
{ id: 2, modelPattern: 'gpt-4o-mini' },
];
const modelCandidates: RouteModelCandidatesByModelName = {
'claude-opus-4-6': [
{
modelName: 'will-be-overwritten',
accountId: 11,
tokenId: 101,
tokenName: 'tk-opus',
isDefault: false,
username: 'alice',
siteId: 1,
siteName: 'site-a',
},
{
modelName: 'dup',
accountId: 11,
tokenId: 101,
tokenName: 'tk-opus',
isDefault: false,
username: 'alice',
siteId: 1,
siteName: 'site-a',
},
],
'claude-sonnet-4-6': [
{
modelName: 'will-be-overwritten',
accountId: 11,
tokenId: 102,
tokenName: 'tk-sonnet',
isDefault: true,
username: 'alice',
siteId: 1,
siteName: 'site-a',
},
],
'gpt-4o-mini': [
{
modelName: 'will-be-overwritten',
accountId: 22,
tokenId: 201,
tokenName: 'tk-gpt',
isDefault: true,
username: 'bob',
siteId: 2,
siteName: 'site-b',
},
],
};
const index = buildRouteModelCandidatesIndex(routes, modelCandidates, matchesModelPattern);
expect(index[1]).toBeTruthy();
expect(index[2]).toBeTruthy();
const route1Candidates = index[1].routeCandidates;
expect(route1Candidates).toHaveLength(2);
expect(route1Candidates.map((item) => item.modelName).sort()).toEqual([
'claude-opus-4-6',
'claude-sonnet-4-6',
]);
expect(index[1].accountOptions).toEqual([{ id: 11, label: 'alice @ site-a' }]);
const route1TokenOptions = index[1].tokenOptionsByAccountId[11];
expect(route1TokenOptions.map((item) => `${item.id}:${item.sourceModel}:${item.isDefault}`)).toEqual([
'102:claude-sonnet-4-6:true',
'101:claude-opus-4-6:false',
]);
expect(index[2].routeCandidates).toHaveLength(1);
expect(index[2].routeCandidates[0].modelName).toBe('gpt-4o-mini');
expect(index[2].accountOptions).toEqual([{ id: 22, label: 'bob @ site-b' }]);
});
it('returns empty structures when route model pattern is blank', () => {
const routes = [{ id: 1, modelPattern: ' ' }];
const modelCandidates: RouteModelCandidatesByModelName = {
'gpt-4o-mini': [{
modelName: 'ignored',
accountId: 1,
tokenId: 2,
tokenName: 'tk',
isDefault: false,
username: null,
siteId: 1,
siteName: 'site',
} satisfies IndexedRouteModelCandidate],
};
const index = buildRouteModelCandidatesIndex(routes, modelCandidates, matchesModelPattern);
expect(index[1].routeCandidates).toEqual([]);
expect(index[1].accountOptions).toEqual([]);
expect(index[1].tokenOptionsByAccountId).toEqual({});
});
});
@@ -0,0 +1,113 @@
export type IndexedRouteModelCandidate = {
modelName: string;
accountId: number;
tokenId: number;
tokenName: string;
isDefault: boolean;
username: string | null;
siteId: number;
siteName: string;
};
export type RouteModelCandidatesByModelName = Record<string, IndexedRouteModelCandidate[]>;
export type RouteAccountOption = {
id: number;
label: string;
};
export type RouteTokenOption = {
id: number;
name: string;
isDefault: boolean;
sourceModel?: string;
};
export type RouteCandidateView = {
routeCandidates: IndexedRouteModelCandidate[];
accountOptions: RouteAccountOption[];
tokenOptionsByAccountId: Record<number, RouteTokenOption[]>;
};
export type RouteModelPatternLike = {
id: number;
modelPattern: string;
};
const EMPTY_ROUTE_CANDIDATE_VIEW: RouteCandidateView = {
routeCandidates: [],
accountOptions: [],
tokenOptionsByAccountId: {},
};
export function buildRouteModelCandidatesIndex(
routes: RouteModelPatternLike[],
modelCandidates: RouteModelCandidatesByModelName,
matchesModelPattern: (model: string, pattern: string) => boolean,
): Record<number, RouteCandidateView> {
const index: Record<number, RouteCandidateView> = {};
for (const route of routes || []) {
const modelPattern = (route.modelPattern || '').trim();
if (!modelPattern) {
index[route.id] = EMPTY_ROUTE_CANDIDATE_VIEW;
continue;
}
const deduped = new Map<string, IndexedRouteModelCandidate>();
for (const [modelName, candidates] of Object.entries(modelCandidates || {})) {
if (!matchesModelPattern(modelName, modelPattern)) continue;
for (const candidate of candidates || []) {
const key = `${candidate.tokenId}::${modelName}`;
if (!deduped.has(key)) {
deduped.set(key, {
...candidate,
modelName,
});
}
}
}
const routeCandidates = Array.from(deduped.values()).sort((a, b) => {
if (a.accountId === b.accountId) return a.tokenId - b.tokenId;
return a.accountId - b.accountId;
});
const accountMap = new Map<number, string>();
const tokenOptionsByAccountId: Record<number, RouteTokenOption[]> = {};
for (const candidate of routeCandidates) {
if (!accountMap.has(candidate.accountId)) {
accountMap.set(candidate.accountId, `${candidate.username || `account-${candidate.accountId}`} @ ${candidate.siteName}`);
}
if (!tokenOptionsByAccountId[candidate.accountId]) {
tokenOptionsByAccountId[candidate.accountId] = [];
}
tokenOptionsByAccountId[candidate.accountId].push({
id: candidate.tokenId,
name: candidate.tokenName,
isDefault: candidate.isDefault,
sourceModel: candidate.modelName,
});
}
for (const accountIdText of Object.keys(tokenOptionsByAccountId)) {
const accountId = Number.parseInt(accountIdText, 10);
tokenOptionsByAccountId[accountId].sort((a, b) => {
if (a.isDefault === b.isDefault) {
if (a.id === b.id) return (a.sourceModel || '').localeCompare(b.sourceModel || '', undefined, { sensitivity: 'base' });
return a.id - b.id;
}
return a.isDefault ? -1 : 1;
});
}
const accountOptions = Array.from(accountMap.entries()).map(([id, label]) => ({ id, label }));
index[route.id] = {
routeCandidates,
accountOptions,
tokenOptionsByAccountId,
};
}
return index;
}
+11 -3
View File
@@ -5,7 +5,13 @@ describe('buildSiteSaveAction', () => {
it('returns add action in add mode', () => {
const action = buildSiteSaveAction(
{ mode: 'add' },
{ name: 'site-a', url: 'https://a.example.com/', platform: 'new-api', apiKey: 'sk-1' },
{
name: 'site-a',
url: 'https://a.example.com/',
platform: 'new-api',
apiKey: 'sk-1',
proxyUrl: 'http://127.0.0.1:7890',
},
);
expect(action).toEqual({
@@ -15,6 +21,7 @@ describe('buildSiteSaveAction', () => {
url: 'https://a.example.com/',
platform: 'new-api',
apiKey: 'sk-1',
proxyUrl: 'http://127.0.0.1:7890',
},
});
});
@@ -22,7 +29,7 @@ describe('buildSiteSaveAction', () => {
it('returns update action in edit mode with site id', () => {
const action = buildSiteSaveAction(
{ mode: 'edit', editingSiteId: 12 },
{ name: 'site-b', url: 'https://b.example.com', platform: 'one-api', apiKey: '' },
{ name: 'site-b', url: 'https://b.example.com', platform: 'one-api', apiKey: '', proxyUrl: '' },
);
expect(action).toEqual({
@@ -33,6 +40,7 @@ describe('buildSiteSaveAction', () => {
url: 'https://b.example.com',
platform: 'one-api',
apiKey: '',
proxyUrl: '',
},
});
});
@@ -41,7 +49,7 @@ describe('buildSiteSaveAction', () => {
expect(() =>
buildSiteSaveAction(
{ mode: 'edit' },
{ name: 'site-c', url: 'https://c.example.com', platform: '', apiKey: '' },
{ name: 'site-c', url: 'https://c.example.com', platform: '', apiKey: '', proxyUrl: '' },
),
).toThrow('editingSiteId is required in edit mode');
});
+4 -2
View File
@@ -3,6 +3,7 @@ export type SiteForm = {
url: string;
platform: string;
apiKey: string;
proxyUrl: string;
};
export type SiteEditorState =
@@ -14,15 +15,16 @@ type SiteSaveAction =
| { kind: 'update'; id: number; payload: SiteForm };
export function emptySiteForm(): SiteForm {
return { name: '', url: '', platform: '', apiKey: '' };
return { name: '', url: '', platform: '', apiKey: '', proxyUrl: '' };
}
export function siteFormFromSite(site: Partial<SiteForm>): SiteForm {
export function siteFormFromSite(site: Partial<SiteForm> & { proxyUrl?: string | null }): SiteForm {
return {
name: site.name ?? '',
url: site.url ?? '',
platform: site.platform ?? '',
apiKey: site.apiKey ?? '',
proxyUrl: site.proxyUrl ?? '',
};
}