import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; // Mock fs vi.mock('fs', () => ({ existsSync: vi.fn(), readFileSync: vi.fn(), writeFileSync: vi.fn(), mkdirSync: vi.fn(), })); import * as fs from 'fs'; import { getConfig, loadConfig, saveConfig, loadVisionConfig } from '../../../src/utils/config.js'; describe('Config - 配置管理', () => { const originalEnv = { ...process.env }; beforeEach(() => { vi.clearAllMocks(); // 清理环境变量 delete process.env.AI_PROVIDER; delete process.env.ANTHROPIC_API_KEY; delete process.env.DEEPSEEK_API_KEY; delete process.env.OPENAI_API_KEY; delete process.env.AI_MODEL; delete process.env.AI_MAX_TOKENS; delete process.env.AI_BASE_URL; delete process.env.VISION_PROVIDER; delete process.env.VISION_MODEL; delete process.env.VISION_API_KEY; delete process.env.VISION_BASE_URL; }); afterEach(() => { // 恢复环境变量 process.env = { ...originalEnv }; }); describe('getConfig - 获取原始配置', () => { it('配置文件存在时返回内容', () => { vi.mocked(fs.existsSync).mockReturnValue(true); vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify({ provider: 'anthropic', apiKey: 'test-key', model: 'claude-3-opus', })); const config = getConfig(); expect(config.provider).toBe('anthropic'); expect(config.apiKey).toBe('test-key'); expect(config.model).toBe('claude-3-opus'); }); it('配置文件不存在时返回空对象', () => { vi.mocked(fs.existsSync).mockReturnValue(false); const config = getConfig(); expect(config).toEqual({}); }); it('配置文件解析错误时返回空对象', () => { vi.mocked(fs.existsSync).mockReturnValue(true); vi.mocked(fs.readFileSync).mockReturnValue('invalid json'); const config = getConfig(); expect(config).toEqual({}); }); }); describe('loadConfig - 加载完整配置', () => { it('从环境变量获取 Anthropic 配置', () => { process.env.ANTHROPIC_API_KEY = 'env-anthropic-key'; vi.mocked(fs.existsSync).mockReturnValue(false); const config = loadConfig(); expect(config.provider).toBe('anthropic'); expect(config.apiKey).toBe('env-anthropic-key'); expect(config.model).toBe('claude-sonnet-4-20250514'); }); it('从环境变量获取 DeepSeek 配置', () => { process.env.AI_PROVIDER = 'deepseek'; process.env.DEEPSEEK_API_KEY = 'env-deepseek-key'; vi.mocked(fs.existsSync).mockReturnValue(false); const config = loadConfig(); expect(config.provider).toBe('deepseek'); expect(config.apiKey).toBe('env-deepseek-key'); expect(config.model).toBe('deepseek-chat'); }); it('配置文件优先级高于默认值', () => { process.env.ANTHROPIC_API_KEY = 'env-key'; vi.mocked(fs.existsSync).mockReturnValue(true); vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify({ model: 'custom-model', maxTokens: 8192, })); const config = loadConfig(); expect(config.model).toBe('custom-model'); expect(config.maxTokens).toBe(8192); }); it('配置文件中的 provider 优先', () => { process.env.ANTHROPIC_API_KEY = 'anthropic-key'; process.env.DEEPSEEK_API_KEY = 'deepseek-key'; vi.mocked(fs.existsSync).mockReturnValue(true); vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify({ provider: 'deepseek', deepseekApiKey: 'stored-deepseek-key', })); const config = loadConfig(); expect(config.provider).toBe('deepseek'); // 使用环境变量中的 API Key(优先级更高) expect(config.apiKey).toBe('deepseek-key'); }); it('包含系统提示词', () => { process.env.ANTHROPIC_API_KEY = 'test-key'; vi.mocked(fs.existsSync).mockReturnValue(false); const config = loadConfig(); expect(config.systemPrompt).toBeDefined(); expect(config.systemPrompt).toContain('终端'); }); it('默认 maxTokens 为 4096', () => { process.env.ANTHROPIC_API_KEY = 'test-key'; vi.mocked(fs.existsSync).mockReturnValue(false); const config = loadConfig(); expect(config.maxTokens).toBe(4096); }); it('环境变量设置的 maxTokens', () => { process.env.ANTHROPIC_API_KEY = 'test-key'; process.env.AI_MAX_TOKENS = '16384'; vi.mocked(fs.existsSync).mockReturnValue(false); const config = loadConfig(); expect(config.maxTokens).toBe(16384); }); }); describe('saveConfig - 保存配置', () => { it('创建目录并保存配置', () => { vi.mocked(fs.existsSync).mockReturnValue(false); saveConfig({ provider: 'anthropic', apiKey: 'new-key' }); expect(fs.mkdirSync).toHaveBeenCalledWith( expect.any(String), { recursive: true } ); expect(fs.writeFileSync).toHaveBeenCalledWith( expect.any(String), expect.stringContaining('anthropic') ); }); it('合并现有配置', () => { vi.mocked(fs.existsSync).mockReturnValue(true); vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify({ provider: 'anthropic', apiKey: 'old-key', model: 'old-model', })); saveConfig({ apiKey: 'new-key' }); const writeCall = vi.mocked(fs.writeFileSync).mock.calls[0]; const savedConfig = JSON.parse(writeCall[1] as string); expect(savedConfig.provider).toBe('anthropic'); // 保留 expect(savedConfig.apiKey).toBe('new-key'); // 更新 expect(savedConfig.model).toBe('old-model'); // 保留 }); it('目录已存在时不重新创建', () => { vi.mocked(fs.existsSync) .mockReturnValueOnce(true) // 目录存在 .mockReturnValueOnce(true); // 配置文件存在 vi.mocked(fs.readFileSync).mockReturnValue('{}'); saveConfig({ apiKey: 'test' }); expect(fs.mkdirSync).not.toHaveBeenCalled(); }); }); describe('loadConfig - baseUrl 支持', () => { it('从环境变量获取 baseUrl', () => { process.env.ANTHROPIC_API_KEY = 'test-key'; process.env.AI_BASE_URL = 'https://custom.api.com/v1'; vi.mocked(fs.existsSync).mockReturnValue(false); const config = loadConfig(); expect(config.baseUrl).toBe('https://custom.api.com/v1'); }); it('从配置文件获取 baseUrl', () => { process.env.ANTHROPIC_API_KEY = 'test-key'; vi.mocked(fs.existsSync).mockReturnValue(true); vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify({ baseUrl: 'https://stored.api.com/v1', })); const config = loadConfig(); expect(config.baseUrl).toBe('https://stored.api.com/v1'); }); it('环境变量 baseUrl 优先于配置文件', () => { process.env.ANTHROPIC_API_KEY = 'test-key'; process.env.AI_BASE_URL = 'https://env.api.com/v1'; vi.mocked(fs.existsSync).mockReturnValue(true); vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify({ baseUrl: 'https://stored.api.com/v1', })); const config = loadConfig(); expect(config.baseUrl).toBe('https://env.api.com/v1'); }); it('OpenAI provider 支持 baseUrl', () => { process.env.AI_PROVIDER = 'openai'; process.env.OPENAI_API_KEY = 'test-openai-key'; process.env.AI_BASE_URL = 'https://dashscope.aliyuncs.com/compatible-mode/v1'; vi.mocked(fs.existsSync).mockReturnValue(false); const config = loadConfig(); expect(config.provider).toBe('openai'); expect(config.baseUrl).toBe('https://dashscope.aliyuncs.com/compatible-mode/v1'); }); }); describe('loadVisionConfig - Vision 配置', () => { it('返回 null 当没有配置 Vision API Key', () => { vi.mocked(fs.existsSync).mockReturnValue(false); const visionConfig = loadVisionConfig(); expect(visionConfig).toBeNull(); }); it('从环境变量获取 Vision 配置', () => { process.env.VISION_PROVIDER = 'openai'; process.env.VISION_API_KEY = 'vision-api-key'; process.env.VISION_MODEL = 'gpt-4-vision'; vi.mocked(fs.existsSync).mockReturnValue(false); const visionConfig = loadVisionConfig(); expect(visionConfig).not.toBeNull(); expect(visionConfig?.provider).toBe('openai'); expect(visionConfig?.apiKey).toBe('vision-api-key'); expect(visionConfig?.model).toBe('gpt-4-vision'); }); it('从配置文件获取 Vision 配置', () => { vi.mocked(fs.existsSync).mockReturnValue(true); vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify({ visionProvider: 'anthropic', visionApiKey: 'stored-vision-key', visionModel: 'claude-3-opus', })); const visionConfig = loadVisionConfig(); expect(visionConfig).not.toBeNull(); expect(visionConfig?.provider).toBe('anthropic'); expect(visionConfig?.apiKey).toBe('stored-vision-key'); expect(visionConfig?.model).toBe('claude-3-opus'); }); it('回退到对应 provider 的 API Key', () => { process.env.ANTHROPIC_API_KEY = 'anthropic-main-key'; vi.mocked(fs.existsSync).mockReturnValue(false); const visionConfig = loadVisionConfig(); expect(visionConfig).not.toBeNull(); expect(visionConfig?.provider).toBe('anthropic'); // 默认 expect(visionConfig?.apiKey).toBe('anthropic-main-key'); // 回退 }); it('Vision baseUrl 配置', () => { process.env.VISION_PROVIDER = 'openai'; process.env.VISION_API_KEY = 'vision-key'; process.env.VISION_BASE_URL = 'https://vision.api.com/v1'; vi.mocked(fs.existsSync).mockReturnValue(false); const visionConfig = loadVisionConfig(); expect(visionConfig?.baseUrl).toBe('https://vision.api.com/v1'); }); it('从配置文件回退到 deepseek API key', () => { vi.mocked(fs.existsSync).mockReturnValue(true); vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify({ visionProvider: 'deepseek', deepseekApiKey: 'deepseek-stored-key', })); const visionConfig = loadVisionConfig(); expect(visionConfig?.provider).toBe('deepseek'); expect(visionConfig?.apiKey).toBe('deepseek-stored-key'); }); it('从配置文件回退到 openai API key', () => { vi.mocked(fs.existsSync).mockReturnValue(true); vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify({ visionProvider: 'openai', openaiApiKey: 'openai-stored-key', })); const visionConfig = loadVisionConfig(); expect(visionConfig?.provider).toBe('openai'); expect(visionConfig?.apiKey).toBe('openai-stored-key'); }); it('使用默认 Vision 模型', () => { process.env.ANTHROPIC_API_KEY = 'test-key'; vi.mocked(fs.existsSync).mockReturnValue(false); const visionConfig = loadVisionConfig(); expect(visionConfig?.model).toBe('claude-sonnet-4-20250514'); }); it('Vision 专用 key 优先于 provider key', () => { process.env.ANTHROPIC_API_KEY = 'anthropic-main-key'; process.env.VISION_API_KEY = 'vision-specific-key'; vi.mocked(fs.existsSync).mockReturnValue(false); const visionConfig = loadVisionConfig(); expect(visionConfig?.apiKey).toBe('vision-specific-key'); }); }); });