mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 08:17:11 +00:00
735 lines
24 KiB
TypeScript
735 lines
24 KiB
TypeScript
/**
|
|
* Unit tests for Droid config manager
|
|
*/
|
|
import { describe, it, expect, beforeEach, afterEach } from 'bun:test';
|
|
import * as fs from 'fs';
|
|
import * as path from 'path';
|
|
import * as os from 'os';
|
|
import {
|
|
upsertCcsModel,
|
|
removeCcsModel,
|
|
listCcsModels,
|
|
pruneOrphanedModels,
|
|
} from '../../../src/targets/droid-config-manager';
|
|
|
|
describe('droid-config-manager', () => {
|
|
let tmpDir: string;
|
|
let originalCcsHome: string | undefined;
|
|
|
|
beforeEach(() => {
|
|
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-droid-test-'));
|
|
originalCcsHome = process.env.CCS_HOME;
|
|
process.env.CCS_HOME = tmpDir;
|
|
});
|
|
|
|
afterEach(() => {
|
|
if (originalCcsHome !== undefined) {
|
|
process.env.CCS_HOME = originalCcsHome;
|
|
} else {
|
|
delete process.env.CCS_HOME;
|
|
}
|
|
fs.rmSync(tmpDir, { recursive: true, force: true });
|
|
});
|
|
|
|
describe('upsertCcsModel', () => {
|
|
it('should return a selector reference for the managed model', async () => {
|
|
const ref = await upsertCcsModel('gemini', {
|
|
model: 'claude-opus-4-6',
|
|
displayName: 'CCS gemini',
|
|
baseUrl: 'http://localhost:8317',
|
|
apiKey: 'dummy-key',
|
|
provider: 'anthropic',
|
|
});
|
|
|
|
expect(ref.profile).toBe('gemini');
|
|
expect(ref.selectorAlias).toBe('CCS-gemini-0');
|
|
expect(ref.selector).toBe('custom:CCS-gemini-0');
|
|
expect(ref.index).toBe(0);
|
|
|
|
const settingsPath = path.join(tmpDir, '.factory', 'settings.json');
|
|
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
expect(settings.model).toBe('custom:CCS-gemini-0');
|
|
});
|
|
|
|
it('should create settings.json with customModels', async () => {
|
|
await upsertCcsModel('gemini', {
|
|
model: 'claude-opus-4-6',
|
|
displayName: 'CCS gemini',
|
|
baseUrl: 'http://localhost:8317',
|
|
apiKey: 'dummy-key',
|
|
provider: 'anthropic',
|
|
});
|
|
|
|
const settingsPath = path.join(tmpDir, '.factory', 'settings.json');
|
|
expect(fs.existsSync(settingsPath)).toBe(true);
|
|
|
|
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
expect(settings.customModels).toHaveLength(1);
|
|
expect(settings.customModels[0].displayName).toBe('CCS gemini');
|
|
expect(settings.customModels[0].baseUrl).toBe('http://localhost:8317');
|
|
expect(settings.model).toBe('custom:CCS-gemini-0');
|
|
});
|
|
|
|
it('should update existing entry on second upsert', async () => {
|
|
await upsertCcsModel('gemini', {
|
|
model: 'claude-opus-4-6',
|
|
displayName: 'CCS gemini',
|
|
baseUrl: 'http://localhost:8317',
|
|
apiKey: 'key-1',
|
|
provider: 'anthropic',
|
|
});
|
|
|
|
await upsertCcsModel('gemini', {
|
|
model: 'claude-opus-4-6',
|
|
displayName: 'CCS gemini',
|
|
baseUrl: 'http://localhost:8318',
|
|
apiKey: 'key-2',
|
|
provider: 'anthropic',
|
|
});
|
|
|
|
const settingsPath = path.join(tmpDir, '.factory', 'settings.json');
|
|
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
expect(settings.customModels).toHaveLength(1);
|
|
expect(settings.customModels[0].apiKey).toBe('key-2');
|
|
expect(settings.customModels[0].baseUrl).toBe('http://localhost:8318');
|
|
});
|
|
|
|
it('should persist generic provider reasoning_effort from override', async () => {
|
|
await upsertCcsModel('glm', {
|
|
model: 'glm-4.7',
|
|
displayName: 'CCS glm',
|
|
baseUrl: 'https://api.z.ai/api/coding/paas/v4',
|
|
apiKey: 'glm-key',
|
|
provider: 'generic-chat-completion-api',
|
|
reasoningOverride: 'high',
|
|
});
|
|
|
|
const settingsPath = path.join(tmpDir, '.factory', 'settings.json');
|
|
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
expect(settings.customModels[0].extraArgs?.reasoning_effort).toBe('high');
|
|
expect(settings.customModels[0].extraArgs?.reasoning).toBeUndefined();
|
|
expect(settings.customModels[0].extraArgs?.thinking).toBeUndefined();
|
|
});
|
|
|
|
it('should persist openai provider reasoning.effort from --effort alias override', async () => {
|
|
await upsertCcsModel('codex', {
|
|
model: 'gpt-5.2',
|
|
displayName: 'CCS codex',
|
|
baseUrl: 'https://api.openai.com/v1',
|
|
apiKey: 'openai-key',
|
|
provider: 'openai',
|
|
reasoningOverride: 'xhigh',
|
|
});
|
|
|
|
const settingsPath = path.join(tmpDir, '.factory', 'settings.json');
|
|
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
expect(settings.customModels[0].extraArgs?.reasoning?.effort).toBe('xhigh');
|
|
expect(settings.customModels[0].extraArgs?.reasoning_effort).toBeUndefined();
|
|
});
|
|
|
|
it('should persist anthropic thinking budget from numeric override', async () => {
|
|
await upsertCcsModel('agy', {
|
|
model: 'claude-opus-4-5-thinking',
|
|
displayName: 'CCS agy',
|
|
baseUrl: 'https://api.anthropic.com',
|
|
apiKey: 'anthropic-key',
|
|
provider: 'anthropic',
|
|
reasoningOverride: 40960,
|
|
});
|
|
|
|
const settingsPath = path.join(tmpDir, '.factory', 'settings.json');
|
|
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
expect(settings.customModels[0].extraArgs?.thinking?.type).toBe('enabled');
|
|
expect(settings.customModels[0].extraArgs?.thinking?.budget_tokens).toBe(40960);
|
|
});
|
|
|
|
it('should clear prior reasoning config when override disables thinking', async () => {
|
|
await upsertCcsModel('glm', {
|
|
model: 'glm-4.7',
|
|
displayName: 'CCS glm',
|
|
baseUrl: 'https://api.z.ai/api/coding/paas/v4',
|
|
apiKey: 'glm-key',
|
|
provider: 'generic-chat-completion-api',
|
|
reasoningOverride: 'high',
|
|
});
|
|
|
|
await upsertCcsModel('glm', {
|
|
model: 'glm-4.7',
|
|
displayName: 'CCS glm',
|
|
baseUrl: 'https://api.z.ai/api/coding/paas/v4',
|
|
apiKey: 'glm-key',
|
|
provider: 'generic-chat-completion-api',
|
|
reasoningOverride: 'off',
|
|
});
|
|
|
|
const settingsPath = path.join(tmpDir, '.factory', 'settings.json');
|
|
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
expect(settings.customModels[0].extraArgs).toBeUndefined();
|
|
});
|
|
|
|
it('should preserve user entries', async () => {
|
|
// Create existing settings with user's own custom model
|
|
const factoryDir = path.join(tmpDir, '.factory');
|
|
fs.mkdirSync(factoryDir, { recursive: true });
|
|
fs.writeFileSync(
|
|
path.join(factoryDir, 'settings.json'),
|
|
JSON.stringify({
|
|
customModels: [
|
|
{
|
|
model: 'gpt-4o',
|
|
displayName: 'My GPT',
|
|
baseUrl: 'https://api.openai.com',
|
|
apiKey: 'sk-xxx',
|
|
provider: 'openai',
|
|
},
|
|
],
|
|
})
|
|
);
|
|
|
|
await upsertCcsModel('gemini', {
|
|
model: 'claude-opus-4-6',
|
|
displayName: 'CCS gemini',
|
|
baseUrl: 'http://localhost:8317',
|
|
apiKey: 'dummy',
|
|
provider: 'anthropic',
|
|
});
|
|
|
|
const settings = JSON.parse(fs.readFileSync(path.join(factoryDir, 'settings.json'), 'utf8'));
|
|
expect(settings.customModels).toHaveLength(2);
|
|
expect(settings.customModels[0].displayName).toBe('My GPT');
|
|
expect(settings.customModels[1].displayName).toBe('CCS gemini');
|
|
});
|
|
|
|
it('should preserve user entries with unknown provider strings', async () => {
|
|
const factoryDir = path.join(tmpDir, '.factory');
|
|
fs.mkdirSync(factoryDir, { recursive: true });
|
|
fs.writeFileSync(
|
|
path.join(factoryDir, 'settings.json'),
|
|
JSON.stringify({
|
|
customModels: [
|
|
{
|
|
model: 'user-model',
|
|
displayName: 'My Custom Provider',
|
|
baseUrl: 'https://example.invalid',
|
|
apiKey: 'user-key',
|
|
provider: 'custom-provider',
|
|
},
|
|
],
|
|
})
|
|
);
|
|
|
|
await upsertCcsModel('gemini', {
|
|
model: 'claude-opus-4-6',
|
|
displayName: 'CCS gemini',
|
|
baseUrl: 'http://localhost:8317',
|
|
apiKey: 'dummy',
|
|
provider: 'anthropic',
|
|
});
|
|
|
|
const settings = JSON.parse(fs.readFileSync(path.join(factoryDir, 'settings.json'), 'utf8'));
|
|
expect(settings.customModels).toHaveLength(2);
|
|
expect(settings.customModels[0].provider).toBe('custom-provider');
|
|
expect(settings.customModels[1].displayName).toBe('CCS gemini');
|
|
});
|
|
|
|
it('should write with restricted permissions', async () => {
|
|
await upsertCcsModel('test', {
|
|
model: 'test-model',
|
|
displayName: 'CCS test',
|
|
baseUrl: 'http://localhost:8317',
|
|
apiKey: 'secret',
|
|
provider: 'anthropic',
|
|
});
|
|
|
|
const settingsPath = path.join(tmpDir, '.factory', 'settings.json');
|
|
const stat = fs.statSync(settingsPath);
|
|
// eslint-disable-next-line no-bitwise
|
|
const otherPerms = stat.mode & 0o077;
|
|
expect(otherPerms).toBe(0);
|
|
});
|
|
|
|
it('should reject symlinked temp file path', async () => {
|
|
const factoryDir = path.join(tmpDir, '.factory');
|
|
fs.mkdirSync(factoryDir, { recursive: true });
|
|
fs.writeFileSync(
|
|
path.join(factoryDir, 'settings.json'),
|
|
JSON.stringify({ customModels: [] })
|
|
);
|
|
fs.symlinkSync('/tmp', path.join(factoryDir, 'settings.json.tmp'));
|
|
|
|
await expect(
|
|
upsertCcsModel('gemini', {
|
|
model: 'claude-opus-4-6',
|
|
displayName: 'CCS gemini',
|
|
baseUrl: 'http://localhost:8317',
|
|
apiKey: 'dummy-key',
|
|
provider: 'anthropic',
|
|
})
|
|
).rejects.toThrow(/settings\.json\.tmp is a symlink/);
|
|
});
|
|
|
|
it('should update legacy ccs- alias entry on upsert', async () => {
|
|
const factoryDir = path.join(tmpDir, '.factory');
|
|
fs.mkdirSync(factoryDir, { recursive: true });
|
|
fs.writeFileSync(
|
|
path.join(factoryDir, 'settings.json'),
|
|
JSON.stringify({
|
|
customModels: [
|
|
{
|
|
model: 'claude-opus-4-6',
|
|
displayName: 'ccs-gemini',
|
|
baseUrl: 'http://localhost:8317',
|
|
apiKey: 'old-key',
|
|
provider: 'anthropic',
|
|
},
|
|
],
|
|
})
|
|
);
|
|
|
|
await upsertCcsModel('gemini', {
|
|
model: 'claude-opus-4-6',
|
|
displayName: 'CCS gemini',
|
|
baseUrl: 'http://localhost:8318',
|
|
apiKey: 'new-key',
|
|
provider: 'anthropic',
|
|
});
|
|
|
|
const settings = JSON.parse(fs.readFileSync(path.join(factoryDir, 'settings.json'), 'utf8'));
|
|
expect(settings.customModels).toHaveLength(1);
|
|
expect(settings.customModels[0].displayName).toBe('CCS gemini');
|
|
expect(settings.customModels[0].apiKey).toBe('new-key');
|
|
expect(settings.customModels[0].baseUrl).toBe('http://localhost:8318');
|
|
});
|
|
|
|
it('should reject symlinked settings file on write', async () => {
|
|
const factoryDir = path.join(tmpDir, '.factory');
|
|
fs.mkdirSync(factoryDir, { recursive: true });
|
|
|
|
const realSettings = path.join(factoryDir, 'real-settings.json');
|
|
fs.writeFileSync(realSettings, JSON.stringify({ customModels: [] }));
|
|
fs.symlinkSync(realSettings, path.join(factoryDir, 'settings.json'));
|
|
|
|
await expect(
|
|
upsertCcsModel('gemini', {
|
|
model: 'claude-opus-4-6',
|
|
displayName: 'CCS gemini',
|
|
baseUrl: 'http://localhost:8317',
|
|
apiKey: 'dummy-key',
|
|
provider: 'anthropic',
|
|
})
|
|
).rejects.toThrow(/settings\.json is a symlink/);
|
|
});
|
|
});
|
|
|
|
describe('removeCcsModel', () => {
|
|
it('should remove a CCS entry', async () => {
|
|
await upsertCcsModel('gemini', {
|
|
model: 'claude-opus-4-6',
|
|
displayName: 'CCS gemini',
|
|
baseUrl: 'http://localhost:8317',
|
|
apiKey: 'dummy',
|
|
provider: 'anthropic',
|
|
});
|
|
|
|
await removeCcsModel('gemini');
|
|
|
|
const settingsPath = path.join(tmpDir, '.factory', 'settings.json');
|
|
const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
|
|
expect(settings.customModels).toHaveLength(0);
|
|
});
|
|
|
|
it('should not remove user entries', async () => {
|
|
const factoryDir = path.join(tmpDir, '.factory');
|
|
fs.mkdirSync(factoryDir, { recursive: true });
|
|
fs.writeFileSync(
|
|
path.join(factoryDir, 'settings.json'),
|
|
JSON.stringify({
|
|
customModels: [
|
|
{
|
|
model: 'gpt-4o',
|
|
displayName: 'My GPT',
|
|
baseUrl: 'x',
|
|
apiKey: 'y',
|
|
provider: 'openai',
|
|
},
|
|
{
|
|
model: 'opus',
|
|
displayName: 'CCS gemini',
|
|
baseUrl: 'x',
|
|
apiKey: 'y',
|
|
provider: 'anthropic',
|
|
},
|
|
],
|
|
})
|
|
);
|
|
|
|
await removeCcsModel('gemini');
|
|
|
|
const settings = JSON.parse(fs.readFileSync(path.join(factoryDir, 'settings.json'), 'utf8'));
|
|
expect(settings.customModels).toHaveLength(1);
|
|
expect(settings.customModels[0].displayName).toBe('My GPT');
|
|
});
|
|
|
|
it('should remove legacy ccs- alias entries', async () => {
|
|
const factoryDir = path.join(tmpDir, '.factory');
|
|
fs.mkdirSync(factoryDir, { recursive: true });
|
|
fs.writeFileSync(
|
|
path.join(factoryDir, 'settings.json'),
|
|
JSON.stringify({
|
|
customModels: [
|
|
{
|
|
model: 'opus',
|
|
displayName: 'ccs-gemini',
|
|
baseUrl: 'x',
|
|
apiKey: 'y',
|
|
provider: 'anthropic',
|
|
},
|
|
{
|
|
model: 'gpt-4o',
|
|
displayName: 'My GPT',
|
|
baseUrl: 'x',
|
|
apiKey: 'y',
|
|
provider: 'openai',
|
|
},
|
|
],
|
|
})
|
|
);
|
|
|
|
await removeCcsModel('gemini');
|
|
|
|
const settings = JSON.parse(fs.readFileSync(path.join(factoryDir, 'settings.json'), 'utf8'));
|
|
expect(settings.customModels).toHaveLength(1);
|
|
expect(settings.customModels[0].displayName).toBe('My GPT');
|
|
});
|
|
});
|
|
|
|
describe('listCcsModels', () => {
|
|
it('should list only CCS entries', async () => {
|
|
await upsertCcsModel('gemini', {
|
|
model: 'opus',
|
|
displayName: 'CCS gemini',
|
|
baseUrl: 'http://localhost:8317',
|
|
apiKey: 'dummy',
|
|
provider: 'anthropic',
|
|
});
|
|
|
|
await upsertCcsModel('codex', {
|
|
model: 'sonnet',
|
|
displayName: 'CCS codex',
|
|
baseUrl: 'http://localhost:8317',
|
|
apiKey: 'dummy',
|
|
provider: 'anthropic',
|
|
});
|
|
|
|
const models = await listCcsModels();
|
|
expect(models.size).toBe(2);
|
|
expect(models.has('gemini')).toBe(true);
|
|
expect(models.has('codex')).toBe(true);
|
|
});
|
|
|
|
it('should return empty map when no settings file', async () => {
|
|
const models = await listCcsModels();
|
|
expect(models.size).toBe(0);
|
|
});
|
|
|
|
it('should normalize legacy object-map customModels', async () => {
|
|
const factoryDir = path.join(tmpDir, '.factory');
|
|
fs.mkdirSync(factoryDir, { recursive: true });
|
|
fs.writeFileSync(
|
|
path.join(factoryDir, 'settings.json'),
|
|
JSON.stringify({
|
|
customModels: {
|
|
gemini: {
|
|
model: 'opus',
|
|
displayName: 'CCS gemini',
|
|
baseUrl: 'http://localhost:8317',
|
|
apiKey: 'dummy',
|
|
provider: 'anthropic',
|
|
},
|
|
invalid: {
|
|
model: 'x',
|
|
baseUrl: 'x',
|
|
},
|
|
},
|
|
})
|
|
);
|
|
|
|
const models = await listCcsModels();
|
|
expect(models.size).toBe(1);
|
|
expect(models.has('gemini')).toBe(true);
|
|
});
|
|
|
|
it('should ignore malformed customModels entries', async () => {
|
|
const factoryDir = path.join(tmpDir, '.factory');
|
|
fs.mkdirSync(factoryDir, { recursive: true });
|
|
fs.writeFileSync(
|
|
path.join(factoryDir, 'settings.json'),
|
|
JSON.stringify({
|
|
customModels: [
|
|
null,
|
|
123,
|
|
'bad',
|
|
{ displayName: 'CCS ok', model: 'x', baseUrl: 'x', apiKey: 'y', provider: 'anthropic' },
|
|
],
|
|
})
|
|
);
|
|
|
|
const models = await listCcsModels();
|
|
expect(models.size).toBe(1);
|
|
expect(models.has('ok')).toBe(true);
|
|
});
|
|
|
|
it('should reject symlinked settings file on read', async () => {
|
|
const factoryDir = path.join(tmpDir, '.factory');
|
|
fs.mkdirSync(factoryDir, { recursive: true });
|
|
const target = path.join(factoryDir, 'real-settings.json');
|
|
fs.writeFileSync(target, JSON.stringify({ customModels: [] }));
|
|
fs.symlinkSync(target, path.join(factoryDir, 'settings.json'));
|
|
|
|
await expect(listCcsModels()).rejects.toThrow(/settings\.json is a symlink/);
|
|
});
|
|
|
|
it('should include legacy ccs- alias entries', async () => {
|
|
const factoryDir = path.join(tmpDir, '.factory');
|
|
fs.mkdirSync(factoryDir, { recursive: true });
|
|
fs.writeFileSync(
|
|
path.join(factoryDir, 'settings.json'),
|
|
JSON.stringify({
|
|
customModels: [
|
|
{
|
|
model: 'opus',
|
|
displayName: 'ccs-gemini',
|
|
baseUrl: 'http://localhost:8317',
|
|
apiKey: 'dummy',
|
|
provider: 'anthropic',
|
|
},
|
|
],
|
|
})
|
|
);
|
|
|
|
const models = await listCcsModels();
|
|
expect(models.size).toBe(1);
|
|
expect(models.has('gemini')).toBe(true);
|
|
});
|
|
|
|
it('should ignore malformed managed display names', async () => {
|
|
const factoryDir = path.join(tmpDir, '.factory');
|
|
fs.mkdirSync(factoryDir, { recursive: true });
|
|
fs.writeFileSync(
|
|
path.join(factoryDir, 'settings.json'),
|
|
JSON.stringify({
|
|
customModels: [
|
|
{ model: 'x', displayName: 'CCS ', baseUrl: 'x', apiKey: 'y', provider: 'anthropic' },
|
|
{ model: 'x', displayName: 'ccs-', baseUrl: 'x', apiKey: 'y', provider: 'anthropic' },
|
|
{ model: 'x', displayName: 'CCS ok', baseUrl: 'x', apiKey: 'y', provider: 'anthropic' },
|
|
],
|
|
})
|
|
);
|
|
|
|
const models = await listCcsModels();
|
|
expect(models.size).toBe(1);
|
|
expect(models.has('ok')).toBe(true);
|
|
});
|
|
|
|
it('should recover from corrupted JSON by backing up and returning empty models', async () => {
|
|
const factoryDir = path.join(tmpDir, '.factory');
|
|
fs.mkdirSync(factoryDir, { recursive: true });
|
|
const settingsPath = path.join(factoryDir, 'settings.json');
|
|
|
|
fs.writeFileSync(settingsPath, '{"customModels":[', 'utf8');
|
|
|
|
const models = await listCcsModels();
|
|
expect(models.size).toBe(0);
|
|
expect(fs.existsSync(`${settingsPath}.bak`)).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('pruneOrphanedModels', () => {
|
|
it('should remove orphaned CCS entries', async () => {
|
|
await upsertCcsModel('gemini', {
|
|
model: 'opus',
|
|
displayName: 'CCS gemini',
|
|
baseUrl: 'x',
|
|
apiKey: 'y',
|
|
provider: 'anthropic',
|
|
});
|
|
await upsertCcsModel('codex', {
|
|
model: 'sonnet',
|
|
displayName: 'CCS codex',
|
|
baseUrl: 'x',
|
|
apiKey: 'y',
|
|
provider: 'anthropic',
|
|
});
|
|
|
|
// Only gemini is active — codex should be pruned
|
|
const removed = await pruneOrphanedModels(['gemini']);
|
|
expect(removed).toBe(1);
|
|
|
|
const models = await listCcsModels();
|
|
expect(models.size).toBe(1);
|
|
expect(models.has('gemini')).toBe(true);
|
|
});
|
|
|
|
it('should preserve user entries during prune', async () => {
|
|
const factoryDir = path.join(tmpDir, '.factory');
|
|
fs.mkdirSync(factoryDir, { recursive: true });
|
|
fs.writeFileSync(
|
|
path.join(factoryDir, 'settings.json'),
|
|
JSON.stringify({
|
|
customModels: [
|
|
{
|
|
model: 'gpt-4o',
|
|
displayName: 'My GPT',
|
|
baseUrl: 'x',
|
|
apiKey: 'y',
|
|
provider: 'openai',
|
|
},
|
|
{
|
|
model: 'opus',
|
|
displayName: 'CCS old-profile',
|
|
baseUrl: 'x',
|
|
apiKey: 'y',
|
|
provider: 'anthropic',
|
|
},
|
|
],
|
|
})
|
|
);
|
|
|
|
const removed = await pruneOrphanedModels([]);
|
|
expect(removed).toBe(1);
|
|
|
|
const settings = JSON.parse(fs.readFileSync(path.join(factoryDir, 'settings.json'), 'utf8'));
|
|
expect(settings.customModels).toHaveLength(1);
|
|
expect(settings.customModels[0].displayName).toBe('My GPT');
|
|
});
|
|
|
|
it('should prune orphaned legacy ccs- alias entries', async () => {
|
|
const factoryDir = path.join(tmpDir, '.factory');
|
|
fs.mkdirSync(factoryDir, { recursive: true });
|
|
fs.writeFileSync(
|
|
path.join(factoryDir, 'settings.json'),
|
|
JSON.stringify({
|
|
customModels: [
|
|
{
|
|
model: 'opus',
|
|
displayName: 'ccs-gemini',
|
|
baseUrl: 'x',
|
|
apiKey: 'y',
|
|
provider: 'anthropic',
|
|
},
|
|
{
|
|
model: 'sonnet',
|
|
displayName: 'ccs-codex',
|
|
baseUrl: 'x',
|
|
apiKey: 'y',
|
|
provider: 'anthropic',
|
|
},
|
|
],
|
|
})
|
|
);
|
|
|
|
const removed = await pruneOrphanedModels(['gemini']);
|
|
expect(removed).toBe(1);
|
|
|
|
const models = await listCcsModels();
|
|
expect(models.size).toBe(1);
|
|
expect(models.has('gemini')).toBe(true);
|
|
});
|
|
|
|
it('should prune malformed managed entries while preserving user models', async () => {
|
|
const factoryDir = path.join(tmpDir, '.factory');
|
|
fs.mkdirSync(factoryDir, { recursive: true });
|
|
fs.writeFileSync(
|
|
path.join(factoryDir, 'settings.json'),
|
|
JSON.stringify({
|
|
customModels: [
|
|
{ model: 'x', displayName: 'CCS ', baseUrl: 'x', apiKey: 'y', provider: 'anthropic' },
|
|
{ model: 'x', displayName: 'ccs-', baseUrl: 'x', apiKey: 'y', provider: 'anthropic' },
|
|
{
|
|
model: 'gpt-4o',
|
|
displayName: 'My GPT',
|
|
baseUrl: 'x',
|
|
apiKey: 'y',
|
|
provider: 'openai',
|
|
},
|
|
],
|
|
})
|
|
);
|
|
|
|
const removed = await pruneOrphanedModels([]);
|
|
expect(removed).toBe(2);
|
|
|
|
const settings = JSON.parse(fs.readFileSync(path.join(factoryDir, 'settings.json'), 'utf8'));
|
|
expect(settings.customModels).toHaveLength(1);
|
|
expect(settings.customModels[0].displayName).toBe('My GPT');
|
|
});
|
|
|
|
it('should reject invalid active profile names', async () => {
|
|
await expect(pruneOrphanedModels(['bad profile'])).rejects.toThrow(/Invalid profile name/);
|
|
});
|
|
|
|
it('should use active profile snapshot taken at call time', async () => {
|
|
const factoryDir = path.join(tmpDir, '.factory');
|
|
fs.mkdirSync(factoryDir, { recursive: true });
|
|
fs.writeFileSync(
|
|
path.join(factoryDir, 'settings.json'),
|
|
JSON.stringify({
|
|
customModels: [
|
|
{
|
|
model: 'opus',
|
|
displayName: 'CCS gemini',
|
|
baseUrl: 'x',
|
|
apiKey: 'y',
|
|
provider: 'anthropic',
|
|
},
|
|
{
|
|
model: 'sonnet',
|
|
displayName: 'CCS codex',
|
|
baseUrl: 'x',
|
|
apiKey: 'y',
|
|
provider: 'anthropic',
|
|
},
|
|
],
|
|
})
|
|
);
|
|
|
|
const activeProfiles = ['gemini'];
|
|
const prunePromise = pruneOrphanedModels(activeProfiles);
|
|
activeProfiles.push('codex'); // Mutation after call should not affect in-flight prune decision.
|
|
|
|
const removed = await prunePromise;
|
|
expect(removed).toBe(1);
|
|
|
|
const models = await listCcsModels();
|
|
expect(models.size).toBe(1);
|
|
expect(models.has('gemini')).toBe(true);
|
|
expect(models.has('codex')).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('concurrent writes', () => {
|
|
it('should handle concurrent upserts without data loss', async () => {
|
|
const profiles = Array.from({ length: 10 }, (_, i) => `profile-${i}`);
|
|
|
|
await Promise.all(
|
|
profiles.map((p) =>
|
|
upsertCcsModel(p, {
|
|
model: 'test-model',
|
|
displayName: `CCS ${p}`,
|
|
baseUrl: 'http://localhost:8317',
|
|
apiKey: 'key',
|
|
provider: 'anthropic',
|
|
})
|
|
)
|
|
);
|
|
|
|
const models = await listCcsModels();
|
|
expect(models.size).toBe(10);
|
|
|
|
for (const p of profiles) {
|
|
expect(models.has(p)).toBe(true);
|
|
}
|
|
}, 15000);
|
|
});
|
|
});
|