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 provider registry vi.mock('../../../src/provider/index.js', () => ({ providerRegistry: { getInfo: vi.fn(), getConfig: vi.fn(), }, resolveApiKey: 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(); }); afterEach(() => { mockExit.mockClear(); mockConsoleError.mockClear(); }); describe('getConfig - 获取原始配置', () => { it('配置文件不存在返回空对象', async () => { vi.mocked(fs.existsSync).mockReturnValue(false); 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', 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.model).toBe('claude-sonnet-4-20250514'); }); 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('通过 ProviderRegistry 获取 API Key', async () => { vi.mocked(fs.existsSync).mockReturnValue(true); vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify({ provider: 'anthropic', })); vi.resetModules(); const { resolveApiKey } = await import('../../../src/provider/index.js'); const { loadConfig } = await import('../../../src/utils/config.js'); vi.mocked(resolveApiKey).mockReturnValue('resolved-api-key'); const config = loadConfig(); expect(config.apiKey).toBe('resolved-api-key'); }); it('使用配置文件中的 provider', async () => { vi.mocked(fs.existsSync).mockReturnValue(true); vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify({ provider: 'deepseek', })); vi.resetModules(); const { resolveApiKey } = await import('../../../src/provider/index.js'); const { loadConfig } = await import('../../../src/utils/config.js'); vi.mocked(resolveApiKey).mockReturnValue('deepseek-key'); const config = loadConfig(); expect(config.provider).toBe('deepseek'); expect(config.apiKey).toBe('deepseek-key'); }); it('使用默认模型', async () => { vi.mocked(fs.existsSync).mockReturnValue(false); vi.resetModules(); const { resolveApiKey } = await import('../../../src/provider/index.js'); const { loadConfig } = await import('../../../src/utils/config.js'); vi.mocked(resolveApiKey).mockReturnValue('test-key'); const config = loadConfig(); expect(config.model).toBe('claude-sonnet-4-20250514'); }); it('无 API Key 时退出程序', async () => { vi.mocked(fs.existsSync).mockReturnValue(false); vi.resetModules(); const { resolveApiKey } = await import('../../../src/provider/index.js'); const { loadConfig } = await import('../../../src/utils/config.js'); vi.mocked(resolveApiKey).mockReturnValue(undefined); 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 { resolveApiKey } = await import('../../../src/provider/index.js'); const { loadVisionConfig } = await import('../../../src/utils/config.js'); vi.mocked(resolveApiKey).mockReturnValue(undefined); const config = loadVisionConfig(); expect(config).toBeNull(); }); it('从配置文件获取 Vision 设置', async () => { vi.mocked(fs.existsSync).mockReturnValue(true); vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify({ visionProvider: 'openai', visionModel: 'gpt-4o', visionBaseUrl: 'https://config-vision.example.com', })); vi.resetModules(); const { resolveApiKey } = await import('../../../src/provider/index.js'); const { loadVisionConfig } = await import('../../../src/utils/config.js'); vi.mocked(resolveApiKey).mockReturnValue('config-vision-key'); 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('默认使用 anthropic provider', async () => { vi.mocked(fs.existsSync).mockReturnValue(false); vi.resetModules(); const { resolveApiKey } = await import('../../../src/provider/index.js'); const { loadVisionConfig } = await import('../../../src/utils/config.js'); vi.mocked(resolveApiKey).mockReturnValue('anthropic-key'); const config = loadVisionConfig(); expect(config).not.toBeNull(); expect(config!.provider).toBe('anthropic'); }); }); 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', })); 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'); }); }); });