- 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
265 lines
16 KiB
TypeScript
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]]);
|
|
});
|
|
});
|