Files
EasyCLIProxyAPI/tests/providerEntries.test.ts
T
lzt404 d29a1af69b feat: add plugin management and redesign dashboard and usage analytics
- Add plugin store, version selection, configuration, and OAuth login
- Support Muse (Meta) OAuth device authorization
- Redesign home dashboard and add batch model health checks
- Support per-credential provider editing, removal, and reordering
- Improve usage charts and track actual response models
- Preserve installed plugins and runtime data during portable rebuilds
- Update translations, documentation, and related tests
2026-10-04 17:31:52 +08:00

265 lines
16 KiB
TypeScript

import { describe, expect, test } from 'bun:test';
import {
appendProviderEntry, locateProviderEntry, providerEntries, removeProviderEntry,
reorderProviderEntries, updateProviderEntry,
} from '../src/services/providerEntries';
type RecordValue = Record<string, unknown>;
const groups = (): RecordValue[] => [{
name: 'gateway', 'base-url': 'https://gateway.test/v1', priority: 3,
headers: { 'X-Shared': 'value', 'auth-index': 'a valid header' },
models: [{ name: 'shared', custom: { retained: true } }],
'excluded-models': ['old-*'], 'disable-cooling': true,
'request-retry': 2, 'request-scoped-errors': [{ status: 429, action: 'continue' }],
prefix: 'team', 'proxy-url': 'http://proxy.test:80',
keys: [
{ 'api-key': 'key-a', priority: null, models: null, 'auth-index': 'runtime-a', vendor: { token: 'extension' } },
{ 'api-key': 'key-b', priority: 0, headers: {}, models: [], 'disable-cooling': false, 'excluded-models': [] },
],
}];
const keysOf = (group: RecordValue) => group.keys as RecordValue[];
describe('provider entries over native groups', () => {
test('flattens effective keys while retaining immutable source snapshots', () => {
const original = groups();
const entries = providerEntries('codex-api-key', original);
expect(entries).toHaveLength(2);
expect(entries[0].record).toMatchObject({ priority: 3, models: original[0].models, vendor: { token: 'extension' } });
expect(entries[0].record).not.toHaveProperty('name');
expect(entries[0].record).not.toHaveProperty('keys');
expect(entries[1].record).toMatchObject({ priority: 0, headers: {}, models: [], 'disable-cooling': false });
keysOf(original[0])[0]['api-key'] = 'externally-changed';
expect(keysOf(entries[0].source.group)[0]['api-key']).toBe('key-a');
entries[0].record.headers = {};
expect(entries[0].source.group.headers).toHaveProperty('X-Shared');
});
test('retains fields owned by a key even when they share a group field name', () => {
const entries = providerEntries('codex-api-key', [{ name: 'group-label', keys: [{ 'api-key': 'key', name: 'key-extension' }] }]);
expect(entries[0].record.name).toBe('key-extension');
});
test('keeps compatibility providers as single raw groups', () => {
const original = groups();
const entries = providerEntries('openai-compatibility', original);
expect(entries).toHaveLength(1);
expect(entries[0].record).toEqual(original[0]);
expect(entries[0].source.keyIndex).toBeUndefined();
});
test('generates unique family names and stores a new endpoint at group level', () => {
const result = appendProviderEntry([{ name: 'codex-1', keys: [] }, { name: 'codex-3', keys: [] }], 'codex-api-key', {
'api-key': 'new-key', 'base-url': 'https://new.test', priority: 0,
vendor: { retained: true }, 'auth-index': 'runtime', headers: { 'auth-index': 'valid' },
});
expect(result[2]).toEqual({ name: 'codex-2', 'base-url': 'https://new.test', keys: [{
'api-key': 'new-key', priority: 0, vendor: { retained: true }, headers: { 'auth-index': 'valid' },
}] });
});
test('single-key edits preserve inherited nulls, unknown fields and sibling policy', () => {
const original = groups();
const first = providerEntries('codex-api-key', original)[0];
const result = updateProviderEntry(original, first.source, first.record, { ...first.record, 'api-key': 'rotated', priority: 4 });
expect(result[0].priority).toBe(3);
expect(keysOf(result[0])[0]).toEqual({ 'api-key': 'rotated', priority: 4, models: null, vendor: { token: 'extension' } });
expect(keysOf(result[0])[1]).toEqual(keysOf(original[0])[1]);
expect(keysOf(original[0])[0]['api-key']).toBe('key-a');
expect((result[0].headers as RecordValue)['auth-index']).toBe('a valid header');
});
test('unchanged effective settings do not materialize inherited fields', () => {
const original = groups();
const first = providerEntries('codex-api-key', original)[0];
const result = updateProviderEntry(original, first.source, first.record, structuredClone(first.record));
expect(keysOf(result[0])[0]).toEqual({ 'api-key': 'key-a', priority: null, models: null, vendor: { token: 'extension' } });
expect(keysOf(result[0])[0]).not.toHaveProperty('headers');
});
test('cleared shared fields become empty overrides only for the selected key', () => {
const original = groups();
const first = providerEntries('codex-api-key', original)[0];
const after = { ...first.record };
for (const field of ['headers', 'models', 'excluded-models', 'prefix', 'proxy-url', 'priority', 'request-retry', 'request-scoped-errors']) delete after[field];
const result = updateProviderEntry(original, first.source, first.record, after);
expect(keysOf(result[0])[0]).toMatchObject({
headers: {}, models: [], 'excluded-models': [], prefix: '', 'proxy-url': '', priority: 0,
'request-retry': -1, 'request-scoped-errors': [],
});
expect(result[0].models).toEqual(original[0].models);
expect(keysOf(result[0])[1]).toEqual(keysOf(original[0])[1]);
});
test('removing a cooling override restores inheritance and explicit null remains explicit', () => {
const original = groups();
const second = providerEntries('codex-api-key', original)[1];
const after = { ...second.record, priority: null };
delete after['disable-cooling'];
const result = updateProviderEntry(original, second.source, second.record, after);
expect(keysOf(result[0])[1]).not.toHaveProperty('disable-cooling');
expect(keysOf(result[0])[1].priority).toBeNull();
expect(providerEntries('codex-api-key', result)[1].record).toMatchObject({ priority: 3, 'disable-cooling': true });
});
test('changing an endpoint isolates the selected key without changing shared policy', () => {
const original = groups();
original.push({ name: 'gateway-2', keys: [] });
const first = providerEntries('codex-api-key', original)[0];
const result = updateProviderEntry(original, first.source, first.record, { ...first.record, 'base-url': 'https://other.test', priority: 8 });
expect(result).toHaveLength(3);
expect(result[0]['base-url']).toBe('https://gateway.test/v1');
expect(keysOf(result[0])).toEqual([keysOf(original[0])[1]]);
expect(result[1]).toMatchObject({ name: 'gateway-3', 'base-url': 'https://other.test', priority: 3, headers: original[0].headers });
expect(keysOf(result[1])).toEqual([{ 'api-key': 'key-a', priority: 8, models: null, vendor: { token: 'extension' } }]);
expect(keysOf(result[1])[0]).not.toHaveProperty('base-url');
});
test('a single-key endpoint edit preserves the existing group identity', () => {
const original = [{ name: 'one', 'base-url': 'https://old.test', keys: [{ 'api-key': 'key' }] }];
const entry = providerEntries('codex-api-key', original)[0];
const result = updateProviderEntry(original, entry.source, entry.record, { ...entry.record, 'base-url': 'https://new.test' });
expect(result).toEqual([{ ...original[0], 'base-url': 'https://new.test' }]);
});
test('removing one key preserves its siblings and the final key removes its group', () => {
const original = groups();
const result = removeProviderEntry(original, providerEntries('codex-api-key', original)[0].source);
expect(keysOf(result[0])).toEqual([keysOf(original[0])[1]]);
expect(removeProviderEntry(result, providerEntries('codex-api-key', result)[0].source)).toEqual([]);
});
test('identical keys retain their positional identity within an unchanged group', () => {
const original = [{ name: 'duplicates', keys: [{ 'api-key': 'same' }, { 'api-key': 'same' }] }];
const second = providerEntries('codex-api-key', original)[1];
const result = updateProviderEntry(original, second.source, second.record, { ...second.record, weight: 2 });
expect(keysOf(result[0])).toEqual([{ 'api-key': 'same' }, { 'api-key': 'same', weight: 2 }]);
});
test('rejects stale edits when a sibling or a shared setting changed', () => {
const original = groups();
const first = providerEntries('codex-api-key', original)[0];
for (const change of [(value: RecordValue[]) => { keysOf(value[0])[1].priority = 9; },
(value: RecordValue[]) => { value[0].priority = 9; }]) {
const changed = structuredClone(original);
change(changed);
expect(() => updateProviderEntry(changed, first.source, first.record, first.record)).toThrow('configuration changed');
expect(() => removeProviderEntry(changed, first.source)).toThrow();
}
});
test('can locate a uniquely unchanged group after unrelated groups were reordered', () => {
const original = [...groups(), { name: 'other', keys: [{ 'api-key': 'other' }] }];
const first = providerEntries('codex-api-key', original)[0];
expect(locateProviderEntry([original[1], original[0]], first.source)).toEqual({ groupIndex: 1, keyIndex: 0 });
});
test('rejects ambiguous duplicate-group relocation even when only one old match remains', () => {
const original = [groups()[0], groups()[0]];
const entry = providerEntries('codex-api-key', original)[2];
expect(locateProviderEntry(original, entry.source)).toEqual({ groupIndex: 1, keyIndex: 0 });
expect(() => locateProviderEntry([original[0]], entry.source)).toThrow();
const changed = [...original, { name: 'third', keys: [] }];
expect(() => locateProviderEntry(changed, entry.source)).toThrow();
});
test('ignores transient response metadata but never header contents during stale checks', () => {
const original = groups();
const first = providerEntries('codex-api-key', original)[0];
const changed = structuredClone(original);
keysOf(changed[0])[0]['auth-index'] = 'different-runtime-index';
expect(locateProviderEntry(changed, first.source)).toEqual({ groupIndex: 0, keyIndex: 0 });
(changed[0].headers as RecordValue)['auth-index'] = 'different-header';
expect(() => locateProviderEntry(changed, first.source)).toThrow();
});
test('reordering keys rebuilds group fragments without losing inheritance or unknown fields', () => {
const original = [...groups(), { name: 'other', 'base-url': 'https://other.test', custom: { kept: true },
keys: [{ 'api-key': 'key-c', weight: 5, vendor: { retained: true } }] }, { name: 'empty', keys: [] }];
const entries = providerEntries('codex-api-key', original);
const result = reorderProviderEntries(original, 'codex-api-key', entries[2].source, entries[1].source);
expect(providerEntries('codex-api-key', result).map((entry) => entry.record['api-key'])).toEqual(['key-a', 'key-c', 'key-b']);
expect(result.map((group) => group.name)).toEqual(['gateway', 'other', 'gateway-2', 'empty']);
expect(result[0].headers).toEqual(original[0].headers);
expect(result[2].headers).toEqual(original[0].headers);
expect(keysOf(result[0])[0]).toMatchObject({ priority: null, models: null, vendor: { token: 'extension' } });
expect(keysOf(result[2])[0]).toEqual(keysOf(original[0])[1]);
expect(result[1]).toEqual(original[1]);
expect(original).toHaveLength(3);
});
test('reorders within a shared group without changing group settings', () => {
const original = groups();
const entries = providerEntries('codex-api-key', original);
const result = reorderProviderEntries(original, 'codex-api-key', entries[0].source, entries[1].source);
expect(result).toHaveLength(1);
expect(result[0].name).toBe('gateway');
expect(keysOf(result[0]).map((key) => key['api-key'])).toEqual(['key-b', 'key-a']);
expect(result[0].models).toEqual(original[0].models);
});
test('filtered key reordering leaves hidden credentials in their complete-list slots', () => {
const original = [{ name: 'all', 'base-url': 'https://gateway.test', keys: [
{ 'api-key': 'a' }, { 'api-key': 'hidden-a' }, { 'api-key': 'b' }, { 'api-key': 'hidden-b' }, { 'api-key': 'c' },
] }];
const entries = providerEntries('codex-api-key', original);
const visible = [entries[0].source, entries[2].source, entries[4].source];
const result = reorderProviderEntries(original, 'codex-api-key', entries[0].source, entries[4].source, visible);
expect(keysOf(result[0]).map((key) => key['api-key'])).toEqual(['b', 'hidden-a', 'c', 'hidden-b', 'a']);
expect(result[0].name).toBe('all');
expect(keysOf(original[0])[0]['api-key']).toBe('a');
});
test('filtered cross-group reordering preserves each fragment policy and hidden group position', () => {
const original = [...groups(), { name: 'hidden-deepseek', 'base-url': 'https://api.deepseek.com',
custom: { hidden: true }, keys: [{ 'api-key': 'hidden', weight: 7 }] },
{ name: 'other', 'base-url': 'https://other.test', headers: { 'X-Other': 'retained' },
keys: [{ 'api-key': 'key-c', weight: 5, vendor: { retained: true } }] }, { name: 'empty', keys: [] }];
const entries = providerEntries('codex-api-key', original);
const visible = [entries[0].source, entries[1].source, entries[3].source];
const result = reorderProviderEntries(original, 'codex-api-key', entries[3].source, entries[1].source, visible);
const effective = providerEntries('codex-api-key', result);
expect(effective.map((entry) => entry.record['api-key'])).toEqual(['key-a', 'key-c', 'hidden', 'key-b']);
expect(result.map((group) => group.name)).toEqual(['gateway', 'other', 'hidden-deepseek', 'gateway-2', 'empty']);
expect(result[2]).toEqual(original[1]);
expect(result[0].headers).toEqual(original[0].headers);
expect(result[3].headers).toEqual(original[0].headers);
expect(result[3].models).toEqual(original[0].models);
expect(keysOf(result[0])[0]).toMatchObject({ models: null, priority: null, vendor: { token: 'extension' } });
expect(keysOf(result[3])[0]).toEqual(keysOf(original[0])[1]);
expect(result[1]).toEqual(original[2]);
});
test('validates every visible snapshot before reordering even when source and target are unchanged', () => {
const original = ['a', 'b', 'c'].map((key) => ({ name: key, keys: [{ 'api-key': key }] }));
const entries = providerEntries('codex-api-key', original);
const changed = structuredClone(original);
keysOf(changed[1])[0].weight = 9;
expect(() => reorderProviderEntries(changed, 'codex-api-key', entries[0].source, entries[2].source,
entries.map((entry) => entry.source))).toThrow('configuration changed');
expect(() => reorderProviderEntries(changed, 'codex-api-key', entries[0].source, entries[0].source,
entries.map((entry) => entry.source))).toThrow('configuration changed');
expect(() => reorderProviderEntries(original, 'codex-api-key', entries[0].source, entries[2].source,
[entries[0].source, entries[1].source])).toThrow();
});
test('filtered compatibility reordering leaves hidden providers at their existing positions', () => {
const original = ['a', 'hidden', 'b'].map((name) => ({ name, keys: [{ 'api-key': name }] }));
const entries = providerEntries('openai-compatibility', original);
const result = reorderProviderEntries(original, 'openai-compatibility', entries[0].source, entries[2].source,
[entries[0].source, entries[2].source]);
expect(result).toEqual([original[2], original[1], original[0]]);
});
test('compatibility operations preserve provider policies and move whole providers', () => {
const original = [...groups(), { name: 'other', keys: [{ 'api-key': 'other' }], disabled: true }];
const entries = providerEntries('openai-compatibility', original);
const updated = updateProviderEntry(original, entries[0].source, entries[0].record, { ...entries[0].record, disabled: true });
expect(updated[0].disabled).toBe(true);
expect(keysOf(updated[0])[0]).not.toHaveProperty('auth-index');
expect(updated[0].headers).toEqual(original[0].headers);
expect(reorderProviderEntries(original, 'openai-compatibility', entries[0].source, entries[1].source)[0]).toEqual(original[1]);
expect(removeProviderEntry(original, entries[0].source)).toEqual([original[1]]);
expect(appendProviderEntry([], 'openai-compatibility', original[1])).toEqual([original[1]]);
});
});