test: 补充单元测试提升代码覆盖率

新增测试文件:
- 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
This commit is contained in:
2025-12-11 20:37:03 +08:00
parent f8b0cd4bec
commit bca19b7741
24 changed files with 6347 additions and 4 deletions
+429
View File
@@ -0,0 +1,429 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
import type { LanguageModel } from 'ai';
import type { Tool, AgentConfig } from '../../../src/types/index.js';
import type { AgentInfo, AgentExecutionContext } from '../../../src/agent/types.js';
// Mock ai module
const mockGenerateText = vi.fn();
const mockStreamText = vi.fn();
vi.mock('ai', () => ({
generateText: (...args: unknown[]) => mockGenerateText(...args),
streamText: (...args: unknown[]) => mockStreamText(...args),
stepCountIs: vi.fn((n) => n),
}));
// Mock buildZodSchema
vi.mock('../../../src/types/index.js', async (importOriginal) => {
const actual = await importOriginal() as Record<string, unknown>;
return {
...actual,
buildZodSchema: vi.fn(() => ({})),
};
});
// Mock ToolRegistry
class MockToolRegistry {
getAllTools = vi.fn(() => []);
}
vi.mock('../../../src/tools/registry.js', () => ({
ToolRegistry: MockToolRegistry,
}));
// Mock permission merger
const mockCheckBashPermission = vi.fn();
vi.mock('../../../src/agent/permission-merger.js', () => ({
checkBashPermission: (...args: unknown[]) => mockCheckBashPermission(...args),
}));
// Mock providers
const mockGetModelFactory = vi.fn();
vi.mock('../../../src/core/providers.js', () => ({
getModelFactory: (...args: unknown[]) => mockGetModelFactory(...args),
}));
import { AgentExecutor } from '../../../src/agent/executor.js';
describe('AgentExecutor - Agent 执行器扩展测试', () => {
let executor: AgentExecutor;
let mockToolRegistry: MockToolRegistry;
let mockModel: LanguageModel;
const baseConfig: AgentConfig = {
provider: 'anthropic',
apiKey: 'test-key',
model: 'claude-sonnet-4-20250514',
maxTokens: 4096,
systemPrompt: 'You are a helpful assistant',
};
const basicAgentInfo: AgentInfo = {
name: 'test-agent',
description: 'Test agent',
mode: 'subagent',
};
const createMockTool = (name: string): Tool => ({
name,
description: `${name} tool`,
parameters: {},
execute: vi.fn().mockResolvedValue({ success: true, output: 'ok' }),
});
beforeEach(() => {
vi.clearAllMocks();
mockToolRegistry = new MockToolRegistry();
mockModel = {} as LanguageModel;
mockGetModelFactory.mockReturnValue(() => mockModel);
mockGenerateText.mockResolvedValue({
text: 'Response text',
steps: [{ text: 'step1' }],
});
mockStreamText.mockReturnValue({
textStream: (async function* () {
yield 'chunk1';
yield 'chunk2';
})(),
response: Promise.resolve({}),
});
mockCheckBashPermission.mockReturnValue('allow');
executor = new AgentExecutor(basicAgentInfo, baseConfig, mockToolRegistry as any);
});
describe('constructor - 构造函数', () => {
it('初始化执行器', () => {
expect(executor).toBeDefined();
expect(mockGetModelFactory).toHaveBeenCalledWith('anthropic', expect.any(Object));
});
it('使用 Agent 指定的 provider', () => {
const agentWithProvider: AgentInfo = {
...basicAgentInfo,
model: { provider: 'openai', model: 'gpt-4' },
};
new AgentExecutor(agentWithProvider, baseConfig, mockToolRegistry as any);
expect(mockGetModelFactory).toHaveBeenCalledWith('openai', expect.any(Object));
});
});
describe('execute - 执行任务', () => {
it('非流式模式返回结果', async () => {
const context: AgentExecutionContext = {};
const result = await executor.execute('test prompt', context);
expect(result.success).toBe(true);
expect(result.text).toBe('Response text');
expect(result.steps).toBe(1);
expect(mockGenerateText).toHaveBeenCalled();
});
it('流式模式调用回调', async () => {
const onStream = vi.fn();
const context: AgentExecutionContext = { onStream };
const result = await executor.execute('test prompt', context);
expect(result.success).toBe(true);
expect(onStream).toHaveBeenCalledWith('chunk1');
expect(onStream).toHaveBeenCalledWith('chunk2');
expect(mockStreamText).toHaveBeenCalled();
});
it('使用 Agent 的系统提示词', async () => {
const agentWithPrompt: AgentInfo = {
...basicAgentInfo,
prompt: 'Custom system prompt',
};
const customExecutor = new AgentExecutor(agentWithPrompt, baseConfig, mockToolRegistry as any);
await customExecutor.execute('test', {});
expect(mockGenerateText).toHaveBeenCalledWith(
expect.objectContaining({
system: 'Custom system prompt',
})
);
});
it('使用基础配置的系统提示词', async () => {
await executor.execute('test', {});
expect(mockGenerateText).toHaveBeenCalledWith(
expect.objectContaining({
system: 'You are a helpful assistant',
})
);
});
it('处理执行错误', async () => {
mockGenerateText.mockRejectedValue(new Error('API Error'));
const result = await executor.execute('test', {});
expect(result.success).toBe(false);
expect(result.error).toContain('API Error');
});
it('包含图片时构建多模态消息', async () => {
const context: AgentExecutionContext = {
images: [{ data: 'base64data', mimeType: 'image/png' }],
};
await executor.execute('describe this', context);
expect(mockGenerateText).toHaveBeenCalledWith(
expect.objectContaining({
messages: expect.arrayContaining([
expect.objectContaining({
content: expect.arrayContaining([
expect.objectContaining({ type: 'image' }),
expect.objectContaining({ type: 'text' }),
]),
}),
]),
})
);
});
it('使用 Agent 的 maxSteps 配置', async () => {
const agentWithSteps: AgentInfo = {
...basicAgentInfo,
maxSteps: 20,
};
const customExecutor = new AgentExecutor(agentWithSteps, baseConfig, mockToolRegistry as any);
await customExecutor.execute('test', {});
expect(mockGenerateText).toHaveBeenCalledWith(
expect.objectContaining({
stopWhen: 20,
})
);
});
it('使用 Agent 的 maxTokens 配置', async () => {
const agentWithTokens: AgentInfo = {
...basicAgentInfo,
model: { model: 'claude-sonnet', maxTokens: 8000 },
};
const customExecutor = new AgentExecutor(agentWithTokens, baseConfig, mockToolRegistry as any);
await customExecutor.execute('test', {});
expect(mockGenerateText).toHaveBeenCalledWith(
expect.objectContaining({
maxOutputTokens: 8000,
})
);
});
});
describe('getFilteredTools - 工具过滤', () => {
it('无配置返回所有工具', async () => {
const tools = [createMockTool('read'), createMockTool('write')];
mockToolRegistry.getAllTools.mockReturnValue(tools);
await executor.execute('test', {});
// 验证所有工具都被使用
expect(mockGenerateText).toHaveBeenCalledWith(
expect.objectContaining({
tools: expect.objectContaining({
read: expect.any(Object),
write: expect.any(Object),
}),
})
);
});
it('enabled 只保留指定工具', async () => {
const agentWithEnabled: AgentInfo = {
...basicAgentInfo,
tools: { enabled: ['read'] },
};
const tools = [createMockTool('read'), createMockTool('write')];
mockToolRegistry.getAllTools.mockReturnValue(tools);
const customExecutor = new AgentExecutor(agentWithEnabled, baseConfig, mockToolRegistry as any);
await customExecutor.execute('test', {});
expect(mockGenerateText).toHaveBeenCalledWith(
expect.objectContaining({
tools: expect.objectContaining({
read: expect.any(Object),
}),
})
);
// write 不应该存在
const call = mockGenerateText.mock.calls[0][0];
expect(call.tools.write).toBeUndefined();
});
it('disabled 移除指定工具', async () => {
const agentWithDisabled: AgentInfo = {
...basicAgentInfo,
tools: { disabled: ['write'] },
};
const tools = [createMockTool('read'), createMockTool('write')];
mockToolRegistry.getAllTools.mockReturnValue(tools);
const customExecutor = new AgentExecutor(agentWithDisabled, baseConfig, mockToolRegistry as any);
await customExecutor.execute('test', {});
const call = mockGenerateText.mock.calls[0][0];
expect(call.tools.read).toBeDefined();
expect(call.tools.write).toBeUndefined();
});
it('noTask 移除 task 工具', async () => {
const agentWithNoTask: AgentInfo = {
...basicAgentInfo,
tools: { noTask: true },
};
const tools = [createMockTool('read'), createMockTool('task')];
mockToolRegistry.getAllTools.mockReturnValue(tools);
const customExecutor = new AgentExecutor(agentWithNoTask, baseConfig, mockToolRegistry as any);
await customExecutor.execute('test', {});
const call = mockGenerateText.mock.calls[0][0];
expect(call.tools.task).toBeUndefined();
});
});
describe('checkToolPermission - 权限检查', () => {
it('无权限配置时允许所有', async () => {
const tools = [createMockTool('bash')];
mockToolRegistry.getAllTools.mockReturnValue(tools);
await executor.execute('test', {});
// 工具应该被包含
const call = mockGenerateText.mock.calls[0][0];
expect(call.tools.bash).toBeDefined();
});
it('bash 命令被禁止时拒绝', async () => {
const agentWithBashDeny: AgentInfo = {
...basicAgentInfo,
permission: {
bash: { allow: [], deny: ['rm'], ask: [] },
},
};
mockCheckBashPermission.mockReturnValue('deny');
const bashTool = createMockTool('bash');
mockToolRegistry.getAllTools.mockReturnValue([bashTool]);
const customExecutor = new AgentExecutor(agentWithBashDeny, baseConfig, mockToolRegistry as any);
await customExecutor.execute('test', {});
// 模拟工具执行
const call = mockGenerateText.mock.calls[0][0];
const toolResult = await call.tools.bash.execute({ command: 'rm -rf /' });
expect(toolResult.success).toBe(false);
expect(toolResult.error).toContain('权限拒绝');
});
it('文件写入被禁止时拒绝', async () => {
const agentWithFileDeny: AgentInfo = {
...basicAgentInfo,
permission: {
file: { write: 'deny', edit: 'allow', delete: 'allow' },
},
};
const writeTool = createMockTool('write_file');
mockToolRegistry.getAllTools.mockReturnValue([writeTool]);
const customExecutor = new AgentExecutor(agentWithFileDeny, baseConfig, mockToolRegistry as any);
await customExecutor.execute('test', {});
const call = mockGenerateText.mock.calls[0][0];
const toolResult = await call.tools.write_file.execute({ path: '/test.txt' });
expect(toolResult.success).toBe(false);
expect(toolResult.error).toContain('权限拒绝');
});
it('Git 写操作被禁止时拒绝', async () => {
const agentWithGitDeny: AgentInfo = {
...basicAgentInfo,
permission: {
git: { write: 'deny' },
},
};
const gitCommitTool = createMockTool('git_commit');
mockToolRegistry.getAllTools.mockReturnValue([gitCommitTool]);
const customExecutor = new AgentExecutor(agentWithGitDeny, baseConfig, mockToolRegistry as any);
await customExecutor.execute('test', {});
const call = mockGenerateText.mock.calls[0][0];
const toolResult = await call.tools.git_commit.execute({ message: 'test' });
expect(toolResult.success).toBe(false);
expect(toolResult.error).toContain('Git 写操作被禁止');
});
});
describe('buildMessageContent - 构建消息内容', () => {
it('无图片时返回纯文本', async () => {
await executor.execute('simple text', {});
expect(mockGenerateText).toHaveBeenCalledWith(
expect.objectContaining({
messages: [{ role: 'user', content: 'simple text' }],
})
);
});
it('有图片时返回多模态内容', async () => {
const context: AgentExecutionContext = {
images: [
{ data: 'img1', mimeType: 'image/png' },
{ data: 'img2', mimeType: 'image/jpeg' },
],
};
await executor.execute('describe images', context);
const call = mockGenerateText.mock.calls[0][0];
const content = call.messages[0].content;
expect(Array.isArray(content)).toBe(true);
expect(content.length).toBe(3); // 2 images + 1 text
expect(content[0].type).toBe('image');
expect(content[1].type).toBe('image');
expect(content[2].type).toBe('text');
});
});
describe('sessionId 处理', () => {
it('有 parentSessionId 时使用它', async () => {
const context: AgentExecutionContext = {
parentSessionId: 'parent-123',
};
const result = await executor.execute('test', context);
expect(result.sessionId).toBe('parent-123');
});
it('无 parentSessionId 时使用 standalone', async () => {
const result = await executor.execute('test', {});
expect(result.sessionId).toBe('standalone');
});
});
});
+71
View File
@@ -251,5 +251,76 @@ describe('AgentManager - Agent 管理器', () => {
// 这里只验证 cleanup 不会崩溃
expect(true).toBe(true);
});
it('使用默认 maxAge', async () => {
const agentId = await manager.runInBackground(
mockAgentInfo,
'任务',
'执行',
mockConfig,
{} as any,
mockContext
);
// 等待完成
await new Promise((r) => setTimeout(r, 300));
// 使用默认 maxAge(1 小时),不应该清理刚完成的
manager.cleanup();
// Agent 应该还在(因为未超过 1 小时)
const agent = manager.getAgent(agentId);
expect(agent).not.toBeNull();
});
});
describe('完成回调', () => {
it('多个等待者都收到通知', async () => {
const agentId = await manager.runInBackground(
mockAgentInfo,
'任务',
'执行',
mockConfig,
{} as any,
mockContext
);
// 并发等待
const [result1, result2] = await Promise.all([
manager.getAgentOutput(agentId, true, 5),
manager.getAgentOutput(agentId, true, 5),
]);
expect(result1).not.toBeNull();
expect(result2).not.toBeNull();
expect(result1!.id).toBe(agentId);
expect(result2!.id).toBe(agentId);
});
});
describe('已完成 Agent 的阻塞查询', () => {
it('已完成的 Agent 阻塞查询立即返回', async () => {
const agentId = await manager.runInBackground(
mockAgentInfo,
'任务',
'执行',
mockConfig,
{} as any,
mockContext
);
// 等待完成
await manager.getAgentOutput(agentId, true, 5);
// 再次阻塞查询应立即返回
const startTime = Date.now();
const result = await manager.getAgentOutput(agentId, true, 10);
const elapsed = Date.now() - startTime;
expect(result).not.toBeNull();
expect(result!.status).not.toBe('running');
// 应该立即返回,不需要等 10 秒
expect(elapsed).toBeLessThan(1000);
});
});
});
+201
View File
@@ -0,0 +1,201 @@
import { describe, it, expect } from 'vitest';
import {
presetAgents,
getPresetAgentNames,
isPresetAgent,
generalAgent,
exploreAgent,
codeReviewerAgent,
buildAgent,
planAgent,
visionAgent,
} from '../../../../src/agent/presets/index.js';
describe('Agent Presets - 预设 Agent', () => {
describe('presetAgents 集合', () => {
it('包含所有预设 Agent', () => {
expect(Object.keys(presetAgents)).toHaveLength(6);
});
it('包含 general Agent', () => {
expect(presetAgents['general']).toBeDefined();
expect(presetAgents['general']).toBe(generalAgent);
});
it('包含 explore Agent', () => {
expect(presetAgents['explore']).toBeDefined();
expect(presetAgents['explore']).toBe(exploreAgent);
});
it('包含 code-reviewer Agent', () => {
expect(presetAgents['code-reviewer']).toBeDefined();
expect(presetAgents['code-reviewer']).toBe(codeReviewerAgent);
});
it('包含 build Agent', () => {
expect(presetAgents['build']).toBeDefined();
expect(presetAgents['build']).toBe(buildAgent);
});
it('包含 plan Agent', () => {
expect(presetAgents['plan']).toBeDefined();
expect(presetAgents['plan']).toBe(planAgent);
});
it('包含 vision Agent', () => {
expect(presetAgents['vision']).toBeDefined();
expect(presetAgents['vision']).toBe(visionAgent);
});
});
describe('getPresetAgentNames - 获取预设名称', () => {
it('返回所有预设 Agent 名称', () => {
const names = getPresetAgentNames();
expect(names).toContain('general');
expect(names).toContain('explore');
expect(names).toContain('code-reviewer');
expect(names).toContain('build');
expect(names).toContain('plan');
expect(names).toContain('vision');
});
it('返回数组类型', () => {
const names = getPresetAgentNames();
expect(Array.isArray(names)).toBe(true);
});
it('返回正确数量', () => {
const names = getPresetAgentNames();
expect(names).toHaveLength(6);
});
});
describe('isPresetAgent - 检查是否为预设', () => {
it('识别 general', () => {
expect(isPresetAgent('general')).toBe(true);
});
it('识别 explore', () => {
expect(isPresetAgent('explore')).toBe(true);
});
it('识别 code-reviewer', () => {
expect(isPresetAgent('code-reviewer')).toBe(true);
});
it('识别 build', () => {
expect(isPresetAgent('build')).toBe(true);
});
it('识别 plan', () => {
expect(isPresetAgent('plan')).toBe(true);
});
it('识别 vision', () => {
expect(isPresetAgent('vision')).toBe(true);
});
it('不识别未知 Agent', () => {
expect(isPresetAgent('unknown')).toBe(false);
});
it('不识别空字符串', () => {
expect(isPresetAgent('')).toBe(false);
});
it('区分大小写', () => {
expect(isPresetAgent('General')).toBe(false);
expect(isPresetAgent('GENERAL')).toBe(false);
});
});
describe('generalAgent - 通用 Agent', () => {
it('有正确的描述', () => {
expect(generalAgent.description).toContain('通用');
});
it('mode 为 subagent', () => {
expect(generalAgent.mode).toBe('subagent');
});
it('禁止嵌套 Task', () => {
expect(generalAgent.tools?.noTask).toBe(true);
});
it('有 maxSteps 限制', () => {
expect(generalAgent.maxSteps).toBeDefined();
expect(generalAgent.maxSteps).toBeGreaterThan(0);
});
});
describe('exploreAgent - 探索 Agent', () => {
it('有描述', () => {
expect(exploreAgent.description).toBeDefined();
});
it('mode 为 subagent', () => {
expect(exploreAgent.mode).toBe('subagent');
});
});
describe('codeReviewerAgent - 代码审查 Agent', () => {
it('有描述', () => {
expect(codeReviewerAgent.description).toBeDefined();
});
it('mode 为 subagent', () => {
expect(codeReviewerAgent.mode).toBe('subagent');
});
});
describe('buildAgent - 构建 Agent', () => {
it('有描述', () => {
expect(buildAgent.description).toBeDefined();
});
it('mode 为 primary(主模式)', () => {
expect(buildAgent.mode).toBe('primary');
});
});
describe('planAgent - 计划 Agent', () => {
it('有描述', () => {
expect(planAgent.description).toBeDefined();
});
it('mode 为 primary(主模式)', () => {
expect(planAgent.mode).toBe('primary');
});
});
describe('visionAgent - 视觉 Agent', () => {
it('有描述', () => {
expect(visionAgent.description).toBeDefined();
});
it('mode 为 subagent', () => {
expect(visionAgent.mode).toBe('subagent');
});
});
describe('所有预设 Agent 共同特性', () => {
it('都有 description', () => {
Object.entries(presetAgents).forEach(([name, agent]) => {
expect(agent.description, `${name} 缺少 description`).toBeDefined();
});
});
it('都有 mode', () => {
Object.entries(presetAgents).forEach(([name, agent]) => {
expect(agent.mode, `${name} 缺少 mode`).toBeDefined();
});
});
it('mode 值是 subagent 或 primary', () => {
Object.entries(presetAgents).forEach(([name, agent]) => {
expect(['subagent', 'primary'], `${name} mode 无效`).toContain(agent.mode);
});
});
});
});