feat: 重构为 Monorepo 架构并实现 HTTP Server

架构变更:
- 采用 pnpm workspaces 实现 Monorepo 结构
- 将现有代码迁移到 packages/core
- 新增 packages/server HTTP 服务层

Server 功能:
- REST API: 会话管理、工具管理、配置管理
- WebSocket: 实时双向通信支持
- SSE: 服务端事件推送
- Hono + Bun 作为运行时

API 端点:
- GET/POST /api/sessions - 会话 CRUD
- GET/POST /api/sessions/:id/messages - 消息管理
- GET /api/sessions/:id/events - SSE 事件流
- WS /api/ws/:sessionId - WebSocket 连接
- GET/POST /api/tools - 工具管理
- GET/PUT /api/config - 配置管理
This commit is contained in:
2025-12-12 10:42:20 +08:00
parent 59dbed926e
commit 5e32375f0e
301 changed files with 3281 additions and 43 deletions
@@ -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();
});
});
@@ -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');
});
});
});
@@ -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);
});
});
});
@@ -0,0 +1,326 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { AgentManager, resetAgentManager, getAgentManager } from '../../../src/agent/manager.js';
import type { AgentInfo } from '../../../src/agent/types.js';
import type { AgentConfig } from '../../../src/types/index.js';
// Mock AgentExecutor - 使用延迟执行
vi.mock('../../../src/agent/executor.js', () => ({
AgentExecutor: vi.fn().mockImplementation(() => ({
execute: vi.fn().mockImplementation(async () => {
// 延迟 200ms,让测试能在执行完成前检查状态
await new Promise((r) => setTimeout(r, 200));
return {
success: true,
text: '任务完成',
steps: 3,
sessionId: 'test-session',
};
}),
})),
}));
describe('AgentManager - Agent 管理器', () => {
let manager: AgentManager;
const mockAgentInfo: AgentInfo = {
name: 'test-agent',
description: '测试 Agent',
mode: 'subagent',
prompt: '你是一个测试助手',
};
const mockConfig: AgentConfig = {
provider: 'anthropic',
apiKey: 'test-key',
model: 'claude-3-5-sonnet-20241022',
maxTokens: 4096,
systemPrompt: '测试系统提示词',
};
const mockContext = {
parentSessionId: 'parent-123',
workdir: '/test',
};
beforeEach(() => {
vi.clearAllMocks();
resetAgentManager();
manager = new AgentManager();
});
describe('getAgentManager - 单例获取', () => {
it('返回相同的实例', () => {
resetAgentManager();
const instance1 = getAgentManager();
const instance2 = getAgentManager();
expect(instance1).toBe(instance2);
});
it('resetAgentManager 重置实例', () => {
const instance1 = getAgentManager();
resetAgentManager();
const instance2 = getAgentManager();
expect(instance1).not.toBe(instance2);
});
});
describe('runInBackground - 后台运行', () => {
it('返回 agent ID', async () => {
const agentId = await manager.runInBackground(
mockAgentInfo,
'测试任务',
'执行测试',
mockConfig,
{} as any,
mockContext
);
expect(agentId).toBeDefined();
expect(typeof agentId).toBe('string');
expect(agentId.length).toBe(8); // 短 ID
});
it('创建初始状态的记录', async () => {
const agentId = await manager.runInBackground(
mockAgentInfo,
'测试任务',
'执行测试',
mockConfig,
{} as any,
mockContext
);
const agent = manager.getAgent(agentId);
expect(agent).not.toBeNull();
expect(agent!.agentName).toBe('test-agent');
expect(agent!.description).toBe('测试任务');
expect(agent!.prompt).toBe('执行测试');
expect(agent!.startedAt).toBeInstanceOf(Date);
// 由于是异步执行,初始状态应该是 running
// 但由于测试环境的原因,可能已经变成其他状态
expect(['running', 'completed', 'failed']).toContain(agent!.status);
});
});
describe('getAgent - 获取 Agent', () => {
it('返回存在的 Agent', async () => {
const agentId = await manager.runInBackground(
mockAgentInfo,
'测试任务',
'执行测试',
mockConfig,
{} as any,
mockContext
);
const agent = manager.getAgent(agentId);
expect(agent).not.toBeNull();
expect(agent!.id).toBe(agentId);
});
it('返回 null 当 Agent 不存在', () => {
const agent = manager.getAgent('non-existent');
expect(agent).toBeNull();
});
});
describe('getAgentOutput - 获取输出', () => {
it('返回 null 当 Agent 不存在', async () => {
const result = await manager.getAgentOutput('non-existent', false);
expect(result).toBeNull();
});
it('非阻塞模式立即返回状态', async () => {
const agentId = await manager.runInBackground(
mockAgentInfo,
'测试任务',
'执行测试',
mockConfig,
{} as any,
mockContext
);
const result = await manager.getAgentOutput(agentId, false);
expect(result).not.toBeNull();
expect(result!.id).toBe(agentId);
});
it('阻塞模式等待完成', async () => {
const agentId = await manager.runInBackground(
mockAgentInfo,
'测试任务',
'执行测试',
mockConfig,
{} as any,
mockContext
);
// 使用阻塞模式等待
const result = await manager.getAgentOutput(agentId, true, 5);
expect(result).not.toBeNull();
// 等待后应该是完成状态
expect(['completed', 'failed']).toContain(result!.status);
});
});
describe('listAgents - 列出所有 Agent', () => {
it('返回所有 Agent', async () => {
await manager.runInBackground(
mockAgentInfo,
'任务1',
'执行1',
mockConfig,
{} as any,
mockContext
);
await manager.runInBackground(
mockAgentInfo,
'任务2',
'执行2',
mockConfig,
{} as any,
mockContext
);
const agents = manager.listAgents();
expect(agents.length).toBe(2);
});
it('空管理器返回空数组', () => {
const agents = manager.listAgents();
expect(agents).toEqual([]);
});
});
describe('listRunningAgents - 列出运行中的 Agent', () => {
it('返回数组', async () => {
await manager.runInBackground(
mockAgentInfo,
'任务1',
'执行1',
mockConfig,
{} as any,
mockContext
);
const running = manager.listRunningAgents();
expect(Array.isArray(running)).toBe(true);
});
});
describe('cleanup - 清理', () => {
it('清理已完成的 Agent', async () => {
const agentId = await manager.runInBackground(
mockAgentInfo,
'任务',
'执行',
mockConfig,
{} as any,
mockContext
);
// 等待一段时间让 Agent 完成(无论成功或失败)
await new Promise((r) => setTimeout(r, 100));
// 确认 Agent 不再是 running 状态
const beforeCleanup = manager.getAgent(agentId);
expect(beforeCleanup).not.toBeNull();
expect(['completed', 'failed']).toContain(beforeCleanup?.status);
// 设置 maxAge 为 0,立即清理
manager.cleanup(0);
const agent = manager.getAgent(agentId);
expect(agent).toBeNull();
});
it('不清理运行中的 Agent', async () => {
const agentId = await manager.runInBackground(
mockAgentInfo,
'任务',
'执行',
mockConfig,
{} as any,
mockContext
);
// 立即清理(不等待)
manager.cleanup(0);
// cleanup 应该不会报错
// 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);
});
});
});
@@ -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');
});
});
@@ -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);
});
});
});
});
@@ -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);
});
});