mirror of
https://github.com/tiennm99/ccs.git
synced 2026-07-16 18:16:08 +00:00
690 lines
20 KiB
TypeScript
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');
|
|
});
|
|
});
|