feat: 添加完整的单元测试套件
- 新增 vitest 测试框架配置 - 添加 54 个测试文件,共 951 个测试用例 - 覆盖核心模块: - Agent: executor, registry, config-loader, permission-merger - Context: manager, compaction, prune, token-counter - Permission: manager, bash/file/git/web checkers, wildcard - Session: manager, storage - Tools: filesystem (12个), git (10个), web, shell, todo, task - LSP: client, server, language - Utils: config, diff - UI: terminal
This commit is contained in:
@@ -0,0 +1,363 @@
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
|
||||
// Mock state
|
||||
const mockState = {
|
||||
generateTextResult: {
|
||||
text: '任务完成',
|
||||
steps: [{ toolCalls: [] }],
|
||||
},
|
||||
streamTextResult: {
|
||||
textStream: (async function* () {
|
||||
yield '流式';
|
||||
yield '输出';
|
||||
})(),
|
||||
response: Promise.resolve({}),
|
||||
},
|
||||
};
|
||||
|
||||
// Mock @ai-sdk/anthropic
|
||||
vi.mock('@ai-sdk/anthropic', () => ({
|
||||
createAnthropic: vi.fn(() => vi.fn(() => ({ modelId: 'claude-3' }))),
|
||||
}));
|
||||
|
||||
// Mock @ai-sdk/deepseek
|
||||
vi.mock('@ai-sdk/deepseek', () => ({
|
||||
createDeepSeek: vi.fn(() => vi.fn(() => ({ modelId: 'deepseek' }))),
|
||||
}));
|
||||
|
||||
// Mock ai package
|
||||
vi.mock('ai', () => ({
|
||||
generateText: vi.fn(async () => mockState.generateTextResult),
|
||||
streamText: vi.fn(() => mockState.streamTextResult),
|
||||
stepCountIs: vi.fn(() => () => false),
|
||||
}));
|
||||
|
||||
// Mock permission-merger
|
||||
vi.mock('../../../src/agent/permission-merger.js', () => ({
|
||||
checkBashPermission: vi.fn(() => 'allow'),
|
||||
}));
|
||||
|
||||
// Mock types
|
||||
vi.mock('../../../src/types/index.js', () => ({
|
||||
buildZodSchema: vi.fn(() => ({})),
|
||||
}));
|
||||
|
||||
import { AgentExecutor } from '../../../src/agent/executor.js';
|
||||
import { generateText, streamText } from 'ai';
|
||||
import { checkBashPermission } from '../../../src/agent/permission-merger.js';
|
||||
|
||||
describe('AgentExecutor - Agent 执行器', () => {
|
||||
let executor: AgentExecutor;
|
||||
let mockToolRegistry: any;
|
||||
let mockAgentInfo: any;
|
||||
let mockBaseConfig: any;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
|
||||
mockToolRegistry = {
|
||||
getAllTools: vi.fn(() => [
|
||||
{
|
||||
name: 'bash',
|
||||
description: '执行命令',
|
||||
parameters: { command: { type: 'string', required: true } },
|
||||
execute: vi.fn().mockResolvedValue({ success: true, output: 'ok' }),
|
||||
},
|
||||
{
|
||||
name: 'read_file',
|
||||
description: '读取文件',
|
||||
parameters: { path: { type: 'string', required: true } },
|
||||
execute: vi.fn().mockResolvedValue({ success: true, output: 'content' }),
|
||||
},
|
||||
{
|
||||
name: 'task',
|
||||
description: '子任务',
|
||||
parameters: { prompt: { type: 'string', required: true } },
|
||||
execute: vi.fn().mockResolvedValue({ success: true, output: 'done' }),
|
||||
},
|
||||
]),
|
||||
};
|
||||
|
||||
mockAgentInfo = {
|
||||
name: 'test-agent',
|
||||
description: '测试 Agent',
|
||||
mode: 'subagent',
|
||||
prompt: '你是测试助手',
|
||||
};
|
||||
|
||||
mockBaseConfig = {
|
||||
provider: 'anthropic',
|
||||
model: 'claude-3-sonnet',
|
||||
apiKey: 'test-api-key',
|
||||
maxTokens: 4096,
|
||||
systemPrompt: '默认系统提示词',
|
||||
};
|
||||
|
||||
// 重置 mock 结果
|
||||
mockState.generateTextResult = {
|
||||
text: '任务完成',
|
||||
steps: [{ toolCalls: [] }],
|
||||
};
|
||||
|
||||
executor = new AgentExecutor(mockAgentInfo, mockBaseConfig, mockToolRegistry);
|
||||
});
|
||||
|
||||
describe('构造函数', () => {
|
||||
it('成功创建 Anthropic provider', () => {
|
||||
const exec = new AgentExecutor(mockAgentInfo, mockBaseConfig, mockToolRegistry);
|
||||
expect(exec).toBeDefined();
|
||||
});
|
||||
|
||||
it('成功创建 DeepSeek provider', () => {
|
||||
const config = { ...mockBaseConfig, provider: 'deepseek' as const };
|
||||
const exec = new AgentExecutor(mockAgentInfo, config, mockToolRegistry);
|
||||
expect(exec).toBeDefined();
|
||||
});
|
||||
|
||||
it('不支持的 provider 抛出错误', () => {
|
||||
const config = { ...mockBaseConfig, provider: 'unknown' as any };
|
||||
expect(() => new AgentExecutor(mockAgentInfo, config, mockToolRegistry)).toThrow('不支持的 provider');
|
||||
});
|
||||
|
||||
it('使用 Agent 指定的 provider', () => {
|
||||
const agentInfo = {
|
||||
...mockAgentInfo,
|
||||
model: { provider: 'deepseek' as const, model: 'deepseek-chat' },
|
||||
};
|
||||
const exec = new AgentExecutor(agentInfo, mockBaseConfig, mockToolRegistry);
|
||||
expect(exec).toBeDefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('execute - 执行', () => {
|
||||
it('非流式模式成功执行', async () => {
|
||||
const result = await executor.execute('测试任务', {});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.text).toBe('任务完成');
|
||||
expect(generateText).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('流式模式成功执行', async () => {
|
||||
const onStream = vi.fn();
|
||||
|
||||
// 重置流式结果
|
||||
mockState.streamTextResult = {
|
||||
textStream: (async function* () {
|
||||
yield '流式';
|
||||
yield '输出';
|
||||
})(),
|
||||
response: Promise.resolve({}),
|
||||
};
|
||||
|
||||
const result = await executor.execute('测试任务', { onStream });
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.text).toBe('流式输出');
|
||||
expect(streamText).toHaveBeenCalled();
|
||||
expect(onStream).toHaveBeenCalledWith('流式');
|
||||
expect(onStream).toHaveBeenCalledWith('输出');
|
||||
});
|
||||
|
||||
it('执行失败返回错误', async () => {
|
||||
vi.mocked(generateText).mockRejectedValueOnce(new Error('API 错误'));
|
||||
|
||||
const result = await executor.execute('测试任务', {});
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toContain('API 错误');
|
||||
});
|
||||
|
||||
it('传递父会话 ID', async () => {
|
||||
const result = await executor.execute('测试任务', {
|
||||
parentSessionId: 'parent-123',
|
||||
});
|
||||
|
||||
expect(result.sessionId).toBe('parent-123');
|
||||
});
|
||||
|
||||
it('无父会话 ID 使用 standalone', async () => {
|
||||
const result = await executor.execute('测试任务', {});
|
||||
|
||||
expect(result.sessionId).toBe('standalone');
|
||||
});
|
||||
});
|
||||
|
||||
describe('工具过滤', () => {
|
||||
it('无配置返回所有工具', async () => {
|
||||
await executor.execute('测试', {});
|
||||
|
||||
const call = vi.mocked(generateText).mock.calls[0][0];
|
||||
expect(Object.keys(call.tools || {})).toHaveLength(3);
|
||||
});
|
||||
|
||||
it('enabled 配置只保留指定工具', async () => {
|
||||
mockAgentInfo.tools = { enabled: ['bash'] };
|
||||
executor = new AgentExecutor(mockAgentInfo, mockBaseConfig, mockToolRegistry);
|
||||
|
||||
await executor.execute('测试', {});
|
||||
|
||||
const call = vi.mocked(generateText).mock.calls[0][0];
|
||||
expect(Object.keys(call.tools || {})).toContain('bash');
|
||||
expect(Object.keys(call.tools || {})).not.toContain('read_file');
|
||||
});
|
||||
|
||||
it('disabled 配置移除指定工具', async () => {
|
||||
mockAgentInfo.tools = { disabled: ['task'] };
|
||||
executor = new AgentExecutor(mockAgentInfo, mockBaseConfig, mockToolRegistry);
|
||||
|
||||
await executor.execute('测试', {});
|
||||
|
||||
const call = vi.mocked(generateText).mock.calls[0][0];
|
||||
expect(Object.keys(call.tools || {})).not.toContain('task');
|
||||
});
|
||||
|
||||
it('noTask 配置移除 task 工具', async () => {
|
||||
mockAgentInfo.tools = { noTask: true };
|
||||
executor = new AgentExecutor(mockAgentInfo, mockBaseConfig, mockToolRegistry);
|
||||
|
||||
await executor.execute('测试', {});
|
||||
|
||||
const call = vi.mocked(generateText).mock.calls[0][0];
|
||||
expect(Object.keys(call.tools || {})).not.toContain('task');
|
||||
});
|
||||
});
|
||||
|
||||
describe('权限检查', () => {
|
||||
it('bash 命令被拒绝', async () => {
|
||||
vi.mocked(checkBashPermission).mockReturnValue('deny');
|
||||
mockAgentInfo.permission = { bash: { deny: ['rm *'] } };
|
||||
executor = new AgentExecutor(mockAgentInfo, mockBaseConfig, mockToolRegistry);
|
||||
|
||||
// 获取工具并执行
|
||||
await executor.execute('测试', {});
|
||||
|
||||
const call = vi.mocked(generateText).mock.calls[0][0];
|
||||
const bashTool = call.tools?.bash;
|
||||
|
||||
if (bashTool && 'execute' in bashTool) {
|
||||
const result = await bashTool.execute({ command: 'rm -rf /' });
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toContain('权限拒绝');
|
||||
}
|
||||
});
|
||||
|
||||
it('文件写入被拒绝', async () => {
|
||||
mockAgentInfo.permission = { file: { write: 'deny' } };
|
||||
|
||||
// 添加 write_file 工具
|
||||
mockToolRegistry.getAllTools.mockReturnValue([
|
||||
{
|
||||
name: 'write_file',
|
||||
description: '写文件',
|
||||
parameters: { path: { type: 'string', required: true } },
|
||||
execute: vi.fn().mockResolvedValue({ success: true, output: 'ok' }),
|
||||
},
|
||||
]);
|
||||
|
||||
executor = new AgentExecutor(mockAgentInfo, mockBaseConfig, mockToolRegistry);
|
||||
await executor.execute('测试', {});
|
||||
|
||||
const call = vi.mocked(generateText).mock.calls[0][0];
|
||||
const writeTool = call.tools?.write_file;
|
||||
|
||||
if (writeTool && 'execute' in writeTool) {
|
||||
const result = await writeTool.execute({ path: '/test.txt', content: 'test' });
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toContain('权限拒绝');
|
||||
}
|
||||
});
|
||||
|
||||
it('Git 写操作被拒绝', async () => {
|
||||
mockAgentInfo.permission = { git: { write: 'deny' } };
|
||||
|
||||
mockToolRegistry.getAllTools.mockReturnValue([
|
||||
{
|
||||
name: 'git_push',
|
||||
description: 'Git push',
|
||||
parameters: {},
|
||||
execute: vi.fn().mockResolvedValue({ success: true, output: 'ok' }),
|
||||
},
|
||||
]);
|
||||
|
||||
executor = new AgentExecutor(mockAgentInfo, mockBaseConfig, mockToolRegistry);
|
||||
await executor.execute('测试', {});
|
||||
|
||||
const call = vi.mocked(generateText).mock.calls[0][0];
|
||||
const gitTool = call.tools?.git_push;
|
||||
|
||||
if (gitTool && 'execute' in gitTool) {
|
||||
const result = await gitTool.execute({});
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toContain('Git 写操作被禁止');
|
||||
}
|
||||
});
|
||||
|
||||
it('无权限配置允许所有操作', async () => {
|
||||
// 无 permission 配置
|
||||
delete mockAgentInfo.permission;
|
||||
executor = new AgentExecutor(mockAgentInfo, mockBaseConfig, mockToolRegistry);
|
||||
await executor.execute('测试', {});
|
||||
|
||||
const call = vi.mocked(generateText).mock.calls[0][0];
|
||||
const bashTool = call.tools?.bash;
|
||||
|
||||
if (bashTool && 'execute' in bashTool) {
|
||||
const result = await bashTool.execute({ command: 'ls' });
|
||||
expect(result.success).toBe(true);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
describe('系统提示词', () => {
|
||||
it('使用 Agent 自定义提示词', async () => {
|
||||
mockAgentInfo.prompt = '自定义提示词';
|
||||
executor = new AgentExecutor(mockAgentInfo, mockBaseConfig, mockToolRegistry);
|
||||
|
||||
await executor.execute('测试', {});
|
||||
|
||||
const call = vi.mocked(generateText).mock.calls[0][0];
|
||||
expect(call.system).toBe('自定义提示词');
|
||||
});
|
||||
|
||||
it('无自定义提示词使用基础配置', async () => {
|
||||
delete mockAgentInfo.prompt;
|
||||
executor = new AgentExecutor(mockAgentInfo, mockBaseConfig, mockToolRegistry);
|
||||
|
||||
await executor.execute('测试', {});
|
||||
|
||||
const call = vi.mocked(generateText).mock.calls[0][0];
|
||||
expect(call.system).toBe('默认系统提示词');
|
||||
});
|
||||
});
|
||||
|
||||
describe('模型配置', () => {
|
||||
it('使用 Agent 指定的模型', async () => {
|
||||
mockAgentInfo.model = { model: 'claude-3-opus' };
|
||||
executor = new AgentExecutor(mockAgentInfo, mockBaseConfig, mockToolRegistry);
|
||||
|
||||
await executor.execute('测试', {});
|
||||
|
||||
expect(generateText).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('使用 Agent 指定的 maxSteps', async () => {
|
||||
mockAgentInfo.maxSteps = 5;
|
||||
executor = new AgentExecutor(mockAgentInfo, mockBaseConfig, mockToolRegistry);
|
||||
|
||||
await executor.execute('测试', {});
|
||||
|
||||
expect(generateText).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('使用 Agent 指定的 maxTokens', async () => {
|
||||
mockAgentInfo.model = { maxTokens: 8192 };
|
||||
executor = new AgentExecutor(mockAgentInfo, mockBaseConfig, mockToolRegistry);
|
||||
|
||||
await executor.execute('测试', {});
|
||||
|
||||
const call = vi.mocked(generateText).mock.calls[0][0];
|
||||
expect(call.maxOutputTokens).toBe(8192);
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user