/** * Tests for CLIProxy Command - Profile Management * * Tests the CRUD operations for CLIProxy variant profiles: * - create: Creates new variant with provider + model * - list: Lists all custom variants * - remove: Removes variant and settings file */ const assert = require('assert'); const fs = require('fs'); const path = require('path'); const os = require('os'); describe('CLIProxy Command - Profile Management', () => { // Test fixtures directory let testDir; let testConfigPath; let testCcsDir; beforeEach(() => { // Create isolated test directory testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-cliproxy-test-')); testCcsDir = path.join(testDir, '.ccs'); testConfigPath = path.join(testCcsDir, 'config.json'); fs.mkdirSync(testCcsDir, { recursive: true }); }); afterEach(() => { // Cleanup test directory if (testDir && fs.existsSync(testDir)) { fs.rmSync(testDir, { recursive: true, force: true }); } }); describe('validateProfileName', () => { // Import validation logic pattern from source const validateProfileName = (name) => { if (!name) return 'Profile name is required'; if (!/^[a-zA-Z][a-zA-Z0-9._-]*$/.test(name)) { return 'Name must start with letter, contain only letters, numbers, dot, dash, underscore'; } if (name.length > 32) return 'Name must be 32 characters or less'; const reserved = [ 'default', 'auth', 'api', 'doctor', 'sync', 'update', 'help', 'version', 'cliproxy', 'create', 'list', 'remove', 'gemini', 'codex', 'agy', 'qwen' ]; if (reserved.includes(name.toLowerCase())) return `'${name}' is a reserved name`; return null; }; it('rejects empty name', () => { assert.strictEqual(validateProfileName(''), 'Profile name is required'); assert.strictEqual(validateProfileName(null), 'Profile name is required'); assert.strictEqual(validateProfileName(undefined), 'Profile name is required'); }); it('rejects names starting with number', () => { const result = validateProfileName('3test'); assert(result !== null, 'Should reject name starting with number'); assert(result.includes('start with letter')); }); it('rejects names starting with special character', () => { assert(validateProfileName('-test') !== null); assert(validateProfileName('_test') !== null); assert(validateProfileName('.test') !== null); }); it('accepts valid names', () => { assert.strictEqual(validateProfileName('g3'), null); assert.strictEqual(validateProfileName('flash'), null); assert.strictEqual(validateProfileName('pro-v2'), null); assert.strictEqual(validateProfileName('test_model'), null); assert.strictEqual(validateProfileName('MyModel.v1'), null); }); it('rejects names over 32 characters', () => { const longName = 'a'.repeat(33); const result = validateProfileName(longName); assert(result !== null); assert(result.includes('32 characters')); }); it('accepts names exactly 32 characters', () => { const exactName = 'a'.repeat(32); assert.strictEqual(validateProfileName(exactName), null); }); it('rejects reserved names (case insensitive)', () => { const reserved = ['gemini', 'GEMINI', 'Gemini', 'codex', 'agy', 'qwen', 'default', 'cliproxy']; reserved.forEach((name) => { const result = validateProfileName(name); assert(result !== null, `Should reject reserved name: ${name}`); assert(result.includes('reserved'), `Error should mention reserved: ${name}`); }); }); it('rejects command names as profile names', () => { const commands = ['create', 'list', 'remove', 'help', 'version']; commands.forEach((name) => { const result = validateProfileName(name); assert(result !== null, `Should reject command name: ${name}`); }); }); }); describe('Settings File Format', () => { it('creates settings with all 6 required env fields', () => { // Simulate what createCliproxySettingsFile produces const model = 'gemini-3-pro-preview'; const settings = { env: { ANTHROPIC_BASE_URL: 'http://127.0.0.1:8317/api/provider/gemini', ANTHROPIC_AUTH_TOKEN: 'ccs-internal-managed', ANTHROPIC_MODEL: model, ANTHROPIC_DEFAULT_OPUS_MODEL: model, ANTHROPIC_DEFAULT_SONNET_MODEL: model, ANTHROPIC_DEFAULT_HAIKU_MODEL: model, }, }; // Verify all required fields present assert(settings.env.ANTHROPIC_BASE_URL, 'Missing ANTHROPIC_BASE_URL'); assert(settings.env.ANTHROPIC_AUTH_TOKEN, 'Missing ANTHROPIC_AUTH_TOKEN'); assert(settings.env.ANTHROPIC_MODEL, 'Missing ANTHROPIC_MODEL'); assert(settings.env.ANTHROPIC_DEFAULT_OPUS_MODEL, 'Missing ANTHROPIC_DEFAULT_OPUS_MODEL'); assert(settings.env.ANTHROPIC_DEFAULT_SONNET_MODEL, 'Missing ANTHROPIC_DEFAULT_SONNET_MODEL'); assert(settings.env.ANTHROPIC_DEFAULT_HAIKU_MODEL, 'Missing ANTHROPIC_DEFAULT_HAIKU_MODEL'); // Verify model applied to all 4 model fields assert.strictEqual(settings.env.ANTHROPIC_MODEL, model); assert.strictEqual(settings.env.ANTHROPIC_DEFAULT_OPUS_MODEL, model); assert.strictEqual(settings.env.ANTHROPIC_DEFAULT_SONNET_MODEL, model); assert.strictEqual(settings.env.ANTHROPIC_DEFAULT_HAIKU_MODEL, model); }); it('uses correct provider endpoint in BASE_URL', () => { const providers = ['gemini', 'codex', 'agy', 'qwen']; providers.forEach((provider) => { const expectedUrl = `http://127.0.0.1:8317/api/provider/${provider}`; assert(expectedUrl.includes(provider), `URL should contain provider: ${provider}`); assert(expectedUrl.startsWith('http://127.0.0.1:8317'), 'Should use localhost:8317'); }); }); it('settings file is valid JSON', () => { const settingsPath = path.join(testCcsDir, 'gemini-test.settings.json'); const settings = { env: { ANTHROPIC_BASE_URL: 'http://127.0.0.1:8317/api/provider/gemini', ANTHROPIC_AUTH_TOKEN: 'ccs-internal-managed', ANTHROPIC_MODEL: 'gemini-3-pro-preview', ANTHROPIC_DEFAULT_OPUS_MODEL: 'gemini-3-pro-preview', ANTHROPIC_DEFAULT_SONNET_MODEL: 'gemini-3-pro-preview', ANTHROPIC_DEFAULT_HAIKU_MODEL: 'gemini-3-pro-preview', }, }; fs.writeFileSync(settingsPath, JSON.stringify(settings, null, 2) + '\n'); // Verify it can be read back const readBack = JSON.parse(fs.readFileSync(settingsPath, 'utf8')); assert.deepStrictEqual(readBack, settings); }); }); describe('Config.json CLIProxy Section', () => { it('adds cliproxy section when creating first variant', () => { const config = { profiles: {} }; // Simulate adding variant if (!config.cliproxy) config.cliproxy = {}; config.cliproxy['g3'] = { provider: 'gemini', settings: '~/.ccs/gemini-g3.settings.json', }; assert(config.cliproxy, 'cliproxy section should exist'); assert(config.cliproxy.g3, 'g3 variant should exist'); assert.strictEqual(config.cliproxy.g3.provider, 'gemini'); }); it('preserves existing profiles when adding variant', () => { const config = { profiles: { glm: '~/.ccs/glm.settings.json', kimi: '~/.ccs/kimi.settings.json', }, }; // Simulate adding variant if (!config.cliproxy) config.cliproxy = {}; config.cliproxy['flash'] = { provider: 'gemini', settings: '~/.ccs/gemini-flash.settings.json', }; // Verify profiles preserved assert.strictEqual(Object.keys(config.profiles).length, 2); assert(config.profiles.glm); assert(config.profiles.kimi); // Verify variant added assert(config.cliproxy.flash); }); it('removes cliproxy section when last variant removed', () => { const config = { profiles: { glm: '~/.ccs/glm.settings.json' }, cliproxy: { g3: { provider: 'gemini', settings: '~/.ccs/gemini-g3.settings.json' }, }, }; // Simulate removing last variant delete config.cliproxy.g3; if (Object.keys(config.cliproxy).length === 0) { delete config.cliproxy; } assert(!config.cliproxy, 'cliproxy section should be removed when empty'); assert(config.profiles.glm, 'profiles should be preserved'); }); it('keeps cliproxy section when other variants exist', () => { const config = { profiles: {}, cliproxy: { g3: { provider: 'gemini', settings: '~/.ccs/gemini-g3.settings.json' }, flash: { provider: 'gemini', settings: '~/.ccs/gemini-flash.settings.json' }, }, }; // Simulate removing one variant delete config.cliproxy.flash; if (Object.keys(config.cliproxy).length === 0) { delete config.cliproxy; } assert(config.cliproxy, 'cliproxy section should remain'); assert(config.cliproxy.g3, 'g3 should still exist'); assert(!config.cliproxy.flash, 'flash should be removed'); }); it('uses tilde (~) path format for portability', () => { const variant = { provider: 'gemini', settings: '~/.ccs/gemini-g3.settings.json', }; assert(variant.settings.startsWith('~/'), 'Path should start with ~/'); assert(variant.settings.includes('.ccs'), 'Path should include .ccs'); }); }); describe('Provider Validation', () => { const validProviders = ['gemini', 'codex', 'agy', 'qwen']; it('accepts all valid providers', () => { validProviders.forEach((provider) => { assert(validProviders.includes(provider), `${provider} should be valid`); }); }); it('rejects invalid providers', () => { const invalidProviders = ['openai', 'anthropic', 'claude', 'gpt', 'invalid']; invalidProviders.forEach((provider) => { assert(!validProviders.includes(provider), `${provider} should be invalid`); }); }); }); describe('Atomic Config Writes', () => { it('uses temp file + rename pattern for safety', () => { const configPath = testConfigPath; const tempPath = configPath + '.tmp'; const config = { profiles: {}, cliproxy: { test: { provider: 'gemini', settings: '~/.ccs/test.json' } } }; // Write to temp fs.writeFileSync(tempPath, JSON.stringify(config, null, 2) + '\n'); assert(fs.existsSync(tempPath), 'Temp file should exist'); // Rename (atomic on most filesystems) fs.renameSync(tempPath, configPath); assert(fs.existsSync(configPath), 'Config should exist after rename'); assert(!fs.existsSync(tempPath), 'Temp should not exist after rename'); // Verify content const readBack = JSON.parse(fs.readFileSync(configPath, 'utf8')); assert.deepStrictEqual(readBack, config); }); }); describe('Settings File Naming Convention', () => { it('uses provider-name.settings.json format', () => { const testCases = [ { provider: 'gemini', name: 'g3', expected: 'gemini-g3.settings.json' }, { provider: 'gemini', name: 'flash', expected: 'gemini-flash.settings.json' }, { provider: 'codex', name: 'turbo', expected: 'codex-turbo.settings.json' }, { provider: 'agy', name: 'opus', expected: 'agy-opus.settings.json' }, ]; testCases.forEach(({ provider, name, expected }) => { const filename = `${provider}-${name}.settings.json`; assert.strictEqual(filename, expected, `Wrong filename for ${provider}/${name}`); }); }); }); }); describe('API Command - Model Fields Fix', () => { describe('createSettingsFile', () => { it('includes all 4 model fields', () => { // Simulate what createSettingsFile now produces const model = 'claude-sonnet-4-5-20250929'; const settings = { env: { ANTHROPIC_BASE_URL: 'https://api.example.com', ANTHROPIC_AUTH_TOKEN: 'test-key', ANTHROPIC_MODEL: model, ANTHROPIC_DEFAULT_OPUS_MODEL: model, ANTHROPIC_DEFAULT_SONNET_MODEL: model, ANTHROPIC_DEFAULT_HAIKU_MODEL: model, }, }; // Verify all 4 model fields present and equal const modelFields = [ 'ANTHROPIC_MODEL', 'ANTHROPIC_DEFAULT_OPUS_MODEL', 'ANTHROPIC_DEFAULT_SONNET_MODEL', 'ANTHROPIC_DEFAULT_HAIKU_MODEL', ]; modelFields.forEach((field) => { assert(settings.env[field], `Missing ${field}`); assert.strictEqual(settings.env[field], model, `${field} should equal model`); }); }); it('applies single model input to all 4 fields', () => { const inputModel = 'custom-model-v1'; // This is the fix: single model applied to all 4 fields const env = { ANTHROPIC_MODEL: inputModel, ANTHROPIC_DEFAULT_OPUS_MODEL: inputModel, ANTHROPIC_DEFAULT_SONNET_MODEL: inputModel, ANTHROPIC_DEFAULT_HAIKU_MODEL: inputModel, }; // All should be identical assert.strictEqual(env.ANTHROPIC_MODEL, inputModel); assert.strictEqual(env.ANTHROPIC_DEFAULT_OPUS_MODEL, inputModel); assert.strictEqual(env.ANTHROPIC_DEFAULT_SONNET_MODEL, inputModel); assert.strictEqual(env.ANTHROPIC_DEFAULT_HAIKU_MODEL, inputModel); }); }); }); describe('CLIProxy Command - Proxy Lifecycle', () => { // Test isolation environment let testHome; let sessionLockPath; beforeEach(() => { testHome = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-cliproxy-lifecycle-')); process.env.CCS_HOME = testHome; const cliproxyDir = path.join(testHome, '.ccs', 'cliproxy'); fs.mkdirSync(cliproxyDir, { recursive: true }); sessionLockPath = path.join(cliproxyDir, 'sessions.json'); }); afterEach(() => { if (testHome && fs.existsSync(testHome)) { fs.rmSync(testHome, { recursive: true, force: true }); } delete process.env.CCS_HOME; }); describe('Status Command Logic', () => { it('returns not running when no session lock exists', () => { // Simulate getProxyStatus behavior const lockExists = fs.existsSync(sessionLockPath); assert.strictEqual(lockExists, false); // Status should indicate not running const status = { running: false }; assert.strictEqual(status.running, false); }); it('returns running with details when session lock exists', () => { // Create mock session lock const lock = { port: 8317, pid: process.pid, sessions: ['session1', 'session2'], startedAt: new Date().toISOString(), }; fs.writeFileSync(sessionLockPath, JSON.stringify(lock, null, 2)); // Read and verify const data = JSON.parse(fs.readFileSync(sessionLockPath, 'utf-8')); assert.strictEqual(data.port, 8317); assert.strictEqual(data.pid, process.pid); assert.strictEqual(data.sessions.length, 2); assert(data.startedAt); }); it('formats uptime correctly', () => { // Test uptime formatting logic const formatUptime = (ms) => { const hours = Math.floor(ms / (1000 * 60 * 60)); const minutes = Math.floor((ms % (1000 * 60 * 60)) / (1000 * 60)); if (hours > 0) return `${hours}h ${minutes}m`; return `${minutes}m`; }; assert.strictEqual(formatUptime(30 * 60 * 1000), '30m'); // 30 minutes assert.strictEqual(formatUptime(90 * 60 * 1000), '1h 30m'); // 1.5 hours assert.strictEqual(formatUptime(2 * 60 * 60 * 1000 + 15 * 60 * 1000), '2h 15m'); // 2h 15m }); }); describe('Stop Command Logic', () => { it('returns error when no session lock exists', () => { // Verify lock doesn't exist assert.strictEqual(fs.existsSync(sessionLockPath), false); // Stop should fail with appropriate error const result = { stopped: false, error: 'No active CLIProxy session found' }; assert.strictEqual(result.stopped, false); assert.strictEqual(result.error, 'No active CLIProxy session found'); }); it('cleans up stale lock for dead process', () => { // Create lock with non-existent PID const lock = { port: 8317, pid: 999999999, // Very unlikely to exist sessions: ['session1'], startedAt: new Date().toISOString(), }; fs.writeFileSync(sessionLockPath, JSON.stringify(lock, null, 2)); // Verify lock was created assert.strictEqual(fs.existsSync(sessionLockPath), true); // Simulate isProcessRunning check const isRunning = (() => { try { process.kill(999999999, 0); return true; } catch { return false; } })(); assert.strictEqual(isRunning, false); // Cleanup should remove lock if (!isRunning) { fs.unlinkSync(sessionLockPath); } assert.strictEqual(fs.existsSync(sessionLockPath), false); }); it('returns session count when stopping active proxy', () => { // Create lock with current process const lock = { port: 8317, pid: process.pid, sessions: ['session1', 'session2', 'session3'], startedAt: new Date().toISOString(), }; fs.writeFileSync(sessionLockPath, JSON.stringify(lock, null, 2)); // Read and verify session count const data = JSON.parse(fs.readFileSync(sessionLockPath, 'utf-8')); assert.strictEqual(data.sessions.length, 3); // Result structure should include count const result = { stopped: true, pid: data.pid, sessionCount: data.sessions.length, }; assert.strictEqual(result.stopped, true); assert.strictEqual(result.sessionCount, 3); }); }); describe('Command Routing', () => { it('routes "start" subcommand correctly', () => { const args = ['cliproxy', 'start']; const subcommand = args[1]; assert.strictEqual(subcommand, 'start'); }); it('routes "restart" subcommand correctly', () => { const args = ['cliproxy', 'restart']; const subcommand = args[1]; assert.strictEqual(subcommand, 'restart'); }); it('routes "stop" subcommand correctly', () => { const args = ['cliproxy', 'stop']; const subcommand = args[1]; assert.strictEqual(subcommand, 'stop'); }); it('routes "status" subcommand correctly', () => { const args = ['cliproxy', 'status']; const subcommand = args[1]; assert.strictEqual(subcommand, 'status'); }); it('handles unknown subcommand', () => { const validSubcommands = ['start', 'restart', 'stop', 'status', 'create', 'list', 'remove']; const unknownCommand = 'invalid'; assert.strictEqual(validSubcommands.includes(unknownCommand), false); }); }); });