refactor(core): 统一配置系统,移除 config.json

- 移除 config.json,所有配置统一从 agents.json 和 providers.json 读取
- config-loader.ts 从全局目录 ~/.ai-terminal-assistant/ 加载配置
- loadConfig() 从 agentRegistry.getGlobalConfig() 获取 defaults.model
- 添加 loadVisionConfig() 支持 Vision 模型配置
- Tavily API Key 仅从环境变量读取
- UI AgentDefaultsEditor 添加 Vision 模型配置界面
- 更新相关测试
This commit is contained in:
2025-12-16 00:33:29 +08:00
parent 76b1ae1573
commit 9376887995
14 changed files with 414 additions and 643 deletions
@@ -8,12 +8,8 @@ vi.mock('@tavily/core', () => ({
})),
}));
// Mock config
vi.mock('../../../../src/utils/config.js', () => ({
getConfig: vi.fn(() => ({
tavilyApiKey: 'test-api-key',
})),
}));
// Mock environment variable for Tavily API Key
vi.stubEnv('TAVILY_API_KEY', 'test-api-key');
// Mock permission manager
vi.mock('../../../../src/permission/index.js', () => ({
@@ -32,7 +28,6 @@ vi.mock('../../../../src/tools/load_description.js', () => ({
import { webExtractTool } from '../../../../src/tools/web/web_extract.js';
import { getPermissionManager } from '../../../../src/permission/index.js';
import { getConfig } from '../../../../src/utils/config.js';
describe('webExtractTool - 网页内容提取工具', () => {
beforeEach(() => {
@@ -198,9 +193,7 @@ describe('webExtractTool - 网页内容提取工具', () => {
});
it('无 API Key 返回错误', async () => {
vi.mocked(getConfig).mockReturnValue({} as any);
const originalEnv = process.env.TAVILY_API_KEY;
delete process.env.TAVILY_API_KEY;
vi.unstubAllEnvs();
const result = await webExtractTool.execute({
urls: ['https://example.com'],
@@ -209,7 +202,7 @@ describe('webExtractTool - 网页内容提取工具', () => {
expect(result.success).toBe(false);
expect(result.error).toContain('未配置 Tavily API Key');
process.env.TAVILY_API_KEY = originalEnv;
vi.stubEnv('TAVILY_API_KEY', 'test-api-key');
});
it('权限被拒绝时返回错误', async () => {
@@ -8,12 +8,8 @@ vi.mock('@tavily/core', () => ({
})),
}));
// Mock config
vi.mock('../../../../src/utils/config.js', () => ({
getConfig: vi.fn(() => ({
tavilyApiKey: 'test-api-key',
})),
}));
// Mock environment variable for Tavily API Key
vi.stubEnv('TAVILY_API_KEY', 'test-api-key');
// Mock permission manager
vi.mock('../../../../src/permission/index.js', () => ({
@@ -32,7 +28,6 @@ vi.mock('../../../../src/tools/load_description.js', () => ({
import { webSearchTool } from '../../../../src/tools/web/web_search.js';
import { getPermissionManager } from '../../../../src/permission/index.js';
import { getConfig } from '../../../../src/utils/config.js';
describe('webSearchTool - 网络搜索工具', () => {
beforeEach(() => {
@@ -131,16 +126,14 @@ describe('webSearchTool - 网络搜索工具', () => {
});
it('无 API Key 返回错误', async () => {
vi.mocked(getConfig).mockReturnValue({} as any);
const originalEnv = process.env.TAVILY_API_KEY;
delete process.env.TAVILY_API_KEY;
vi.unstubAllEnvs();
const result = await webSearchTool.execute({ query: 'test' });
expect(result.success).toBe(false);
expect(result.error).toContain('未配置 Tavily API Key');
process.env.TAVILY_API_KEY = originalEnv;
vi.stubEnv('TAVILY_API_KEY', 'test-api-key');
});
it('权限被拒绝时返回错误', async () => {
@@ -1,243 +0,0 @@
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');
});
});
});
+149 -167
View File
@@ -1,108 +1,103 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
// Mock fs
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(),
getModelInfo: vi.fn(),
},
resolveApiKey: vi.fn(),
}));
import * as fs from 'fs';
// Mock agent registry
vi.mock('../../../src/agent/registry.js', () => ({
agentRegistry: {
getGlobalConfig: vi.fn(),
},
}));
import { providerRegistry, resolveApiKey } from '../../../src/provider/index.js';
import { getConfig, loadConfig, saveConfig, loadVisionConfig } from '../../../src/utils/config.js';
import { agentRegistry } from '../../../src/agent/registry.js';
import { loadConfig, loadVisionConfig, ConfigurationError } from '../../../src/utils/config.js';
describe('Config - 配置管理', () => {
beforeEach(() => {
vi.clearAllMocks();
});
describe('getConfig - 获取原始配置', () => {
it('配置文件存在时返回内容', () => {
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify({
provider: 'anthropic',
model: 'claude-3-opus',
}));
const config = getConfig();
expect(config.provider).toBe('anthropic');
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('通过 ProviderRegistry 获取 Anthropic 配置', () => {
vi.mocked(fs.existsSync).mockReturnValue(false);
vi.mocked(providerRegistry.getConfig).mockReturnValue(undefined);
vi.mocked(resolveApiKey).mockReturnValue('resolved-anthropic-key');
it('从 AgentRegistry defaults.model 获取配置', () => {
vi.mocked(agentRegistry.getGlobalConfig).mockReturnValue({
model: {
provider: 'openai',
model: 'gpt-4o',
maxTokens: 8192,
},
});
vi.mocked(providerRegistry.getConfig).mockReturnValue({
id: 'openai',
baseUrl: 'https://api.openai.com/v1',
});
vi.mocked(resolveApiKey).mockReturnValue('openai-api-key');
const config = loadConfig();
expect(config.provider).toBe('anthropic');
expect(config.apiKey).toBe('resolved-anthropic-key');
expect(config.model).toBe('claude-sonnet-4-20250514');
});
it('从配置文件获取 DeepSeek provider', () => {
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify({
provider: 'deepseek',
}));
vi.mocked(providerRegistry.getConfig).mockReturnValue(undefined);
vi.mocked(resolveApiKey).mockReturnValue('resolved-deepseek-key');
const config = loadConfig();
expect(config.provider).toBe('deepseek');
expect(config.apiKey).toBe('resolved-deepseek-key');
expect(config.model).toBe('deepseek-chat');
});
it('配置文件中的 model 和 maxTokens', () => {
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify({
model: 'custom-model',
maxTokens: 8192,
}));
vi.mocked(providerRegistry.getConfig).mockReturnValue(undefined);
vi.mocked(resolveApiKey).mockReturnValue('test-key');
const config = loadConfig();
expect(config.model).toBe('custom-model');
expect(config.provider).toBe('openai');
expect(config.model).toBe('gpt-4o');
expect(config.apiKey).toBe('openai-api-key');
expect(config.maxTokens).toBe(8192);
expect(config.baseUrl).toBe('https://api.openai.com/v1');
});
it('未配置 defaults.model 时使用第一个有 API Key 的 provider', () => {
vi.mocked(agentRegistry.getGlobalConfig).mockReturnValue(null);
// anthropic 没有 API Key
vi.mocked(providerRegistry.getConfig).mockImplementation((id) => {
if (id === 'openai') {
return { id: 'openai' };
}
return undefined;
});
vi.mocked(resolveApiKey).mockImplementation((config) => {
if (config?.id === 'openai') {
return 'openai-fallback-key';
}
return undefined;
});
const config = loadConfig();
expect(config.provider).toBe('openai');
expect(config.apiKey).toBe('openai-fallback-key');
expect(config.model).toBe('gpt-4o'); // 默认模型
});
it('没有任何 provider 配置 API Key 时抛出错误', () => {
vi.mocked(agentRegistry.getGlobalConfig).mockReturnValue(null);
vi.mocked(providerRegistry.getConfig).mockReturnValue(undefined);
vi.mocked(resolveApiKey).mockReturnValue(undefined);
expect(() => loadConfig()).toThrow(ConfigurationError);
expect(() => loadConfig()).toThrow('请先在 Providers 面板配置一个模型提供商的 API Key');
});
it('配置了 provider 但没有 API Key 时抛出错误', () => {
vi.mocked(agentRegistry.getGlobalConfig).mockReturnValue({
model: {
provider: 'deepseek',
},
});
vi.mocked(providerRegistry.getConfig).mockReturnValue({ id: 'deepseek' });
vi.mocked(resolveApiKey).mockReturnValue(undefined);
expect(() => loadConfig()).toThrow(ConfigurationError);
expect(() => loadConfig()).toThrow('请在 Providers 面板配置 deepseek 的 API Key');
});
it('包含系统提示词', () => {
vi.mocked(fs.existsSync).mockReturnValue(false);
vi.mocked(agentRegistry.getGlobalConfig).mockReturnValue(null);
vi.mocked(providerRegistry.getConfig).mockReturnValue({ id: 'anthropic' });
vi.mocked(resolveApiKey).mockReturnValue('test-key');
const config = loadConfig();
@@ -112,7 +107,12 @@ describe('Config - 配置管理', () => {
});
it('默认 maxTokens 为 4096', () => {
vi.mocked(fs.existsSync).mockReturnValue(false);
vi.mocked(agentRegistry.getGlobalConfig).mockReturnValue({
model: {
provider: 'anthropic',
},
});
vi.mocked(providerRegistry.getConfig).mockReturnValue({ id: 'anthropic' });
vi.mocked(resolveApiKey).mockReturnValue('test-key');
const config = loadConfig();
@@ -120,11 +120,16 @@ describe('Config - 配置管理', () => {
expect(config.maxTokens).toBe(4096);
});
it('从配置文件获取 baseUrl', () => {
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify({
it('从 ProviderConfig 获取 baseUrl', () => {
vi.mocked(agentRegistry.getGlobalConfig).mockReturnValue({
model: {
provider: 'openai',
},
});
vi.mocked(providerRegistry.getConfig).mockReturnValue({
id: 'openai',
baseUrl: 'https://custom.api.com/v1',
}));
});
vi.mocked(resolveApiKey).mockReturnValue('test-key');
const config = loadConfig();
@@ -132,67 +137,44 @@ describe('Config - 配置管理', () => {
expect(config.baseUrl).toBe('https://custom.api.com/v1');
});
it('从 ProviderConfig 获取 baseUrl', () => {
vi.mocked(fs.existsSync).mockReturnValue(false);
vi.mocked(providerRegistry.getConfig).mockReturnValue({
id: 'anthropic',
baseUrl: 'https://provider-config.api.com/v1',
it('获取模型的 contextWindow', () => {
vi.mocked(agentRegistry.getGlobalConfig).mockReturnValue({
model: {
provider: 'anthropic',
model: 'claude-sonnet-4-20250514',
},
});
vi.mocked(providerRegistry.getConfig).mockReturnValue({ id: 'anthropic' });
vi.mocked(providerRegistry.getModelInfo).mockReturnValue({
id: 'claude-sonnet-4-20250514',
name: 'Claude Sonnet 4',
contextWindow: 200000,
});
vi.mocked(resolveApiKey).mockReturnValue('test-key');
const config = loadConfig();
expect(config.baseUrl).toBe('https://provider-config.api.com/v1');
});
});
describe('saveConfig - 保存配置', () => {
it('创建目录并保存配置', () => {
vi.mocked(fs.existsSync).mockReturnValue(false);
saveConfig({ provider: 'anthropic' });
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',
model: 'old-model',
}));
saveConfig({ model: 'new-model' });
const writeCall = vi.mocked(fs.writeFileSync).mock.calls[0];
const savedConfig = JSON.parse(writeCall[1] as string);
expect(savedConfig.provider).toBe('anthropic');
expect(savedConfig.model).toBe('new-model');
});
it('目录已存在时不重新创建', () => {
vi.mocked(fs.existsSync)
.mockReturnValueOnce(true) // 目录存在
.mockReturnValueOnce(true); // 配置文件存在
vi.mocked(fs.readFileSync).mockReturnValue('{}');
saveConfig({ provider: 'test' });
expect(fs.mkdirSync).not.toHaveBeenCalled();
expect(config.contextWindow).toBe(200000);
});
});
describe('loadVisionConfig - Vision 配置', () => {
it('返回 null 当没有配置 Vision API Key', () => {
vi.mocked(fs.existsSync).mockReturnValue(false);
it('返回 null 当没有配置 defaults.vision', () => {
vi.mocked(agentRegistry.getGlobalConfig).mockReturnValue(null);
const visionConfig = loadVisionConfig();
expect(visionConfig).toBeNull();
});
it('返回 null 当 vision provider 没有 API Key', () => {
vi.mocked(agentRegistry.getGlobalConfig).mockReturnValue({
vision: {
provider: 'openai',
model: 'gpt-4o',
},
});
vi.mocked(providerRegistry.getConfig).mockReturnValue({ id: 'openai' });
vi.mocked(resolveApiKey).mockReturnValue(undefined);
const visionConfig = loadVisionConfig();
@@ -200,12 +182,17 @@ describe('Config - 配置管理', () => {
expect(visionConfig).toBeNull();
});
it('通过 ProviderRegistry 获取 Vision 配置', () => {
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify({
visionProvider: 'openai',
visionModel: 'gpt-4-vision',
}));
it('从 defaults.vision 获取 Vision 配置', () => {
vi.mocked(agentRegistry.getGlobalConfig).mockReturnValue({
vision: {
provider: 'openai',
model: 'gpt-4o',
},
});
vi.mocked(providerRegistry.getConfig).mockReturnValue({
id: 'openai',
baseUrl: 'https://api.openai.com/v1',
});
vi.mocked(resolveApiKey).mockReturnValue('vision-api-key');
const visionConfig = loadVisionConfig();
@@ -213,33 +200,17 @@ describe('Config - 配置管理', () => {
expect(visionConfig).not.toBeNull();
expect(visionConfig?.provider).toBe('openai');
expect(visionConfig?.apiKey).toBe('vision-api-key');
expect(visionConfig?.model).toBe('gpt-4-vision');
expect(visionConfig?.model).toBe('gpt-4o');
expect(visionConfig?.baseUrl).toBe('https://api.openai.com/v1');
});
it('默认使用 anthropic provider', () => {
vi.mocked(fs.existsSync).mockReturnValue(false);
vi.mocked(resolveApiKey).mockReturnValue('anthropic-key');
const visionConfig = loadVisionConfig();
expect(visionConfig).not.toBeNull();
expect(visionConfig?.provider).toBe('anthropic');
});
it('Vision baseUrl 配置', () => {
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.readFileSync).mockReturnValue(JSON.stringify({
visionBaseUrl: 'https://vision.api.com/v1',
}));
vi.mocked(resolveApiKey).mockReturnValue('vision-key');
const visionConfig = loadVisionConfig();
expect(visionConfig?.baseUrl).toBe('https://vision.api.com/v1');
});
it('使用默认 Vision 模型', () => {
vi.mocked(fs.existsSync).mockReturnValue(false);
it('使用默认模型当 vision.model 未指定', () => {
vi.mocked(agentRegistry.getGlobalConfig).mockReturnValue({
vision: {
provider: 'anthropic',
},
});
vi.mocked(providerRegistry.getConfig).mockReturnValue({ id: 'anthropic' });
vi.mocked(resolveApiKey).mockReturnValue('test-key');
const visionConfig = loadVisionConfig();
@@ -247,4 +218,15 @@ describe('Config - 配置管理', () => {
expect(visionConfig?.model).toBe('claude-sonnet-4-20250514');
});
});
describe('ConfigurationError', () => {
it('包含 provider 和 missingKey 信息', () => {
const error = new ConfigurationError('Test error', 'openai', 'apiKey');
expect(error.name).toBe('ConfigurationError');
expect(error.message).toBe('Test error');
expect(error.provider).toBe('openai');
expect(error.missingKey).toBe('apiKey');
});
});
});