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,298 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import type { Tool } from '../../../src/types/index.js';
|
||||
import type { AgentInfo, AgentToolConfig } from '../../../src/agent/types.js';
|
||||
|
||||
/**
|
||||
* 模拟 Agent 类中的 filterToolsByAgentConfig 逻辑
|
||||
* 由于 Agent 类有复杂的依赖,我们提取核心过滤逻辑进行测试
|
||||
*/
|
||||
function filterToolsByAgentConfig(
|
||||
tools: Tool[],
|
||||
toolConfig: AgentToolConfig | undefined
|
||||
): Tool[] {
|
||||
if (!toolConfig) return tools;
|
||||
|
||||
let filteredTools = tools;
|
||||
|
||||
// 如果设置了 enabled 列表,只保留这些工具
|
||||
if (toolConfig.enabled && toolConfig.enabled.length > 0) {
|
||||
const enabledSet = new Set(toolConfig.enabled);
|
||||
filteredTools = filteredTools.filter((t) => enabledSet.has(t.name));
|
||||
}
|
||||
|
||||
// 如果设置了 disabled 列表,排除这些工具
|
||||
if (toolConfig.disabled && toolConfig.disabled.length > 0) {
|
||||
const disabledSet = new Set(toolConfig.disabled);
|
||||
filteredTools = filteredTools.filter((t) => !disabledSet.has(t.name));
|
||||
}
|
||||
|
||||
// 如果禁止嵌套 Task,移除 task 工具
|
||||
if (toolConfig.noTask) {
|
||||
filteredTools = filteredTools.filter((t) => t.name !== 'task');
|
||||
}
|
||||
|
||||
return filteredTools;
|
||||
}
|
||||
|
||||
// 创建测试用的 mock 工具
|
||||
function createMockTool(name: string): Tool {
|
||||
return {
|
||||
name,
|
||||
description: `Mock tool: ${name}`,
|
||||
parameters: { type: 'object', properties: {}, required: [] },
|
||||
execute: async () => ({ success: true, output: 'mock' }),
|
||||
};
|
||||
}
|
||||
|
||||
describe('Agent 工具过滤 - filterToolsByAgentConfig', () => {
|
||||
let allTools: Tool[];
|
||||
|
||||
beforeEach(() => {
|
||||
// 创建一组测试工具
|
||||
allTools = [
|
||||
createMockTool('read_file'),
|
||||
createMockTool('write_file'),
|
||||
createMockTool('bash'),
|
||||
createMockTool('task'),
|
||||
createMockTool('tool_search'),
|
||||
createMockTool('glob'),
|
||||
createMockTool('grep'),
|
||||
];
|
||||
});
|
||||
|
||||
describe('无过滤配置', () => {
|
||||
it('toolConfig 为 undefined 时返回所有工具', () => {
|
||||
const result = filterToolsByAgentConfig(allTools, undefined);
|
||||
expect(result).toHaveLength(allTools.length);
|
||||
expect(result).toEqual(allTools);
|
||||
});
|
||||
|
||||
it('toolConfig 为空对象时返回所有工具', () => {
|
||||
const result = filterToolsByAgentConfig(allTools, {});
|
||||
expect(result).toHaveLength(allTools.length);
|
||||
});
|
||||
});
|
||||
|
||||
describe('enabled 白名单过滤', () => {
|
||||
it('只保留 enabled 列表中的工具', () => {
|
||||
const config: AgentToolConfig = {
|
||||
enabled: ['read_file', 'glob', 'grep'],
|
||||
};
|
||||
const result = filterToolsByAgentConfig(allTools, config);
|
||||
|
||||
expect(result).toHaveLength(3);
|
||||
expect(result.map((t) => t.name)).toEqual(['read_file', 'glob', 'grep']);
|
||||
});
|
||||
|
||||
it('enabled 为空数组时返回空列表', () => {
|
||||
const config: AgentToolConfig = {
|
||||
enabled: [],
|
||||
};
|
||||
const result = filterToolsByAgentConfig(allTools, config);
|
||||
expect(result).toHaveLength(allTools.length); // 空数组不触发过滤
|
||||
});
|
||||
|
||||
it('enabled 中不存在的工具被忽略', () => {
|
||||
const config: AgentToolConfig = {
|
||||
enabled: ['read_file', 'non_existent_tool'],
|
||||
};
|
||||
const result = filterToolsByAgentConfig(allTools, config);
|
||||
|
||||
expect(result).toHaveLength(1);
|
||||
expect(result[0].name).toBe('read_file');
|
||||
});
|
||||
});
|
||||
|
||||
describe('disabled 黑名单过滤', () => {
|
||||
it('排除 disabled 列表中的工具', () => {
|
||||
const config: AgentToolConfig = {
|
||||
disabled: ['bash', 'write_file'],
|
||||
};
|
||||
const result = filterToolsByAgentConfig(allTools, config);
|
||||
|
||||
expect(result).toHaveLength(5);
|
||||
expect(result.map((t) => t.name)).not.toContain('bash');
|
||||
expect(result.map((t) => t.name)).not.toContain('write_file');
|
||||
});
|
||||
|
||||
it('disabled 为空数组时返回所有工具', () => {
|
||||
const config: AgentToolConfig = {
|
||||
disabled: [],
|
||||
};
|
||||
const result = filterToolsByAgentConfig(allTools, config);
|
||||
expect(result).toHaveLength(allTools.length);
|
||||
});
|
||||
|
||||
it('disabled 中不存在的工具被忽略', () => {
|
||||
const config: AgentToolConfig = {
|
||||
disabled: ['non_existent_tool'],
|
||||
};
|
||||
const result = filterToolsByAgentConfig(allTools, config);
|
||||
expect(result).toHaveLength(allTools.length);
|
||||
});
|
||||
});
|
||||
|
||||
describe('noTask 过滤', () => {
|
||||
it('noTask=true 时移除 task 工具', () => {
|
||||
const config: AgentToolConfig = {
|
||||
noTask: true,
|
||||
};
|
||||
const result = filterToolsByAgentConfig(allTools, config);
|
||||
|
||||
expect(result).toHaveLength(6);
|
||||
expect(result.map((t) => t.name)).not.toContain('task');
|
||||
});
|
||||
|
||||
it('noTask=false 时保留 task 工具', () => {
|
||||
const config: AgentToolConfig = {
|
||||
noTask: false,
|
||||
};
|
||||
const result = filterToolsByAgentConfig(allTools, config);
|
||||
|
||||
expect(result).toHaveLength(allTools.length);
|
||||
expect(result.map((t) => t.name)).toContain('task');
|
||||
});
|
||||
|
||||
it('noTask 未设置时保留 task 工具', () => {
|
||||
const config: AgentToolConfig = {};
|
||||
const result = filterToolsByAgentConfig(allTools, config);
|
||||
|
||||
expect(result.map((t) => t.name)).toContain('task');
|
||||
});
|
||||
});
|
||||
|
||||
describe('组合过滤', () => {
|
||||
it('enabled + noTask 组合', () => {
|
||||
const config: AgentToolConfig = {
|
||||
enabled: ['read_file', 'task', 'glob'],
|
||||
noTask: true,
|
||||
};
|
||||
const result = filterToolsByAgentConfig(allTools, config);
|
||||
|
||||
// enabled 先过滤为 [read_file, task, glob]
|
||||
// noTask 再移除 task
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result.map((t) => t.name)).toEqual(['read_file', 'glob']);
|
||||
});
|
||||
|
||||
it('disabled + noTask 组合', () => {
|
||||
const config: AgentToolConfig = {
|
||||
disabled: ['bash'],
|
||||
noTask: true,
|
||||
};
|
||||
const result = filterToolsByAgentConfig(allTools, config);
|
||||
|
||||
// 原始 7 个工具,移除 bash 和 task
|
||||
expect(result).toHaveLength(5);
|
||||
expect(result.map((t) => t.name)).not.toContain('bash');
|
||||
expect(result.map((t) => t.name)).not.toContain('task');
|
||||
});
|
||||
|
||||
it('enabled + disabled 组合(enabled 优先)', () => {
|
||||
const config: AgentToolConfig = {
|
||||
enabled: ['read_file', 'bash', 'glob'],
|
||||
disabled: ['bash'], // bash 在 enabled 中,也在 disabled 中
|
||||
};
|
||||
const result = filterToolsByAgentConfig(allTools, config);
|
||||
|
||||
// enabled 先过滤为 [read_file, bash, glob]
|
||||
// disabled 再移除 bash
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result.map((t) => t.name)).toEqual(['read_file', 'glob']);
|
||||
});
|
||||
|
||||
it('enabled + disabled + noTask 全部组合', () => {
|
||||
const config: AgentToolConfig = {
|
||||
enabled: ['read_file', 'write_file', 'task', 'glob'],
|
||||
disabled: ['write_file'],
|
||||
noTask: true,
|
||||
};
|
||||
const result = filterToolsByAgentConfig(allTools, config);
|
||||
|
||||
// enabled: [read_file, write_file, task, glob]
|
||||
// disabled: 移除 write_file -> [read_file, task, glob]
|
||||
// noTask: 移除 task -> [read_file, glob]
|
||||
expect(result).toHaveLength(2);
|
||||
expect(result.map((t) => t.name)).toEqual(['read_file', 'glob']);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('AgentInfo 工具配置集成', () => {
|
||||
it('explore agent 典型配置:只读工具', () => {
|
||||
const exploreAgent: AgentInfo = {
|
||||
name: 'explore',
|
||||
description: '代码探索 Agent',
|
||||
mode: 'subagent',
|
||||
tools: {
|
||||
enabled: ['read_file', 'glob', 'grep', 'tool_search'],
|
||||
noTask: true,
|
||||
},
|
||||
};
|
||||
|
||||
const allTools = [
|
||||
createMockTool('read_file'),
|
||||
createMockTool('write_file'),
|
||||
createMockTool('bash'),
|
||||
createMockTool('task'),
|
||||
createMockTool('glob'),
|
||||
createMockTool('grep'),
|
||||
createMockTool('tool_search'),
|
||||
];
|
||||
|
||||
const result = filterToolsByAgentConfig(allTools, exploreAgent.tools);
|
||||
|
||||
expect(result).toHaveLength(4);
|
||||
expect(result.map((t) => t.name).sort()).toEqual(['glob', 'grep', 'read_file', 'tool_search']);
|
||||
});
|
||||
|
||||
it('code-reviewer agent 典型配置:禁用写操作', () => {
|
||||
const reviewerAgent: AgentInfo = {
|
||||
name: 'code-reviewer',
|
||||
description: '代码审查 Agent',
|
||||
mode: 'subagent',
|
||||
tools: {
|
||||
disabled: ['write_file', 'bash'],
|
||||
noTask: true,
|
||||
},
|
||||
};
|
||||
|
||||
const allTools = [
|
||||
createMockTool('read_file'),
|
||||
createMockTool('write_file'),
|
||||
createMockTool('bash'),
|
||||
createMockTool('task'),
|
||||
createMockTool('glob'),
|
||||
createMockTool('grep'),
|
||||
];
|
||||
|
||||
const result = filterToolsByAgentConfig(allTools, reviewerAgent.tools);
|
||||
|
||||
// 移除 write_file, bash, task
|
||||
expect(result).toHaveLength(3);
|
||||
expect(result.map((t) => t.name).sort()).toEqual(['glob', 'grep', 'read_file']);
|
||||
});
|
||||
|
||||
it('build agent 典型配置:允许嵌套 Task', () => {
|
||||
const buildAgent: AgentInfo = {
|
||||
name: 'build',
|
||||
description: '构建 Agent',
|
||||
mode: 'primary',
|
||||
tools: {
|
||||
noTask: false, // 明确允许 task
|
||||
},
|
||||
};
|
||||
|
||||
const allTools = [
|
||||
createMockTool('read_file'),
|
||||
createMockTool('write_file'),
|
||||
createMockTool('bash'),
|
||||
createMockTool('task'),
|
||||
];
|
||||
|
||||
const result = filterToolsByAgentConfig(allTools, buildAgent.tools);
|
||||
|
||||
expect(result).toHaveLength(4);
|
||||
expect(result.map((t) => t.name)).toContain('task');
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user