test: 补充单元测试提升代码覆盖率

新增测试文件:
- agent/executor-extended.test.ts, presets/
- context/manager-extended.test.ts
- core/agent.test.ts, providers.test.ts
- lsp/cli.test.ts, client-extended.test.ts, index.test.ts
- permission/file-prompt.test.ts, prompt.test.ts
- skills/builtin/
- tools/filesystem/write_file-extended.test.ts
- tools/git/git_commit-extended.test.ts
- tools/load_description.test.ts
- tools/todo/todo-manager.test.ts
- tools/tool-search.test.ts
- types/
- utils/config-extended.test.ts, diff-extended.test.ts

修改现有测试:
- agent/manager.test.ts
- tools/skill/skill.test.ts
- utils/config.test.ts, diff.test.ts, image.test.ts
This commit is contained in:
2025-12-11 20:37:03 +08:00
parent f8b0cd4bec
commit bca19b7741
24 changed files with 6347 additions and 4 deletions
@@ -0,0 +1,258 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { writeFileTool } from '../../../../src/tools/filesystem/write_file.js';
// Mock fs/promises
vi.mock('fs/promises', () => ({
mkdir: vi.fn().mockResolvedValue(undefined),
writeFile: vi.fn().mockResolvedValue(undefined),
}));
// Mock permission manager
const mockCheckFilePermission = vi.fn();
vi.mock('../../../../src/permission/index.js', () => ({
getPermissionManager: vi.fn(() => ({
checkFilePermission: mockCheckFilePermission,
})),
}));
// Mock loadDescription
vi.mock('../../../../src/tools/load_description.js', () => ({
loadDescription: vi.fn(() => '写入文件内容'),
}));
// Mock LSP
const mockTouchFile = vi.fn();
const mockGetFormattedFileDiagnostics = vi.fn();
const mockIsLanguageSupported = vi.fn();
vi.mock('../../../../src/lsp/index.js', () => ({
touchFile: (...args: unknown[]) => mockTouchFile(...args),
getFormattedFileDiagnostics: (...args: unknown[]) => mockGetFormattedFileDiagnostics(...args),
isLanguageSupported: (...args: unknown[]) => mockIsLanguageSupported(...args),
}));
import * as fs from 'fs/promises';
describe('writeFileTool - 写入文件工具扩展测试', () => {
beforeEach(() => {
vi.clearAllMocks();
// 重置所有 mock 返回值
vi.mocked(fs.mkdir).mockResolvedValue(undefined);
vi.mocked(fs.writeFile).mockResolvedValue(undefined);
mockCheckFilePermission.mockResolvedValue({
allowed: true,
action: 'allow',
});
mockIsLanguageSupported.mockReturnValue(false);
});
describe('LSP 集成', () => {
it('支持的语言触发 LSP 诊断', async () => {
mockIsLanguageSupported.mockReturnValue(true);
mockTouchFile.mockResolvedValue(false);
mockGetFormattedFileDiagnostics.mockResolvedValue(null);
const result = await writeFileTool.execute({
path: './test.ts',
content: 'const x = 1;',
});
expect(result.success).toBe(true);
expect(mockTouchFile).toHaveBeenCalled();
expect(mockGetFormattedFileDiagnostics).toHaveBeenCalled();
});
it('首次启动等待更长时间', async () => {
mockIsLanguageSupported.mockReturnValue(true);
mockTouchFile.mockResolvedValue(true); // 首次启动返回 true
mockGetFormattedFileDiagnostics.mockResolvedValue(null);
const startTime = Date.now();
await writeFileTool.execute({
path: './test.ts',
content: 'const x = 1;',
});
const elapsed = Date.now() - startTime;
// 首次启动应该等待更长时间(2000ms vs 300ms
expect(elapsed).toBeGreaterThanOrEqual(1900);
});
it('有诊断问题时在输出中显示', async () => {
mockIsLanguageSupported.mockReturnValue(true);
mockTouchFile.mockResolvedValue(false);
mockGetFormattedFileDiagnostics.mockResolvedValue(
'\n<file_diagnostics>\n1:1 ERROR: Type error\n</file_diagnostics>'
);
const result = await writeFileTool.execute({
path: './test.ts',
content: 'const x: string = 1;',
});
expect(result.success).toBe(true);
expect(result.output).toContain('代码检查发现问题');
expect(result.output).toContain('file_diagnostics');
});
it('LSP 错误不影响主流程', async () => {
mockIsLanguageSupported.mockReturnValue(true);
mockTouchFile.mockRejectedValue(new Error('LSP error'));
const result = await writeFileTool.execute({
path: './test.ts',
content: 'const x = 1;',
});
// 即使 LSP 失败,文件写入仍然成功
expect(result.success).toBe(true);
expect(result.output).toContain('文件已写入');
});
it('不支持的语言不触发 LSP', async () => {
mockIsLanguageSupported.mockReturnValue(false);
await writeFileTool.execute({
path: './test.txt',
content: 'plain text',
});
expect(mockTouchFile).not.toHaveBeenCalled();
expect(mockGetFormattedFileDiagnostics).not.toHaveBeenCalled();
});
});
describe('路径处理', () => {
it('相对路径转换为绝对路径', async () => {
await writeFileTool.execute({
path: './relative/path/file.txt',
content: 'content',
});
expect(fs.mkdir).toHaveBeenCalledWith(
expect.stringMatching(/relative\/path$/),
{ recursive: true }
);
});
it('绝对路径保持不变', async () => {
await writeFileTool.execute({
path: '/absolute/path/file.txt',
content: 'content',
});
expect(fs.writeFile).toHaveBeenCalledWith(
'/absolute/path/file.txt',
'content',
'utf-8'
);
});
});
describe('权限检查详情', () => {
it('权限检查包含正确的上下文', async () => {
await writeFileTool.execute({
path: './test.txt',
content: 'file content',
});
expect(mockCheckFilePermission).toHaveBeenCalledWith(
expect.objectContaining({
operation: 'write',
newContent: 'file content',
})
);
});
it('被拒绝时返回具体原因', async () => {
mockCheckFilePermission.mockResolvedValue({
allowed: false,
action: 'deny',
reason: '系统文件不允许修改',
});
const result = await writeFileTool.execute({
path: '/etc/passwd',
content: 'malicious',
});
expect(result.success).toBe(false);
expect(result.error).toContain('权限被拒绝');
expect(result.error).toContain('系统文件不允许修改');
});
it('需要确认时返回具体原因', async () => {
mockCheckFilePermission.mockResolvedValue({
allowed: false,
action: 'ask',
needsConfirmation: true,
reason: '首次写入此文件',
});
const result = await writeFileTool.execute({
path: './new-file.txt',
content: 'content',
});
expect(result.success).toBe(false);
expect(result.error).toContain('需要用户确认');
expect(result.error).toContain('首次写入此文件');
});
});
describe('错误处理', () => {
it('文件系统错误返回错误信息', async () => {
vi.mocked(fs.writeFile).mockRejectedValue(new Error('EACCES: permission denied'));
const result = await writeFileTool.execute({
path: './test.txt',
content: 'content',
});
expect(result.success).toBe(false);
expect(result.error).toContain('EACCES');
});
it('目录创建失败返回错误信息', async () => {
vi.mocked(fs.mkdir).mockRejectedValue(new Error('ENOENT'));
const result = await writeFileTool.execute({
path: './deep/nested/file.txt',
content: 'content',
});
expect(result.success).toBe(false);
expect(result.error).toContain('ENOENT');
});
it('非 Error 对象也能正确处理', async () => {
vi.mocked(fs.writeFile).mockRejectedValue('String error');
const result = await writeFileTool.execute({
path: './test.txt',
content: 'content',
});
expect(result.success).toBe(false);
expect(result.error).toContain('String error');
});
});
describe('工具元数据', () => {
it('包含完整的元数据', () => {
expect(writeFileTool.metadata.category).toBe('filesystem');
expect(writeFileTool.metadata.keywords).toContain('write');
expect(writeFileTool.metadata.keywords).toContain('save');
expect(writeFileTool.metadata.keywords).toContain('create');
expect(writeFileTool.metadata.keywords).toContain('写入');
expect(writeFileTool.metadata.deferLoading).toBe(false);
});
it('参数定义正确', () => {
expect(writeFileTool.parameters.path.required).toBe(true);
expect(writeFileTool.parameters.content.required).toBe(true);
expect(writeFileTool.parameters.path.type).toBe('string');
expect(writeFileTool.parameters.content.type).toBe('string');
});
});
});
@@ -0,0 +1,275 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
// 定义可控的 mock 变量
let mockExecAsyncResult: { stdout: string; stderr: string } | Error = {
stdout: '[main abc1234] Test commit\n 1 file changed, 1 insertion(+)',
stderr: '',
};
let mockPermissionResult = {
allowed: true,
action: 'allow' as const,
reason: undefined as string | undefined,
needsConfirmation: false,
};
// Mock child_process
vi.mock('child_process', () => ({
exec: vi.fn(),
}));
// Mock util - 返回函数使用外部变量
vi.mock('util', () => ({
promisify: vi.fn(() => vi.fn(async () => {
if (mockExecAsyncResult instanceof Error) {
throw mockExecAsyncResult;
}
return mockExecAsyncResult;
})),
}));
// Mock permission manager
vi.mock('../../../../src/permission/index.js', () => ({
getPermissionManager: vi.fn(() => ({
checkGitPermission: vi.fn(async () => mockPermissionResult),
})),
}));
// Mock loadDescription
vi.mock('../../../../src/tools/load_description.js', () => ({
loadDescription: vi.fn(() => '提交 Git 变更'),
}));
import { gitCommitTool } from '../../../../src/tools/git/git_commit.js';
import { getPermissionManager } from '../../../../src/permission/index.js';
describe('gitCommitTool - Git 提交工具扩展测试', () => {
beforeEach(() => {
vi.clearAllMocks();
mockExecAsyncResult = {
stdout: '[main abc1234] Test commit\n 1 file changed, 1 insertion(+)',
stderr: '',
};
mockPermissionResult = {
allowed: true,
action: 'allow',
reason: undefined,
needsConfirmation: false,
};
});
describe('基本提交', () => {
it('成功提交变更', async () => {
const result = await gitCommitTool.execute({
message: 'Test commit message',
});
expect(result.success).toBe(true);
expect(result.output).toContain('Test commit');
});
it('没有 message 返回错误', async () => {
const result = await gitCommitTool.execute({});
expect(result.success).toBe(false);
expect(result.error).toContain('提交信息是必填的');
});
it('amend 模式无 message 允许', async () => {
const result = await gitCommitTool.execute({
amend: true,
});
expect(result.success).toBe(true);
});
});
describe('提交选项', () => {
it('使用 -a 选项暂存所有变更', async () => {
const result = await gitCommitTool.execute({
message: 'Auto stage commit',
all: true,
});
expect(result.success).toBe(true);
});
it('amend 带 message', async () => {
const result = await gitCommitTool.execute({
message: 'Updated message',
amend: true,
});
expect(result.success).toBe(true);
});
it('转义 message 中的引号', async () => {
const result = await gitCommitTool.execute({
message: 'Message with "quotes"',
});
expect(result.success).toBe(true);
});
});
describe('权限检查', () => {
it('权限拒绝返回错误', async () => {
mockPermissionResult = {
allowed: false,
action: 'deny',
reason: '不允许提交到此仓库',
needsConfirmation: false,
};
const result = await gitCommitTool.execute({
message: 'Test commit',
});
expect(result.success).toBe(false);
expect(result.error).toContain('权限被拒绝');
expect(result.error).toContain('不允许提交到此仓库');
});
it('需要确认时返回提示', async () => {
mockPermissionResult = {
allowed: false,
action: 'ask',
reason: '首次提交',
needsConfirmation: true,
};
const result = await gitCommitTool.execute({
message: 'First commit',
});
expect(result.success).toBe(false);
expect(result.error).toContain('需要用户确认');
expect(result.error).toContain('首次提交');
});
it('权限检查包含正确上下文', async () => {
const mockCheck = vi.fn().mockResolvedValue({
allowed: true,
action: 'allow',
});
vi.mocked(getPermissionManager).mockReturnValue({
checkGitPermission: mockCheck,
} as any);
await gitCommitTool.execute({
message: 'Check context',
});
expect(mockCheck).toHaveBeenCalledWith({
operation: 'commit',
message: 'Check context',
});
});
});
describe('错误处理', () => {
it('没有变更可提交', async () => {
mockExecAsyncResult = Object.assign(
new Error('nothing to commit, working tree clean'),
{ stdout: '', stderr: '', message: 'nothing to commit, working tree clean' }
);
const result = await gitCommitTool.execute({
message: 'Empty commit',
});
expect(result.success).toBe(false);
expect(result.error).toContain('没有变更需要提交');
expect(result.error).toContain('git_add');
});
it('stderr 包含 nothing to commit', async () => {
mockExecAsyncResult = Object.assign(
new Error('Command failed'),
{ stdout: '', stderr: 'nothing to commit', message: 'Command failed' }
);
const result = await gitCommitTool.execute({
message: 'Empty commit',
});
expect(result.success).toBe(false);
expect(result.error).toContain('没有变更需要提交');
});
it('其他 Git 错误', async () => {
mockExecAsyncResult = Object.assign(
new Error('fatal: not a git repository'),
{ stdout: '', stderr: 'fatal: not a git repository', message: 'fatal: not a git repository' }
);
const result = await gitCommitTool.execute({
message: 'Test commit',
});
expect(result.success).toBe(false);
expect(result.error).toContain('not a git repository');
});
it('保留 stdout 在错误中', async () => {
mockExecAsyncResult = Object.assign(
new Error('error'),
{ stdout: 'some output', stderr: 'error message', message: 'error' }
);
const result = await gitCommitTool.execute({
message: 'Test',
});
expect(result.output).toBe('some output');
expect(result.error).toBe('error message');
});
});
describe('工具元数据', () => {
it('包含正确的名称', () => {
expect(gitCommitTool.name).toBe('git_commit');
});
it('包含正确的类别', () => {
expect(gitCommitTool.metadata.category).toBe('git');
});
it('包含关键词', () => {
expect(gitCommitTool.metadata.keywords).toContain('git');
expect(gitCommitTool.metadata.keywords).toContain('commit');
expect(gitCommitTool.metadata.keywords).toContain('提交');
});
it('参数定义正确', () => {
expect(gitCommitTool.parameters.message.required).toBe(true);
expect(gitCommitTool.parameters.amend.required).toBe(false);
expect(gitCommitTool.parameters.all.required).toBe(false);
});
});
describe('输出格式', () => {
it('包含 stdout', async () => {
mockExecAsyncResult = {
stdout: 'commit output',
stderr: '',
};
const result = await gitCommitTool.execute({ message: 'test' });
expect(result.output).toBe('commit output');
});
it('包含 stdout 和 stderr', async () => {
mockExecAsyncResult = {
stdout: 'commit output',
stderr: 'warning message',
};
const result = await gitCommitTool.execute({ message: 'test' });
expect(result.output).toContain('commit output');
expect(result.output).toContain('warning message');
});
});
});
+205
View File
@@ -0,0 +1,205 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
// Mock fs
vi.mock('fs', () => ({
readFileSync: vi.fn(),
}));
import * as fs from 'fs';
import { loadDescription } from '../../../src/tools/load_description.js';
describe('loadDescription', () => {
beforeEach(() => {
vi.clearAllMocks();
});
describe('带分类的工具', () => {
it('加载 shell 类工具描述', () => {
vi.mocked(fs.readFileSync).mockReturnValue(' Bash 工具描述 ');
const result = loadDescription('bash');
expect(result).toBe('Bash 工具描述');
expect(fs.readFileSync).toHaveBeenCalledWith(
expect.stringContaining('descriptions/shell/bash.txt'),
'utf-8'
);
});
it('加载 filesystem 类工具描述', () => {
vi.mocked(fs.readFileSync).mockReturnValue('读取文件描述');
const result = loadDescription('read_file');
expect(result).toBe('读取文件描述');
expect(fs.readFileSync).toHaveBeenCalledWith(
expect.stringContaining('descriptions/filesystem/read_file.txt'),
'utf-8'
);
});
it('加载 write_file 描述', () => {
vi.mocked(fs.readFileSync).mockReturnValue('写入文件描述');
loadDescription('write_file');
expect(fs.readFileSync).toHaveBeenCalledWith(
expect.stringContaining('descriptions/filesystem/write_file.txt'),
'utf-8'
);
});
it('加载 edit_file 描述', () => {
vi.mocked(fs.readFileSync).mockReturnValue('编辑文件描述');
loadDescription('edit_file');
expect(fs.readFileSync).toHaveBeenCalledWith(
expect.stringContaining('descriptions/filesystem/edit_file.txt'),
'utf-8'
);
});
it('加载 git 类工具描述', () => {
vi.mocked(fs.readFileSync).mockReturnValue('Git status 描述');
loadDescription('git_status');
expect(fs.readFileSync).toHaveBeenCalledWith(
expect.stringContaining('descriptions/git/git_status.txt'),
'utf-8'
);
});
it('加载 web 类工具描述', () => {
vi.mocked(fs.readFileSync).mockReturnValue('Web search 描述');
loadDescription('web_search');
expect(fs.readFileSync).toHaveBeenCalledWith(
expect.stringContaining('descriptions/web/web_search.txt'),
'utf-8'
);
});
it('加载 todo 类工具描述', () => {
vi.mocked(fs.readFileSync).mockReturnValue('Todo read 描述');
loadDescription('todo_read');
expect(fs.readFileSync).toHaveBeenCalledWith(
expect.stringContaining('descriptions/todo/todo_read.txt'),
'utf-8'
);
});
});
describe('不带分类的工具', () => {
it('未知工具使用根目录', () => {
vi.mocked(fs.readFileSync).mockReturnValue('未知工具描述');
loadDescription('unknown_tool');
expect(fs.readFileSync).toHaveBeenCalledWith(
expect.stringContaining('descriptions/unknown_tool.txt'),
'utf-8'
);
// 确保不包含子目录
expect(fs.readFileSync).not.toHaveBeenCalledWith(
expect.stringContaining('descriptions/filesystem/unknown_tool.txt'),
'utf-8'
);
});
});
describe('错误处理', () => {
it('文件不存在时抛出错误', () => {
vi.mocked(fs.readFileSync).mockImplementation(() => {
throw new Error('ENOENT');
});
expect(() => loadDescription('nonexistent')).toThrow('无法加载工具描述文件');
});
it('错误信息包含文件路径', () => {
vi.mocked(fs.readFileSync).mockImplementation(() => {
throw new Error('ENOENT');
});
expect(() => loadDescription('some_tool')).toThrow('some_tool.txt');
});
});
describe('内容处理', () => {
it('去除前后空白', () => {
vi.mocked(fs.readFileSync).mockReturnValue('\n\n 内容 \n\n');
const result = loadDescription('bash');
expect(result).toBe('内容');
});
it('保留中间空白', () => {
vi.mocked(fs.readFileSync).mockReturnValue('第一行\n第二行\n第三行');
const result = loadDescription('bash');
expect(result).toBe('第一行\n第二行\n第三行');
});
it('空文件返回空字符串', () => {
vi.mocked(fs.readFileSync).mockReturnValue(' ');
const result = loadDescription('bash');
expect(result).toBe('');
});
});
describe('所有映射的工具', () => {
const toolCategories = [
// shell
{ tool: 'bash', category: 'shell' },
// filesystem
{ tool: 'read_file', category: 'filesystem' },
{ tool: 'write_file', category: 'filesystem' },
{ tool: 'edit_file', category: 'filesystem' },
{ tool: 'list_directory', category: 'filesystem' },
{ tool: 'create_directory', category: 'filesystem' },
{ tool: 'search_files', category: 'filesystem' },
{ tool: 'grep_content', category: 'filesystem' },
{ tool: 'get_file_info', category: 'filesystem' },
{ tool: 'move_file', category: 'filesystem' },
{ tool: 'copy_file', category: 'filesystem' },
{ tool: 'delete_file', category: 'filesystem' },
// web
{ tool: 'web_search', category: 'web' },
{ tool: 'web_extract', category: 'web' },
// git
{ tool: 'git_status', category: 'git' },
{ tool: 'git_diff', category: 'git' },
{ tool: 'git_log', category: 'git' },
{ tool: 'git_branch', category: 'git' },
{ tool: 'git_add', category: 'git' },
{ tool: 'git_commit', category: 'git' },
{ tool: 'git_push', category: 'git' },
{ tool: 'git_pull', category: 'git' },
{ tool: 'git_checkout', category: 'git' },
{ tool: 'git_stash', category: 'git' },
// todo
{ tool: 'todo_read', category: 'todo' },
{ tool: 'todo_write', category: 'todo' },
];
it.each(toolCategories)('$tool 映射到 $category 目录', ({ tool, category }) => {
vi.mocked(fs.readFileSync).mockReturnValue('描述');
loadDescription(tool);
expect(fs.readFileSync).toHaveBeenCalledWith(
expect.stringContaining(`descriptions/${category}/${tool}.txt`),
'utf-8'
);
});
});
});
+59 -1
View File
@@ -1,5 +1,5 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { skillTool } from '../../../../src/tools/skill/skill.js';
import { skillTool, updateSkillDescription } from '../../../../src/tools/skill/skill.js';
import { getSkillRegistry, resetSkillRegistry } from '../../../../src/skills/registry.js';
import type { Skill } from '../../../../src/skills/types.js';
@@ -144,4 +144,62 @@ describe('skillTool - Skill 工具', () => {
expect(result.metadata?.source).toBe('builtin');
});
});
describe('工具描述', () => {
it('更新后描述包含可用 Skill 列表', () => {
// beforeEach 中已初始化 registry,现在更新描述
updateSkillDescription();
expect(skillTool.description).toContain('code-review');
expect(skillTool.description).toContain('代码审查');
});
it('更新后描述包含分类信息', () => {
updateSkillDescription();
expect(skillTool.description).toContain('[development]');
});
it('更新后描述包含使用示例', () => {
updateSkillDescription();
expect(skillTool.description).toContain('使用示例');
expect(skillTool.description).toContain('skill_name');
});
it('禁用的 Skill 不在描述中', () => {
updateSkillDescription();
expect(skillTool.description).not.toContain('disabled-skill');
});
});
describe('updateSkillDescription', () => {
it('调用后描述被更新', async () => {
updateSkillDescription();
// 描述应该包含当前注册的 Skill
expect(skillTool.description).toContain('code-review');
expect(typeof skillTool.description).toBe('string');
});
});
describe('边界情况', () => {
it('params 为 undefined 时使用空对象', async () => {
const result = await skillTool.execute({
skill_name: 'code-review',
// 不提供 params
});
expect(result.success).toBe(false);
expect(result.error).toContain('缺少必需参数');
});
it('skill_name 为空字符串时返回错误', async () => {
const result = await skillTool.execute({
skill_name: '',
params: {},
});
expect(result.success).toBe(false);
expect(result.error).toContain('不存在');
});
});
});
+274
View File
@@ -0,0 +1,274 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { todoManager } from '../../../../src/tools/todo/todo-manager.js';
import type { SessionManager } from '../../../../src/session/index.js';
import type { Todo } from '../../../../src/session/types.js';
describe('TodoManager', () => {
let mockSessionManager: SessionManager;
let mockTodos: Todo[];
beforeEach(() => {
mockTodos = [];
mockSessionManager = {
getTodos: vi.fn(() => mockTodos),
setTodos: vi.fn(async (todos: Todo[]) => {
mockTodos = todos;
}),
} as unknown as SessionManager;
// 重置 todoManager 状态
todoManager.setSessionManager(mockSessionManager);
});
describe('setSessionManager', () => {
it('设置会话管理器后 isInitialized 返回 true', () => {
const freshManager = {
getTodos: vi.fn(() => []),
setTodos: vi.fn(),
} as unknown as SessionManager;
todoManager.setSessionManager(freshManager);
expect(todoManager.isInitialized()).toBe(true);
});
});
describe('getTodos', () => {
it('返回空数组当没有 todo', () => {
const todos = todoManager.getTodos();
expect(todos).toEqual([]);
});
it('返回现有的 todo 列表', () => {
mockTodos = [
{ id: '1', content: 'Task 1', status: 'pending', createdAt: '', updatedAt: '' },
{ id: '2', content: 'Task 2', status: 'completed', createdAt: '', updatedAt: '' },
];
const todos = todoManager.getTodos();
expect(todos).toHaveLength(2);
expect(todos[0].content).toBe('Task 1');
expect(todos[1].content).toBe('Task 2');
});
it('正确调用 sessionManager.getTodos', () => {
todoManager.getTodos();
expect(mockSessionManager.getTodos).toHaveBeenCalled();
});
});
describe('setTodos', () => {
it('更新 todo 列表', async () => {
const newTodos: Todo[] = [
{ id: '1', content: 'New Task', status: 'pending', createdAt: '', updatedAt: '' },
];
await todoManager.setTodos(newTodos);
expect(mockSessionManager.setTodos).toHaveBeenCalledWith(newTodos);
expect(mockTodos).toHaveLength(1);
});
it('可以设置空列表', async () => {
mockTodos = [
{ id: '1', content: 'Task', status: 'pending', createdAt: '', updatedAt: '' },
];
await todoManager.setTodos([]);
expect(mockTodos).toHaveLength(0);
});
});
describe('addTodo', () => {
it('添加新的 todo(默认状态 pending', async () => {
const todo = await todoManager.addTodo('新任务');
expect(todo.content).toBe('新任务');
expect(todo.status).toBe('pending');
expect(todo.id).toBeDefined();
expect(todo.createdAt).toBeDefined();
expect(todo.updatedAt).toBeDefined();
expect(mockTodos).toHaveLength(1);
});
it('添加指定状态的 todo', async () => {
const todo = await todoManager.addTodo('进行中的任务', 'in_progress');
expect(todo.content).toBe('进行中的任务');
expect(todo.status).toBe('in_progress');
});
it('添加已完成状态的 todo', async () => {
const todo = await todoManager.addTodo('已完成任务', 'completed');
expect(todo.status).toBe('completed');
});
it('生成唯一 ID', async () => {
const todo1 = await todoManager.addTodo('Task 1');
const todo2 = await todoManager.addTodo('Task 2');
expect(todo1.id).not.toBe(todo2.id);
});
it('追加到现有列表', async () => {
mockTodos = [
{ id: 'existing', content: 'Existing', status: 'pending', createdAt: '', updatedAt: '' },
];
await todoManager.addTodo('New Task');
expect(mockTodos).toHaveLength(2);
expect(mockTodos[0].content).toBe('Existing');
expect(mockTodos[1].content).toBe('New Task');
});
});
describe('updateTodoStatus', () => {
beforeEach(() => {
mockTodos = [
{ id: 'todo-1', content: 'Task 1', status: 'pending', createdAt: '2024-01-01', updatedAt: '2024-01-01' },
{ id: 'todo-2', content: 'Task 2', status: 'pending', createdAt: '2024-01-01', updatedAt: '2024-01-01' },
];
});
it('更新存在的 todo 状态', async () => {
const result = await todoManager.updateTodoStatus('todo-1', 'completed');
expect(result).toBe(true);
expect(mockTodos[0].status).toBe('completed');
});
it('更新 updatedAt 时间戳', async () => {
const originalUpdatedAt = mockTodos[0].updatedAt;
await todoManager.updateTodoStatus('todo-1', 'in_progress');
expect(mockTodos[0].updatedAt).not.toBe(originalUpdatedAt);
});
it('不存在的 todo 返回 false', async () => {
const result = await todoManager.updateTodoStatus('non-existent', 'completed');
expect(result).toBe(false);
});
it('可以更新为任意状态', async () => {
await todoManager.updateTodoStatus('todo-1', 'in_progress');
expect(mockTodos[0].status).toBe('in_progress');
await todoManager.updateTodoStatus('todo-1', 'completed');
expect(mockTodos[0].status).toBe('completed');
await todoManager.updateTodoStatus('todo-1', 'pending');
expect(mockTodos[0].status).toBe('pending');
});
});
describe('deleteTodo', () => {
beforeEach(() => {
mockTodos = [
{ id: 'todo-1', content: 'Task 1', status: 'pending', createdAt: '', updatedAt: '' },
{ id: 'todo-2', content: 'Task 2', status: 'completed', createdAt: '', updatedAt: '' },
{ id: 'todo-3', content: 'Task 3', status: 'in_progress', createdAt: '', updatedAt: '' },
];
});
it('删除存在的 todo', async () => {
const result = await todoManager.deleteTodo('todo-2');
expect(result).toBe(true);
expect(mockTodos).toHaveLength(2);
expect(mockTodos.find(t => t.id === 'todo-2')).toBeUndefined();
});
it('删除第一个 todo', async () => {
const result = await todoManager.deleteTodo('todo-1');
expect(result).toBe(true);
expect(mockTodos[0].id).toBe('todo-2');
});
it('删除最后一个 todo', async () => {
const result = await todoManager.deleteTodo('todo-3');
expect(result).toBe(true);
expect(mockTodos).toHaveLength(2);
});
it('不存在的 todo 返回 false', async () => {
const result = await todoManager.deleteTodo('non-existent');
expect(result).toBe(false);
expect(mockTodos).toHaveLength(3);
});
it('删除后其他 todo 保持不变', async () => {
await todoManager.deleteTodo('todo-2');
expect(mockTodos[0].content).toBe('Task 1');
expect(mockTodos[1].content).toBe('Task 3');
});
});
describe('clearTodos', () => {
it('清空所有 todo', async () => {
mockTodos = [
{ id: '1', content: 'Task 1', status: 'pending', createdAt: '', updatedAt: '' },
{ id: '2', content: 'Task 2', status: 'completed', createdAt: '', updatedAt: '' },
];
await todoManager.clearTodos();
expect(mockTodos).toHaveLength(0);
expect(mockSessionManager.setTodos).toHaveBeenCalledWith([]);
});
it('空列表时调用也不报错', async () => {
mockTodos = [];
await expect(todoManager.clearTodos()).resolves.not.toThrow();
expect(mockTodos).toHaveLength(0);
});
});
describe('isInitialized', () => {
it('初始化后返回 true', () => {
expect(todoManager.isInitialized()).toBe(true);
});
});
describe('边界情况', () => {
it('处理包含特殊字符的 content', async () => {
const specialContent = '任务: "测试" & <script>alert(1)</script>';
const todo = await todoManager.addTodo(specialContent);
expect(todo.content).toBe(specialContent);
});
it('处理非常长的 content', async () => {
const longContent = 'A'.repeat(10000);
const todo = await todoManager.addTodo(longContent);
expect(todo.content).toBe(longContent);
});
it('处理空字符串 content', async () => {
const todo = await todoManager.addTodo('');
expect(todo.content).toBe('');
});
it('连续操作保持数据一致性', async () => {
await todoManager.addTodo('Task 1');
await todoManager.addTodo('Task 2');
await todoManager.updateTodoStatus(mockTodos[0].id, 'completed');
await todoManager.deleteTodo(mockTodos[1].id);
await todoManager.addTodo('Task 3');
expect(mockTodos).toHaveLength(2);
expect(mockTodos[0].status).toBe('completed');
expect(mockTodos[1].content).toBe('Task 3');
});
});
});
+143
View File
@@ -0,0 +1,143 @@
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { toolSearchTool } from '../../../src/tools/tool-search.js';
// Mock toolRegistry
vi.mock('../../../src/tools/registry.js', () => ({
toolRegistry: {
search: vi.fn(),
},
}));
import { toolRegistry } from '../../../src/tools/registry.js';
describe('toolSearchTool', () => {
beforeEach(() => {
vi.clearAllMocks();
});
describe('metadata', () => {
it('工具名为 tool_search', () => {
expect(toolSearchTool.name).toBe('tool_search');
});
it('有描述信息', () => {
expect(toolSearchTool.description).toBeDefined();
expect(toolSearchTool.description.length).toBeGreaterThan(0);
});
it('描述中包含功能类别', () => {
expect(toolSearchTool.description).toContain('文件操作');
expect(toolSearchTool.description).toContain('目录操作');
expect(toolSearchTool.description).toContain('Shell');
});
it('有 query 参数', () => {
expect(toolSearchTool.parameters.query).toBeDefined();
expect(toolSearchTool.parameters.query.type).toBe('string');
expect(toolSearchTool.parameters.query.required).toBe(true);
});
it('metadata 设置正确', () => {
expect(toolSearchTool.metadata).toBeDefined();
expect(toolSearchTool.metadata?.category).toBe('core');
expect(toolSearchTool.metadata?.deferLoading).toBe(false);
});
it('metadata 包含搜索关键词', () => {
expect(toolSearchTool.metadata?.keywords).toContain('search');
expect(toolSearchTool.metadata?.keywords).toContain('搜索');
});
});
describe('execute', () => {
it('空 query 返回错误', async () => {
const result = await toolSearchTool.execute({ query: '' });
expect(result.success).toBe(false);
expect(result.error).toBe('请提供搜索关键词');
});
it('空白 query 返回错误', async () => {
const result = await toolSearchTool.execute({ query: ' ' });
expect(result.success).toBe(false);
expect(result.error).toBe('请提供搜索关键词');
});
it('undefined query 返回错误', async () => {
const result = await toolSearchTool.execute({});
expect(result.success).toBe(false);
expect(result.error).toBe('请提供搜索关键词');
});
it('没有匹配结果时返回提示', async () => {
vi.mocked(toolRegistry.search).mockReturnValue([]);
const result = await toolSearchTool.execute({ query: 'nonexistent' });
expect(result.success).toBe(true);
expect(result.output).toContain('没有找到');
expect(result.output).toContain('nonexistent');
});
it('有匹配结果时返回工具列表', async () => {
vi.mocked(toolRegistry.search).mockReturnValue([
{ name: 'read_file', description: '读取文件', category: 'filesystem' },
{ name: 'write_file', description: '写入文件', category: 'filesystem' },
] as any);
const result = await toolSearchTool.execute({ query: '文件' });
expect(result.success).toBe(true);
expect(result.output).toContain('找到 2 个相关工具');
expect(result.output).toContain('read_file');
expect(result.output).toContain('write_file');
expect(result.output).toContain('[filesystem]');
});
it('返回结果包含使用提示', async () => {
vi.mocked(toolRegistry.search).mockReturnValue([
{ name: 'bash', description: '执行命令', category: 'shell' },
] as any);
const result = await toolSearchTool.execute({ query: '命令' });
expect(result.output).toContain('可以直接调用');
});
it('调用 registry.search 时传递正确参数', async () => {
vi.mocked(toolRegistry.search).mockReturnValue([]);
await toolSearchTool.execute({ query: '搜索关键词' });
expect(toolRegistry.search).toHaveBeenCalledWith('搜索关键词', 5);
});
it('搜索结果格式化正确', async () => {
vi.mocked(toolRegistry.search).mockReturnValue([
{ name: 'grep_content', description: '搜索文件内容', category: 'filesystem' },
] as any);
const result = await toolSearchTool.execute({ query: 'grep' });
expect(result.output).toContain('- grep_content: 搜索文件内容 [filesystem]');
});
it('多个搜索结果正确排列', async () => {
vi.mocked(toolRegistry.search).mockReturnValue([
{ name: 'tool1', description: 'desc1', category: 'cat1' },
{ name: 'tool2', description: 'desc2', category: 'cat2' },
{ name: 'tool3', description: 'desc3', category: 'cat3' },
] as any);
const result = await toolSearchTool.execute({ query: 'test' });
expect(result.output).toContain('找到 3 个相关工具');
// 验证每个工具都在输出中
expect(result.output).toContain('tool1');
expect(result.output).toContain('tool2');
expect(result.output).toContain('tool3');
});
});
});