bca19b7741
新增测试文件: - agent/executor-extended.test.ts, presets/ - context/manager-extended.test.ts - core/agent.test.ts, providers.test.ts - lsp/cli.test.ts, client-extended.test.ts, index.test.ts - permission/file-prompt.test.ts, prompt.test.ts - skills/builtin/ - tools/filesystem/write_file-extended.test.ts - tools/git/git_commit-extended.test.ts - tools/load_description.test.ts - tools/todo/todo-manager.test.ts - tools/tool-search.test.ts - types/ - utils/config-extended.test.ts, diff-extended.test.ts 修改现有测试: - agent/manager.test.ts - tools/skill/skill.test.ts - utils/config.test.ts, diff.test.ts, image.test.ts
358 lines
11 KiB
TypeScript
358 lines
11 KiB
TypeScript
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');
|
|
});
|
|
});
|
|
});
|