Add projects
This commit is contained in:
parent
2d3a9ad623
commit
8b607dd700
1802 changed files with 503346 additions and 2 deletions
210
frontend/tests/excludedModelRules.test.ts
Normal file
210
frontend/tests/excludedModelRules.test.ts
Normal 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` 做**顺序敏感**的
|
||||
* 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',
|
||||
]);
|
||||
});
|
||||
});
|
||||
Loading…
Reference in a new issue