import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; // Mock fs module vi.mock('fs', () => ({ existsSync: vi.fn(), readFileSync: vi.fn(), writeFileSync: vi.fn(), mkdirSync: vi.fn(), })); // Mock process.exit const mockExit = vi.spyOn(process, 'exit').mockImplementation(() => { throw new Error('process.exit called'); }); // Mock console.error const mockConsoleError = vi.spyOn(console, 'error').mockImplementation(() => {}); describe('Config - 配置管理扩展测试', () => { const CONFIG_DIR = path.join(os.homedir(), '.ai-terminal-assistant'); const CONFIG_FILE = path.join(CONFIG_DIR, 'config.json'); 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(() => { mockExit.mockClear(); mockConsoleError.mockClear(); }); describe('getConfig - 获取原始配置', () => { it('配置文件不存在返回空对象', async () => { vi.mocked(fs.existsSync).mockReturnValue(false); // 需要重新导入以获取最新的 mock vi.resetModules(); const { getConfig } = await import('../../../src/utils/config.js'); const config = getConfig(); expect(config).toEqual({}); }); it('配置文件存在返回解析后的内容', async () => { vi.mocked(fs.existsSync).mockReturnValue(true); vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify({ provider: 'anthropic', apiKey: 'test-key', model: 'claude-sonnet-4-20250514', })); vi.resetModules(); const { getConfig } = await import('../../../src/utils/config.js'); const config = getConfig(); expect(config.provider).toBe('anthropic'); expect(config.apiKey).toBe('test-key'); }); it('配置文件解析错误返回空对象', async () => { vi.mocked(fs.existsSync).mockReturnValue(true); vi.mocked(fs.readFileSync).mockReturnValue('invalid json'); vi.resetModules(); const { getConfig } = await import('../../../src/utils/config.js'); const config = getConfig(); expect(config).toEqual({}); }); }); describe('loadConfig - 加载配置', () => { it('优先使用环境变量中的 API Key', async () => { process.env.ANTHROPIC_API_KEY = 'env-api-key'; vi.mocked(fs.existsSync).mockReturnValue(true); vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify({ provider: 'anthropic', apiKey: 'file-api-key', })); vi.resetModules(); const { loadConfig } = await import('../../../src/utils/config.js'); const config = loadConfig(); expect(config.apiKey).toBe('env-api-key'); }); it('使用配置文件中的 provider', async () => { process.env.DEEPSEEK_API_KEY = 'deepseek-key'; vi.mocked(fs.existsSync).mockReturnValue(true); vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify({ provider: 'deepseek', })); vi.resetModules(); const { loadConfig } = await import('../../../src/utils/config.js'); const config = loadConfig(); expect(config.provider).toBe('deepseek'); expect(config.apiKey).toBe('deepseek-key'); }); it('OpenAI provider 使用 OPENAI_API_KEY', async () => { process.env.OPENAI_API_KEY = 'openai-key'; vi.mocked(fs.existsSync).mockReturnValue(true); vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify({ provider: 'openai', })); vi.resetModules(); const { loadConfig } = await import('../../../src/utils/config.js'); const config = loadConfig(); expect(config.provider).toBe('openai'); expect(config.apiKey).toBe('openai-key'); }); it('环境变量 AI_MODEL 覆盖配置文件', async () => { process.env.ANTHROPIC_API_KEY = 'test-key'; process.env.AI_MODEL = 'claude-opus-4-20250514'; vi.mocked(fs.existsSync).mockReturnValue(true); vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify({ provider: 'anthropic', model: 'claude-sonnet-4-20250514', })); vi.resetModules(); const { loadConfig } = await import('../../../src/utils/config.js'); const config = loadConfig(); expect(config.model).toBe('claude-opus-4-20250514'); }); it('使用默认模型', async () => { process.env.ANTHROPIC_API_KEY = 'test-key'; vi.mocked(fs.existsSync).mockReturnValue(false); vi.resetModules(); const { loadConfig } = await import('../../../src/utils/config.js'); const config = loadConfig(); expect(config.model).toBe('claude-sonnet-4-20250514'); }); it('环境变量 AI_BASE_URL 覆盖配置文件', async () => { process.env.ANTHROPIC_API_KEY = 'test-key'; process.env.AI_BASE_URL = 'https://custom-api.example.com'; vi.mocked(fs.existsSync).mockReturnValue(true); vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify({ provider: 'anthropic', baseUrl: 'https://config-api.example.com', })); vi.resetModules(); const { loadConfig } = await import('../../../src/utils/config.js'); const config = loadConfig(); expect(config.baseUrl).toBe('https://custom-api.example.com'); }); it('无 API Key 时退出程序', async () => { vi.mocked(fs.existsSync).mockReturnValue(false); vi.resetModules(); const { loadConfig } = await import('../../../src/utils/config.js'); expect(() => loadConfig()).toThrow('process.exit called'); expect(mockConsoleError).toHaveBeenCalled(); }); }); describe('loadVisionConfig - 加载 Vision 配置', () => { it('返回 null 当没有 API Key', async () => { vi.mocked(fs.existsSync).mockReturnValue(false); vi.resetModules(); const { loadVisionConfig } = await import('../../../src/utils/config.js'); const config = loadVisionConfig(); expect(config).toBeNull(); }); it('使用环境变量配置', async () => { process.env.VISION_PROVIDER = 'openai'; process.env.VISION_MODEL = 'gpt-4o'; process.env.VISION_API_KEY = 'vision-key'; process.env.VISION_BASE_URL = 'https://vision-api.example.com'; vi.mocked(fs.existsSync).mockReturnValue(false); vi.resetModules(); const { loadVisionConfig } = await import('../../../src/utils/config.js'); const config = loadVisionConfig(); expect(config).not.toBeNull(); expect(config!.provider).toBe('openai'); expect(config!.model).toBe('gpt-4o'); expect(config!.apiKey).toBe('vision-key'); expect(config!.baseUrl).toBe('https://vision-api.example.com'); }); it('默认使用 anthropic provider', async () => { process.env.ANTHROPIC_API_KEY = 'anthropic-key'; vi.mocked(fs.existsSync).mockReturnValue(false); vi.resetModules(); const { loadVisionConfig } = await import('../../../src/utils/config.js'); const config = loadVisionConfig(); expect(config).not.toBeNull(); expect(config!.provider).toBe('anthropic'); }); it('Vision 专用 Key 优先于 provider Key', async () => { process.env.ANTHROPIC_API_KEY = 'anthropic-key'; process.env.VISION_API_KEY = 'vision-specific-key'; vi.mocked(fs.existsSync).mockReturnValue(false); vi.resetModules(); const { loadVisionConfig } = await import('../../../src/utils/config.js'); const config = loadVisionConfig(); expect(config!.apiKey).toBe('vision-specific-key'); }); it('从配置文件加载 Vision 设置', async () => { vi.mocked(fs.existsSync).mockReturnValue(true); vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify({ visionProvider: 'openai', visionModel: 'gpt-4o', visionApiKey: 'config-vision-key', visionBaseUrl: 'https://config-vision.example.com', })); vi.resetModules(); const { loadVisionConfig } = await import('../../../src/utils/config.js'); const config = loadVisionConfig(); expect(config).not.toBeNull(); expect(config!.provider).toBe('openai'); expect(config!.model).toBe('gpt-4o'); expect(config!.apiKey).toBe('config-vision-key'); }); it('DeepSeek provider 回退到 deepseekApiKey', async () => { process.env.DEEPSEEK_API_KEY = 'deepseek-key'; vi.mocked(fs.existsSync).mockReturnValue(true); vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify({ visionProvider: 'deepseek', })); vi.resetModules(); const { loadVisionConfig } = await import('../../../src/utils/config.js'); const config = loadVisionConfig(); expect(config).not.toBeNull(); expect(config!.apiKey).toBe('deepseek-key'); }); }); describe('saveConfig - 保存配置', () => { it('创建配置目录(如果不存在)', async () => { vi.mocked(fs.existsSync).mockReturnValue(false); vi.resetModules(); const { saveConfig } = await import('../../../src/utils/config.js'); saveConfig({ provider: 'anthropic' }); expect(fs.mkdirSync).toHaveBeenCalledWith(CONFIG_DIR, { recursive: true }); }); it('合并现有配置', async () => { vi.mocked(fs.existsSync).mockImplementation((p) => p === CONFIG_FILE); vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify({ provider: 'anthropic', apiKey: 'existing-key', })); vi.resetModules(); const { saveConfig } = await import('../../../src/utils/config.js'); saveConfig({ model: 'claude-opus-4-20250514' }); expect(fs.writeFileSync).toHaveBeenCalledWith( CONFIG_FILE, expect.stringContaining('"model": "claude-opus-4-20250514"') ); expect(fs.writeFileSync).toHaveBeenCalledWith( CONFIG_FILE, expect.stringContaining('"provider": "anthropic"') ); }); it('覆盖相同字段', async () => { vi.mocked(fs.existsSync).mockImplementation((p) => p === CONFIG_FILE); vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify({ provider: 'anthropic', model: 'old-model', })); vi.resetModules(); const { saveConfig } = await import('../../../src/utils/config.js'); saveConfig({ model: 'new-model' }); const writeCall = vi.mocked(fs.writeFileSync).mock.calls[0]; const savedConfig = JSON.parse(writeCall[1] as string); expect(savedConfig.model).toBe('new-model'); }); it('解析错误时使用空对象合并', async () => { vi.mocked(fs.existsSync).mockImplementation((p) => p === CONFIG_FILE); vi.mocked(fs.readFileSync).mockReturnValue('invalid json'); vi.resetModules(); const { saveConfig } = await import('../../../src/utils/config.js'); saveConfig({ provider: 'deepseek' }); expect(fs.writeFileSync).toHaveBeenCalled(); }); }); });