100 lines
6.1 KiB
TypeScript
100 lines
6.1 KiB
TypeScript
import { describe, expect, test } from 'bun:test';
|
|
import { buildProviderGroupRecord, providerDraftFromRecord, reorderProviderRecords, resolveProviderRecordIndex } from '../src/pages/ApiAccessPage';
|
|
import { cleanProviderGroup, effectiveProviderKey, providerGroupStatus, providerKeyDraft, serializeProviderKey } from '../src/services/providerGroups';
|
|
|
|
const group = () => ({
|
|
name: 'shared-gateway', 'base-url': 'https://gateway.test/v1',
|
|
priority: 3, 'proxy-url': 'http://proxy.test:80', 'excluded-models': ['old-*'],
|
|
headers: { 'X-Group': 'shared' }, 'request-retry': 0,
|
|
models: [{ name: 'upstream' }, { name: 'upstream', alias: 'upstream-fast', thinking: { levels: ['high'] } }],
|
|
keys: [
|
|
{ 'api-key': 'key-a', weight: 2, 'auth-index': 'runtime-a' },
|
|
{ 'api-key': 'key-b', weight: 5, priority: null, headers: {}, models: null,
|
|
'excluded-models': [], 'disable-cooling': false, websockets: false, 'request-scoped-errors': [{ status: 429, action: 'continue' }] },
|
|
{ 'api-key': 'key-c', priority: 0, models: [{ name: 'private', alias: 'private-alias' }], 'proxy-url': 'direct' },
|
|
],
|
|
});
|
|
|
|
describe('native v8 provider groups', () => {
|
|
for (const section of ['codex-api-key', 'claude-api-key', 'gemini-api-key', 'openai-compatibility'] as const) {
|
|
test(`${section} keeps one group, key order and overrides on a shared edit`, () => {
|
|
const original = group();
|
|
const draft = providerDraftFromRecord(section, original);
|
|
draft.name = 'renamed'; draft.priority = '4';
|
|
const saved = buildProviderGroupRecord(section, draft, original);
|
|
expect(saved).toEqual({ ...cleanProviderGroup(original), name: 'renamed', priority: 4 });
|
|
expect(original.keys[0]['auth-index']).toBe('runtime-a');
|
|
expect(saved).not.toHaveProperty('api-key');
|
|
expect(saved).not.toHaveProperty('api-key-entries');
|
|
});
|
|
}
|
|
test('preserves absent, null and empty shared fields on an unrelated edit', () => {
|
|
for (const fields of [{}, { models: null, headers: null, 'proxy-url': null }, { models: [], headers: {}, 'proxy-url': '' }]) {
|
|
const original = { name: 'empty', keys: [], ...fields };
|
|
const draft = providerDraftFromRecord('codex-api-key', original);
|
|
expect(buildProviderGroupRecord('codex-api-key', draft, original)).toEqual(original);
|
|
}
|
|
});
|
|
test('shared model edits keep existing aliases and independent key models', () => {
|
|
const original = group();
|
|
const draft = providerDraftFromRecord('codex-api-key', original);
|
|
draft.models = [...draft.models, { name: 'new-model' }];
|
|
const saved = buildProviderGroupRecord('codex-api-key', draft, original);
|
|
expect(saved.models).toEqual(expect.arrayContaining([...original.models, { name: 'new-model' }]));
|
|
expect((saved.models as unknown[]).length).toBe(3);
|
|
expect(saved.keys).toEqual(cleanProviderGroup(original).keys);
|
|
});
|
|
test('key rotation and removal do not transfer settings to another key', () => {
|
|
const original = group();
|
|
const draft = providerDraftFromRecord('codex-api-key', original);
|
|
const second = draft.groupKeys![1];
|
|
second.value['api-key'] = 'rotated';
|
|
draft.groupKeys = [second, providerKeyDraft({ 'api-key': 'new' })];
|
|
const saved = buildProviderGroupRecord('codex-api-key', draft, original);
|
|
expect(saved.keys).toEqual([{ ...original.keys[1], 'api-key': 'rotated' }, { 'api-key': 'new' }]);
|
|
});
|
|
test('all aliases of one upstream can be edited and removed individually', () => {
|
|
const original = group();
|
|
const draft = providerDraftFromRecord('codex-api-key', original);
|
|
expect(draft.models.map((model) => model.alias ?? '')).toEqual(['', 'upstream-fast']);
|
|
draft.models[1].alias = 'upstream-renamed';
|
|
const renamed = buildProviderGroupRecord('codex-api-key', draft, original);
|
|
expect(renamed.models).toEqual([original.models[0], { ...original.models[1], alias: 'upstream-renamed' }]);
|
|
draft.models = [draft.models[1]];
|
|
expect(buildProviderGroupRecord('codex-api-key', draft, original).models)
|
|
.toEqual([{ ...original.models[1], alias: 'upstream-renamed' }]);
|
|
});
|
|
test('stale checks distinguish inherited null from explicit empty overrides', () => {
|
|
const original = group();
|
|
const changed = structuredClone(original);
|
|
changed.keys[1].models = [] as never;
|
|
expect(resolveProviderRecordIndex([changed], { section: 'codex-api-key', name: original.name,
|
|
apiKey: 'key-a', baseUrl: original['base-url'], index: 0, record: original })).toBe(-1);
|
|
});
|
|
test('inheritance respects null, explicit false, zero and entire map/list overrides', () => {
|
|
const original = group();
|
|
const effective = effectiveProviderKey(original, original.keys[1]);
|
|
expect(effective).toMatchObject({ priority: 3, models: original.models, headers: {}, 'excluded-models': [], 'disable-cooling': false });
|
|
expect(effectiveProviderKey(original, original.keys[2]).priority).toBe(0);
|
|
expect(providerGroupStatus({ ...original, 'excluded-models': ['*'] })).toBe('partial');
|
|
expect(providerGroupStatus({ ...original, disabled: true })).toBe('disabled');
|
|
});
|
|
test('serializes explicit blank overrides and rejects malformed headers', () => {
|
|
const draft = providerKeyDraft({ 'api-key': 'key', headers: null, 'excluded-models': ['old'] });
|
|
draft.text = { headers: '', 'excluded-models': '' };
|
|
expect(serializeProviderKey(draft)).toEqual({ 'api-key': 'key', headers: {}, 'excluded-models': [] });
|
|
draft.text.headers = 'bad header';
|
|
expect(() => serializeProviderKey(draft)).toThrow();
|
|
});
|
|
test('external changes to a sibling key reject stale group saves', () => {
|
|
const original = group();
|
|
const identity = { section: 'codex-api-key' as const, name: original.name, apiKey: 'key-a', baseUrl: original['base-url'], index: 0, record: original };
|
|
const changed = group(); changed.keys[1].weight = 10;
|
|
expect(resolveProviderRecordIndex([changed], identity)).toBe(-1);
|
|
const other = { ...group(), name: 'other' };
|
|
const second = { ...identity, index: 1, name: 'other', record: other };
|
|
expect(reorderProviderRecords([original, other], [identity, second], identity, second))
|
|
.toEqual([cleanProviderGroup(other), cleanProviderGroup(original)]);
|
|
});
|
|
});
|