Enhance support for the v8 kernel

This commit is contained in:
lzt404 committed 2026-09-28 16:42:22 +08:00
1 parent 9991aea818
commit 59b05e9281
25 files changed
+1463 -185

No files matched your search

+19
View File
@@ -74,6 +74,8 @@ const path = require('node:path');
await form.locator('.provider-advanced-settings summary').click();
const headers = form.getByLabel(/Custom Headers/);
await headers.fill('X-Layout-Test: reachable');
await form.getByRole('combobox', { name: 'Disable Cooldown', exact: true }).selectOption('false');
await form.getByRole('combobox', { name: 'Cache User ID', exact: true }).selectOption('false');
await assertDialogControls(form);
const scroll = await form.evaluate((element) => ({ top: element.scrollTop, max: element.scrollHeight - element.clientHeight }));
assert.ok(scroll.top > 0 && scroll.max > 0, 'Advanced settings must be reachable by scrolling inside the dialog');
@@ -97,13 +99,30 @@ const path = require('node:path');
const record = await page.evaluate(() => window.providerFixture.records[0]);
assert.equal(record.headers['X-Layout-Test'], 'reachable');
assert.equal(record.models.length, 30);
assert.equal(record['disable-cooling'], false);
assert.equal(record.cloak['cache-user-id'], false);
assert.equal(await page.evaluate(() => document.body.style.overflow), '');
await page.locator('.real-provider-row').getByRole('button', { name: 'Edit', exact: true }).click();
await form.locator('.provider-advanced-settings summary').click();
for (const name of ['Disable Cooldown', 'Cache User ID']) {
const control = form.getByRole('combobox', { name, exact: true });
assert.equal(await control.inputValue(), 'false');
await control.selectOption('');
}
await form.getByRole('button', { name: 'Save', exact: true }).click();
await form.waitFor({ state: 'detached' });
const inherited = await page.evaluate(() => window.providerFixture.records[0]);
assert.equal(inherited['disable-cooling'], undefined);
assert.equal(inherited.cloak?.['cache-user-id'], undefined);
await page.locator('.provider-category-panel button').filter({ hasText: 'OpenAI' }).click();
await page.getByRole('button', { name: 'Add', exact: true }).click();
const add = page.getByRole('dialog', { name: 'Add API Connection', exact: true });
await assertViewportOverlay('.config-dialog-backdrop');
await assertDialogControls(add);
await add.locator('.provider-advanced-settings summary').click();
assert.equal(await add.getByLabel('Test Model', { exact: true }).count(), 0);
await add.getByRole('button', { name: 'Cancel', exact: true }).click();
await add.waitFor({ state: 'detached' });
assert.deepEqual(errors, []);
+173
View File
@@ -0,0 +1,173 @@
import { afterAll, beforeAll, describe, expect, it } from 'bun:test';
import { clearMocks, mockIPC } from '@tauri-apps/api/mocks';
import { spawn, type ChildProcess } from 'node:child_process';
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
import { createServer } from 'node:net';
import { tmpdir } from 'node:os';
import { join, resolve, sep } from 'node:path';
import { managementApi, responseList } from '../src/services/managementApi';
import { loadAuthFileSettings, saveAuthFileSettings } from '../src/services/authFileSettings';
import { setOAuthCredentialFileDisabled } from '../src/services/authFiles';
import { loadOAuthModelSettings, saveOAuthModelSettings } from '../src/services/oauthModelSettings';
import { fetchModels } from '../src/services/modelService';
const executable = process.env.CPA_V8_TEST_CORE;
describe.skipIf(!executable)('real v8 management operations', () => {
let child: ChildProcess;
let work = '';
let origin = '';
let originalWindow: PropertyDescriptor | undefined;
const headers = { Authorization: 'Bearer isolated-test-secret', 'Content-Type': 'application/json' };
const name = 'contract-codex.json';
const call = async (method: string, path: string, query?: Record<string, string>, body?: unknown) => {
const url = new URL(origin + path);
Object.entries(query ?? {}).forEach(([key, value]) => url.searchParams.set(key, value));
const response = await fetch(url, { method, headers, body: body === undefined ? undefined : JSON.stringify(body) });
const text = await response.text();
let result: any;
try { result = JSON.parse(text); } catch { result = text; }
if (!response.ok) throw new Error(`Management API error (${response.status}): ${result.error ?? text}${result.message ? `: ${result.message}` : ''}`);
return result;
};
beforeAll(async () => {
work = await mkdtemp(join(tmpdir(), 'easycpa-v8-operations-'));
const listener = createServer();
await new Promise<void>((done) => listener.listen(0, '127.0.0.1', done));
const port = (listener.address() as { port: number }).port;
await new Promise<void>((done) => listener.close(() => done()));
const config = join(work, 'config.yaml');
await writeFile(config, `config-version: 8
server: {host: 127.0.0.1, port: ${port}}
management: {secret-key: isolated-test-secret, disable-control-panel: true, disable-auto-update-panel: true}
access: {api-keys: [isolated-client-key]}
oauth: {auth-dir: ${JSON.stringify(join(work, 'auth'))}}
`);
child = spawn(resolve(executable!), ['-config', config, '-local-model'], {
cwd: work, windowsHide: true, stdio: 'ignore',
});
let startupError: Error | undefined;
child.on('error', (error) => { startupError = error; });
origin = `http://127.0.0.1:${port}/v8/management`;
let ready = false;
for (let attempt = 0; attempt < 100; attempt++) {
if (startupError) throw startupError;
if (child.exitCode !== null) throw new Error(`Kernel exited: ${child.exitCode}`);
try { await call('GET', '/config'); ready = true; break; } catch {}
await Bun.sleep(100);
}
if (!ready) throw new Error('Kernel startup timed out');
originalWindow = Object.getOwnPropertyDescriptor(globalThis, 'window');
Object.defineProperty(globalThis, 'window', { value: {}, writable: true, configurable: true });
mockIPC(async (command, args) => {
if (command === 'upload_auth_file') {
return call('POST', '/credentials', { name: args!.name as string }, JSON.parse(new TextDecoder().decode(new Uint8Array(args!.data as number[]))));
}
expect(command).toBe('management_request');
const request = args!.request as { method: string; path: string; query?: Record<string, string>; body?: unknown };
return call(request.method, request.path, request.query, request.body);
});
}, 15000);
afterAll(async () => {
clearMocks();
if (originalWindow) Object.defineProperty(globalThis, 'window', originalWindow);
else Reflect.deleteProperty(globalThis, 'window');
if (child?.pid && child.exitCode === null) {
const exited = new Promise<void>((done) => child.once('exit', () => done()));
child.kill();
await exited;
}
if (work && resolve(work).startsWith(resolve(tmpdir()) + sep + 'easycpa-v8-operations-')) {
await rm(work, { recursive: true, force: true });
}
});
it('uploads, edits every credential setting, clears overrides, toggles and deletes', async () => {
await managementApi.uploadAuthFile(new File([JSON.stringify({
type: 'codex', access_token: 'isolated-access-token', expired: '2099-01-01T00:00:00Z',
email: 'test@example.invalid', custom_metadata: { preserve: true },
})], name, { type: 'application/json' }));
let file: Record<string, unknown> | undefined;
for (let attempt = 0; attempt < 50; attempt++) {
file = responseList(await managementApi.get('/auth-files'), 'files').find((item) => item.name === name);
if (file) break;
await Bun.sleep(100);
}
expect(file).toBeDefined();
const original = await loadAuthFileSettings(name);
const edited = {
...original, prefix: 'team', proxy_url: 'direct', priority: '-2', weight: '3',
disable_cooling: 'false' as const, websockets: 'false' as const,
excluded_models: 'old-*\nretired', headers: '{"X-Test":"one","X-Remove":"two"}', note: 'test note',
};
await saveAuthFileSettings(name, original, edited);
const reloaded = await loadAuthFileSettings(name);
expect(reloaded).toMatchObject({ ...edited, headers: reloaded.headers });
expect(JSON.parse(reloaded.headers)).toEqual(JSON.parse(edited.headers));
const models = await loadOAuthModelSettings({ scope: 'credential', provider: 'codex', label: 'test', name });
expect(models.catalogError).toBe('');
expect(models.excludedRules).toEqual(['old-*', 'retired']);
await saveOAuthModelSettings(models, ['next-*']);
const beforeClear = await loadAuthFileSettings(name);
await saveAuthFileSettings(name, beforeClear, {
...beforeClear, prefix: '', proxy_url: '', priority: '', weight: '',
disable_cooling: '', websockets: '', excluded_models: '', headers: '{"X-Test":"updated"}', note: '',
});
const cleared = await loadAuthFileSettings(name);
expect(cleared).toMatchObject({ prefix: '', proxy_url: '', priority: '0', weight: '',
disable_cooling: '', websockets: '', excluded_models: '', note: '' });
expect(JSON.parse(cleared.headers)).toEqual({ 'X-Test': 'updated' });
const metadata = await managementApi.get<Record<string, unknown>>('/auth-files/download', { name });
expect(metadata.custom_metadata).toEqual({ preserve: true });
expect(metadata.access_token).toBe('isolated-access-token');
await setOAuthCredentialFileDisabled(file!, true);
expect(responseList(await managementApi.get('/auth-files'), 'files').find((item) => item.name === name)?.disabled).toBe(true);
await setOAuthCredentialFileDisabled(file!, false);
expect(responseList(await managementApi.get('/auth-files'), 'files').find((item) => item.name === name)?.disabled).toBe(false);
await managementApi.delete('/auth-files', { query: { name } });
expect(responseList(await managementApi.get('/auth-files'), 'files').some((item) => item.name === name)).toBe(false);
});
it('edits provider model exclusions without changing siblings and clears the last rule', async () => {
await call('PUT', '/config/oauth/excluded-models', undefined, null);
const target = { scope: 'provider' as const, provider: 'codex', label: 'Codex' };
const original = await loadOAuthModelSettings(target);
expect(original.catalogError).toBe('');
expect(original.models.length).toBeGreaterThan(0);
expect(original.excludedRules).toEqual([]);
await managementApi.patch('/oauth-excluded-models', { provider: 'claude', models: ['keep-*'] });
await saveOAuthModelSettings(original, ['hide-*']);
const edited = await loadOAuthModelSettings(target);
expect(edited.excludedRules).toEqual(['hide-*']);
await saveOAuthModelSettings(edited, []);
expect((await loadOAuthModelSettings(target)).excludedRules).toEqual([]);
expect(await managementApi.get('/oauth-excluded-models')).toEqual({ 'oauth-excluded-models': { claude: ['keep-*'] } });
await managementApi.delete('/oauth-excluded-models', { query: { provider: 'claude' } });
});
it('discovers models and forwards API-call requests to an isolated local upstream', async () => {
const upstream = Bun.serve({ hostname: '127.0.0.1', port: 0, fetch(request) {
if (request.headers.get('Authorization') !== 'Bearer local-key') return new Response('wrong key', { status: 401 });
return Response.json({ data: [{ id: 'local-model' }] });
} });
try {
const models = await fetchModels('openai', `http://127.0.0.1:${upstream.port}/v1`, 'local-key');
expect(models.map((model) => model.name)).toEqual(['local-model']);
const response = await managementApi.post<Record<string, unknown>>('/api-call', {
method: 'GET', url: `http://127.0.0.1:${upstream.port}/usage`, header: { Authorization: 'Bearer local-key' },
});
expect(response.status_code).toBe(200);
expect(JSON.parse(response.body as string)).toEqual({ data: [{ id: 'local-model' }] });
} finally { await upstream.stop(true); }
});
it('uses the v8 usage queue and OAuth validation/session endpoints', async () => {
expect(await call('GET', '/observability/usage/queue', { count: '10' })).toEqual([]);
const status = await call('GET', '/oauth/status', { state: 'isolated-unknown-state' });
expect(typeof status.status).toBe('string');
await expect(call('GET', '/oauth/auth-url', { provider: 'unsupported-test-provider' })).rejects.toThrow('(404): provider_not_found');
await expect(call('POST', '/oauth/callback', undefined, { provider: 'codex', redirect_url: 'http://localhost/callback' })).rejects.toThrow('(400)');
await managementApi.delete('/oauth-session', { query: { state: 'isolated-unknown-state' } });
});
});
+69 -3
View File
@@ -7,8 +7,15 @@ import {
legacyManagementConfigView,
managementApi,
} from '../src/services/managementApi';
import {
apiAccessRemarkLocatorFromRecord,
buildProviderRecord,
createProviderDraft,
providerCategoryMatchesRecord,
providerRecordWithDisabledState,
} from '../src/pages/ApiAccessPage';
describe('optional v8 config reads', () => {
describe('v8 Management API requests', () => {
let originalWindow: PropertyDescriptor | undefined;
beforeEach(() => {
originalWindow = Object.getOwnPropertyDescriptor(globalThis, 'window');
@@ -38,6 +45,48 @@ describe('optional v8 config reads', () => {
}
}
});
it('saves and reloads DeepSeek through the v8 Codex group without credential names', async () => {
for (const baseUrl of ['https://api.deepseek.com', 'https://gateway.example.test/v1']) {
let persisted: unknown = [];
mockIPC((command, args) => {
expect(command).toBe('management_request');
const request = args?.request as { method: string; path: string; body: unknown };
expect(request.path).toBe('/config/api-keys/codex');
if (request.method === 'GET') return persisted;
expect(request.method).toBe('PUT');
const groups = request.body as Record<string, unknown>[];
for (const group of groups) {
for (const key of group.keys as Record<string, unknown>[]) {
if ('name' in key) throw new Error('invalid_config: field name not found in type config.CodexKey');
}
}
persisted = request.body;
return { status: 'ok' };
});
const original = buildProviderRecord('codex-api-key', {
...createProviderDraft('deepseek'), apiKey: 'deepseek-test-key', baseUrl,
models: [{ name: 'deepseek-chat', alias: 'my-deepseek' }],
priority: '5', proxyUrl: 'direct', headersText: 'X-Team: test',
});
const regular = buildProviderRecord('codex-api-key', {
...createProviderDraft('codex-api-key'), apiKey: 'codex-test-key',
baseUrl: 'https://codex.example.test/v1', models: [{ name: 'gpt-test' }],
});
for (const record of [original, providerRecordWithDisabledState('codex-api-key', original, true)]) {
await managementApi.put('/codex-api-key', [regular, record]);
const loaded = await managementApi.get<{ 'codex-api-key': Record<string, unknown>[] }>('/codex-api-key');
expect(loaded['codex-api-key']).toEqual([regular, record]);
expect(providerCategoryMatchesRecord('deepseek', loaded['codex-api-key'][1])).toBe(true);
expect(apiAccessRemarkLocatorFromRecord('codex-api-key', loaded['codex-api-key'][1]))
.toEqual(apiAccessRemarkLocatorFromRecord('codex-api-key', original));
await managementApi.put('/codex-api-key', [record, regular]);
expect((await managementApi.get('/codex-api-key'))).toEqual({ 'codex-api-key': [record, regular] });
}
await managementApi.put('/codex-api-key', [regular]);
expect(await managementApi.get('/codex-api-key')).toEqual({ 'codex-api-key': [regular] });
}
});
});
describe('apiCallErrorMessage', () => {
@@ -72,10 +121,12 @@ describe('v8 Management API compatibility view', () => {
}];
expect(flattenV8ProviderGroups('codex', groups)).toEqual([
{
name: 'production',
'base-url': 'https://example.test/v1', priority: 10,
models: [{ name: 'model-a' }], 'api-key': 'first', 'proxy-url': 'direct',
},
{
name: 'production',
'base-url': 'https://example.test/v1', priority: 0,
models: [{ name: 'model-a' }], 'api-key': 'second',
},
@@ -90,13 +141,28 @@ describe('v8 Management API compatibility view', () => {
models: [{ name: 'claude-test' }],
cloak: { mode: 'always' },
}])).toEqual([{
name: 'claude-1',
name: 'delegated-key',
'base-url': 'https://claude.test',
models: [{ name: 'claude-test' }],
keys: [{ name: 'delegated-key', 'api-key': 'secret', cloak: { mode: 'always' } }],
keys: [{ 'api-key': 'secret', cloak: { mode: 'always' } }],
}]);
});
it('repairs legacy credential names while preserving policies and unnamed record identities', () => {
const records = flattenV8ProviderGroups('codex', [{
name: 'codex-1', 'base-url': 'https://gateway.example.test/v1', priority: 5,
keys: [
{ name: 'DeepSeek', 'api-key': 'first', weight: 2 },
{ 'api-key': 'second', priority: 0 },
],
}]);
const groups = groupLegacyProviderRecords('codex', records);
expect(groups[0]).toMatchObject({ name: 'DeepSeek', keys: [{ 'api-key': 'first', weight: 2 }] });
expect(groups[1]).toMatchObject({ name: 'codex-2', keys: [{ 'api-key': 'second' }] });
expect(flattenV8ProviderGroups('codex', groups)).toEqual(records);
expect(records[1]).not.toHaveProperty('name');
});
it('projects the v8 config tree into the legacy semantic view used by the UI', () => {
const view = legacyManagementConfigView({
access: { 'api-keys': ['client-key'] },
+170
View File
@@ -0,0 +1,170 @@
import { afterAll, beforeAll, describe, expect, it } from 'bun:test';
import { spawn, type ChildProcess } from 'node:child_process';
import { mkdtemp, rm, writeFile } from 'node:fs/promises';
import { createServer } from 'node:net';
import { tmpdir } from 'node:os';
import { join, resolve, sep } from 'node:path';
import {
applyProviderPreset, buildProviderRecord, createProviderDraft,
type ProviderSection,
} from '../src/pages/ApiAccessPage';
import { flattenV8ProviderGroups, groupLegacyProviderRecords } from '../src/services/managementApi';
const executable = process.env.CPA_V8_TEST_CORE;
describe.skipIf(!executable)('real v8 provider configuration', () => {
let child: ChildProcess;
let work = '';
let origin = '';
const headers = { Authorization: 'Bearer isolated-test-secret', 'Content-Type': 'application/json' };
beforeAll(async () => {
work = await mkdtemp(join(tmpdir(), 'easycpa-v8-fields-'));
const listener = createServer();
await new Promise<void>((done) => listener.listen(0, '127.0.0.1', done));
const port = (listener.address() as { port: number }).port;
await new Promise<void>((done) => listener.close(() => done()));
const config = join(work, 'config.yaml');
await writeFile(config, `config-version: 8
server: {host: 127.0.0.1, port: ${port}}
management: {secret-key: isolated-test-secret, disable-control-panel: true, disable-auto-update-panel: true}
access: {api-keys: [isolated-client-key]}
oauth: {auth-dir: ${JSON.stringify(join(work, 'auth'))}}
`);
child = spawn(resolve(executable!), ['--config', config, '--local-model'], {
cwd: work, windowsHide: true, stdio: 'ignore',
});
let startupError: Error | undefined;
child.on('error', (error) => { startupError = error; });
origin = `http://127.0.0.1:${port}/v8/management`;
for (let attempt = 0; attempt < 100; attempt++) {
if (startupError) throw startupError;
if (child.exitCode !== null) throw new Error(`Kernel exited: ${child.exitCode}`);
try { if ((await fetch(`${origin}/config`, { headers })).ok) return; } catch {}
await Bun.sleep(100);
}
throw new Error('Kernel startup timed out');
}, 15000);
afterAll(async () => {
if (child?.pid && child.exitCode === null) {
const exited = new Promise<void>((done) => child.once('exit', () => done()));
child.kill();
await exited;
}
if (work && resolve(work).startsWith(resolve(tmpdir()) + sep + 'easycpa-v8-fields-')) {
await rm(work, { recursive: true, force: true });
}
});
const save = async (provider: string, records: Record<string, unknown>[]) => {
const url = `${origin}/config/api-keys/${provider}`;
const response = await fetch(url, {
method: 'PUT', headers, body: JSON.stringify(groupLegacyProviderRecords(provider, records)),
});
const result = await response.text();
expect({ status: response.status, result: response.ok ? 'ok' : result }).toEqual({ status: 200, result: 'ok' });
const loaded = await fetch(url, { headers });
expect(loaded.status).toBe(200);
return flattenV8ProviderGroups(provider, await loaded.json());
};
for (const category of ['codex-api-key', 'deepseek', 'claude-api-key', 'gemini-api-key', 'openai-compatibility'] as const) {
const section: ProviderSection = category === 'deepseek' ? 'codex-api-key' : category;
const provider = section.replace('-api-key', '');
it(`${category}: accepts advanced fields and discovered reasoning metadata`, async () => {
const draft = {
...createProviderDraft(category), name: category === 'deepseek' ? 'DeepSeek' : 'integration',
apiKey: 'test-key', baseUrl: 'https://gateway.example.test/v1', priority: '-2',
proxyUrl: 'direct', prefix: 'team', headersText: 'X-Team: test', excludedModelsText: 'old-*',
disableCooling: true, websockets: true,
cloakMode: 'always', cloakStrictMode: true, cloakSensitiveWordsText: 'internal', cloakCacheUserId: true,
models: [{ name: 'upstream', alias: 'public', thinking: {
min: 0, max: 8192, zero_allowed: true, dynamic_allowed: true, levels: ['low', 'high'],
} }],
};
const [loaded] = await save(provider, [buildProviderRecord(section, draft)]);
expect(loaded).toMatchObject({ priority: -2, prefix: 'team', headers: { 'X-Team': 'test' }, 'disable-cooling': true });
expect(loaded.models).toMatchObject([{ name: 'upstream', alias: 'public', thinking: {
min: 0, max: 8192, 'zero-allowed': true, 'dynamic-allowed': true, levels: ['low', 'high'],
} }]);
if (section === 'openai-compatibility') expect(loaded['api-key-entries']).toMatchObject([{ 'proxy-url': 'direct' }]);
else expect(loaded).toMatchObject({ 'proxy-url': 'direct', 'excluded-models': ['old-*'] });
if (section === 'codex-api-key') expect(loaded.websockets).toBe(true);
if (section === 'claude-api-key') expect(loaded.cloak).toMatchObject({
mode: 'always', 'strict-mode': true, 'sensitive-words': ['internal'], 'cache-user-id': true,
});
const editedDraft = { ...draft, priority: '0', prefix: '', headersText: '', proxyUrl: '',
excludedModelsText: '', disableCooling: false, websockets: false,
cloakMode: 'never', cloakStrictMode: false, cloakSensitiveWordsText: '', cloakCacheUserId: false,
models: [{ name: 'upstream', alias: 'renamed' }],
};
const [edited] = await save(provider, [buildProviderRecord(section, editedDraft, loaded)]);
expect(edited.priority ?? 0).toBe(0);
for (const field of ['prefix', 'headers', 'proxy-url', 'excluded-models']) expect(edited).not.toHaveProperty(field);
expect(edited['disable-cooling']).toBe(false);
expect(edited.models).toMatchObject([{ name: 'upstream', alias: 'renamed' }]);
if (section === 'codex-api-key') expect(edited.websockets ?? false).toBe(false);
if (section === 'openai-compatibility') {
expect((edited['api-key-entries'] as Record<string, unknown>[])[0]).not.toHaveProperty('proxy-url');
}
if (section === 'claude-api-key') {
expect(edited.cloak).toMatchObject({ mode: 'never', 'cache-user-id': false });
expect((edited.cloak as Record<string, unknown>)['strict-mode'] ?? false).toBe(false);
expect(edited.cloak).not.toHaveProperty('sensitive-words');
}
const [inherited] = await save(provider, [buildProviderRecord(section, {
...editedDraft, disableCooling: null, cloakMode: '', cloakCacheUserId: null,
}, edited)]);
expect(inherited).not.toHaveProperty('disable-cooling');
if (section === 'claude-api-key') expect(inherited).not.toHaveProperty('cloak');
});
}
for (const provider of ['interactions', 'vertex', 'xai', 'meta']) {
it(`${provider}: round-trips the additional provider families supported by the adapter`, async () => {
const [loaded] = await save(provider, [{
'api-key': 'test-key', 'base-url': 'https://gateway.example.test/v1', weight: 2,
'disable-cooling': false, 'request-retry': 0,
models: [{ name: 'model', thinking: { zero_allowed: false, dynamic_allowed: true } }],
}]);
expect(loaded).toMatchObject({ weight: 2, 'disable-cooling': false, 'request-retry': 0 });
expect(loaded.models).toMatchObject([{ thinking: { 'zero-allowed': false, 'dynamic-allowed': true } }]);
});
}
it('removes obsolete OpenAI test-model metadata when saving old records', async () => {
const [loaded] = await save('openai-compatibility', [{
name: 'compat', 'base-url': 'https://gateway.example.test/v1',
'api-key-entries': [{ 'api-key': 'test-key' }], models: [{ name: 'model' }], 'test-model': 'model',
}]);
expect(loaded).not.toHaveProperty('test-model');
});
it('persists explicit false for cooldown and Claude user ID caching', async () => {
const draft = { ...createProviderDraft('claude-api-key'), apiKey: 'test-key',
baseUrl: 'https://gateway.example.test', disableCooling: false,
cloakMode: 'always', cloakCacheUserId: false,
};
const [loaded] = await save('claude', [buildProviderRecord('claude-api-key', draft)]);
expect(loaded['disable-cooling']).toBe(false);
expect(loaded.cloak).toMatchObject({ 'cache-user-id': false });
});
it('clears reasoning levels without restoring the previous values or dropping model options', async () => {
const current = { name: 'compat', 'base-url': 'https://gateway.example.test/v1',
'api-key-entries': [{ 'api-key': 'test-key' }], models: [
{ name: 'model', alias: 'first', 'max-context-length': 65536, thinking: { levels: ['low', 'high'] } },
{ name: 'model', alias: 'second', thinking: { min: 128, max: 8192, levels: ['high'] } },
],
};
const draft = applyProviderPreset('openai-compatibility', {
...createProviderDraft('openai-compatibility'), name: 'compat', apiKey: 'test-key',
baseUrl: current['base-url'], thinkingLevels: [], thinkingLevelsEdited: true,
models: [{ name: 'model', alias: 'first', thinking: { levels: ['low', 'high'] } }],
});
const [loaded] = await save('openai-compatibility', [buildProviderRecord('openai-compatibility', draft, current)]);
expect(loaded.models).toEqual([
{ name: 'model', alias: 'first', 'max-context-length': 65536 },
{ name: 'model', alias: 'second', thinking: { min: 128, max: 8192 } },
]);
});
});
+37 -2
View File
@@ -23,6 +23,7 @@ import {
stripResponseFields,
} from '../src/pages/ApiAccessPage';
import { modelsFromRecord } from '../src/services/modelService';
import { flattenV8ProviderGroups } from '../src/services/managementApi';
describe('shared-credential provider entries (#276)', () => {
const first = {
@@ -68,6 +69,39 @@ describe('shared-credential provider entries (#276)', () => {
expect(hasDuplicateProviderRecord('codex-api-key', [first], [second])).toBe(false);
});
it('rejects duplicate Codex and Claude credentials regardless of v8 group labels', () => {
for (const section of ['codex-api-key', 'claude-api-key'] as const) {
const records = flattenV8ProviderGroups(section.replace('-api-key', ''), [{
name: 'production', 'base-url': first['base-url'], priority: 10,
models: first.models, keys: [{ 'api-key': 'shared-key' }],
}]);
const candidate = buildProviderRecord(section, {
...createProviderDraft(section), apiKey: 'shared-key',
baseUrl: first['base-url'], priority: '10', models: first.models,
});
expect(hasDuplicateProviderRecord(section, records, [candidate])).toBe(true);
expect(hasDuplicateProviderRecord(section, records, [{ ...candidate, name: 'another-group' }])).toBe(true);
expect(hasDuplicateProviderRecord(section, records, [candidate], 0)).toBe(false);
expect(hasDuplicateProviderRecord(section, records, [{ ...candidate, priority: 1 }])).toBe(false);
expect(hasDuplicateProviderRecord(section, records, [{ ...candidate, models: second.models }])).toBe(false);
expect(records[0].name).toBe('production');
expect(candidate).not.toHaveProperty('name');
}
expect(hasDuplicateProviderRecord('openai-compatibility', [{ ...first, name: 'production' }], [
{ ...first, name: 'another-group' },
])).toBe(false);
});
it('keeps group labels in edit and remark identities when checking for duplicates', () => {
const production = { ...first, name: 'production' };
const staging = { ...first, name: 'staging' };
expect(hasDuplicateProviderRecord('claude-api-key', [production], [staging])).toBe(true);
expect(resolveProviderRecordIndex([staging, production], row(production, 0))).toBe(1);
expect(apiAccessRemarkLocatorFromRecord('claude-api-key', production).providerName).toBe('production');
expect(providerRemarkIdentity('claude-api-key', apiAccessRemarkLocatorFromRecord('claude-api-key', production)))
.not.toBe(providerRemarkIdentity('claude-api-key', apiAccessRemarkLocatorFromRecord('claude-api-key', staging)));
});
it('locates the second entry for edit/toggle/delete, even after an external reorder', () => {
const selected = row({ ...second, 'auth-index': 'runtime-second' }, 1);
expect(resolveProviderRecordIndex([first, second], selected)).toBe(1);
@@ -325,9 +359,10 @@ describe('API 接入配置合并', () => {
name: 'custom-openai',
baseUrl: 'https://api.example.com',
thinkingLevels: ['fast', 'ultra'],
thinkingLevelsEdited: true,
models: [
{ name: 'reasoning-a' },
{ name: 'reasoning-b', thinking: { effort: 'high' } },
{ name: 'reasoning-b', thinking: { min: 128, max: 8192 } },
],
});
const result = buildProviderRecord('openai-compatibility', draft);
@@ -336,7 +371,7 @@ describe('API 接入配置合并', () => {
{ name: 'reasoning-a', thinking: { levels: ['fast', 'ultra'] } },
{
name: 'reasoning-b',
thinking: { effort: 'high', levels: ['fast', 'ultra'] },
thinking: { min: 128, max: 8192, levels: ['fast', 'ultra'] },
},
]);
});
+63
View File
@@ -0,0 +1,63 @@
import { describe, expect, it } from 'bun:test';
import { applyProviderPreset, buildProviderRecord, createProviderDraft, stripResponseFields } from '../src/pages/ApiAccessPage';
import { modelsFromDiscoveredPayload, modelsFromRecord } from '../src/services/modelService';
describe('provider field editing semantics', () => {
it('distinguishes explicit false, inherited defaults and untouched optional settings', () => {
for (const section of ['codex-api-key', 'claude-api-key', 'gemini-api-key', 'openai-compatibility'] as const) {
const draft = { ...createProviderDraft(section), name: 'provider', apiKey: 'key', baseUrl: 'https://example.test/v1' };
const current = { 'disable-cooling': false, 'request-retry': 0, weight: 0 };
for (const value of [true, false, null, undefined]) {
const result = buildProviderRecord(section, { ...draft, disableCooling: value }, current);
if (value === null) expect(result).not.toHaveProperty('disable-cooling');
else expect(result['disable-cooling']).toBe(value ?? false);
expect(result['request-retry']).toBe(0);
expect(result.weight).toBe(0);
}
}
});
it('preserves other cloak fields during a partial update and can restore inherited caching', () => {
const draft = { name: '', remark: '', apiKey: 'key', baseUrl: '', priority: '', models: [] };
const current = { cloak: { mode: 'always', 'strict-mode': true, 'cache-user-id': true, 'sensitive-words': ['private'] } };
for (const value of [false, true, null]) {
const result = buildProviderRecord('claude-api-key', { ...draft, cloakCacheUserId: value }, current);
expect(result.cloak).toMatchObject({ mode: 'always', 'strict-mode': true, 'sensitive-words': ['private'] });
if (value === null) expect(result.cloak).not.toHaveProperty('cache-user-id');
else expect(result.cloak).toMatchObject({ 'cache-user-id': value });
}
expect(current.cloak['cache-user-id']).toBe(true);
});
it('leaves each model reasoning policy intact when only another field was edited', () => {
const current = { name: 'compat', models: [
{ name: 'first', thinking: { levels: ['low', 'high'] } },
{ name: 'second', thinking: { min: 128, max: 8192, levels: ['medium'] } },
] };
const draft = applyProviderPreset('openai-compatibility', {
...createProviderDraft('openai-compatibility'), name: 'compat', apiKey: 'key',
models: modelsFromRecord(current.models), thinkingLevels: ['low', 'high', 'medium'],
thinkingLevelsEdited: false, priority: '2',
});
expect(buildProviderRecord('openai-compatibility', draft, current).models).toEqual(current.models);
});
it('normalizes discovery JSON names and ignores non-config upstream reasoning metadata', () => {
const models = modelsFromDiscoveredPayload({ data: [{ id: 'model', thinking: {
min: 128, max: 8192, zero_allowed: true, 'zero-allowed': false, dynamic_allowed: true,
levels: ['low', 'high'], effort: 'high', vendor_metadata: { arbitrary: true },
} }, { id: 'bad-metadata', thinking: { min: 'oops', max: 1.5, levels: [1], zero_allowed: 'yes' } }] });
expect(models[0].thinking).toEqual({ min: 128, max: 8192, 'zero-allowed': false, 'dynamic-allowed': true, levels: ['low', 'high'] });
expect(models[1].thinking).toBeUndefined();
expect(stripResponseFields({ 'test-model': 'old', models: [{ name: 'model', thinking: { zero_allowed: true }, custom: true }] }))
.toEqual({ models: [{ name: 'model', thinking: { 'zero-allowed': true }, custom: true }] });
});
it('accepts negative priorities and rejects fractional, nonnumeric and unsafe values', () => {
const draft = { ...createProviderDraft('codex-api-key'), apiKey: 'key' };
expect(buildProviderRecord('codex-api-key', { ...draft, priority: '-2' }).priority).toBe(-2);
for (const priority of ['1.5', 'abc', 'Infinity', '9007199254740992']) {
expect(() => buildProviderRecord('codex-api-key', { ...draft, priority })).toThrow('Priority must be a safe integer');
}
});
});