mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 12:15:57 +00:00
148 lines
5.1 KiB
TypeScript
148 lines
5.1 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it } from 'bun:test';
|
|
import * as fs from 'fs';
|
|
import * as os from 'os';
|
|
import * as path from 'path';
|
|
import * as yaml from 'js-yaml';
|
|
|
|
function getCliproxyConfigPath(homeDir: string): string {
|
|
return path.join(homeDir, '.ccs', 'cliproxy', 'config.yaml');
|
|
}
|
|
|
|
function writeCliproxyConfig(homeDir: string, value: Record<string, unknown>): void {
|
|
const configPath = getCliproxyConfigPath(homeDir);
|
|
fs.mkdirSync(path.dirname(configPath), { recursive: true });
|
|
fs.writeFileSync(configPath, yaml.dump(value), 'utf8');
|
|
}
|
|
|
|
function readCliproxyConfig(homeDir: string): Record<string, any> {
|
|
return (
|
|
(yaml.load(fs.readFileSync(getCliproxyConfigPath(homeDir), 'utf8')) as Record<string, any>) || {}
|
|
);
|
|
}
|
|
|
|
async function loadAiProviderService() {
|
|
return import(`../../../src/cliproxy/ai-providers/service?stable-id=${Date.now()}`);
|
|
}
|
|
|
|
describe('ai-provider service stable ids', () => {
|
|
let tempHome = '';
|
|
let originalCcsHome: string | undefined;
|
|
|
|
beforeEach(() => {
|
|
tempHome = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-ai-provider-ids-'));
|
|
originalCcsHome = process.env.CCS_HOME;
|
|
process.env.CCS_HOME = tempHome;
|
|
});
|
|
|
|
afterEach(() => {
|
|
if (originalCcsHome !== undefined) {
|
|
process.env.CCS_HOME = originalCcsHome;
|
|
} else {
|
|
delete process.env.CCS_HOME;
|
|
}
|
|
|
|
if (tempHome && fs.existsSync(tempHome)) {
|
|
fs.rmSync(tempHome, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
it('backfills and persists stable ids for api-key provider entries', async () => {
|
|
const { listAiProviders } = await loadAiProviderService();
|
|
|
|
writeCliproxyConfig(tempHome, {
|
|
'gemini-api-key': [{ 'api-key': 'alpha' }, { 'api-key': 'beta' }],
|
|
});
|
|
|
|
const listed = await listAiProviders();
|
|
const family = listed.families.find((entry) => entry.id === 'gemini-api-key');
|
|
|
|
expect(family).toBeDefined();
|
|
expect(family?.entries).toHaveLength(2);
|
|
expect(family?.entries[0]?.id).toBeTruthy();
|
|
expect(family?.entries[1]?.id).toBeTruthy();
|
|
expect(family?.entries[0]?.id).not.toBe(family?.entries[1]?.id);
|
|
|
|
const persisted = readCliproxyConfig(tempHome)['gemini-api-key'] as Array<Record<string, unknown>>;
|
|
expect(persisted[0]?.id).toBe(family?.entries[0]?.id);
|
|
expect(persisted[1]?.id).toBe(family?.entries[1]?.id);
|
|
});
|
|
|
|
it('updates and deletes api-key entries by stable id while preserving the id', async () => {
|
|
const { updateAiProviderEntry, deleteAiProviderEntry } = await loadAiProviderService();
|
|
|
|
writeCliproxyConfig(tempHome, {
|
|
'gemini-api-key': [
|
|
{ 'api-key': 'alpha', id: 'gemini-a' },
|
|
{ 'api-key': 'beta', id: 'gemini-b' },
|
|
],
|
|
});
|
|
|
|
await updateAiProviderEntry('gemini-api-key', 'gemini-a', {
|
|
apiKey: 'gamma',
|
|
baseUrl: 'https://example.test/gemini',
|
|
});
|
|
|
|
let persisted = readCliproxyConfig(tempHome)['gemini-api-key'] as Array<Record<string, unknown>>;
|
|
expect(persisted[0]?.id).toBe('gemini-a');
|
|
expect(persisted[0]?.['api-key']).toBe('gamma');
|
|
expect(persisted[0]?.['base-url']).toBe('https://example.test/gemini');
|
|
|
|
await deleteAiProviderEntry('gemini-api-key', 'gemini-a');
|
|
|
|
persisted = readCliproxyConfig(tempHome)['gemini-api-key'] as Array<Record<string, unknown>>;
|
|
expect(persisted).toHaveLength(1);
|
|
expect(persisted[0]?.id).toBe('gemini-b');
|
|
});
|
|
|
|
it('keeps legacy numeric index updates working during the route transition', async () => {
|
|
const { updateAiProviderEntry } = await loadAiProviderService();
|
|
|
|
writeCliproxyConfig(tempHome, {
|
|
'gemini-api-key': [{ 'api-key': 'alpha', id: 'gemini-a' }],
|
|
});
|
|
|
|
await updateAiProviderEntry('gemini-api-key', '0', {
|
|
apiKey: 'legacy-index-update',
|
|
});
|
|
|
|
const persisted = readCliproxyConfig(tempHome)['gemini-api-key'] as Array<Record<string, unknown>>;
|
|
expect(persisted[0]?.id).toBe('gemini-a');
|
|
expect(persisted[0]?.['api-key']).toBe('legacy-index-update');
|
|
});
|
|
|
|
it('backfills and preserves stable ids for openai-compatible connectors', async () => {
|
|
const { listAiProviders, updateAiProviderEntry } = await loadAiProviderService();
|
|
|
|
writeCliproxyConfig(tempHome, {
|
|
'openai-compatibility': [
|
|
{
|
|
name: 'openrouter',
|
|
'base-url': 'https://openrouter.ai/api/v1',
|
|
'api-key-entries': [{ 'api-key': 'sk-openrouter' }],
|
|
},
|
|
],
|
|
});
|
|
|
|
const listed = await listAiProviders();
|
|
const family = listed.families.find((entry) => entry.id === 'openai-compatibility');
|
|
const connectorId = family?.entries[0]?.id;
|
|
|
|
expect(connectorId).toBeTruthy();
|
|
|
|
await updateAiProviderEntry('openai-compatibility', connectorId!, {
|
|
name: 'openrouter',
|
|
baseUrl: 'https://router.example/v1',
|
|
preserveSecrets: true,
|
|
});
|
|
|
|
const persisted = readCliproxyConfig(tempHome)['openai-compatibility'] as Array<
|
|
Record<string, unknown>
|
|
>;
|
|
expect(persisted[0]?.id).toBe(connectorId);
|
|
expect(persisted[0]?.['base-url']).toBe('https://router.example/v1');
|
|
expect((persisted[0]?.['api-key-entries'] as Array<Record<string, unknown>>)[0]?.['api-key']).toBe(
|
|
'sk-openrouter'
|
|
);
|
|
});
|
|
});
|