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
339 lines
11 KiB
TypeScript
339 lines
11 KiB
TypeScript
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();
|
|
});
|
|
});
|
|
});
|