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 - 配置管理
279 lines
8.6 KiB
TypeScript
279 lines
8.6 KiB
TypeScript
import { describe, it, expect } from 'vitest';
|
|
import { searchTools } from '../../../src/tools/search.js';
|
|
import type { ToolMetadata, ToolCategory } from '../../../src/tools/types.js';
|
|
|
|
// 创建测试用的工具元数据
|
|
function createToolMetadata(
|
|
name: string,
|
|
options: Partial<{
|
|
category: ToolCategory;
|
|
description: string;
|
|
keywords: string[];
|
|
deferLoading: boolean;
|
|
}> = {}
|
|
): ToolMetadata {
|
|
return {
|
|
name,
|
|
category: options.category ?? 'core',
|
|
description: options.description ?? `Description for ${name}`,
|
|
keywords: options.keywords ?? [name],
|
|
deferLoading: options.deferLoading ?? true,
|
|
};
|
|
}
|
|
|
|
describe('searchTools - 工具搜索算法', () => {
|
|
const testTools: ToolMetadata[] = [
|
|
createToolMetadata('read_file', {
|
|
category: 'filesystem',
|
|
description: '读取文件内容',
|
|
keywords: ['read', 'file', 'open', 'cat', '文件', '读取'],
|
|
}),
|
|
createToolMetadata('write_file', {
|
|
category: 'filesystem',
|
|
description: '写入文件内容',
|
|
keywords: ['write', 'file', 'save', 'create', '文件', '写入'],
|
|
}),
|
|
createToolMetadata('bash', {
|
|
category: 'shell',
|
|
description: '执行 bash 命令',
|
|
keywords: ['bash', 'shell', 'command', 'execute', 'run', '命令', '执行'],
|
|
}),
|
|
createToolMetadata('glob', {
|
|
category: 'filesystem',
|
|
description: '搜索匹配模式的文件',
|
|
keywords: ['glob', 'pattern', 'find', 'search', 'file', '搜索', '模式'],
|
|
}),
|
|
createToolMetadata('grep', {
|
|
category: 'filesystem',
|
|
description: '在文件内容中搜索',
|
|
keywords: ['grep', 'search', 'content', 'find', '搜索', '内容'],
|
|
}),
|
|
createToolMetadata('git_status', {
|
|
category: 'git',
|
|
description: '查看 Git 仓库状态',
|
|
keywords: ['git', 'status', 'repository', '状态', '仓库'],
|
|
}),
|
|
createToolMetadata('git_commit', {
|
|
category: 'git',
|
|
description: '提交代码更改',
|
|
keywords: ['git', 'commit', 'save', '提交', '保存'],
|
|
}),
|
|
];
|
|
|
|
describe('基础搜索功能', () => {
|
|
it('按名称精确匹配得最高分', () => {
|
|
const results = searchTools('bash', testTools);
|
|
|
|
expect(results.length).toBeGreaterThan(0);
|
|
expect(results[0].name).toBe('bash');
|
|
expect(results[0].score).toBeGreaterThanOrEqual(10); // 名称精确匹配 +10
|
|
});
|
|
|
|
it('按名称包含匹配', () => {
|
|
const results = searchTools('file', testTools);
|
|
|
|
expect(results.length).toBeGreaterThan(0);
|
|
// read_file 和 write_file 都应该匹配
|
|
const fileTools = results.filter((r) => r.name.includes('file'));
|
|
expect(fileTools.length).toBe(2);
|
|
});
|
|
|
|
it('按关键词精确匹配', () => {
|
|
const results = searchTools('shell', testTools);
|
|
|
|
expect(results.length).toBeGreaterThan(0);
|
|
expect(results[0].name).toBe('bash');
|
|
});
|
|
|
|
it('按描述内容匹配', () => {
|
|
const results = searchTools('仓库', testTools);
|
|
|
|
expect(results.length).toBeGreaterThan(0);
|
|
expect(results.some((r) => r.name === 'git_status')).toBe(true);
|
|
});
|
|
|
|
it('中文关键词搜索', () => {
|
|
const results = searchTools('文件', testTools);
|
|
|
|
expect(results.length).toBeGreaterThan(0);
|
|
// read_file 和 write_file 都有 '文件' 关键词
|
|
expect(results.some((r) => r.name === 'read_file')).toBe(true);
|
|
expect(results.some((r) => r.name === 'write_file')).toBe(true);
|
|
});
|
|
});
|
|
|
|
describe('分词功能', () => {
|
|
it('空格分隔的多词查询', () => {
|
|
const results = searchTools('read file', testTools);
|
|
|
|
expect(results.length).toBeGreaterThan(0);
|
|
// read_file 应该得到最高分(匹配 read 和 file)
|
|
expect(results[0].name).toBe('read_file');
|
|
});
|
|
|
|
it('逗号分隔的多词查询', () => {
|
|
const results = searchTools('git,status', testTools);
|
|
|
|
expect(results.length).toBeGreaterThan(0);
|
|
expect(results[0].name).toBe('git_status');
|
|
});
|
|
|
|
it('中文逗号分隔', () => {
|
|
const results = searchTools('读取,文件', testTools);
|
|
|
|
expect(results.length).toBeGreaterThan(0);
|
|
expect(results[0].name).toBe('read_file');
|
|
});
|
|
|
|
it('下划线分隔', () => {
|
|
const results = searchTools('git_commit', testTools);
|
|
|
|
expect(results.length).toBeGreaterThan(0);
|
|
expect(results[0].name).toBe('git_commit');
|
|
});
|
|
|
|
it('连字符分隔', () => {
|
|
const results = searchTools('read-write', testTools);
|
|
|
|
// 应该匹配到包含 read 或 write 关键词的工具
|
|
expect(results.length).toBeGreaterThan(0);
|
|
});
|
|
|
|
it('顿号分隔(中文)', () => {
|
|
const results = searchTools('搜索、文件', testTools);
|
|
|
|
expect(results.length).toBeGreaterThan(0);
|
|
});
|
|
});
|
|
|
|
describe('评分规则', () => {
|
|
it('名称精确匹配优先于包含匹配', () => {
|
|
const tools: ToolMetadata[] = [
|
|
createToolMetadata('bash', { keywords: ['shell'] }),
|
|
createToolMetadata('bash_advanced', { keywords: ['advanced'] }), // 移除 bash 关键词,避免额外得分
|
|
];
|
|
|
|
const results = searchTools('bash', tools);
|
|
|
|
expect(results[0].name).toBe('bash'); // 精确匹配得分更高
|
|
});
|
|
|
|
it('关键词精确匹配优先于包含匹配', () => {
|
|
const tools: ToolMetadata[] = [
|
|
createToolMetadata('tool_a', { keywords: ['git'] }),
|
|
createToolMetadata('tool_b', { keywords: ['github', 'gitlab'] }),
|
|
];
|
|
|
|
const results = searchTools('git', tools);
|
|
|
|
expect(results[0].name).toBe('tool_a'); // 关键词精确匹配得分更高
|
|
});
|
|
|
|
it('多词查询累加分数', () => {
|
|
const results = searchTools('git commit save', testTools);
|
|
|
|
// git_commit 应该匹配 git, commit, save (在关键词中)
|
|
expect(results[0].name).toBe('git_commit');
|
|
});
|
|
});
|
|
|
|
describe('结果限制', () => {
|
|
it('默认返回最多 5 个结果', () => {
|
|
const results = searchTools('file', testTools);
|
|
|
|
expect(results.length).toBeLessThanOrEqual(5);
|
|
});
|
|
|
|
it('自定义限制结果数量', () => {
|
|
const results = searchTools('file', testTools, 2);
|
|
|
|
expect(results.length).toBeLessThanOrEqual(2);
|
|
});
|
|
|
|
it('limit 为 0 时返回空数组', () => {
|
|
const results = searchTools('file', testTools, 0);
|
|
|
|
expect(results).toHaveLength(0);
|
|
});
|
|
});
|
|
|
|
describe('只搜索延迟加载的工具', () => {
|
|
it('跳过 deferLoading=false 的工具', () => {
|
|
const tools: ToolMetadata[] = [
|
|
createToolMetadata('core_tool', {
|
|
keywords: ['test'],
|
|
deferLoading: false,
|
|
}),
|
|
createToolMetadata('deferred_tool', {
|
|
keywords: ['test'],
|
|
deferLoading: true,
|
|
}),
|
|
];
|
|
|
|
const results = searchTools('test', tools);
|
|
|
|
expect(results).toHaveLength(1);
|
|
expect(results[0].name).toBe('deferred_tool');
|
|
});
|
|
|
|
it('全部为 deferLoading=false 时返回空数组', () => {
|
|
const tools: ToolMetadata[] = [
|
|
createToolMetadata('tool_a', { deferLoading: false }),
|
|
createToolMetadata('tool_b', { deferLoading: false }),
|
|
];
|
|
|
|
const results = searchTools('tool', tools);
|
|
|
|
expect(results).toHaveLength(0);
|
|
});
|
|
});
|
|
|
|
describe('边界情况', () => {
|
|
it('空查询返回空数组', () => {
|
|
const results = searchTools('', testTools);
|
|
expect(results).toHaveLength(0);
|
|
});
|
|
|
|
it('只有空格的查询返回空数组', () => {
|
|
const results = searchTools(' ', testTools);
|
|
expect(results).toHaveLength(0);
|
|
});
|
|
|
|
it('空工具列表返回空数组', () => {
|
|
const results = searchTools('test', []);
|
|
expect(results).toHaveLength(0);
|
|
});
|
|
|
|
it('无匹配时返回空数组', () => {
|
|
const results = searchTools('xyznonexistent', testTools);
|
|
expect(results).toHaveLength(0);
|
|
});
|
|
|
|
it('大小写不敏感', () => {
|
|
const results = searchTools('BASH', testTools);
|
|
|
|
expect(results.length).toBeGreaterThan(0);
|
|
expect(results[0].name).toBe('bash');
|
|
});
|
|
});
|
|
|
|
describe('搜索结果结构', () => {
|
|
it('返回正确的结果结构', () => {
|
|
const results = searchTools('bash', testTools);
|
|
|
|
expect(results[0]).toHaveProperty('name');
|
|
expect(results[0]).toHaveProperty('description');
|
|
expect(results[0]).toHaveProperty('category');
|
|
expect(results[0]).toHaveProperty('score');
|
|
});
|
|
|
|
it('结果按分数降序排列', () => {
|
|
const results = searchTools('file', testTools);
|
|
|
|
for (let i = 1; i < results.length; i++) {
|
|
expect(results[i - 1].score).toBeGreaterThanOrEqual(results[i].score);
|
|
}
|
|
});
|
|
});
|
|
});
|