mirror of
https://github.com/router-for-me/Cli-Proxy-API-Management-Center.git
synced 2026-08-28 16:31:13 +08:00
4abd41f947
- Removed deprecated localization keys related to excluded models in zh-TW.json. - Introduced a new structure for excluded models with improved UI components. - Refactored AuthFilesOAuthExcludedEditPage to utilize ExcludedModelsPicker for better handling of model exclusions. - Consolidated styles in AuthFilesOAuthExcludedEditPage.module.scss by removing unused classes. - Added comprehensive tests for excluded model rules and their matching logic. - Deleted obsolete tests for OAuth excluded rules and replaced them with more relevant tests for the new structure. - Ensured that the disabled state of providers is correctly managed and reflected in the UI.
211 lines
7.6 KiB
TypeScript
211 lines
7.6 KiB
TypeScript
import { describe, expect, test } from 'bun:test';
|
||
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` 做**顺序敏感**的
|
||
* diff(useAuthFilesPrefixProxyEditor.ts:327)。picker 只要在读写之间保持顺序不变,
|
||
* 「打开但不修改就保存」就永远不会写出与原文件不同的内容。
|
||
*/
|
||
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',
|
||
]);
|
||
});
|
||
});
|