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:
2025-12-11 14:45:24 +08:00
parent f4df6483a6
commit 729fb2d42a
58 changed files with 14320 additions and 3 deletions
+288
View File
@@ -0,0 +1,288 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
import {
loadAgentConfig,
saveAgentConfig,
getConfigTemplate,
} from '../../../src/agent/config-loader.js';
import type { AgentConfigFile } from '../../../src/agent/types.js';
// Mock fs
vi.mock('fs', () => ({
existsSync: vi.fn(),
promises: {
readFile: vi.fn(),
writeFile: vi.fn(),
mkdir: vi.fn(),
},
}));
// Mock js-yaml
vi.mock('js-yaml', () => ({
load: vi.fn((content: string) => JSON.parse(content)),
dump: vi.fn((obj: unknown) => JSON.stringify(obj, null, 2)),
}));
import * as fs from 'fs';
describe('loadAgentConfig - 加载 Agent 配置', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('无配置文件时返回 null', async () => {
vi.mocked(fs.existsSync).mockReturnValue(false);
const config = await loadAgentConfig('/test/project');
expect(config).toBeNull();
});
it('加载 JSON 配置文件', async () => {
const mockConfig: AgentConfigFile = {
defaults: {
maxSteps: 20,
},
agents: {
'custom-agent': {
description: '自定义 Agent',
mode: 'subagent',
prompt: '你是助手',
},
},
};
vi.mocked(fs.existsSync).mockImplementation((path: unknown) =>
String(path).endsWith('.json')
);
vi.mocked(fs.promises.readFile).mockResolvedValue(JSON.stringify(mockConfig));
const config = await loadAgentConfig('/test/project');
expect(config).not.toBeNull();
expect(config?.defaults?.maxSteps).toBe(20);
expect(config?.agents?.['custom-agent']).toBeDefined();
});
it('加载 YAML 配置文件', async () => {
const mockConfig: AgentConfigFile = {
defaults: {
maxSteps: 15,
},
agents: {},
};
vi.mocked(fs.existsSync).mockImplementation((path: unknown) =>
String(path).endsWith('.yaml')
);
vi.mocked(fs.promises.readFile).mockResolvedValue(JSON.stringify(mockConfig));
const config = await loadAgentConfig('/test/project');
expect(config).not.toBeNull();
});
it('无效配置格式返回 null', async () => {
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.promises.readFile).mockResolvedValue('invalid json');
// 会在解析时失败
const config = await loadAgentConfig('/test/project');
// 取决于实现,可能是 null 或抛出错误后 null
expect(config).toBeNull();
});
it('配置搜索顺序', async () => {
// 测试搜索多个路径
const calls: string[] = [];
vi.mocked(fs.existsSync).mockImplementation((path: unknown) => {
calls.push(String(path));
return false;
});
await loadAgentConfig('/test/project');
// 应该搜索多个配置路径
expect(calls.length).toBeGreaterThan(0);
expect(calls.some(p => p.includes('.ai-assist'))).toBe(true);
});
});
describe('saveAgentConfig - 保存 Agent 配置', () => {
beforeEach(() => {
vi.clearAllMocks();
vi.mocked(fs.existsSync).mockReturnValue(false);
});
it('保存 JSON 格式配置', async () => {
const config: AgentConfigFile = {
defaults: {
maxSteps: 10,
},
agents: {},
};
await saveAgentConfig('/test/project', config, 'json');
expect(fs.promises.mkdir).toHaveBeenCalled();
expect(fs.promises.writeFile).toHaveBeenCalledWith(
expect.stringContaining('agents.json'),
expect.any(String),
'utf-8'
);
});
it('保存 YAML 格式配置', async () => {
const config: AgentConfigFile = {
defaults: {
maxSteps: 10,
},
agents: {},
};
await saveAgentConfig('/test/project', config, 'yaml');
expect(fs.promises.writeFile).toHaveBeenCalledWith(
expect.stringContaining('agents.yaml'),
expect.any(String),
'utf-8'
);
});
it('创建配置目录如果不存在', async () => {
vi.mocked(fs.existsSync).mockReturnValue(false);
await saveAgentConfig('/test/project', { agents: {} }, 'json');
expect(fs.promises.mkdir).toHaveBeenCalledWith(
expect.stringContaining('.ai-assist'),
{ recursive: true }
);
});
it('目录已存在时不重复创建', async () => {
vi.mocked(fs.existsSync).mockReturnValue(true);
await saveAgentConfig('/test/project', { agents: {} }, 'json');
expect(fs.promises.mkdir).not.toHaveBeenCalled();
});
});
describe('getConfigTemplate - 获取配置模板', () => {
it('返回有效的配置模板', () => {
const template = getConfigTemplate();
expect(template).toBeDefined();
expect(template.defaults).toBeDefined();
expect(template.agents).toBeDefined();
});
it('模板包含默认配置', () => {
const template = getConfigTemplate();
expect(template.defaults?.maxSteps).toBeDefined();
expect(template.defaults?.model).toBeDefined();
});
it('模板包含示例 Agent', () => {
const template = getConfigTemplate();
expect(template.agents).toBeDefined();
expect(Object.keys(template.agents || {}).length).toBeGreaterThan(0);
});
it('示例 Agent 包含必要字段', () => {
const template = getConfigTemplate();
const agents = template.agents || {};
const firstAgent = Object.values(agents)[0];
expect(firstAgent).toBeDefined();
expect(firstAgent.description).toBeDefined();
expect(firstAgent.mode).toBeDefined();
});
it('模板包含权限配置示例', () => {
const template = getConfigTemplate();
expect(template.defaults?.permission).toBeDefined();
expect(template.defaults?.permission?.bash).toBeDefined();
});
});
describe('配置验证', () => {
beforeEach(() => {
vi.clearAllMocks();
});
it('空对象是有效配置', async () => {
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.promises.readFile).mockResolvedValue(JSON.stringify({}));
const config = await loadAgentConfig('/test/project');
expect(config).not.toBeNull();
});
it('只有 defaults 的配置有效', async () => {
const mockConfig = {
defaults: {
maxSteps: 10,
},
};
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.promises.readFile).mockResolvedValue(JSON.stringify(mockConfig));
const config = await loadAgentConfig('/test/project');
expect(config).not.toBeNull();
expect(config?.defaults?.maxSteps).toBe(10);
});
it('只有 agents 的配置有效', async () => {
const mockConfig = {
agents: {
'test-agent': {
description: 'Test',
mode: 'subagent',
},
},
};
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.promises.readFile).mockResolvedValue(JSON.stringify(mockConfig));
const config = await loadAgentConfig('/test/project');
expect(config).not.toBeNull();
expect(config?.agents?.['test-agent']).toBeDefined();
});
it('defaults 为非对象时配置无效', async () => {
const mockConfig = {
defaults: 'invalid',
};
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.promises.readFile).mockResolvedValue(JSON.stringify(mockConfig));
const config = await loadAgentConfig('/test/project');
// 应该返回 null(无效配置)
expect(config).toBeNull();
});
it('agents 为非对象时配置无效', async () => {
const mockConfig = {
agents: 'invalid',
};
vi.mocked(fs.existsSync).mockReturnValue(true);
vi.mocked(fs.promises.readFile).mockResolvedValue(JSON.stringify(mockConfig));
const config = await loadAgentConfig('/test/project');
expect(config).toBeNull();
});
});
+363
View File
@@ -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);
});
});
});
+234
View File
@@ -0,0 +1,234 @@
import { describe, it, expect } from 'vitest';
import {
mergePermissions,
matchRule,
checkBashPermission,
checkFilePathPermission,
SYSTEM_DEFAULT_PERMISSION,
} from '../../../src/agent/permission-merger.js';
import type { AgentPermission, AgentBashPermission } from '../../../src/agent/types.js';
describe('matchRule - 命令规则匹配', () => {
describe('通配符 * 匹配', () => {
it('git diff* 匹配 git diff --staged', () => {
expect(matchRule('git diff --staged', 'git diff*')).toBe(true);
});
it('git diff* 不匹配 git status', () => {
expect(matchRule('git status', 'git diff*')).toBe(false);
});
it('rm -rf* 匹配危险命令', () => {
expect(matchRule('rm -rf /', 'rm -rf*')).toBe(true);
expect(matchRule('rm -rf /home/user', 'rm -rf*')).toBe(true);
});
it('rm -rf* 不匹配普通 rm', () => {
expect(matchRule('rm file.txt', 'rm -rf*')).toBe(false);
});
});
describe('精确匹配', () => {
it('pwd 精确匹配', () => {
expect(matchRule('pwd', 'pwd')).toBe(true);
});
it('pwd 不匹配带参数', () => {
expect(matchRule('pwd /home', 'pwd')).toBe(false);
});
});
describe('大小写不敏感', () => {
it('忽略大小写', () => {
expect(matchRule('GIT DIFF', 'git diff*')).toBe(true);
});
});
});
describe('checkBashPermission - Bash 权限检查', () => {
it('禁用时返回 deny', () => {
const permission: AgentBashPermission = { enabled: false };
expect(checkBashPermission('ls', permission)).toBe('deny');
});
it('匹配 allow 规则', () => {
const permission: AgentBashPermission = {
enabled: true,
rules: [{ pattern: 'ls *', action: 'allow' }],
default: 'deny',
};
expect(checkBashPermission('ls -la', permission)).toBe('allow');
});
it('匹配 deny 规则', () => {
const permission: AgentBashPermission = {
enabled: true,
rules: [{ pattern: 'rm -rf*', action: 'deny' }],
default: 'allow',
};
expect(checkBashPermission('rm -rf /', permission)).toBe('deny');
});
it('无匹配时返回默认值', () => {
const permission: AgentBashPermission = {
enabled: true,
rules: [],
default: 'ask',
};
expect(checkBashPermission('npm install', permission)).toBe('ask');
});
it('规则优先级:先匹配的规则优先', () => {
const permission: AgentBashPermission = {
enabled: true,
rules: [
{ pattern: 'git push --force*', action: 'deny' },
{ pattern: 'git push*', action: 'ask' },
],
default: 'allow',
};
expect(checkBashPermission('git push --force origin', permission)).toBe('deny');
expect(checkBashPermission('git push origin', permission)).toBe('ask');
});
});
describe('checkFilePathPermission - 文件路径权限检查', () => {
it('无敏感路径规则返回 null', () => {
expect(checkFilePathPermission('/home/user/file.txt', undefined)).toBeNull();
expect(checkFilePathPermission('/home/user/file.txt', [])).toBeNull();
});
it('匹配敏感路径规则', () => {
const rules = [
{ pattern: '*.env', action: 'deny' as const },
{ pattern: '/etc/*', action: 'ask' as const },
];
expect(checkFilePathPermission('.env', rules)).toBe('deny');
expect(checkFilePathPermission('/etc/passwd', rules)).toBe('ask');
});
it('不匹配时返回 null', () => {
const rules = [{ pattern: '*.env', action: 'deny' as const }];
expect(checkFilePathPermission('config.json', rules)).toBeNull();
});
});
describe('mergePermissions - 权限合并', () => {
describe('优先级:Agent > Global > System', () => {
it('Agent 配置覆盖 Global 和 System', () => {
const system: AgentPermission = { file: { read: 'allow', write: 'ask' } };
const global: AgentPermission = { file: { write: 'allow' } };
const agent: AgentPermission = { file: { write: 'deny' } };
const merged = mergePermissions(system, global, agent);
expect(merged.file?.write).toBe('deny');
});
it('Global 配置覆盖 System', () => {
const system: AgentPermission = { file: { write: 'ask' } };
const global: AgentPermission = { file: { write: 'allow' } };
const merged = mergePermissions(system, global, undefined);
expect(merged.file?.write).toBe('allow');
});
it('无覆盖时使用 System 默认值', () => {
const merged = mergePermissions(SYSTEM_DEFAULT_PERMISSION, undefined, undefined);
expect(merged.file?.read).toBe('allow');
expect(merged.file?.write).toBe('ask');
});
});
describe('Bash 权限合并', () => {
it('Agent 禁用 bash 覆盖全局', () => {
const system: AgentPermission = { bash: { enabled: true } };
const global: AgentPermission = { bash: { enabled: true } };
const agent: AgentPermission = { bash: { enabled: false } };
const merged = mergePermissions(system, global, agent);
expect(merged.bash?.enabled).toBe(false);
});
it('Global 禁用 bash 且 Agent 未覆盖', () => {
const system: AgentPermission = { bash: { enabled: true } };
const global: AgentPermission = { bash: { enabled: false } };
const merged = mergePermissions(system, global, undefined);
expect(merged.bash?.enabled).toBe(false);
});
it('规则按优先级合并:Agent > Global > System', () => {
const system: AgentPermission = {
bash: { rules: [{ pattern: 'ls *', action: 'allow' }] },
};
const global: AgentPermission = {
bash: { rules: [{ pattern: 'cat *', action: 'allow' }] },
};
const agent: AgentPermission = {
bash: { rules: [{ pattern: 'rm *', action: 'deny' }] },
};
const merged = mergePermissions(system, global, agent);
// Agent 规则在前
expect(merged.bash?.rules?.[0].pattern).toBe('rm *');
expect(merged.bash?.rules?.[1].pattern).toBe('cat *');
expect(merged.bash?.rules?.[2].pattern).toBe('ls *');
});
});
describe('Git 权限合并', () => {
it('合并所有级别的 Git 权限', () => {
const system: AgentPermission = { git: { read: 'allow', write: 'ask', dangerous: 'deny' } };
const agent: AgentPermission = { git: { write: 'deny' } };
const merged = mergePermissions(system, undefined, agent);
expect(merged.git?.read).toBe('allow'); // 来自 system
expect(merged.git?.write).toBe('deny'); // 来自 agent
expect(merged.git?.dangerous).toBe('deny'); // 来自 system
});
});
describe('Web 权限合并', () => {
it('合并 Web 权限', () => {
const system: AgentPermission = { web: 'ask' };
const agent: AgentPermission = { web: 'deny' };
const merged = mergePermissions(system, undefined, agent);
expect(merged.web).toBe('deny');
});
});
});
describe('SYSTEM_DEFAULT_PERMISSION - 系统默认权限', () => {
it('文件读取默认允许', () => {
expect(SYSTEM_DEFAULT_PERMISSION.file?.read).toBe('allow');
});
it('文件写入默认询问', () => {
expect(SYSTEM_DEFAULT_PERMISSION.file?.write).toBe('ask');
});
it('Bash 默认启用', () => {
expect(SYSTEM_DEFAULT_PERMISSION.bash?.enabled).toBe(true);
});
it('包含安全命令白名单', () => {
const rules = SYSTEM_DEFAULT_PERMISSION.bash?.rules ?? [];
const lsRule = rules.find((r) => r.pattern === 'ls *');
expect(lsRule?.action).toBe('allow');
});
it('包含危险命令黑名单', () => {
const rules = SYSTEM_DEFAULT_PERMISSION.bash?.rules ?? [];
const rmRule = rules.find((r) => r.pattern === 'rm -rf *');
expect(rmRule?.action).toBe('deny');
});
it('Git 读取默认允许', () => {
expect(SYSTEM_DEFAULT_PERMISSION.git?.read).toBe('allow');
});
it('Git 危险操作默认拒绝', () => {
expect(SYSTEM_DEFAULT_PERMISSION.git?.dangerous).toBe('deny');
});
});
+350
View File
@@ -0,0 +1,350 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { AgentRegistry } from '../../../src/agent/registry.js';
import type { AgentInfo, AgentConfigFile } from '../../../src/agent/types.js';
// Mock config-loader
vi.mock('../../../src/agent/config-loader.js', () => ({
loadAgentConfig: vi.fn(),
}));
// Mock presets
vi.mock('../../../src/agent/presets/index.js', () => ({
presetAgents: {
explore: {
description: '代码探索 Agent',
mode: 'subagent' as const,
prompt: '你是代码探索助手',
maxSteps: 5,
},
'code-reviewer': {
description: '代码审查 Agent',
mode: 'subagent' as const,
prompt: '你是代码审查助手',
},
build: {
description: '构建 Agent',
mode: 'all' as const,
prompt: '你是构建助手',
},
},
}));
import { loadAgentConfig } from '../../../src/agent/config-loader.js';
describe('AgentRegistry - Agent 注册表', () => {
let registry: AgentRegistry;
beforeEach(() => {
registry = new AgentRegistry();
vi.clearAllMocks();
vi.mocked(loadAgentConfig).mockResolvedValue(null);
});
describe('init - 初始化', () => {
it('初始化后注册预设 Agent', async () => {
await registry.init('/test/project');
expect(registry.has('explore')).toBe(true);
expect(registry.has('code-reviewer')).toBe(true);
expect(registry.has('build')).toBe(true);
});
it('初始化加载用户配置', async () => {
const userConfig: AgentConfigFile = {
defaults: {
maxSteps: 20,
},
agents: {
'custom-agent': {
description: '自定义 Agent',
mode: 'subagent',
prompt: '你是自定义助手',
},
},
};
vi.mocked(loadAgentConfig).mockResolvedValue(userConfig);
await registry.init('/test/project');
expect(registry.has('custom-agent')).toBe(true);
});
it('重复初始化只执行一次', async () => {
await registry.init('/test/project');
await registry.init('/test/project');
expect(loadAgentConfig).toHaveBeenCalledTimes(1);
});
});
describe('get - 获取 Agent', () => {
beforeEach(async () => {
await registry.init('/test/project');
});
it('获取存在的 Agent', () => {
const agent = registry.get('explore');
expect(agent).toBeDefined();
expect(agent?.name).toBe('explore');
expect(agent?.description).toBe('代码探索 Agent');
});
it('获取不存在的 Agent 返回 undefined', () => {
const agent = registry.get('non-existent');
expect(agent).toBeUndefined();
});
it('获取的 Agent 应用全局配置', async () => {
vi.mocked(loadAgentConfig).mockResolvedValue({
defaults: {
maxSteps: 25,
},
});
const newRegistry = new AgentRegistry();
await newRegistry.init('/test/project');
const agent = newRegistry.get('code-reviewer');
// code-reviewer 没有设置 maxSteps,应该使用全局默认值
expect(agent?.maxSteps).toBe(25);
});
it('Agent 自己的配置优先于全局配置', async () => {
vi.mocked(loadAgentConfig).mockResolvedValue({
defaults: {
maxSteps: 25,
},
});
const newRegistry = new AgentRegistry();
await newRegistry.init('/test/project');
const agent = newRegistry.get('explore');
// explore 设置了 maxSteps: 5
expect(agent?.maxSteps).toBe(5);
});
});
describe('list - 列出 Agent', () => {
beforeEach(async () => {
await registry.init('/test/project');
});
it('列出所有 Agent', () => {
const agents = registry.list();
expect(agents.length).toBeGreaterThan(0);
expect(agents.some(a => a.name === 'explore')).toBe(true);
expect(agents.some(a => a.name === 'code-reviewer')).toBe(true);
});
it('按 mode 过滤 Agent', () => {
const subagents = registry.list('subagent');
expect(subagents.every(a => a.mode === 'subagent' || a.mode === 'all')).toBe(true);
});
});
describe('listSubagents - 列出子 Agent', () => {
beforeEach(async () => {
await registry.init('/test/project');
});
it('排除 primary-only 的 Agent', () => {
const subagents = registry.listSubagents();
expect(subagents.every(a => a.mode !== 'primary')).toBe(true);
});
});
describe('listPrimaryAgents - 列出主 Agent', () => {
beforeEach(async () => {
await registry.init('/test/project');
});
it('排除 subagent-only 的 Agent', () => {
const primaryAgents = registry.listPrimaryAgents();
expect(primaryAgents.every(a => a.mode !== 'subagent')).toBe(true);
});
});
describe('register - 动态注册', () => {
beforeEach(async () => {
await registry.init('/test/project');
});
it('注册新 Agent', () => {
const newAgent: AgentInfo = {
name: 'dynamic-agent',
description: '动态注册的 Agent',
mode: 'subagent',
prompt: '你是动态 Agent',
};
registry.register(newAgent);
expect(registry.has('dynamic-agent')).toBe(true);
expect(registry.get('dynamic-agent')?.description).toBe('动态注册的 Agent');
});
it('覆盖已有 Agent', () => {
const updatedAgent: AgentInfo = {
name: 'explore',
description: '更新后的探索 Agent',
mode: 'all',
prompt: '更新后的提示',
};
registry.register(updatedAgent);
const agent = registry.get('explore');
expect(agent?.description).toBe('更新后的探索 Agent');
});
});
describe('remove - 移除 Agent', () => {
beforeEach(async () => {
await registry.init('/test/project');
});
it('移除存在的 Agent', () => {
const result = registry.remove('explore');
expect(result).toBe(true);
expect(registry.has('explore')).toBe(false);
});
it('移除不存在的 Agent 返回 false', () => {
const result = registry.remove('non-existent');
expect(result).toBe(false);
});
});
describe('has - 检查 Agent 是否存在', () => {
beforeEach(async () => {
await registry.init('/test/project');
});
it('存在的 Agent 返回 true', () => {
expect(registry.has('explore')).toBe(true);
});
it('不存在的 Agent 返回 false', () => {
expect(registry.has('non-existent')).toBe(false);
});
});
describe('size - 获取 Agent 数量', () => {
beforeEach(async () => {
await registry.init('/test/project');
});
it('返回正确的数量', () => {
expect(registry.size).toBeGreaterThan(0);
});
it('添加后数量增加', () => {
const initialSize = registry.size;
registry.register({
name: 'new-agent',
description: 'New',
mode: 'subagent',
prompt: 'New agent',
});
expect(registry.size).toBe(initialSize + 1);
});
it('移除后数量减少', () => {
const initialSize = registry.size;
registry.remove('explore');
expect(registry.size).toBe(initialSize - 1);
});
});
describe('getNames - 获取所有 Agent 名称', () => {
beforeEach(async () => {
await registry.init('/test/project');
});
it('返回所有 Agent 名称', () => {
const names = registry.getNames();
expect(names).toContain('explore');
expect(names).toContain('code-reviewer');
expect(names).toContain('build');
});
});
describe('getGlobalConfig - 获取全局配置', () => {
it('无用户配置时返回 null', async () => {
vi.mocked(loadAgentConfig).mockResolvedValue(null);
await registry.init('/test/project');
expect(registry.getGlobalConfig()).toBeNull();
});
it('有用户配置时返回 defaults', async () => {
vi.mocked(loadAgentConfig).mockResolvedValue({
defaults: {
maxSteps: 30,
model: {
temperature: 0.5,
},
},
});
const newRegistry = new AgentRegistry();
await newRegistry.init('/test/project');
const globalConfig = newRegistry.getGlobalConfig();
expect(globalConfig?.maxSteps).toBe(30);
expect(globalConfig?.model?.temperature).toBe(0.5);
});
});
describe('generateSubagentDescription - 生成子 Agent 描述', () => {
beforeEach(async () => {
await registry.init('/test/project');
});
it('生成包含所有子 Agent 的描述', () => {
const description = registry.generateSubagentDescription();
expect(description).toContain('explore');
expect(description).toContain('代码探索');
});
it('无子 Agent 时返回提示信息', async () => {
// 移除所有 Agent
for (const name of registry.getNames()) {
registry.remove(name);
}
const description = registry.generateSubagentDescription();
expect(description).toContain('没有可用');
});
});
});
describe('agentRegistry 单例', () => {
it('导出单例实例', async () => {
const { agentRegistry } = await import('../../../src/agent/registry.js');
expect(agentRegistry).toBeDefined();
expect(agentRegistry).toBeInstanceOf(AgentRegistry);
});
});