5e32375f0e
架构变更: - 采用 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 - 配置管理
361 lines
12 KiB
TypeScript
361 lines
12 KiB
TypeScript
import { describe, it, expect, beforeEach } from 'vitest';
|
|
import { ToolRegistry } from '../../../src/tools/registry.js';
|
|
import type { ToolWithMetadata, ToolMetadata, ToolCategory } from '../../../src/tools/types.js';
|
|
|
|
// 创建 mock 工具
|
|
function createMockToolWithMetadata(
|
|
name: string,
|
|
options: Partial<{
|
|
category: ToolCategory;
|
|
deferLoading: boolean;
|
|
keywords: string[];
|
|
description: string;
|
|
}> = {}
|
|
): ToolWithMetadata {
|
|
const metadata: ToolMetadata = {
|
|
name,
|
|
category: options.category ?? 'core',
|
|
description: options.description ?? `Mock tool: ${name}`,
|
|
keywords: options.keywords ?? [name],
|
|
deferLoading: options.deferLoading ?? false,
|
|
};
|
|
|
|
return {
|
|
name,
|
|
description: metadata.description,
|
|
parameters: { type: 'object', properties: {}, required: [] },
|
|
execute: async () => ({ success: true, output: `executed ${name}` }),
|
|
metadata,
|
|
};
|
|
}
|
|
|
|
describe('ToolRegistry - 工具注册表', () => {
|
|
let registry: ToolRegistry;
|
|
|
|
beforeEach(() => {
|
|
registry = new ToolRegistry();
|
|
});
|
|
|
|
describe('register / registerAll', () => {
|
|
it('注册单个工具', () => {
|
|
const tool = createMockToolWithMetadata('test_tool');
|
|
registry.register(tool);
|
|
|
|
expect(registry.has('test_tool')).toBe(true);
|
|
expect(registry.size).toBe(1);
|
|
});
|
|
|
|
it('批量注册工具', () => {
|
|
const tools = [
|
|
createMockToolWithMetadata('tool_a'),
|
|
createMockToolWithMetadata('tool_b'),
|
|
createMockToolWithMetadata('tool_c'),
|
|
];
|
|
registry.registerAll(tools);
|
|
|
|
expect(registry.size).toBe(3);
|
|
expect(registry.has('tool_a')).toBe(true);
|
|
expect(registry.has('tool_b')).toBe(true);
|
|
expect(registry.has('tool_c')).toBe(true);
|
|
});
|
|
|
|
it('同名工具覆盖注册', () => {
|
|
const tool1 = createMockToolWithMetadata('test_tool', { description: 'version 1' });
|
|
const tool2 = createMockToolWithMetadata('test_tool', { description: 'version 2' });
|
|
|
|
registry.register(tool1);
|
|
registry.register(tool2);
|
|
|
|
expect(registry.size).toBe(1);
|
|
const retrieved = registry.getTool('test_tool');
|
|
expect(retrieved?.description).toBe('version 2');
|
|
});
|
|
});
|
|
|
|
describe('getTool / getTools', () => {
|
|
beforeEach(() => {
|
|
registry.registerAll([
|
|
createMockToolWithMetadata('read_file'),
|
|
createMockToolWithMetadata('write_file'),
|
|
createMockToolWithMetadata('bash'),
|
|
]);
|
|
});
|
|
|
|
it('获取存在的工具', () => {
|
|
const tool = registry.getTool('read_file');
|
|
expect(tool).toBeDefined();
|
|
expect(tool?.name).toBe('read_file');
|
|
});
|
|
|
|
it('获取不存在的工具返回 undefined', () => {
|
|
const tool = registry.getTool('non_existent');
|
|
expect(tool).toBeUndefined();
|
|
});
|
|
|
|
it('批量获取工具', () => {
|
|
const tools = registry.getTools(['read_file', 'bash']);
|
|
expect(tools).toHaveLength(2);
|
|
expect(tools.map((t) => t.name)).toContain('read_file');
|
|
expect(tools.map((t) => t.name)).toContain('bash');
|
|
});
|
|
|
|
it('批量获取时跳过不存在的工具', () => {
|
|
const tools = registry.getTools(['read_file', 'non_existent', 'bash']);
|
|
expect(tools).toHaveLength(2);
|
|
});
|
|
|
|
it('批量获取空列表返回空数组', () => {
|
|
const tools = registry.getTools([]);
|
|
expect(tools).toHaveLength(0);
|
|
});
|
|
});
|
|
|
|
describe('getCoreTools - 核心工具(非延迟加载)', () => {
|
|
it('只返回 deferLoading=false 的工具', () => {
|
|
registry.registerAll([
|
|
createMockToolWithMetadata('core_tool_1', { deferLoading: false }),
|
|
createMockToolWithMetadata('core_tool_2', { deferLoading: false }),
|
|
createMockToolWithMetadata('deferred_tool', { deferLoading: true }),
|
|
]);
|
|
|
|
const coreTools = registry.getCoreTools();
|
|
|
|
expect(coreTools).toHaveLength(2);
|
|
expect(coreTools.map((t) => t.name)).toContain('core_tool_1');
|
|
expect(coreTools.map((t) => t.name)).toContain('core_tool_2');
|
|
expect(coreTools.map((t) => t.name)).not.toContain('deferred_tool');
|
|
});
|
|
|
|
it('所有工具都是延迟加载时返回空数组', () => {
|
|
registry.registerAll([
|
|
createMockToolWithMetadata('tool_1', { deferLoading: true }),
|
|
createMockToolWithMetadata('tool_2', { deferLoading: true }),
|
|
]);
|
|
|
|
const coreTools = registry.getCoreTools();
|
|
expect(coreTools).toHaveLength(0);
|
|
});
|
|
|
|
it('默认 deferLoading=false 作为核心工具', () => {
|
|
registry.register(createMockToolWithMetadata('default_tool'));
|
|
const coreTools = registry.getCoreTools();
|
|
expect(coreTools).toHaveLength(1);
|
|
});
|
|
});
|
|
|
|
describe('getAllTools / getAllMetadata', () => {
|
|
beforeEach(() => {
|
|
registry.registerAll([
|
|
createMockToolWithMetadata('tool_a', { category: 'filesystem', deferLoading: false }),
|
|
createMockToolWithMetadata('tool_b', { category: 'shell', deferLoading: true }),
|
|
createMockToolWithMetadata('tool_c', { category: 'core', deferLoading: false }),
|
|
]);
|
|
});
|
|
|
|
it('获取所有工具', () => {
|
|
const allTools = registry.getAllTools();
|
|
expect(allTools).toHaveLength(3);
|
|
});
|
|
|
|
it('获取所有元数据', () => {
|
|
const metadata = registry.getAllMetadata();
|
|
|
|
expect(metadata).toHaveLength(3);
|
|
expect(metadata.find((m) => m.name === 'tool_a')?.category).toBe('filesystem');
|
|
expect(metadata.find((m) => m.name === 'tool_b')?.deferLoading).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('has / size', () => {
|
|
it('空注册表', () => {
|
|
expect(registry.size).toBe(0);
|
|
expect(registry.has('any')).toBe(false);
|
|
});
|
|
|
|
it('注册后正确检测', () => {
|
|
registry.register(createMockToolWithMetadata('test'));
|
|
|
|
expect(registry.size).toBe(1);
|
|
expect(registry.has('test')).toBe(true);
|
|
expect(registry.has('other')).toBe(false);
|
|
});
|
|
});
|
|
|
|
describe('search - 工具搜索', () => {
|
|
// 注意:searchTools 只搜索 deferLoading=true 的工具
|
|
beforeEach(() => {
|
|
registry.registerAll([
|
|
createMockToolWithMetadata('read_file', {
|
|
category: 'filesystem',
|
|
keywords: ['read', 'file', 'open', 'cat'],
|
|
description: '读取文件内容',
|
|
deferLoading: true, // 必须为 true 才能被搜索
|
|
}),
|
|
createMockToolWithMetadata('write_file', {
|
|
category: 'filesystem',
|
|
keywords: ['write', 'file', 'save', 'create'],
|
|
description: '写入文件内容',
|
|
deferLoading: true,
|
|
}),
|
|
createMockToolWithMetadata('bash', {
|
|
category: 'shell',
|
|
keywords: ['bash', 'shell', 'command', 'execute', 'run'],
|
|
description: '执行 bash 命令',
|
|
deferLoading: true,
|
|
}),
|
|
createMockToolWithMetadata('glob', {
|
|
category: 'filesystem',
|
|
keywords: ['glob', 'pattern', 'find', 'search', 'file'],
|
|
description: '搜索匹配模式的文件',
|
|
deferLoading: true,
|
|
}),
|
|
]);
|
|
});
|
|
|
|
it('按关键词搜索', () => {
|
|
const results = registry.search('file');
|
|
|
|
expect(results.length).toBeGreaterThan(0);
|
|
// file 相关工具应该排在前面
|
|
const fileTools = results.filter((r) => r.name.includes('file') || r.category === 'filesystem');
|
|
expect(fileTools.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('限制返回结果数量', () => {
|
|
const results = registry.search('file', 2);
|
|
expect(results.length).toBeLessThanOrEqual(2);
|
|
});
|
|
|
|
it('搜索 shell 相关', () => {
|
|
const results = registry.search('shell');
|
|
|
|
expect(results.length).toBeGreaterThan(0);
|
|
expect(results.some((r) => r.name === 'bash')).toBe(true);
|
|
});
|
|
|
|
it('无匹配时返回空数组或低分结果', () => {
|
|
const results = registry.search('xyznonexistent');
|
|
// 可能返回空数组或低分结果,取决于搜索实现
|
|
expect(Array.isArray(results)).toBe(true);
|
|
});
|
|
|
|
it('只搜索 deferLoading=true 的工具', () => {
|
|
// 创建新的注册表
|
|
const testRegistry = new ToolRegistry();
|
|
testRegistry.registerAll([
|
|
createMockToolWithMetadata('core_tool', {
|
|
keywords: ['test'],
|
|
deferLoading: false, // 核心工具,不被搜索
|
|
}),
|
|
createMockToolWithMetadata('deferred_tool', {
|
|
keywords: ['test'],
|
|
deferLoading: true, // 延迟加载,可被搜索
|
|
}),
|
|
]);
|
|
|
|
const results = testRegistry.search('test');
|
|
|
|
expect(results).toHaveLength(1);
|
|
expect(results[0].name).toBe('deferred_tool');
|
|
});
|
|
});
|
|
|
|
describe('toBasicTool - 转换为基础工具类型', () => {
|
|
it('转换后只包含基础属性', () => {
|
|
const toolWithMeta = createMockToolWithMetadata('test', {
|
|
category: 'filesystem',
|
|
deferLoading: true,
|
|
keywords: ['test', 'mock'],
|
|
});
|
|
registry.register(toolWithMeta);
|
|
|
|
const basicTool = registry.getTool('test');
|
|
|
|
expect(basicTool).toBeDefined();
|
|
expect(basicTool).toHaveProperty('name');
|
|
expect(basicTool).toHaveProperty('description');
|
|
expect(basicTool).toHaveProperty('parameters');
|
|
expect(basicTool).toHaveProperty('execute');
|
|
// 不应该包含 metadata
|
|
expect(basicTool).not.toHaveProperty('metadata');
|
|
});
|
|
|
|
it('execute 函数正常工作', async () => {
|
|
registry.register(createMockToolWithMetadata('test'));
|
|
const tool = registry.getTool('test');
|
|
|
|
const result = await tool?.execute({});
|
|
|
|
expect(result).toEqual({ success: true, output: 'executed test' });
|
|
});
|
|
});
|
|
});
|
|
|
|
describe('ToolRegistry 实际使用场景', () => {
|
|
let registry: ToolRegistry;
|
|
|
|
beforeEach(() => {
|
|
registry = new ToolRegistry();
|
|
});
|
|
|
|
it('模拟工具发现流程', () => {
|
|
// 注册一批工具,部分为核心工具,部分为延迟加载
|
|
registry.registerAll([
|
|
createMockToolWithMetadata('tool_search', {
|
|
deferLoading: false,
|
|
category: 'core',
|
|
keywords: ['search', 'discover', 'find', 'tool'],
|
|
}),
|
|
createMockToolWithMetadata('read_file', {
|
|
deferLoading: false,
|
|
category: 'filesystem',
|
|
keywords: ['read', 'file'],
|
|
}),
|
|
createMockToolWithMetadata('advanced_git', {
|
|
deferLoading: true,
|
|
category: 'git',
|
|
keywords: ['git', 'version', 'control'],
|
|
}),
|
|
createMockToolWithMetadata('database_query', {
|
|
deferLoading: true,
|
|
category: 'database',
|
|
keywords: ['database', 'sql', 'query'],
|
|
}),
|
|
]);
|
|
|
|
// 1. 获取核心工具(会话开始时)
|
|
const coreTools = registry.getCoreTools();
|
|
expect(coreTools).toHaveLength(2);
|
|
expect(coreTools.map((t) => t.name)).toContain('tool_search');
|
|
|
|
// 2. 搜索工具
|
|
const gitResults = registry.search('git');
|
|
expect(gitResults.some((r) => r.name === 'advanced_git')).toBe(true);
|
|
|
|
// 3. 按需加载发现的工具
|
|
const discoveredTools = registry.getTools(['advanced_git']);
|
|
expect(discoveredTools).toHaveLength(1);
|
|
expect(discoveredTools[0].name).toBe('advanced_git');
|
|
});
|
|
|
|
it('模拟多分类工具注册', () => {
|
|
const categories: ToolCategory[] = ['core', 'filesystem', 'shell', 'git', 'web'];
|
|
|
|
for (const category of categories) {
|
|
registry.register(
|
|
createMockToolWithMetadata(`${category}_tool`, {
|
|
category,
|
|
keywords: [category],
|
|
})
|
|
);
|
|
}
|
|
|
|
const metadata = registry.getAllMetadata();
|
|
|
|
expect(metadata).toHaveLength(5);
|
|
for (const category of categories) {
|
|
expect(metadata.some((m) => m.category === category)).toBe(true);
|
|
}
|
|
});
|
|
});
|