From 7dd4ce5453b7e22ff1d378f396e4f8d61300a543 Mon Sep 17 00:00:00 2001 From: Tam Nhu Tran Date: Fri, 13 Feb 2026 06:19:46 +0700 Subject: [PATCH] test(cliproxy): add regression coverage for update consistency - verify provider change requires model and rewrites provider-specific env - verify composite updates preserve custom settings path and fields --- .../composite-variant-service.test.ts | 61 ++++++++++ .../cliproxy/variant-update-service.test.ts | 110 ++++++++++++++++++ 2 files changed, 171 insertions(+) create mode 100644 tests/unit/cliproxy/variant-update-service.test.ts diff --git a/tests/unit/cliproxy/composite-variant-service.test.ts b/tests/unit/cliproxy/composite-variant-service.test.ts index 5f291315..0ab54d77 100644 --- a/tests/unit/cliproxy/composite-variant-service.test.ts +++ b/tests/unit/cliproxy/composite-variant-service.test.ts @@ -221,6 +221,67 @@ cliproxy: 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-5-thinking' }, + 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-5-thinking', + ANTHROPIC_DEFAULT_OPUS_MODEL: 'gemini-3-pro-preview', + ANTHROPIC_DEFAULT_SONNET_MODEL: 'claude-sonnet-4-5-thinking', + 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-5-thinking(high)' }, + }, + }); + + expect(result.success).toBe(true); + expect(result.variant?.settings).toBe(customSettingsPath); + + const updatedSettings = JSON.parse(fs.readFileSync(customSettingsPath, 'utf-8')) as { + env: Record; + hooks: { PreToolUse: unknown[] }; + customPreset: boolean; + }; + expect(updatedSettings.env.ANTHROPIC_MODEL).toBe('claude-sonnet-4-5-thinking(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: { diff --git a/tests/unit/cliproxy/variant-update-service.test.ts b/tests/unit/cliproxy/variant-update-service.test.ts new file mode 100644 index 00000000..d23a2e4e --- /dev/null +++ b/tests/unit/cliproxy/variant-update-service.test.ts @@ -0,0 +1,110 @@ +/** + * Unit tests for single-variant provider/model update behavior. + */ + +import * as fs from 'fs'; +import * as os from 'os'; +import * as path from 'path'; +import { afterEach, beforeEach, describe, expect, it } from 'bun:test'; +import { updateVariant } from '../../../src/cliproxy/services/variant-service'; +import { loadOrCreateUnifiedConfig } from '../../../src/config/unified-config-loader'; + +describe('updateVariant - provider/model consistency', () => { + let tmpDir: string; + let originalCcsDir: string | undefined; + + beforeEach(() => { + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'ccs-variant-update-test-')); + originalCcsDir = process.env.CCS_DIR; + process.env.CCS_DIR = tmpDir; + + const settingsPath = path.join(tmpDir, 'gemini-demo.settings.json'); + fs.writeFileSync( + settingsPath, + JSON.stringify( + { + env: { + ANTHROPIC_BASE_URL: 'http://127.0.0.1:8318/api/provider/gemini', + ANTHROPIC_AUTH_TOKEN: 'ccs-internal-managed', + ANTHROPIC_MODEL: 'gemini-2.5-pro', + ANTHROPIC_DEFAULT_OPUS_MODEL: 'gemini-2.5-pro', + ANTHROPIC_DEFAULT_SONNET_MODEL: 'gemini-2.5-pro', + ANTHROPIC_DEFAULT_HAIKU_MODEL: 'gemini-2.5-flash', + CUSTOM_FLAG: 'keep-me', + }, + hooks: { PreToolUse: [{ matcher: 'WebSearch', hooks: [] }] }, + }, + null, + 2 + ), + 'utf-8' + ); + + fs.writeFileSync( + path.join(tmpDir, 'config.yaml'), + `version: 2 +accounts: {} +profiles: {} +preferences: + theme: system + telemetry: false + auto_update: true +cliproxy: + oauth_accounts: {} + providers: + - gemini + - codex + - agy + variants: + demo: + provider: gemini + settings: ${settingsPath} + port: 8318 +`, + '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('rejects provider change without model update', () => { + const result = updateVariant('demo', { provider: 'codex' }); + expect(result.success).toBe(false); + expect(result.error).toContain('Changing provider requires model update'); + }); + + it('updates provider and regenerates provider-specific core env in same settings file', () => { + const result = updateVariant('demo', { + provider: 'codex', + model: 'gpt-5.1-codex-mini', + }); + + expect(result.success).toBe(true); + expect(result.variant?.provider).toBe('codex'); + expect(result.variant?.model).toBe('gpt-5.1-codex-mini'); + + const settingsPath = path.join(tmpDir, 'gemini-demo.settings.json'); + const settings = JSON.parse(fs.readFileSync(settingsPath, 'utf-8')) as { + env: Record; + hooks: { PreToolUse: unknown[] }; + }; + + expect(settings.env.ANTHROPIC_BASE_URL).toContain('/api/provider/codex'); + expect(settings.env.ANTHROPIC_MODEL).toBe('gpt-5.1-codex-mini'); + expect(settings.env.CUSTOM_FLAG).toBe('keep-me'); + expect(settings.hooks.PreToolUse.length).toBe(1); + + const config = loadOrCreateUnifiedConfig(); + expect(config.cliproxy?.variants?.demo?.provider).toBe('codex'); + }); +});