Files
ccs/tests/unit/cliproxy/composite-variant-service.test.ts
T

690 lines
20 KiB
TypeScript

/**
* Unit tests for composite variant service operations
*/
import * as fs from 'fs';
import * as os from 'os';
import * as path from 'path';
import { describe, it, expect, beforeEach, afterEach } from 'bun:test';
import {
createCompositeVariant,
updateCompositeVariant,
} from '../../../src/cliproxy/services/variant-service';
import {
saveCompositeVariantUnified,
listVariantsFromConfig,
} from '../../../src/cliproxy/services/variant-config-adapter';
import { CompositeVariantConfig } from '../../../src/config/unified-config-types';
describe('updateCompositeVariant', () => {
let tmpDir: string;
let originalCcsDir: string | undefined;
beforeEach(() => {
// Create temp directory for isolated config
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-composite-test-'));
originalCcsDir = process.env.CCS_DIR;
// Use CCS_DIR (not CCS_HOME which appends .ccs)
process.env.CCS_DIR = tmpDir;
// Create unified config file
const configDir = tmpDir;
const configPath = path.join(configDir, 'config.yaml');
fs.mkdirSync(configDir, { recursive: true });
fs.writeFileSync(
configPath,
`version: 2
accounts: {}
profiles: {}
preferences:
theme: system
telemetry: false
auto_update: true
cliproxy:
oauth_accounts: {}
providers:
- gemini
- codex
- agy
variants: {}
`,
'utf-8'
);
});
afterEach(() => {
// Restore original CCS_DIR
if (originalCcsDir !== undefined) {
process.env.CCS_DIR = originalCcsDir;
} else {
delete process.env.CCS_DIR;
}
// Clean up temp directory
if (tmpDir && fs.existsSync(tmpDir)) {
fs.rmSync(tmpDir, { recursive: true, force: true });
}
});
it('should update partial tier config (only opus model)', () => {
// Setup: Create initial composite variant
const initialConfig: CompositeVariantConfig = {
type: 'composite',
default_tier: 'sonnet',
tiers: {
opus: { provider: 'gemini', model: 'gemini-3-pro-preview' },
sonnet: { provider: 'agy', model: 'claude-sonnet-4-6' },
haiku: { provider: 'agy', model: 'claude-haiku-4-5-20251001' },
},
settings: 'cliproxy/composite-test.settings.json',
port: 8318,
};
saveCompositeVariantUnified('test', initialConfig);
// Create dummy settings file to avoid deletion error
const settingsDir = path.join(tmpDir, 'cliproxy');
fs.mkdirSync(settingsDir, { recursive: true });
fs.writeFileSync(
path.join(settingsDir, 'composite-test.settings.json'),
JSON.stringify({ env: {} }),
'utf-8'
);
// Test: Update only opus tier
const result = updateCompositeVariant('test', {
tiers: {
opus: { provider: 'agy', model: 'claude-opus-4-6-thinking' },
},
});
expect(result.success).toBe(true);
expect(result.variant?.tiers?.opus.model).toBe('claude-opus-4-6-thinking');
expect(result.variant?.tiers?.sonnet.model).toBe('claude-sonnet-4-6'); // Unchanged
expect(result.variant?.tiers?.haiku.model).toBe('claude-haiku-4-5-20251001'); // Unchanged
});
it('should update default tier', () => {
// Setup: Create initial composite variant
const initialConfig: CompositeVariantConfig = {
type: 'composite',
default_tier: 'sonnet',
tiers: {
opus: { provider: 'gemini', model: 'gemini-3-pro-preview' },
sonnet: { provider: 'agy', model: 'claude-sonnet-4-6' },
haiku: { provider: 'agy', model: 'claude-haiku-4-5-20251001' },
},
settings: 'cliproxy/composite-test.settings.json',
port: 8318,
};
saveCompositeVariantUnified('test', initialConfig);
// Create dummy settings file
const settingsDir = path.join(tmpDir, 'cliproxy');
fs.mkdirSync(settingsDir, { recursive: true });
fs.writeFileSync(
path.join(settingsDir, 'composite-test.settings.json'),
JSON.stringify({ env: {} }),
'utf-8'
);
// Test: Update default tier
const result = updateCompositeVariant('test', {
defaultTier: 'opus',
});
expect(result.success).toBe(true);
expect(result.variant?.default_tier).toBe('opus');
expect(result.variant?.provider).toBe('gemini'); // Provider from opus tier
});
it('should update all tiers', () => {
// Setup: Create initial composite variant
const initialConfig: CompositeVariantConfig = {
type: 'composite',
default_tier: 'sonnet',
tiers: {
opus: { provider: 'gemini', model: 'gemini-3-pro-preview' },
sonnet: { provider: 'agy', model: 'claude-sonnet-4-6' },
haiku: { provider: 'agy', model: 'claude-haiku-4-5-20251001' },
},
settings: 'cliproxy/composite-test.settings.json',
port: 8318,
};
saveCompositeVariantUnified('test', initialConfig);
// Create dummy settings file
const settingsDir = path.join(tmpDir, 'cliproxy');
fs.mkdirSync(settingsDir, { recursive: true });
fs.writeFileSync(
path.join(settingsDir, 'composite-test.settings.json'),
JSON.stringify({ env: {} }),
'utf-8'
);
// Test: Update all tiers
const result = updateCompositeVariant('test', {
tiers: {
opus: { provider: 'agy', model: 'claude-opus-4-6-thinking' },
sonnet: { provider: 'codex', model: 'codex-sonnet-4-5' },
haiku: { provider: 'gemini', model: 'gemini-2.5-flash' },
},
});
expect(result.success).toBe(true);
expect(result.variant?.tiers?.opus.provider).toBe('agy');
expect(result.variant?.tiers?.sonnet.provider).toBe('codex');
expect(result.variant?.tiers?.haiku.provider).toBe('gemini');
});
it('should preserve optional tier fields when updating provider/model only', () => {
const initialConfig: CompositeVariantConfig = {
type: 'composite',
default_tier: 'sonnet',
tiers: {
opus: {
provider: 'agy',
model: 'claude-opus-4-6-thinking',
fallback: { provider: 'gemini', model: 'gemini-2.5-flash' },
thinking: 'xhigh',
account: 'team-a',
},
sonnet: { provider: 'agy', model: 'claude-sonnet-4-6' },
haiku: { provider: 'agy', model: 'claude-haiku-4-5-20251001' },
},
settings: 'cliproxy/composite-test.settings.json',
port: 8318,
};
saveCompositeVariantUnified('test', initialConfig);
const settingsDir = path.join(tmpDir, 'cliproxy');
fs.mkdirSync(settingsDir, { recursive: true });
fs.writeFileSync(
path.join(settingsDir, 'composite-test.settings.json'),
JSON.stringify({ env: {} }),
'utf-8'
);
const result = updateCompositeVariant('test', {
tiers: {
opus: { provider: 'gemini', model: 'gemini-2.5-pro' },
},
});
expect(result.success).toBe(true);
expect(result.variant?.tiers?.opus.provider).toBe('gemini');
expect(result.variant?.tiers?.opus.model).toBe('gemini-2.5-pro');
expect(result.variant?.tiers?.opus.fallback).toEqual({
provider: 'gemini',
model: 'gemini-2.5-flash',
});
expect(result.variant?.tiers?.opus.thinking).toBe('xhigh');
expect(result.variant?.tiers?.opus.account).toBe('team-a');
});
it('should preserve existing custom settings path and custom settings fields', () => {
const customSettingsPath = path.join(tmpDir, 'custom', 'my-composite.settings.json');
const initialConfig: CompositeVariantConfig = {
type: 'composite',
default_tier: 'sonnet',
tiers: {
opus: { provider: 'gemini', model: 'gemini-3-pro-preview' },
sonnet: { provider: 'agy', model: 'claude-sonnet-4-6' },
haiku: { provider: 'agy', model: 'claude-haiku-4-5-20251001' },
},
settings: customSettingsPath,
port: 8318,
};
saveCompositeVariantUnified('test', initialConfig);
fs.mkdirSync(path.dirname(customSettingsPath), { recursive: true });
fs.writeFileSync(
customSettingsPath,
JSON.stringify(
{
env: {
ANTHROPIC_BASE_URL: 'http://127.0.0.1:8318',
ANTHROPIC_AUTH_TOKEN: 'ccs-internal-managed',
ANTHROPIC_MODEL: 'claude-sonnet-4-6',
ANTHROPIC_DEFAULT_OPUS_MODEL: 'gemini-3-pro-preview',
ANTHROPIC_DEFAULT_SONNET_MODEL: 'claude-sonnet-4-6',
ANTHROPIC_DEFAULT_HAIKU_MODEL: 'claude-haiku-4-5-20251001',
CUSTOM_ENV: 'preserve-this',
},
hooks: { PreToolUse: [{ matcher: 'WebSearch', hooks: [] }] },
customPreset: true,
},
null,
2
),
'utf-8'
);
const result = updateCompositeVariant('test', {
tiers: {
sonnet: { provider: 'agy', model: 'claude-sonnet-4-6(high)' },
},
});
expect(result.success).toBe(true);
expect(result.variant?.settings).toBe(customSettingsPath);
const updatedSettings = JSON.parse(fs.readFileSync(customSettingsPath, 'utf-8')) as {
env: Record<string, string>;
hooks: { PreToolUse: unknown[] };
customPreset: boolean;
};
expect(updatedSettings.env.ANTHROPIC_MODEL).toBe('claude-sonnet-4-6(high)');
expect(updatedSettings.env.CUSTOM_ENV).toBe('preserve-this');
expect(updatedSettings.hooks.PreToolUse.length).toBe(1);
expect(updatedSettings.customPreset).toBe(true);
const variants = listVariantsFromConfig();
expect(variants.test.settings).toBe(customSettingsPath);
});
it('should return error when variant does not exist', () => {
const result = updateCompositeVariant('nonexistent', {
tiers: {
opus: { provider: 'agy', model: 'claude-opus-4-6-thinking' },
},
});
expect(result.success).toBe(false);
expect(result.error).toContain('not found');
});
it('should return error when variant is not composite type', () => {
// Setup: Create non-composite variant in unified config
const configPath = path.join(tmpDir, 'config.yaml');
const yamlContent = `version: 2
accounts: {}
profiles: {}
preferences:
theme: system
telemetry: false
auto_update: true
cliproxy:
oauth_accounts: {}
providers:
- gemini
- codex
- agy
variants:
simple:
provider: gemini
settings: cliproxy/simple.settings.json
port: 8318
`;
fs.writeFileSync(configPath, yamlContent, 'utf-8');
const result = updateCompositeVariant('simple', {
tiers: {
opus: { provider: 'agy', model: 'claude-opus-4-6-thinking' },
},
});
expect(result.success).toBe(false);
expect(result.error).toContain('not a composite variant');
});
});
describe('createCompositeVariant', () => {
let tmpDir: string;
let originalCcsDir: string | undefined;
beforeEach(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-composite-test-'));
originalCcsDir = process.env.CCS_DIR;
process.env.CCS_DIR = tmpDir;
const configPath = path.join(tmpDir, 'config.yaml');
fs.writeFileSync(
configPath,
`version: 2
accounts: {}
profiles: {}
preferences:
theme: system
telemetry: false
auto_update: true
cliproxy:
oauth_accounts: {}
providers:
- gemini
- codex
- agy
variants: {}
`,
'utf-8'
);
});
afterEach(() => {
if (originalCcsDir !== undefined) {
process.env.CCS_DIR = originalCcsDir;
} else {
delete process.env.CCS_DIR;
}
if (tmpDir && fs.existsSync(tmpDir)) {
fs.rmSync(tmpDir, { recursive: true, force: true });
}
});
it('returns validation error for missing required tier in create flow', () => {
const result = createCompositeVariant({
name: 'broken',
defaultTier: 'sonnet',
tiers: {
opus: { provider: 'gemini', model: 'gemini-2.5-pro' },
sonnet: { provider: 'agy', model: 'claude-sonnet-4-6' },
} as unknown as {
opus: { provider: 'gemini' | 'codex' | 'agy'; model: string };
sonnet: { provider: 'gemini' | 'codex' | 'agy'; model: string };
haiku: { provider: 'gemini' | 'codex' | 'agy'; model: string };
},
});
expect(result.success).toBe(false);
expect(result.error).toContain("Missing required tier 'haiku'");
});
it('returns validation error for null tier payload in create flow', () => {
const result = createCompositeVariant({
name: 'broken-null',
defaultTier: 'sonnet',
tiers: {
opus: null,
sonnet: { provider: 'agy', model: 'claude-sonnet-4-6' },
haiku: { provider: 'agy', model: 'claude-haiku-4-5-20251001' },
} as unknown as {
opus: { provider: 'gemini' | 'codex' | 'agy'; model: string };
sonnet: { provider: 'gemini' | 'codex' | 'agy'; model: string };
haiku: { provider: 'gemini' | 'codex' | 'agy'; model: string };
},
});
expect(result.success).toBe(false);
expect(result.error).toContain("Invalid tier config for 'opus'");
});
it('stores composite settings path under active CCS_DIR', () => {
const result = createCompositeVariant({
name: 'scoped',
defaultTier: 'sonnet',
tiers: {
opus: { provider: 'gemini', model: 'gemini-2.5-pro' },
sonnet: { provider: 'agy', model: 'claude-sonnet-4-6' },
haiku: { provider: 'agy', model: 'claude-haiku-4-5-20251001' },
},
});
expect(result.success).toBe(true);
const variants = listVariantsFromConfig();
const expectedPath = path.join(tmpDir, 'composite-scoped.settings.json');
expect(variants.scoped.settings).toBe(expectedPath);
expect(result.settingsPath).toBe(expectedPath);
expect(fs.existsSync(expectedPath)).toBe(true);
});
});
describe('saveCompositeVariantUnified', () => {
let tmpDir: string;
let originalCcsDir: string | undefined;
beforeEach(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-composite-test-'));
originalCcsDir = process.env.CCS_DIR;
process.env.CCS_DIR = tmpDir;
// Create unified config file
const configPath = path.join(tmpDir, 'config.yaml');
fs.writeFileSync(
configPath,
`version: 2
accounts: {}
profiles: {}
preferences:
theme: system
telemetry: false
auto_update: true
cliproxy:
oauth_accounts: {}
providers:
- gemini
- codex
- agy
variants: {}
`,
'utf-8'
);
});
afterEach(() => {
if (originalCcsDir !== undefined) {
process.env.CCS_DIR = originalCcsDir;
} else {
delete process.env.CCS_DIR;
}
if (tmpDir && fs.existsSync(tmpDir)) {
fs.rmSync(tmpDir, { recursive: true, force: true });
}
});
it('should save composite variant to unified config', () => {
const config: CompositeVariantConfig = {
type: 'composite',
default_tier: 'sonnet',
tiers: {
opus: { provider: 'gemini', model: 'gemini-3-pro-preview' },
sonnet: { provider: 'agy', model: 'claude-sonnet-4-6' },
haiku: { provider: 'agy', model: 'claude-haiku-4-5-20251001' },
},
settings: 'cliproxy/composite-test.settings.json',
port: 8318,
};
saveCompositeVariantUnified('test', config);
const variants = listVariantsFromConfig();
expect(variants.test).toBeDefined();
expect(variants.test.type).toBe('composite');
expect(variants.test.default_tier).toBe('sonnet');
expect(variants.test.tiers?.opus.model).toBe('gemini-3-pro-preview');
});
});
describe('listVariantsFromConfig - composite variants', () => {
let tmpDir: string;
let originalCcsDir: string | undefined;
beforeEach(() => {
tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-composite-test-'));
originalCcsDir = process.env.CCS_DIR;
process.env.CCS_DIR = tmpDir;
});
afterEach(() => {
if (originalCcsDir !== undefined) {
process.env.CCS_DIR = originalCcsDir;
} else {
delete process.env.CCS_DIR;
}
if (tmpDir && fs.existsSync(tmpDir)) {
fs.rmSync(tmpDir, { recursive: true, force: true });
}
});
it('should list composite variants with fallback field', () => {
// Create unified config with composite variant that has fallback
const configPath = path.join(tmpDir, 'config.yaml');
const yamlContent = `version: 2
accounts: {}
profiles: {}
preferences:
theme: system
telemetry: false
auto_update: true
cliproxy:
oauth_accounts: {}
providers:
- gemini
- codex
- agy
variants:
test:
type: composite
default_tier: sonnet
tiers:
opus:
provider: agy
model: claude-opus-4-6-thinking
fallback:
provider: gemini
model: gemini-3-pro-preview
sonnet:
provider: agy
model: claude-sonnet-4-6
haiku:
provider: agy
model: claude-haiku-4-5-20251001
settings: cliproxy/composite-test.settings.json
port: 8318
`;
fs.writeFileSync(configPath, yamlContent, 'utf-8');
const variants = listVariantsFromConfig();
expect(variants.test).toBeDefined();
expect(variants.test.hasFallback).toBe(true);
});
it('should list composite variants with thinking field', () => {
// Create unified config with composite variant that has per-tier thinking
const configPath = path.join(tmpDir, 'config.yaml');
const yamlContent = `version: 2
accounts: {}
profiles: {}
preferences:
theme: system
telemetry: false
auto_update: true
cliproxy:
oauth_accounts: {}
providers:
- gemini
- codex
- agy
variants:
test:
type: composite
default_tier: sonnet
tiers:
opus:
provider: agy
model: claude-opus-4-6-thinking
thinking: xhigh
sonnet:
provider: agy
model: claude-sonnet-4-6
thinking: medium
haiku:
provider: agy
model: claude-haiku-4-5-20251001
thinking: off
settings: cliproxy/composite-test.settings.json
port: 8318
`;
fs.writeFileSync(configPath, yamlContent, 'utf-8');
const variants = listVariantsFromConfig();
expect(variants.test).toBeDefined();
expect(variants.test.tiers?.opus.thinking).toBe('xhigh');
expect(variants.test.tiers?.sonnet.thinking).toBe('medium');
expect(variants.test.tiers?.haiku.thinking).toBe('off');
});
it('should have hasFallback=false when no fallbacks configured', () => {
// Create unified config with composite variant WITHOUT fallback
const configPath = path.join(tmpDir, 'config.yaml');
const yamlContent = `version: 2
accounts: {}
profiles: {}
preferences:
theme: system
telemetry: false
auto_update: true
cliproxy:
oauth_accounts: {}
providers:
- gemini
- codex
- agy
variants:
test:
type: composite
default_tier: sonnet
tiers:
opus:
provider: agy
model: claude-opus-4-6-thinking
sonnet:
provider: agy
model: claude-sonnet-4-6
haiku:
provider: agy
model: claude-haiku-4-5-20251001
settings: cliproxy/composite-test.settings.json
port: 8318
`;
fs.writeFileSync(configPath, yamlContent, 'utf-8');
const variants = listVariantsFromConfig();
expect(variants.test).toBeDefined();
expect(variants.test.hasFallback).toBe(false);
});
it('should skip malformed composite variant and keep valid variants', () => {
const configPath = path.join(tmpDir, 'config.yaml');
const yamlContent = `version: 2
accounts: {}
profiles: {}
preferences:
theme: system
telemetry: false
auto_update: true
cliproxy:
oauth_accounts: {}
providers:
- gemini
- codex
- agy
variants:
bad:
type: composite
default_tier: sonnet
tiers:
opus:
provider: agy
model: claude-opus-4-6-thinking
sonnet:
provider: agy
model: claude-sonnet-4-6
settings: cliproxy/bad.settings.json
port: 8318
good:
provider: gemini
settings: cliproxy/good.settings.json
port: 8319
`;
fs.writeFileSync(configPath, yamlContent, 'utf-8');
const variants = listVariantsFromConfig();
expect(variants.bad).toBeUndefined();
expect(variants.good).toBeDefined();
expect(variants.good.provider).toBe('gemini');
});
});