Files
EasyCLIProxyAPI/tests/coreOperations.v8.integration.test.ts
T

174 lines
9.9 KiB
TypeScript

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' } });
});
});