feat: 添加完整的单元测试套件

- 新增 vitest 测试框架配置
- 添加 54 个测试文件,共 951 个测试用例
- 覆盖核心模块:
  - Agent: executor, registry, config-loader, permission-merger
  - Context: manager, compaction, prune, token-counter
  - Permission: manager, bash/file/git/web checkers, wildcard
  - Session: manager, storage
  - Tools: filesystem (12个), git (10个), web, shell, todo, task
  - LSP: client, server, language
  - Utils: config, diff
  - UI: terminal
This commit is contained in:
2025-12-11 14:45:24 +08:00
parent f4df6483a6
commit 729fb2d42a
58 changed files with 14320 additions and 3 deletions
@@ -0,0 +1,173 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
// Mock fs/promises
vi.mock('fs/promises', () => ({
stat: vi.fn(),
copyFile: vi.fn().mockResolvedValue(undefined),
mkdir: vi.fn().mockResolvedValue(undefined),
readdir: vi.fn().mockResolvedValue([]),
}));
// Mock permission manager
vi.mock('../../../../src/permission/index.js', () => ({
getPermissionManager: vi.fn(() => ({
checkFilePermission: vi.fn().mockResolvedValue({
allowed: true,
action: 'allow',
}),
})),
}));
// Mock loadDescription
vi.mock('../../../../src/tools/load_description.js', () => ({
loadDescription: vi.fn(() => '复制文件'),
}));
import { copyFileTool } from '../../../../src/tools/filesystem/copy_file.js';
import * as fs from 'fs/promises';
import { getPermissionManager } from '../../../../src/permission/index.js';
describe('copyFileTool - 文件复制工具', () => {
beforeEach(() => {
vi.clearAllMocks();
vi.mocked(fs.stat).mockResolvedValue({
isDirectory: () => false,
isFile: () => true,
} as any);
});
describe('工具定义', () => {
it('有正确的名称', () => {
expect(copyFileTool.name).toBe('copy_file');
});
it('有正确的元数据', () => {
expect(copyFileTool.metadata.category).toBe('filesystem');
expect(copyFileTool.metadata.keywords).toContain('copy');
expect(copyFileTool.metadata.keywords).toContain('cp');
});
it('定义了必需参数', () => {
expect(copyFileTool.parameters.source.required).toBe(true);
expect(copyFileTool.parameters.destination.required).toBe(true);
});
});
describe('execute - 执行', () => {
it('成功复制文件', async () => {
// 第一次调用检查源文件,第二次调用检查目标是否是目录
vi.mocked(fs.stat)
.mockResolvedValueOnce({ isDirectory: () => false } as any)
.mockRejectedValueOnce(new Error('ENOENT')); // 目标不存在
const result = await copyFileTool.execute({
source: 'src.txt',
destination: 'dest.txt',
});
expect(result.success).toBe(true);
expect(result.output).toContain('已复制');
expect(fs.copyFile).toHaveBeenCalled();
});
it('复制到已存在的目录', async () => {
vi.mocked(fs.stat)
.mockResolvedValueOnce({ isDirectory: () => false } as any) // 源文件
.mockResolvedValueOnce({ isDirectory: () => true } as any); // 目标是目录
const result = await copyFileTool.execute({
source: 'file.txt',
destination: '/target/dir',
});
expect(result.success).toBe(true);
expect(result.output).toContain('file.txt');
});
it('递归复制目录', async () => {
vi.mocked(fs.stat)
.mockResolvedValueOnce({ isDirectory: () => true } as any) // 源是目录
.mockRejectedValueOnce(new Error('ENOENT')); // 目标不存在
vi.mocked(fs.readdir).mockResolvedValueOnce([]);
const result = await copyFileTool.execute({
source: 'src_dir',
destination: 'dest_dir',
});
expect(result.success).toBe(true);
expect(fs.mkdir).toHaveBeenCalled();
});
it('源文件读取权限被拒绝', async () => {
vi.mocked(getPermissionManager).mockReturnValue({
checkFilePermission: vi.fn().mockResolvedValue({
allowed: false,
action: 'deny',
reason: '不允许读取',
}),
} as any);
const result = await copyFileTool.execute({
source: '/etc/passwd',
destination: 'copy.txt',
});
expect(result.success).toBe(false);
expect(result.error).toContain('权限被拒绝');
});
it('目标位置写入权限被拒绝', async () => {
const mockCheck = vi.fn()
.mockResolvedValueOnce({ allowed: true }) // 读取权限
.mockResolvedValueOnce({ allowed: false, action: 'deny', reason: '不允许写入' }); // 复制权限
vi.mocked(getPermissionManager).mockReturnValue({
checkFilePermission: mockCheck,
} as any);
const result = await copyFileTool.execute({
source: 'src.txt',
destination: '/protected/dest.txt',
});
expect(result.success).toBe(false);
expect(result.error).toContain('权限被拒绝');
});
it('源文件需要确认', async () => {
vi.mocked(getPermissionManager).mockReturnValue({
checkFilePermission: vi.fn().mockResolvedValue({
allowed: false,
action: 'ask',
needsConfirmation: true,
}),
} as any);
const result = await copyFileTool.execute({
source: '/sensitive/file',
destination: 'dest.txt',
});
expect(result.success).toBe(false);
expect(result.error).toContain('需要用户确认');
});
it('源文件不存在返回错误', async () => {
vi.mocked(getPermissionManager).mockReturnValue({
checkFilePermission: vi.fn().mockResolvedValue({ allowed: true }),
} as any);
vi.mocked(fs.stat).mockRejectedValue(new Error('ENOENT: no such file'));
const result = await copyFileTool.execute({
source: 'nonexistent.txt',
destination: 'dest.txt',
});
expect(result.success).toBe(false);
expect(result.error).toContain('ENOENT');
});
});
});
@@ -0,0 +1,156 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
// Mock fs/promises
vi.mock('fs/promises', () => ({
stat: vi.fn(),
mkdir: vi.fn().mockResolvedValue(undefined),
}));
// Mock permission manager
vi.mock('../../../../src/permission/index.js', () => ({
getPermissionManager: vi.fn(() => ({
checkFilePermission: vi.fn().mockResolvedValue({
allowed: true,
action: 'allow',
}),
})),
}));
// Mock loadDescription
vi.mock('../../../../src/tools/load_description.js', () => ({
loadDescription: vi.fn(() => '创建目录'),
}));
import { createDirectoryTool } from '../../../../src/tools/filesystem/create_directory.js';
import * as fs from 'fs/promises';
import { getPermissionManager } from '../../../../src/permission/index.js';
describe('createDirectoryTool - 创建目录工具', () => {
beforeEach(() => {
vi.clearAllMocks();
// 默认目录不存在
vi.mocked(fs.stat).mockRejectedValue(new Error('ENOENT'));
});
describe('工具定义', () => {
it('有正确的名称', () => {
expect(createDirectoryTool.name).toBe('create_directory');
});
it('有正确的元数据', () => {
expect(createDirectoryTool.metadata.category).toBe('filesystem');
expect(createDirectoryTool.metadata.keywords).toContain('create');
expect(createDirectoryTool.metadata.keywords).toContain('directory');
expect(createDirectoryTool.metadata.keywords).toContain('mkdir');
});
it('定义了必需的 path 参数', () => {
expect(createDirectoryTool.parameters.path.required).toBe(true);
});
});
describe('execute - 执行', () => {
it('成功创建目录', async () => {
const result = await createDirectoryTool.execute({ path: 'new_dir' });
expect(result.success).toBe(true);
expect(result.output).toContain('已创建目录');
expect(fs.mkdir).toHaveBeenCalledWith(
expect.any(String),
{ recursive: true }
);
});
it('目录已存在返回成功', async () => {
vi.mocked(fs.stat).mockResolvedValue({
isDirectory: () => true,
} as any);
const result = await createDirectoryTool.execute({ path: 'existing_dir' });
expect(result.success).toBe(true);
expect(result.output).toContain('目录已存在');
expect(fs.mkdir).not.toHaveBeenCalled();
});
it('路径是文件返回错误', async () => {
vi.mocked(fs.stat).mockResolvedValue({
isDirectory: () => false,
} as any);
const result = await createDirectoryTool.execute({ path: 'file.txt' });
expect(result.success).toBe(false);
expect(result.error).toContain('路径已存在且不是目录');
});
it('权限被拒绝时返回错误', async () => {
vi.mocked(getPermissionManager).mockReturnValue({
checkFilePermission: vi.fn().mockResolvedValue({
allowed: false,
action: 'deny',
reason: '不允许创建目录',
}),
} as any);
const result = await createDirectoryTool.execute({ path: '/protected/dir' });
expect(result.success).toBe(false);
expect(result.error).toContain('权限被拒绝');
});
it('需要确认时返回提示', async () => {
vi.mocked(getPermissionManager).mockReturnValue({
checkFilePermission: vi.fn().mockResolvedValue({
allowed: false,
action: 'ask',
needsConfirmation: true,
}),
} as any);
const result = await createDirectoryTool.execute({ path: 'new_dir' });
expect(result.success).toBe(false);
expect(result.error).toContain('需要用户确认');
});
it('创建嵌套目录', async () => {
// 确保权限检查通过
vi.mocked(getPermissionManager).mockReturnValue({
checkFilePermission: vi.fn().mockResolvedValue({ allowed: true }),
} as any);
const result = await createDirectoryTool.execute({ path: 'a/b/c/d' });
expect(result.success).toBe(true);
expect(fs.mkdir).toHaveBeenCalledWith(
expect.any(String),
{ recursive: true }
);
});
it('传递正确参数给权限检查', async () => {
const mockCheck = vi.fn().mockResolvedValue({ allowed: true });
vi.mocked(getPermissionManager).mockReturnValue({
checkFilePermission: mockCheck,
} as any);
await createDirectoryTool.execute({ path: 'test_dir' });
expect(mockCheck).toHaveBeenCalledWith(
expect.objectContaining({
operation: 'mkdir',
})
);
});
it('处理创建错误', async () => {
vi.mocked(fs.mkdir).mockRejectedValue(new Error('Permission denied'));
const result = await createDirectoryTool.execute({ path: 'new_dir' });
expect(result.success).toBe(false);
expect(result.error).toContain('Permission denied');
});
});
});
@@ -0,0 +1,173 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
// Mock fs/promises
vi.mock('fs/promises', () => ({
stat: vi.fn(),
unlink: vi.fn().mockResolvedValue(undefined),
rmdir: vi.fn().mockResolvedValue(undefined),
rm: vi.fn().mockResolvedValue(undefined),
readdir: vi.fn().mockResolvedValue([]),
}));
// Mock permission manager
vi.mock('../../../../src/permission/index.js', () => ({
getPermissionManager: vi.fn(() => ({
checkFilePermission: vi.fn().mockResolvedValue({
allowed: true,
action: 'allow',
}),
})),
}));
// Mock loadDescription
vi.mock('../../../../src/tools/load_description.js', () => ({
loadDescription: vi.fn(() => '删除文件'),
}));
import { deleteFileTool } from '../../../../src/tools/filesystem/delete_file.js';
import * as fs from 'fs/promises';
import { getPermissionManager } from '../../../../src/permission/index.js';
describe('deleteFileTool - 文件删除工具', () => {
beforeEach(() => {
vi.clearAllMocks();
});
describe('工具定义', () => {
it('有正确的名称', () => {
expect(deleteFileTool.name).toBe('delete_file');
});
it('有正确的元数据', () => {
expect(deleteFileTool.metadata.category).toBe('filesystem');
expect(deleteFileTool.metadata.keywords).toContain('delete');
expect(deleteFileTool.metadata.keywords).toContain('remove');
expect(deleteFileTool.metadata.keywords).toContain('rm');
});
it('定义了必需的 path 参数', () => {
expect(deleteFileTool.parameters.path.required).toBe(true);
});
it('定义了可选的 recursive 参数', () => {
expect(deleteFileTool.parameters.recursive.required).toBe(false);
});
});
describe('execute - 执行', () => {
it('成功删除文件', async () => {
vi.mocked(fs.stat).mockResolvedValue({
isDirectory: () => false,
} as any);
const result = await deleteFileTool.execute({ path: 'file.txt' });
expect(result.success).toBe(true);
expect(result.output).toContain('已删除文件');
expect(fs.unlink).toHaveBeenCalled();
});
it('删除空目录', async () => {
vi.mocked(fs.stat).mockResolvedValue({
isDirectory: () => true,
} as any);
vi.mocked(fs.readdir).mockResolvedValue([]);
const result = await deleteFileTool.execute({ path: 'empty_dir' });
expect(result.success).toBe(true);
expect(result.output).toContain('已删除目录');
expect(fs.rmdir).toHaveBeenCalled();
});
it('非空目录无 recursive 返回错误', async () => {
vi.mocked(fs.stat).mockResolvedValue({
isDirectory: () => true,
} as any);
vi.mocked(fs.readdir).mockResolvedValue(['file1.txt', 'file2.txt'] as any);
const result = await deleteFileTool.execute({ path: 'nonempty_dir' });
expect(result.success).toBe(false);
expect(result.error).toContain('目录不为空');
expect(result.error).toContain('recursive: true');
});
it('递归删除非空目录', async () => {
vi.mocked(fs.stat).mockResolvedValue({
isDirectory: () => true,
} as any);
const result = await deleteFileTool.execute({
path: 'nonempty_dir',
recursive: true,
});
expect(result.success).toBe(true);
expect(result.output).toContain('已删除目录');
expect(fs.rm).toHaveBeenCalledWith(
expect.any(String),
{ recursive: true }
);
});
it('权限被拒绝时返回错误', async () => {
vi.mocked(getPermissionManager).mockReturnValue({
checkFilePermission: vi.fn().mockResolvedValue({
allowed: false,
action: 'deny',
reason: '不允许删除',
}),
} as any);
const result = await deleteFileTool.execute({ path: '/protected/file' });
expect(result.success).toBe(false);
expect(result.error).toContain('权限被拒绝');
});
it('需要确认时返回提示', async () => {
vi.mocked(getPermissionManager).mockReturnValue({
checkFilePermission: vi.fn().mockResolvedValue({
allowed: false,
action: 'ask',
needsConfirmation: true,
}),
} as any);
const result = await deleteFileTool.execute({ path: 'important.txt' });
expect(result.success).toBe(false);
expect(result.error).toContain('需要用户确认');
});
it('文件不存在返回错误', async () => {
vi.mocked(getPermissionManager).mockReturnValue({
checkFilePermission: vi.fn().mockResolvedValue({ allowed: true }),
} as any);
vi.mocked(fs.stat).mockRejectedValue(new Error('ENOENT'));
const result = await deleteFileTool.execute({ path: 'nonexistent.txt' });
expect(result.success).toBe(false);
expect(result.error).toContain('ENOENT');
});
it('传递正确参数给权限检查', async () => {
const mockCheck = vi.fn().mockResolvedValue({ allowed: true });
vi.mocked(getPermissionManager).mockReturnValue({
checkFilePermission: mockCheck,
} as any);
vi.mocked(fs.stat).mockResolvedValue({ isDirectory: () => false } as any);
await deleteFileTool.execute({ path: 'test.txt' });
expect(mockCheck).toHaveBeenCalledWith(
expect.objectContaining({
operation: 'delete',
})
);
});
});
});
@@ -0,0 +1,201 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
// Mock fs/promises
vi.mock('fs/promises', () => ({
readFile: vi.fn(),
writeFile: vi.fn().mockResolvedValue(undefined),
}));
// Mock permission manager
vi.mock('../../../../src/permission/index.js', () => ({
getPermissionManager: vi.fn(() => ({
checkFilePermission: vi.fn().mockResolvedValue({
allowed: true,
action: 'allow',
}),
})),
}));
// Mock loadDescription
vi.mock('../../../../src/tools/load_description.js', () => ({
loadDescription: vi.fn(() => '编辑文件'),
}));
// Mock LSP
vi.mock('../../../../src/lsp/index.js', () => ({
touchFile: vi.fn().mockResolvedValue(false),
getFormattedFileDiagnostics: vi.fn().mockResolvedValue(null),
isLanguageSupported: vi.fn().mockReturnValue(false),
}));
import { editFileTool } from '../../../../src/tools/filesystem/edit_file.js';
import * as fs from 'fs/promises';
import { getPermissionManager } from '../../../../src/permission/index.js';
import { isLanguageSupported, touchFile, getFormattedFileDiagnostics } from '../../../../src/lsp/index.js';
describe('editFileTool - 文件编辑工具', () => {
beforeEach(() => {
vi.clearAllMocks();
vi.mocked(fs.readFile).mockResolvedValue('original content here');
});
describe('工具定义', () => {
it('有正确的名称', () => {
expect(editFileTool.name).toBe('edit_file');
});
it('有正确的元数据', () => {
expect(editFileTool.metadata.category).toBe('filesystem');
expect(editFileTool.metadata.keywords).toContain('edit');
expect(editFileTool.metadata.keywords).toContain('replace');
});
it('定义了必需参数', () => {
expect(editFileTool.parameters.path.required).toBe(true);
expect(editFileTool.parameters.old_string.required).toBe(true);
expect(editFileTool.parameters.new_string.required).toBe(true);
});
});
describe('execute - 执行', () => {
it('成功编辑文件', async () => {
vi.mocked(fs.readFile).mockResolvedValue('hello world');
const result = await editFileTool.execute({
path: 'test.txt',
old_string: 'world',
new_string: 'universe',
});
expect(result.success).toBe(true);
expect(result.output).toContain('文件已编辑');
expect(fs.writeFile).toHaveBeenCalledWith(
expect.any(String),
'hello universe',
'utf-8'
);
});
it('old_string 不存在返回错误', async () => {
vi.mocked(fs.readFile).mockResolvedValue('hello world');
const result = await editFileTool.execute({
path: 'test.txt',
old_string: 'notfound',
new_string: 'replacement',
});
expect(result.success).toBe(false);
expect(result.error).toContain('未找到要替换的字符串');
});
it('old_string 多次出现返回错误', async () => {
vi.mocked(fs.readFile).mockResolvedValue('hello hello hello');
const result = await editFileTool.execute({
path: 'test.txt',
old_string: 'hello',
new_string: 'hi',
});
expect(result.success).toBe(false);
expect(result.error).toContain('3 处匹配');
expect(result.error).toContain('必须唯一');
});
it('权限被拒绝时返回错误', async () => {
vi.mocked(fs.readFile).mockResolvedValue('content');
vi.mocked(getPermissionManager).mockReturnValue({
checkFilePermission: vi.fn().mockResolvedValue({
allowed: false,
action: 'deny',
reason: '不允许编辑',
}),
} as any);
const result = await editFileTool.execute({
path: 'test.txt',
old_string: 'content',
new_string: 'new',
});
expect(result.success).toBe(false);
expect(result.error).toContain('权限被拒绝');
});
it('需要确认时返回提示', async () => {
vi.mocked(fs.readFile).mockResolvedValue('content');
vi.mocked(getPermissionManager).mockReturnValue({
checkFilePermission: vi.fn().mockResolvedValue({
allowed: false,
action: 'ask',
needsConfirmation: true,
}),
} as any);
const result = await editFileTool.execute({
path: 'test.txt',
old_string: 'content',
new_string: 'new',
});
expect(result.success).toBe(false);
expect(result.error).toContain('需要用户确认');
});
it('文件不存在返回错误', async () => {
vi.mocked(fs.readFile).mockRejectedValue(new Error('ENOENT: no such file'));
const result = await editFileTool.execute({
path: 'nonexistent.txt',
old_string: 'text',
new_string: 'new',
});
expect(result.success).toBe(false);
expect(result.error).toContain('ENOENT');
});
it('支持 LSP 时获取诊断信息', async () => {
// 确保权限检查通过
vi.mocked(getPermissionManager).mockReturnValue({
checkFilePermission: vi.fn().mockResolvedValue({ allowed: true }),
} as any);
vi.mocked(fs.readFile).mockResolvedValue('const x = 1');
vi.mocked(isLanguageSupported).mockReturnValue(true);
vi.mocked(touchFile).mockResolvedValue(false);
vi.mocked(getFormattedFileDiagnostics).mockResolvedValue('\n错误: 类型不匹配');
const result = await editFileTool.execute({
path: 'test.ts',
old_string: 'const x = 1',
new_string: 'const x: string = 1',
});
expect(result.success).toBe(true);
expect(result.output).toContain('代码检查发现问题');
});
it('传递正确参数给权限检查', async () => {
vi.mocked(fs.readFile).mockResolvedValue('old text');
const mockCheck = vi.fn().mockResolvedValue({ allowed: true });
vi.mocked(getPermissionManager).mockReturnValue({
checkFilePermission: mockCheck,
} as any);
await editFileTool.execute({
path: 'test.txt',
old_string: 'old text',
new_string: 'new text',
});
expect(mockCheck).toHaveBeenCalledWith(
expect.objectContaining({
operation: 'edit',
oldContent: 'old text',
newContent: 'new text',
})
);
});
});
});
@@ -0,0 +1,188 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
// Mock fs/promises
vi.mock('fs/promises', () => ({
stat: vi.fn(),
readlink: vi.fn(),
readdir: vi.fn(),
}));
// Mock permission manager
vi.mock('../../../../src/permission/index.js', () => ({
getPermissionManager: vi.fn(() => ({
checkFilePermission: vi.fn().mockResolvedValue({
allowed: true,
action: 'allow',
}),
})),
}));
// Mock loadDescription
vi.mock('../../../../src/tools/load_description.js', () => ({
loadDescription: vi.fn(() => '获取文件信息'),
}));
import { getFileInfoTool } from '../../../../src/tools/filesystem/get_file_info.js';
import * as fs from 'fs/promises';
import { getPermissionManager } from '../../../../src/permission/index.js';
describe('getFileInfoTool - 获取文件信息工具', () => {
const mockStats = {
isDirectory: () => false,
isFile: () => true,
isSymbolicLink: () => false,
size: 1024,
mode: 0o100644,
birthtime: new Date('2024-01-01'),
mtime: new Date('2024-01-15'),
atime: new Date('2024-01-20'),
ino: 12345,
nlink: 1,
};
beforeEach(() => {
vi.clearAllMocks();
vi.mocked(fs.stat).mockResolvedValue(mockStats as any);
});
describe('工具定义', () => {
it('有正确的名称', () => {
expect(getFileInfoTool.name).toBe('get_file_info');
});
it('有正确的元数据', () => {
expect(getFileInfoTool.metadata.category).toBe('filesystem');
expect(getFileInfoTool.metadata.keywords).toContain('file');
expect(getFileInfoTool.metadata.keywords).toContain('info');
expect(getFileInfoTool.metadata.keywords).toContain('stat');
});
it('定义了必需的 path 参数', () => {
expect(getFileInfoTool.parameters.path.required).toBe(true);
});
});
describe('execute - 执行', () => {
it('成功获取文件信息', async () => {
const result = await getFileInfoTool.execute({ path: 'test.txt' });
expect(result.success).toBe(true);
expect(result.output).toContain('路径:');
expect(result.output).toContain('类型: 文件');
expect(result.output).toContain('大小:');
expect(result.output).toContain('权限:');
expect(result.output).toContain('创建时间:');
expect(result.output).toContain('修改时间:');
expect(result.output).toContain('inode:');
});
it('正确显示目录信息', async () => {
vi.mocked(fs.stat).mockResolvedValue({
...mockStats,
isDirectory: () => true,
isFile: () => false,
} as any);
vi.mocked(fs.readdir).mockResolvedValue(['file1', 'file2', 'dir1'] as any);
const result = await getFileInfoTool.execute({ path: 'test_dir' });
expect(result.success).toBe(true);
expect(result.output).toContain('类型: 目录');
expect(result.output).toContain('子项数量: 3');
});
it('正确显示符号链接信息', async () => {
vi.mocked(fs.stat).mockResolvedValue({
...mockStats,
isSymbolicLink: () => true,
isFile: () => false,
} as any);
vi.mocked(fs.readlink).mockResolvedValue('/real/path');
const result = await getFileInfoTool.execute({ path: 'link' });
expect(result.success).toBe(true);
expect(result.output).toContain('类型: 符号链接');
expect(result.output).toContain('链接目标: /real/path');
});
it('正确格式化文件大小', async () => {
// 测试不同大小
const sizes = [
{ size: 500, expected: 'B' },
{ size: 1024, expected: 'KB' },
{ size: 1024 * 1024, expected: 'MB' },
{ size: 1024 * 1024 * 1024, expected: 'GB' },
];
for (const { size, expected } of sizes) {
vi.mocked(fs.stat).mockResolvedValue({
...mockStats,
size,
} as any);
const result = await getFileInfoTool.execute({ path: 'test.txt' });
expect(result.output).toContain(expected);
}
});
it('权限被拒绝时返回错误', async () => {
vi.mocked(getPermissionManager).mockReturnValue({
checkFilePermission: vi.fn().mockResolvedValue({
allowed: false,
action: 'deny',
reason: '不允许获取信息',
}),
} as any);
const result = await getFileInfoTool.execute({ path: '/protected/file' });
expect(result.success).toBe(false);
expect(result.error).toContain('权限被拒绝');
});
it('需要确认时返回提示', async () => {
vi.mocked(getPermissionManager).mockReturnValue({
checkFilePermission: vi.fn().mockResolvedValue({
allowed: false,
action: 'ask',
needsConfirmation: true,
}),
} as any);
const result = await getFileInfoTool.execute({ path: 'file.txt' });
expect(result.success).toBe(false);
expect(result.error).toContain('需要用户确认');
});
it('文件不存在返回错误', async () => {
vi.mocked(getPermissionManager).mockReturnValue({
checkFilePermission: vi.fn().mockResolvedValue({ allowed: true }),
} as any);
vi.mocked(fs.stat).mockRejectedValue(new Error('ENOENT'));
const result = await getFileInfoTool.execute({ path: 'nonexistent.txt' });
expect(result.success).toBe(false);
expect(result.error).toContain('ENOENT');
});
it('传递正确参数给权限检查', async () => {
const mockCheck = vi.fn().mockResolvedValue({ allowed: true });
vi.mocked(getPermissionManager).mockReturnValue({
checkFilePermission: mockCheck,
} as any);
await getFileInfoTool.execute({ path: 'test.txt' });
expect(mockCheck).toHaveBeenCalledWith(
expect.objectContaining({
operation: 'info',
})
);
});
});
});
@@ -0,0 +1,225 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
// Mock fs/promises
vi.mock('fs/promises', () => ({
readdir: vi.fn(),
readFile: vi.fn(),
}));
// Mock permission manager
vi.mock('../../../../src/permission/index.js', () => ({
getPermissionManager: vi.fn(() => ({
checkFilePermission: vi.fn().mockResolvedValue({
allowed: true,
action: 'allow',
}),
})),
}));
// Mock loadDescription
vi.mock('../../../../src/tools/load_description.js', () => ({
loadDescription: vi.fn(() => '在文件内容中搜索文本'),
}));
import { grepContentTool } from '../../../../src/tools/filesystem/grep_content.js';
import * as fs from 'fs/promises';
import { getPermissionManager } from '../../../../src/permission/index.js';
describe('grepContentTool - 内容搜索工具', () => {
beforeEach(() => {
vi.clearAllMocks();
});
describe('工具定义', () => {
it('有正确的名称', () => {
expect(grepContentTool.name).toBe('grep_content');
});
it('有正确的元数据', () => {
expect(grepContentTool.metadata.category).toBe('filesystem');
expect(grepContentTool.metadata.keywords).toContain('grep');
expect(grepContentTool.metadata.keywords).toContain('search');
expect(grepContentTool.metadata.keywords).toContain('content');
});
it('定义了必需参数', () => {
expect(grepContentTool.parameters.directory.required).toBe(true);
expect(grepContentTool.parameters.pattern.required).toBe(true);
});
it('定义了可选参数', () => {
expect(grepContentTool.parameters.file_pattern.required).toBe(false);
expect(grepContentTool.parameters.max_results.required).toBe(false);
});
});
describe('execute - 执行', () => {
it('成功搜索并返回匹配结果', async () => {
vi.mocked(fs.readdir).mockResolvedValue([
{ name: 'test.ts', isDirectory: () => false, isFile: () => true },
] as any);
vi.mocked(fs.readFile).mockResolvedValue('const hello = "world";\nconst foo = "bar";');
const result = await grepContentTool.execute({
directory: '.',
pattern: 'hello',
});
expect(result.success).toBe(true);
expect(result.output).toContain('找到');
expect(result.output).toContain('hello');
});
it('没有匹配时返回提示', async () => {
vi.mocked(fs.readdir).mockResolvedValue([
{ name: 'test.ts', isDirectory: () => false, isFile: () => true },
] as any);
vi.mocked(fs.readFile).mockResolvedValue('const foo = "bar";');
const result = await grepContentTool.execute({
directory: '.',
pattern: 'notfound',
});
expect(result.success).toBe(true);
expect(result.output).toContain('没有找到匹配的内容');
});
it('按文件模式过滤', async () => {
vi.mocked(fs.readdir).mockResolvedValue([
{ name: 'test.ts', isDirectory: () => false, isFile: () => true },
{ name: 'test.js', isDirectory: () => false, isFile: () => true },
] as any);
vi.mocked(fs.readFile).mockResolvedValue('const hello = "world";');
const result = await grepContentTool.execute({
directory: '.',
pattern: 'hello',
file_pattern: '*.ts',
});
expect(result.success).toBe(true);
// 只搜索 .ts 文件
expect(fs.readFile).toHaveBeenCalledTimes(1);
});
it('限制最大结果数', async () => {
vi.mocked(fs.readdir).mockResolvedValue([
{ name: 'test.ts', isDirectory: () => false, isFile: () => true },
] as any);
// 多行匹配内容
vi.mocked(fs.readFile).mockResolvedValue(
'hello1\nhello2\nhello3\nhello4\nhello5'
);
const result = await grepContentTool.execute({
directory: '.',
pattern: 'hello',
max_results: 2,
});
expect(result.success).toBe(true);
expect(result.output).toContain('已达上限');
});
it('跳过隐藏文件和 node_modules', async () => {
// 第一次调用返回根目录内容,第二次调用返回 src 目录内容
vi.mocked(fs.readdir)
.mockResolvedValueOnce([
{ name: '.hidden', isDirectory: () => true, isFile: () => false },
{ name: 'node_modules', isDirectory: () => true, isFile: () => false },
{ name: 'src', isDirectory: () => true, isFile: () => false },
] as any)
.mockResolvedValueOnce([
{ name: 'index.ts', isDirectory: () => false, isFile: () => true },
] as any);
vi.mocked(fs.readFile).mockResolvedValue('const test = 1;');
await grepContentTool.execute({
directory: '.',
pattern: 'test',
});
// 不应该进入隐藏目录或 node_modules,只进入 src
expect(fs.readdir).toHaveBeenCalledTimes(2); // 根目录 + src
});
it('权限被拒绝时返回错误', async () => {
vi.mocked(getPermissionManager).mockReturnValue({
checkFilePermission: vi.fn().mockResolvedValue({
allowed: false,
action: 'deny',
reason: '不允许搜索',
}),
} as any);
const result = await grepContentTool.execute({
directory: '/protected',
pattern: 'test',
});
expect(result.success).toBe(false);
expect(result.error).toContain('权限被拒绝');
});
it('需要确认时返回提示', async () => {
vi.mocked(getPermissionManager).mockReturnValue({
checkFilePermission: vi.fn().mockResolvedValue({
allowed: false,
action: 'ask',
needsConfirmation: true,
}),
} as any);
const result = await grepContentTool.execute({
directory: '.',
pattern: 'test',
});
expect(result.success).toBe(false);
expect(result.error).toContain('需要用户确认');
});
it('递归搜索子目录', async () => {
// 恢复权限检查
vi.mocked(getPermissionManager).mockReturnValue({
checkFilePermission: vi.fn().mockResolvedValue({ allowed: true }),
} as any);
vi.mocked(fs.readdir)
.mockResolvedValueOnce([
{ name: 'src', isDirectory: () => true, isFile: () => false },
] as any)
.mockResolvedValueOnce([
{ name: 'index.ts', isDirectory: () => false, isFile: () => true },
] as any);
vi.mocked(fs.readFile).mockResolvedValue('const test = 1;');
const result = await grepContentTool.execute({
directory: '.',
pattern: 'test',
});
expect(result.success).toBe(true);
expect(fs.readdir).toHaveBeenCalledTimes(2);
});
it('传递正确参数给权限检查', async () => {
const mockCheck = vi.fn().mockResolvedValue({ allowed: true });
vi.mocked(getPermissionManager).mockReturnValue({
checkFilePermission: mockCheck,
} as any);
vi.mocked(fs.readdir).mockResolvedValue([]);
await grepContentTool.execute({
directory: 'src',
pattern: 'test',
});
expect(mockCheck).toHaveBeenCalledWith(
expect.objectContaining({
operation: 'grep',
})
);
});
});
});
@@ -0,0 +1,143 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { listDirTool } from '../../../../src/tools/filesystem/list_directory.js';
// Mock fs/promises
vi.mock('fs/promises', () => ({
readdir: vi.fn(),
}));
// Mock permission manager
vi.mock('../../../../src/permission/index.js', () => ({
getPermissionManager: vi.fn(() => ({
checkFilePermission: vi.fn().mockResolvedValue({
allowed: true,
action: 'allow',
}),
})),
}));
// Mock loadDescription
vi.mock('../../../../src/tools/load_description.js', () => ({
loadDescription: vi.fn(() => '列出目录内容'),
}));
import * as fs from 'fs/promises';
import { getPermissionManager } from '../../../../src/permission/index.js';
describe('listDirTool - 列出目录工具', () => {
beforeEach(() => {
vi.clearAllMocks();
});
describe('工具定义', () => {
it('有正确的名称', () => {
expect(listDirTool.name).toBe('list_directory');
});
it('有正确的元数据', () => {
expect(listDirTool.metadata.category).toBe('filesystem');
expect(listDirTool.metadata.keywords).toContain('list');
expect(listDirTool.metadata.keywords).toContain('directory');
expect(listDirTool.metadata.keywords).toContain('ls');
});
it('定义了必需的 path 参数', () => {
expect(listDirTool.parameters.path).toBeDefined();
expect(listDirTool.parameters.path.required).toBe(true);
});
});
describe('execute - 执行', () => {
it('成功列出目录内容', async () => {
vi.mocked(fs.readdir).mockResolvedValue([
{ name: 'file1.txt', isDirectory: () => false },
{ name: 'folder', isDirectory: () => true },
{ name: 'file2.js', isDirectory: () => false },
] as any);
const result = await listDirTool.execute({ path: './' });
expect(result.success).toBe(true);
expect(result.output).toContain('file1.txt');
expect(result.output).toContain('folder');
expect(result.output).toContain('file2.js');
});
it('使用正确的图标区分文件和目录', async () => {
vi.mocked(fs.readdir).mockResolvedValue([
{ name: 'file.txt', isDirectory: () => false },
{ name: 'folder', isDirectory: () => true },
] as any);
const result = await listDirTool.execute({ path: './' });
expect(result.output).toMatch(/📄.*file\.txt/);
expect(result.output).toMatch(/📁.*folder/);
});
it('空目录显示提示', async () => {
vi.mocked(fs.readdir).mockResolvedValue([]);
const result = await listDirTool.execute({ path: './' });
expect(result.success).toBe(true);
expect(result.output).toBe('(空目录)');
});
it('权限被拒绝时返回错误', async () => {
vi.mocked(getPermissionManager).mockReturnValue({
checkFilePermission: vi.fn().mockResolvedValue({
allowed: false,
action: 'deny',
reason: '不允许列出此目录',
}),
} as any);
const result = await listDirTool.execute({ path: '/etc' });
expect(result.success).toBe(false);
expect(result.error).toContain('权限被拒绝');
});
it('需要确认时返回提示', async () => {
vi.mocked(getPermissionManager).mockReturnValue({
checkFilePermission: vi.fn().mockResolvedValue({
allowed: false,
action: 'ask',
needsConfirmation: true,
}),
} as any);
const result = await listDirTool.execute({ path: '/home/user' });
expect(result.success).toBe(false);
expect(result.error).toContain('需要用户确认');
});
it('目录不存在时返回错误', async () => {
vi.mocked(getPermissionManager).mockReturnValue({
checkFilePermission: vi.fn().mockResolvedValue({
allowed: true,
action: 'allow',
}),
} as any);
vi.mocked(fs.readdir).mockRejectedValue(new Error('ENOENT: no such directory'));
const result = await listDirTool.execute({ path: './nonexistent' });
expect(result.success).toBe(false);
expect(result.error).toContain('ENOENT');
});
it('使用 withFileTypes 选项调用 readdir', async () => {
vi.mocked(fs.readdir).mockResolvedValue([]);
await listDirTool.execute({ path: './' });
expect(fs.readdir).toHaveBeenCalledWith(
expect.any(String),
{ withFileTypes: true }
);
});
});
});
@@ -0,0 +1,171 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
// Mock fs/promises
vi.mock('fs/promises', () => ({
access: vi.fn().mockResolvedValue(undefined),
stat: vi.fn(),
rename: vi.fn().mockResolvedValue(undefined),
mkdir: vi.fn().mockResolvedValue(undefined),
}));
// Mock permission manager
vi.mock('../../../../src/permission/index.js', () => ({
getPermissionManager: vi.fn(() => ({
checkFilePermission: vi.fn().mockResolvedValue({
allowed: true,
action: 'allow',
}),
})),
}));
// Mock loadDescription
vi.mock('../../../../src/tools/load_description.js', () => ({
loadDescription: vi.fn(() => '移动文件'),
}));
import { moveFileTool } from '../../../../src/tools/filesystem/move_file.js';
import * as fs from 'fs/promises';
import { getPermissionManager } from '../../../../src/permission/index.js';
describe('moveFileTool - 文件移动工具', () => {
beforeEach(() => {
vi.clearAllMocks();
});
describe('工具定义', () => {
it('有正确的名称', () => {
expect(moveFileTool.name).toBe('move_file');
});
it('有正确的元数据', () => {
expect(moveFileTool.metadata.category).toBe('filesystem');
expect(moveFileTool.metadata.keywords).toContain('move');
expect(moveFileTool.metadata.keywords).toContain('rename');
expect(moveFileTool.metadata.keywords).toContain('mv');
});
it('定义了必需参数', () => {
expect(moveFileTool.parameters.source.required).toBe(true);
expect(moveFileTool.parameters.destination.required).toBe(true);
});
});
describe('execute - 执行', () => {
it('成功移动文件', async () => {
vi.mocked(fs.stat).mockRejectedValue(new Error('ENOENT')); // 目标不存在
const result = await moveFileTool.execute({
source: 'old.txt',
destination: 'new.txt',
});
expect(result.success).toBe(true);
expect(result.output).toContain('已移动');
expect(fs.rename).toHaveBeenCalled();
});
it('移动到已存在的目录', async () => {
vi.mocked(fs.stat).mockResolvedValue({ isDirectory: () => true } as any);
const result = await moveFileTool.execute({
source: 'file.txt',
destination: '/target/dir',
});
expect(result.success).toBe(true);
expect(result.output).toContain('file.txt');
});
it('源文件移动权限被拒绝', async () => {
vi.mocked(getPermissionManager).mockReturnValue({
checkFilePermission: vi.fn().mockResolvedValue({
allowed: false,
action: 'deny',
reason: '不允许移动',
}),
} as any);
const result = await moveFileTool.execute({
source: '/protected/file',
destination: 'dest.txt',
});
expect(result.success).toBe(false);
expect(result.error).toContain('权限被拒绝');
});
it('目标位置写入权限被拒绝', async () => {
const mockCheck = vi.fn()
.mockResolvedValueOnce({ allowed: true }) // 移动权限
.mockResolvedValueOnce({ allowed: false, action: 'deny', reason: '不允许写入' }); // 写入权限
vi.mocked(getPermissionManager).mockReturnValue({
checkFilePermission: mockCheck,
} as any);
const result = await moveFileTool.execute({
source: 'src.txt',
destination: '/protected/dest.txt',
});
expect(result.success).toBe(false);
expect(result.error).toContain('权限被拒绝');
});
it('需要确认时返回提示', async () => {
vi.mocked(getPermissionManager).mockReturnValue({
checkFilePermission: vi.fn().mockResolvedValue({
allowed: false,
action: 'ask',
needsConfirmation: true,
}),
} as any);
const result = await moveFileTool.execute({
source: 'file.txt',
destination: 'new.txt',
});
expect(result.success).toBe(false);
expect(result.error).toContain('需要用户确认');
});
it('源文件不存在返回错误', async () => {
vi.mocked(getPermissionManager).mockReturnValue({
checkFilePermission: vi.fn().mockResolvedValue({ allowed: true }),
} as any);
vi.mocked(fs.access).mockRejectedValue(new Error('ENOENT'));
const result = await moveFileTool.execute({
source: 'nonexistent.txt',
destination: 'dest.txt',
});
expect(result.success).toBe(false);
expect(result.error).toContain('ENOENT');
});
it('创建目标目录', async () => {
// 确保权限检查通过
vi.mocked(getPermissionManager).mockReturnValue({
checkFilePermission: vi.fn().mockResolvedValue({ allowed: true }),
} as any);
// 源文件存在
vi.mocked(fs.access).mockResolvedValue(undefined);
// 目标不存在
vi.mocked(fs.stat).mockRejectedValue(new Error('ENOENT'));
const result = await moveFileTool.execute({
source: 'file.txt',
destination: '/new/path/file.txt',
});
expect(result.success).toBe(true);
expect(fs.mkdir).toHaveBeenCalledWith(
expect.any(String),
{ recursive: true }
);
});
});
});
@@ -0,0 +1,142 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { readFileTool } from '../../../../src/tools/filesystem/read_file.js';
// Mock fs/promises
vi.mock('fs/promises', () => ({
readFile: vi.fn(),
}));
// Mock permission manager
vi.mock('../../../../src/permission/index.js', () => ({
getPermissionManager: vi.fn(() => ({
checkFilePermission: vi.fn().mockResolvedValue({
allowed: true,
action: 'allow',
}),
})),
}));
// Mock loadDescription
vi.mock('../../../../src/tools/load_description.js', () => ({
loadDescription: vi.fn(() => '读取文件内容'),
}));
import * as fs from 'fs/promises';
import { getPermissionManager } from '../../../../src/permission/index.js';
describe('readFileTool - 读取文件工具', () => {
beforeEach(() => {
vi.clearAllMocks();
});
describe('工具定义', () => {
it('有正确的名称', () => {
expect(readFileTool.name).toBe('read_file');
});
it('有正确的元数据', () => {
expect(readFileTool.metadata.category).toBe('filesystem');
expect(readFileTool.metadata.keywords).toContain('read');
expect(readFileTool.metadata.keywords).toContain('file');
});
it('定义了必需的 path 参数', () => {
expect(readFileTool.parameters.path).toBeDefined();
expect(readFileTool.parameters.path.required).toBe(true);
expect(readFileTool.parameters.path.type).toBe('string');
});
});
describe('execute - 执行', () => {
it('成功读取文件', async () => {
const mockContent = 'Hello, World!';
vi.mocked(fs.readFile).mockResolvedValue(mockContent);
const result = await readFileTool.execute({ path: './test.txt' });
expect(result.success).toBe(true);
expect(result.output).toBe(mockContent);
});
it('处理绝对路径', async () => {
vi.mocked(fs.readFile).mockResolvedValue('content');
await readFileTool.execute({ path: '/absolute/path/file.txt' });
expect(fs.readFile).toHaveBeenCalledWith('/absolute/path/file.txt', 'utf-8');
});
it('处理相对路径', async () => {
vi.mocked(fs.readFile).mockResolvedValue('content');
await readFileTool.execute({ path: './relative/file.txt' });
// 应该解析为绝对路径
expect(fs.readFile).toHaveBeenCalled();
const calledPath = vi.mocked(fs.readFile).mock.calls[0][0] as string;
expect(calledPath.endsWith('relative/file.txt')).toBe(true);
});
it('权限被拒绝时返回错误', async () => {
vi.mocked(getPermissionManager).mockReturnValue({
checkFilePermission: vi.fn().mockResolvedValue({
allowed: false,
action: 'deny',
reason: '不允许读取此文件',
}),
} as any);
const result = await readFileTool.execute({ path: '/etc/passwd' });
expect(result.success).toBe(false);
expect(result.error).toContain('权限被拒绝');
});
it('需要确认时返回提示', async () => {
vi.mocked(getPermissionManager).mockReturnValue({
checkFilePermission: vi.fn().mockResolvedValue({
allowed: false,
action: 'ask',
needsConfirmation: true,
reason: '需要确认',
}),
} as any);
const result = await readFileTool.execute({ path: './sensitive.txt' });
expect(result.success).toBe(false);
expect(result.error).toContain('需要用户确认');
});
it('文件不存在时返回错误', async () => {
vi.mocked(getPermissionManager).mockReturnValue({
checkFilePermission: vi.fn().mockResolvedValue({
allowed: true,
action: 'allow',
}),
} as any);
vi.mocked(fs.readFile).mockRejectedValue(new Error('ENOENT: no such file'));
const result = await readFileTool.execute({ path: './nonexistent.txt' });
expect(result.success).toBe(false);
expect(result.error).toContain('ENOENT');
});
it('读取大文件', async () => {
const largeContent = 'x'.repeat(10000);
vi.mocked(getPermissionManager).mockReturnValue({
checkFilePermission: vi.fn().mockResolvedValue({
allowed: true,
action: 'allow',
}),
} as any);
vi.mocked(fs.readFile).mockResolvedValue(largeContent);
const result = await readFileTool.execute({ path: './large.txt' });
expect(result.success).toBe(true);
expect(result.output.length).toBe(10000);
});
});
});
@@ -0,0 +1,199 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
// Mock fs/promises
vi.mock('fs/promises', () => ({
readdir: vi.fn(),
}));
// Mock permission manager
vi.mock('../../../../src/permission/index.js', () => ({
getPermissionManager: vi.fn(() => ({
checkFilePermission: vi.fn().mockResolvedValue({
allowed: true,
action: 'allow',
}),
})),
}));
// Mock loadDescription
vi.mock('../../../../src/tools/load_description.js', () => ({
loadDescription: vi.fn(() => '按文件名搜索文件'),
}));
import { searchFilesTool } from '../../../../src/tools/filesystem/search_files.js';
import * as fs from 'fs/promises';
import { getPermissionManager } from '../../../../src/permission/index.js';
describe('searchFilesTool - 文件搜索工具', () => {
beforeEach(() => {
vi.clearAllMocks();
});
describe('工具定义', () => {
it('有正确的名称', () => {
expect(searchFilesTool.name).toBe('search_files');
});
it('有正确的元数据', () => {
expect(searchFilesTool.metadata.category).toBe('filesystem');
expect(searchFilesTool.metadata.keywords).toContain('search');
expect(searchFilesTool.metadata.keywords).toContain('find');
expect(searchFilesTool.metadata.keywords).toContain('glob');
});
it('定义了必需参数', () => {
expect(searchFilesTool.parameters.directory.required).toBe(true);
expect(searchFilesTool.parameters.pattern.required).toBe(true);
});
});
describe('execute - 执行', () => {
it('成功搜索并返回匹配文件', async () => {
vi.mocked(fs.readdir).mockResolvedValue([
{ name: 'test.ts', isDirectory: () => false, isFile: () => true },
{ name: 'test.js', isDirectory: () => false, isFile: () => true },
] as any);
const result = await searchFilesTool.execute({
directory: '.',
pattern: '*.ts',
});
expect(result.success).toBe(true);
expect(result.output).toContain('test.ts');
expect(result.output).not.toContain('test.js');
});
it('没有匹配时返回提示', async () => {
vi.mocked(fs.readdir).mockResolvedValue([
{ name: 'test.js', isDirectory: () => false, isFile: () => true },
] as any);
const result = await searchFilesTool.execute({
directory: '.',
pattern: '*.tsx',
});
expect(result.success).toBe(true);
expect(result.output).toContain('没有找到匹配的文件');
});
it('递归搜索子目录', async () => {
vi.mocked(fs.readdir)
.mockResolvedValueOnce([
{ name: 'src', isDirectory: () => true, isFile: () => false },
{ name: 'index.ts', isDirectory: () => false, isFile: () => true },
] as any)
.mockResolvedValueOnce([
{ name: 'app.ts', isDirectory: () => false, isFile: () => true },
] as any);
const result = await searchFilesTool.execute({
directory: '.',
pattern: '*.ts',
});
expect(result.success).toBe(true);
expect(result.output).toContain('index.ts');
expect(result.output).toContain('app.ts');
});
it('跳过隐藏文件和 node_modules', async () => {
// 第一次调用返回根目录内容,第二次调用返回 src 目录内容
vi.mocked(fs.readdir)
.mockResolvedValueOnce([
{ name: '.git', isDirectory: () => true, isFile: () => false },
{ name: 'node_modules', isDirectory: () => true, isFile: () => false },
{ name: 'src', isDirectory: () => true, isFile: () => false },
] as any)
.mockResolvedValueOnce([
{ name: 'index.ts', isDirectory: () => false, isFile: () => true },
] as any);
await searchFilesTool.execute({
directory: '.',
pattern: '*',
});
// 不应该进入隐藏目录或 node_modules,只进入 src
expect(fs.readdir).toHaveBeenCalledTimes(2); // 根目录 + src
});
it('权限被拒绝时返回错误', async () => {
vi.mocked(getPermissionManager).mockReturnValue({
checkFilePermission: vi.fn().mockResolvedValue({
allowed: false,
action: 'deny',
reason: '不允许搜索',
}),
} as any);
const result = await searchFilesTool.execute({
directory: '/protected',
pattern: '*',
});
expect(result.success).toBe(false);
expect(result.error).toContain('权限被拒绝');
});
it('需要确认时返回提示', async () => {
vi.mocked(getPermissionManager).mockReturnValue({
checkFilePermission: vi.fn().mockResolvedValue({
allowed: false,
action: 'ask',
needsConfirmation: true,
}),
} as any);
const result = await searchFilesTool.execute({
directory: '.',
pattern: '*',
});
expect(result.success).toBe(false);
expect(result.error).toContain('需要用户确认');
});
it('支持 glob 模式匹配', async () => {
// 恢复权限检查
vi.mocked(getPermissionManager).mockReturnValue({
checkFilePermission: vi.fn().mockResolvedValue({ allowed: true }),
} as any);
vi.mocked(fs.readdir).mockResolvedValue([
{ name: 'component.tsx', isDirectory: () => false, isFile: () => true },
{ name: 'helper.ts', isDirectory: () => false, isFile: () => true },
{ name: 'style.css', isDirectory: () => false, isFile: () => true },
] as any);
// *.tsx 模式会匹配 component.tsx
const result = await searchFilesTool.execute({
directory: '.',
pattern: '*.tsx',
});
expect(result.success).toBe(true);
expect(result.output).toContain('component.tsx');
expect(result.output).not.toContain('style.css');
});
it('传递正确参数给权限检查', async () => {
const mockCheck = vi.fn().mockResolvedValue({ allowed: true });
vi.mocked(getPermissionManager).mockReturnValue({
checkFilePermission: mockCheck,
} as any);
vi.mocked(fs.readdir).mockResolvedValue([]);
await searchFilesTool.execute({
directory: 'src',
pattern: '*.ts',
});
expect(mockCheck).toHaveBeenCalledWith(
expect.objectContaining({
operation: 'search',
})
);
});
});
});
@@ -0,0 +1,158 @@
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
vi.mock('../../../../src/permission/index.js', () => ({
getPermissionManager: vi.fn(() => ({
checkFilePermission: vi.fn().mockResolvedValue({
allowed: true,
action: 'allow',
}),
})),
}));
// Mock loadDescription
vi.mock('../../../../src/tools/load_description.js', () => ({
loadDescription: vi.fn(() => '写入文件内容'),
}));
// Mock LSP
vi.mock('../../../../src/lsp/index.js', () => ({
touchFile: vi.fn().mockResolvedValue(false),
getFormattedFileDiagnostics: vi.fn().mockResolvedValue(null),
isLanguageSupported: vi.fn().mockReturnValue(false),
}));
import * as fs from 'fs/promises';
import { getPermissionManager } from '../../../../src/permission/index.js';
describe('writeFileTool - 写入文件工具', () => {
beforeEach(() => {
vi.clearAllMocks();
});
describe('工具定义', () => {
it('有正确的名称', () => {
expect(writeFileTool.name).toBe('write_file');
});
it('有正确的元数据', () => {
expect(writeFileTool.metadata.category).toBe('filesystem');
expect(writeFileTool.metadata.keywords).toContain('write');
expect(writeFileTool.metadata.keywords).toContain('save');
});
it('定义了必需的参数', () => {
expect(writeFileTool.parameters.path).toBeDefined();
expect(writeFileTool.parameters.path.required).toBe(true);
expect(writeFileTool.parameters.content).toBeDefined();
expect(writeFileTool.parameters.content.required).toBe(true);
});
});
describe('execute - 执行', () => {
it('成功写入文件', async () => {
const result = await writeFileTool.execute({
path: './test.txt',
content: 'Hello, World!',
});
expect(result.success).toBe(true);
expect(result.output).toContain('文件已写入');
});
it('创建必要的目录', async () => {
await writeFileTool.execute({
path: './deep/nested/file.txt',
content: 'content',
});
expect(fs.mkdir).toHaveBeenCalledWith(
expect.any(String),
{ recursive: true }
);
});
it('权限被拒绝时返回错误', async () => {
vi.mocked(getPermissionManager).mockReturnValue({
checkFilePermission: vi.fn().mockResolvedValue({
allowed: false,
action: 'deny',
reason: '不允许写入此文件',
}),
} as any);
const result = await writeFileTool.execute({
path: '/etc/passwd',
content: 'malicious',
});
expect(result.success).toBe(false);
expect(result.error).toContain('权限被拒绝');
});
it('需要确认时返回提示', async () => {
vi.mocked(getPermissionManager).mockReturnValue({
checkFilePermission: vi.fn().mockResolvedValue({
allowed: false,
action: 'ask',
needsConfirmation: true,
}),
} as any);
const result = await writeFileTool.execute({
path: './new-file.txt',
content: 'content',
});
expect(result.success).toBe(false);
expect(result.error).toContain('需要用户确认');
});
it('写入失败时返回错误', async () => {
vi.mocked(getPermissionManager).mockReturnValue({
checkFilePermission: vi.fn().mockResolvedValue({
allowed: true,
action: 'allow',
}),
} as any);
vi.mocked(fs.writeFile).mockRejectedValue(new Error('Write failed'));
const result = await writeFileTool.execute({
path: './test.txt',
content: 'content',
});
expect(result.success).toBe(false);
expect(result.error).toContain('Write failed');
});
it('传递 newContent 给权限检查', async () => {
const mockCheck = vi.fn().mockResolvedValue({
allowed: true,
action: 'allow',
});
vi.mocked(getPermissionManager).mockReturnValue({
checkFilePermission: mockCheck,
} as any);
await writeFileTool.execute({
path: './test.txt',
content: 'new content',
});
expect(mockCheck).toHaveBeenCalledWith(
expect.objectContaining({
operation: 'write',
newContent: 'new content',
})
);
});
});
});
+160
View File
@@ -0,0 +1,160 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
// 定义一个可控的 mock
let mockExecAsyncResult: { stdout: string; stderr: string } | Error = {
stdout: '',
stderr: '',
};
// 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().mockResolvedValue({
allowed: true,
action: 'allow',
}),
})),
}));
// Mock loadDescription
vi.mock('../../../../src/tools/load_description.js', () => ({
loadDescription: vi.fn(() => 'Git add 命令'),
}));
import { gitAddTool } from '../../../../src/tools/git/git_add.js';
import { getPermissionManager } from '../../../../src/permission/index.js';
describe('gitAddTool - Git Add 工具', () => {
beforeEach(() => {
vi.clearAllMocks();
mockExecAsyncResult = {
stdout: '',
stderr: '',
};
});
describe('工具定义', () => {
it('有正确的名称', () => {
expect(gitAddTool.name).toBe('git_add');
});
it('有正确的元数据', () => {
expect(gitAddTool.metadata.category).toBe('git');
expect(gitAddTool.metadata.keywords).toContain('add');
expect(gitAddTool.metadata.keywords).toContain('stage');
});
it('定义了可选参数', () => {
expect(gitAddTool.parameters.files.required).toBe(false);
expect(gitAddTool.parameters.all.required).toBe(false);
expect(gitAddTool.parameters.update.required).toBe(false);
});
});
describe('execute - 执行', () => {
it('暂存所有文件 (all: true)', async () => {
mockExecAsyncResult = { stdout: '', stderr: '' };
const result = await gitAddTool.execute({ all: true });
expect(result.success).toBe(true);
expect(result.output).toContain('文件已暂存');
});
it('暂存指定文件', async () => {
mockExecAsyncResult = { stdout: '', stderr: '' };
const result = await gitAddTool.execute({
files: ['file1.txt', 'file2.txt'],
});
expect(result.success).toBe(true);
});
it('暂存单个文件(字符串)', async () => {
mockExecAsyncResult = { stdout: '', stderr: '' };
const result = await gitAddTool.execute({ files: 'single.txt' });
expect(result.success).toBe(true);
});
it('使用 update 选项', async () => {
mockExecAsyncResult = { stdout: '', stderr: '' };
const result = await gitAddTool.execute({ update: true });
expect(result.success).toBe(true);
});
it('无参数返回错误', async () => {
const result = await gitAddTool.execute({});
expect(result.success).toBe(false);
expect(result.error).toContain('请指定要暂存的文件');
});
it('权限被拒绝时返回错误', async () => {
vi.mocked(getPermissionManager).mockReturnValue({
checkGitPermission: vi.fn().mockResolvedValue({
allowed: false,
action: 'deny',
reason: '操作不被允许',
}),
} as any);
const result = await gitAddTool.execute({ all: true });
expect(result.success).toBe(false);
expect(result.error).toContain('权限被拒绝');
});
it('需要确认时返回提示', async () => {
vi.mocked(getPermissionManager).mockReturnValue({
checkGitPermission: vi.fn().mockResolvedValue({
allowed: false,
action: 'ask',
needsConfirmation: true,
}),
} as any);
const result = await gitAddTool.execute({ all: true });
expect(result.success).toBe(false);
expect(result.error).toContain('需要用户确认');
});
it('命令执行失败返回错误', async () => {
vi.mocked(getPermissionManager).mockReturnValue({
checkGitPermission: vi.fn().mockResolvedValue({
allowed: true,
action: 'allow',
}),
} as any);
mockExecAsyncResult = Object.assign(
new Error('Command failed'),
{ stdout: '', stderr: 'fatal: not a git repository', message: 'Command failed' }
);
const result = await gitAddTool.execute({ all: true });
expect(result.success).toBe(false);
expect(result.error).toContain('not a git repository');
});
});
});
+240
View File
@@ -0,0 +1,240 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
// Mock child_process
const mockExec = vi.fn();
vi.mock('child_process', () => ({
exec: (cmd: string, opts: any, cb?: Function) => {
if (typeof opts === 'function') {
cb = opts;
}
// 使用 setImmediate 模拟异步
setImmediate(() => {
const result = mockExec(cmd);
if (result.error) {
const err = result.error;
err.stdout = result.stdout || '';
err.stderr = result.stderr || '';
cb?.(err, result.stdout || '', result.stderr || '');
} else {
cb?.(null, result.stdout || '', result.stderr || '');
}
});
},
}));
// Mock permission manager
vi.mock('../../../../src/permission/index.js', () => ({
getPermissionManager: vi.fn(() => ({
checkGitPermission: vi.fn().mockResolvedValue({
allowed: true,
action: 'allow',
}),
})),
}));
// Mock loadDescription
vi.mock('../../../../src/tools/load_description.js', () => ({
loadDescription: vi.fn(() => '管理 Git 分支'),
}));
import { gitBranchTool } from '../../../../src/tools/git/git_branch.js';
import { getPermissionManager } from '../../../../src/permission/index.js';
describe('gitBranchTool - Git 分支工具', () => {
beforeEach(() => {
vi.clearAllMocks();
mockExec.mockReturnValue({ stdout: '* main\n develop', stderr: '' });
});
describe('工具定义', () => {
it('有正确的名称', () => {
expect(gitBranchTool.name).toBe('git_branch');
});
it('有正确的元数据', () => {
expect(gitBranchTool.metadata.category).toBe('git');
expect(gitBranchTool.metadata.keywords).toContain('branch');
expect(gitBranchTool.metadata.keywords).toContain('create');
expect(gitBranchTool.metadata.keywords).toContain('delete');
});
it('定义了可选参数', () => {
expect(gitBranchTool.parameters.action.required).toBe(false);
expect(gitBranchTool.parameters.name.required).toBe(false);
expect(gitBranchTool.parameters.new_name.required).toBe(false);
});
});
describe('execute - 执行', () => {
it('列出分支(默认操作)', async () => {
const result = await gitBranchTool.execute({});
expect(result.success).toBe(true);
// 源代码使用 -v 参数,输出可能包含 main 或在空结果时返回空字符串
expect(mockExec).toHaveBeenCalledWith(expect.stringContaining('git branch'));
expect(mockExec).toHaveBeenCalledWith(expect.stringContaining('-v'));
});
it('创建新分支', async () => {
mockExec.mockReturnValue({ stdout: '', stderr: '' });
const result = await gitBranchTool.execute({
action: 'create',
name: 'feature/new-branch',
});
expect(result.success).toBe(true);
expect(mockExec).toHaveBeenCalledWith(expect.stringContaining('git branch feature/new-branch'));
});
it('删除分支', async () => {
mockExec.mockReturnValue({ stdout: '', stderr: '' });
const result = await gitBranchTool.execute({
action: 'delete',
name: 'old-branch',
});
expect(result.success).toBe(true);
expect(mockExec).toHaveBeenCalledWith(expect.stringContaining('git branch -d old-branch'));
});
it('强制删除分支', async () => {
mockExec.mockReturnValue({ stdout: '', stderr: '' });
const result = await gitBranchTool.execute({
action: 'delete',
name: 'unmerged-branch',
force: true,
});
expect(result.success).toBe(true);
expect(mockExec).toHaveBeenCalledWith(expect.stringContaining('git branch -D unmerged-branch'));
});
it('重命名分支', async () => {
mockExec.mockReturnValue({ stdout: '', stderr: '' });
const result = await gitBranchTool.execute({
action: 'rename',
name: 'old-name',
new_name: 'new-name',
});
expect(result.success).toBe(true);
expect(mockExec).toHaveBeenCalledWith(expect.stringContaining('git branch -m old-name new-name'));
});
it('显示远程分支', async () => {
mockExec.mockReturnValue({ stdout: 'origin/main\norigin/develop', stderr: '' });
const result = await gitBranchTool.execute({
action: 'list',
remote: true,
});
expect(result.success).toBe(true);
expect(mockExec).toHaveBeenCalledWith(expect.stringContaining('git branch -r'));
});
it('显示所有分支', async () => {
const result = await gitBranchTool.execute({
action: 'list',
all: true,
});
expect(result.success).toBe(true);
expect(mockExec).toHaveBeenCalledWith(expect.stringContaining('git branch -a'));
});
it('创建分支缺少名称返回错误', async () => {
const result = await gitBranchTool.execute({
action: 'create',
});
expect(result.success).toBe(false);
expect(result.error).toContain('需要提供分支名称');
});
it('删除分支缺少名称返回错误', async () => {
const result = await gitBranchTool.execute({
action: 'delete',
});
expect(result.success).toBe(false);
expect(result.error).toContain('需要提供分支名称');
});
it('重命名缺少参数返回错误', async () => {
const result = await gitBranchTool.execute({
action: 'rename',
name: 'old-name',
});
expect(result.success).toBe(false);
expect(result.error).toContain('新名称');
});
it('未知操作返回错误', async () => {
const result = await gitBranchTool.execute({
action: 'unknown',
});
expect(result.success).toBe(false);
expect(result.error).toContain('未知操作');
});
it('权限被拒绝时返回错误', async () => {
vi.mocked(getPermissionManager).mockReturnValue({
checkGitPermission: vi.fn().mockResolvedValue({
allowed: false,
action: 'deny',
reason: '不允许操作分支',
}),
} as any);
const result = await gitBranchTool.execute({
action: 'create',
name: 'test',
});
expect(result.success).toBe(false);
expect(result.error).toContain('权限被拒绝');
});
it('需要确认时返回提示', async () => {
vi.mocked(getPermissionManager).mockReturnValue({
checkGitPermission: vi.fn().mockResolvedValue({
allowed: false,
action: 'ask',
needsConfirmation: true,
}),
} as any);
const result = await gitBranchTool.execute({
action: 'delete',
name: 'branch',
});
expect(result.success).toBe(false);
expect(result.error).toContain('需要用户确认');
});
it('Git 命令失败返回错误', async () => {
// 恢复权限检查
vi.mocked(getPermissionManager).mockReturnValue({
checkGitPermission: vi.fn().mockResolvedValue({ allowed: true }),
} as any);
mockExec.mockReturnValue({
error: new Error('Command failed'),
stdout: '',
stderr: 'fatal: not a git repository',
});
const result = await gitBranchTool.execute({});
expect(result.success).toBe(false);
expect(result.error).toContain('not a git repository');
});
});
});
+192
View File
@@ -0,0 +1,192 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
// Mock child_process
const mockExec = vi.fn();
vi.mock('child_process', () => ({
exec: (cmd: string, opts: any, cb?: Function) => {
if (typeof opts === 'function') {
cb = opts;
}
setImmediate(() => {
const result = mockExec(cmd);
if (result.error) {
const err = result.error;
err.stdout = result.stdout || '';
err.stderr = result.stderr || '';
cb?.(err, result.stdout || '', result.stderr || '');
} else {
cb?.(null, result.stdout || '', result.stderr || '');
}
});
},
}));
// Mock permission manager
vi.mock('../../../../src/permission/index.js', () => ({
getPermissionManager: vi.fn(() => ({
checkGitPermission: vi.fn().mockResolvedValue({
allowed: true,
action: 'allow',
}),
})),
}));
// Mock loadDescription
vi.mock('../../../../src/tools/load_description.js', () => ({
loadDescription: vi.fn(() => '切换分支或恢复文件'),
}));
import { gitCheckoutTool } from '../../../../src/tools/git/git_checkout.js';
import { getPermissionManager } from '../../../../src/permission/index.js';
describe('gitCheckoutTool - Git Checkout 工具', () => {
beforeEach(() => {
vi.clearAllMocks();
mockExec.mockReturnValue({ stdout: '', stderr: "Switched to branch 'develop'" });
});
describe('工具定义', () => {
it('有正确的名称', () => {
expect(gitCheckoutTool.name).toBe('git_checkout');
});
it('有正确的元数据', () => {
expect(gitCheckoutTool.metadata.category).toBe('git');
expect(gitCheckoutTool.metadata.keywords).toContain('checkout');
expect(gitCheckoutTool.metadata.keywords).toContain('switch');
});
it('定义了必需和可选参数', () => {
expect(gitCheckoutTool.parameters.target.required).toBe(true);
expect(gitCheckoutTool.parameters.create.required).toBe(false);
expect(gitCheckoutTool.parameters.force.required).toBe(false);
expect(gitCheckoutTool.parameters.file.required).toBe(false);
});
});
describe('execute - 执行', () => {
it('成功切换分支', async () => {
const result = await gitCheckoutTool.execute({
target: 'develop',
});
expect(result.success).toBe(true);
expect(result.output).toContain('develop');
expect(mockExec).toHaveBeenCalledWith(expect.stringContaining('git checkout develop'));
});
it('创建并切换到新分支', async () => {
mockExec.mockReturnValue({ stdout: '', stderr: "Switched to a new branch 'feature/test'" });
const result = await gitCheckoutTool.execute({
target: 'feature/test',
create: true,
});
expect(result.success).toBe(true);
expect(mockExec).toHaveBeenCalledWith(expect.stringContaining('git checkout -b feature/test'));
});
it('强制切换分支', async () => {
const result = await gitCheckoutTool.execute({
target: 'develop',
force: true,
});
expect(result.success).toBe(true);
expect(mockExec).toHaveBeenCalledWith(expect.stringContaining('git checkout -f develop'));
});
it('恢复文件', async () => {
mockExec.mockReturnValue({ stdout: '', stderr: '' });
const result = await gitCheckoutTool.execute({
target: 'src/index.ts',
file: true,
});
expect(result.success).toBe(true);
expect(mockExec).toHaveBeenCalledWith(expect.stringContaining('git checkout -- src/index.ts'));
});
it('缺少 target 返回错误', async () => {
const result = await gitCheckoutTool.execute({});
expect(result.success).toBe(false);
expect(result.error).toContain('请指定目标');
});
it('本地变更冲突时返回友好错误', async () => {
// 确保权限通过
vi.mocked(getPermissionManager).mockReturnValue({
checkGitPermission: vi.fn().mockResolvedValue({ allowed: true }),
} as any);
mockExec.mockReturnValue({
error: new Error('Command failed'),
stdout: '',
stderr: 'error: Your local changes would be overwritten by checkout',
});
const result = await gitCheckoutTool.execute({
target: 'main',
});
expect(result.success).toBe(false);
expect(result.error).toContain('本地有未提交的变更');
expect(result.error).toContain('force: true');
});
it('分支不存在时返回友好错误', async () => {
// 确保权限通过
vi.mocked(getPermissionManager).mockReturnValue({
checkGitPermission: vi.fn().mockResolvedValue({ allowed: true }),
} as any);
mockExec.mockReturnValue({
error: new Error('Command failed'),
stdout: '',
stderr: "error: pathspec 'nonexistent' did not match",
});
const result = await gitCheckoutTool.execute({
target: 'nonexistent',
});
expect(result.success).toBe(false);
expect(result.error).toContain('找不到分支或文件');
});
it('权限被拒绝时返回错误', async () => {
vi.mocked(getPermissionManager).mockReturnValue({
checkGitPermission: vi.fn().mockResolvedValue({
allowed: false,
action: 'deny',
reason: '不允许切换分支',
}),
} as any);
const result = await gitCheckoutTool.execute({
target: 'main',
});
expect(result.success).toBe(false);
expect(result.error).toContain('权限被拒绝');
});
it('需要确认时返回提示', async () => {
vi.mocked(getPermissionManager).mockReturnValue({
checkGitPermission: vi.fn().mockResolvedValue({
allowed: false,
action: 'ask',
needsConfirmation: true,
}),
} as any);
const result = await gitCheckoutTool.execute({
target: 'develop',
});
expect(result.success).toBe(false);
expect(result.error).toContain('需要用户确认');
});
});
});
+156
View File
@@ -0,0 +1,156 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
// 定义一个可控的 mock
let mockExecAsyncResult: { stdout: string; stderr: string } | Error = {
stdout: '[main abc1234] test commit\n1 file changed',
stderr: '',
};
// 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().mockResolvedValue({
allowed: true,
action: 'allow',
}),
})),
}));
// 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\n1 file changed',
stderr: '',
};
});
describe('工具定义', () => {
it('有正确的名称', () => {
expect(gitCommitTool.name).toBe('git_commit');
});
it('有正确的元数据', () => {
expect(gitCommitTool.metadata.category).toBe('git');
expect(gitCommitTool.metadata.keywords).toContain('commit');
});
it('定义了必需的 message 参数', () => {
expect(gitCommitTool.parameters.message).toBeDefined();
expect(gitCommitTool.parameters.message.required).toBe(true);
});
it('定义了可选参数', () => {
expect(gitCommitTool.parameters.amend).toBeDefined();
expect(gitCommitTool.parameters.amend.required).toBe(false);
expect(gitCommitTool.parameters.all).toBeDefined();
expect(gitCommitTool.parameters.all.required).toBe(false);
});
});
describe('execute - 执行', () => {
it('成功提交', async () => {
const result = await gitCommitTool.execute({ message: 'test commit' });
expect(result.success).toBe(true);
expect(result.output).toContain('test commit');
});
it('无消息且无 amend 返回错误', async () => {
const result = await gitCommitTool.execute({});
expect(result.success).toBe(false);
expect(result.error).toContain('提交信息是必填的');
});
it('权限被拒绝时返回错误', async () => {
vi.mocked(getPermissionManager).mockReturnValue({
checkGitPermission: vi.fn().mockResolvedValue({
allowed: false,
action: 'deny',
reason: '操作不被允许',
}),
} as any);
const result = await gitCommitTool.execute({ message: 'test' });
expect(result.success).toBe(false);
expect(result.error).toContain('权限被拒绝');
});
it('需要确认时返回提示', async () => {
vi.mocked(getPermissionManager).mockReturnValue({
checkGitPermission: vi.fn().mockResolvedValue({
allowed: false,
action: 'ask',
needsConfirmation: true,
}),
} as any);
const result = await gitCommitTool.execute({ message: 'test' });
expect(result.success).toBe(false);
expect(result.error).toContain('需要用户确认');
});
it('无变更可提交时返回友好提示', async () => {
vi.mocked(getPermissionManager).mockReturnValue({
checkGitPermission: vi.fn().mockResolvedValue({
allowed: true,
action: 'allow',
}),
} as any);
mockExecAsyncResult = Object.assign(
new Error('nothing to commit'),
{ stderr: 'nothing to commit, working tree clean', stdout: '', message: 'nothing to commit' }
);
const result = await gitCommitTool.execute({ message: 'test' });
expect(result.success).toBe(false);
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: 'test message' });
expect(mockCheck).toHaveBeenCalledWith(
expect.objectContaining({
operation: 'commit',
message: 'test message',
})
);
});
});
});
+172
View File
@@ -0,0 +1,172 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
// 定义一个可控的 mock
let mockExecAsyncResult: { stdout: string; stderr: string } | Error = {
stdout: '',
stderr: '',
};
// 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().mockResolvedValue({
allowed: true,
action: 'allow',
}),
})),
}));
// Mock loadDescription
vi.mock('../../../../src/tools/load_description.js', () => ({
loadDescription: vi.fn(() => 'Git diff 命令'),
}));
import { gitDiffTool } from '../../../../src/tools/git/git_diff.js';
import { getPermissionManager } from '../../../../src/permission/index.js';
describe('gitDiffTool - Git Diff 工具', () => {
beforeEach(() => {
vi.clearAllMocks();
mockExecAsyncResult = {
stdout: 'diff --git a/file.txt b/file.txt\n-old\n+new',
stderr: '',
};
});
describe('工具定义', () => {
it('有正确的名称', () => {
expect(gitDiffTool.name).toBe('git_diff');
});
it('有正确的元数据', () => {
expect(gitDiffTool.metadata.category).toBe('git');
expect(gitDiffTool.metadata.keywords).toContain('diff');
expect(gitDiffTool.metadata.keywords).toContain('compare');
});
it('定义了可选参数', () => {
expect(gitDiffTool.parameters.path.required).toBe(false);
expect(gitDiffTool.parameters.staged.required).toBe(false);
expect(gitDiffTool.parameters.commit.required).toBe(false);
expect(gitDiffTool.parameters.stat.required).toBe(false);
});
});
describe('execute - 执行', () => {
it('成功获取差异', async () => {
const result = await gitDiffTool.execute({});
expect(result.success).toBe(true);
expect(result.output).toContain('diff');
});
it('无差异时显示提示', async () => {
mockExecAsyncResult = { stdout: '', stderr: '' };
const result = await gitDiffTool.execute({});
expect(result.success).toBe(true);
expect(result.output).toContain('无差异');
});
it('显示暂存区差异', async () => {
mockExecAsyncResult = { stdout: 'staged changes', stderr: '' };
const result = await gitDiffTool.execute({ staged: true });
expect(result.success).toBe(true);
expect(result.output).toContain('staged changes');
});
it('与指定提交对比', async () => {
mockExecAsyncResult = { stdout: 'commit diff', stderr: '' };
const result = await gitDiffTool.execute({ commit: 'HEAD~1' });
expect(result.success).toBe(true);
});
it('指定文件路径', async () => {
mockExecAsyncResult = { stdout: 'file diff', stderr: '' };
const result = await gitDiffTool.execute({ path: 'src/file.ts' });
expect(result.success).toBe(true);
});
it('仅显示统计信息', async () => {
mockExecAsyncResult = {
stdout: ' file.txt | 2 +-\n 1 file changed, 1 insertion(+), 1 deletion(-)',
stderr: '',
};
const result = await gitDiffTool.execute({ stat: true });
expect(result.success).toBe(true);
expect(result.output).toContain('file changed');
});
it('权限被拒绝时返回错误', async () => {
vi.mocked(getPermissionManager).mockReturnValue({
checkGitPermission: vi.fn().mockResolvedValue({
allowed: false,
action: 'deny',
reason: '操作不被允许',
}),
} as any);
const result = await gitDiffTool.execute({});
expect(result.success).toBe(false);
expect(result.error).toContain('权限被拒绝');
});
it('需要确认时返回提示', async () => {
vi.mocked(getPermissionManager).mockReturnValue({
checkGitPermission: vi.fn().mockResolvedValue({
allowed: false,
action: 'ask',
needsConfirmation: true,
}),
} as any);
const result = await gitDiffTool.execute({});
expect(result.success).toBe(false);
expect(result.error).toContain('需要用户确认');
});
it('命令执行失败返回错误', async () => {
vi.mocked(getPermissionManager).mockReturnValue({
checkGitPermission: vi.fn().mockResolvedValue({
allowed: true,
action: 'allow',
}),
} as any);
mockExecAsyncResult = Object.assign(
new Error('Command failed'),
{ stdout: '', stderr: 'fatal: not a git repository', message: 'Command failed' }
);
const result = await gitDiffTool.execute({});
expect(result.success).toBe(false);
expect(result.error).toContain('not a git repository');
});
});
});
+173
View File
@@ -0,0 +1,173 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
// 定义一个可控的 mock
let mockExecAsyncResult: { stdout: string; stderr: string } | Error = {
stdout: 'abc123 Fix bug (John, 2 days ago)\ndef456 Add feature (Jane, 3 days ago)',
stderr: '',
};
// 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().mockResolvedValue({
allowed: true,
action: 'allow',
}),
})),
}));
// Mock loadDescription
vi.mock('../../../../src/tools/load_description.js', () => ({
loadDescription: vi.fn(() => '查看 Git 提交历史'),
}));
import { gitLogTool } from '../../../../src/tools/git/git_log.js';
import { getPermissionManager } from '../../../../src/permission/index.js';
describe('gitLogTool - Git Log 工具', () => {
beforeEach(() => {
vi.clearAllMocks();
mockExecAsyncResult = {
stdout: 'abc123 Fix bug (John, 2 days ago)\ndef456 Add feature (Jane, 3 days ago)',
stderr: '',
};
});
describe('工具定义', () => {
it('有正确的名称', () => {
expect(gitLogTool.name).toBe('git_log');
});
it('有正确的元数据', () => {
expect(gitLogTool.metadata.category).toBe('git');
expect(gitLogTool.metadata.keywords).toContain('log');
expect(gitLogTool.metadata.keywords).toContain('history');
expect(gitLogTool.metadata.keywords).toContain('commit');
});
it('所有参数都是可选的', () => {
expect(gitLogTool.parameters.limit.required).toBe(false);
expect(gitLogTool.parameters.oneline.required).toBe(false);
expect(gitLogTool.parameters.file.required).toBe(false);
expect(gitLogTool.parameters.author.required).toBe(false);
expect(gitLogTool.parameters.since.required).toBe(false);
expect(gitLogTool.parameters.graph.required).toBe(false);
});
});
describe('execute - 执行', () => {
it('成功获取提交历史', async () => {
const result = await gitLogTool.execute({});
expect(result.success).toBe(true);
expect(result.output).toContain('abc123');
expect(result.output).toContain('Fix bug');
});
it('使用自定义 limit', async () => {
const result = await gitLogTool.execute({ limit: 5 });
expect(result.success).toBe(true);
});
it('使用 oneline 格式', async () => {
const result = await gitLogTool.execute({ oneline: true });
expect(result.success).toBe(true);
});
it('显示分支图', async () => {
const result = await gitLogTool.execute({ graph: true });
expect(result.success).toBe(true);
});
it('按作者筛选', async () => {
const result = await gitLogTool.execute({ author: 'John' });
expect(result.success).toBe(true);
});
it('按日期筛选', async () => {
const result = await gitLogTool.execute({ since: '2024-01-01' });
expect(result.success).toBe(true);
});
it('查看指定文件的历史', async () => {
const result = await gitLogTool.execute({ file: 'src/index.ts' });
expect(result.success).toBe(true);
});
it('没有提交记录时返回提示', async () => {
mockExecAsyncResult = { stdout: '', stderr: '' };
const result = await gitLogTool.execute({});
expect(result.success).toBe(true);
expect(result.output).toContain('无提交记录');
});
it('权限被拒绝时返回错误', async () => {
vi.mocked(getPermissionManager).mockReturnValue({
checkGitPermission: vi.fn().mockResolvedValue({
allowed: false,
action: 'deny',
reason: '不允许查看历史',
}),
} as any);
const result = await gitLogTool.execute({});
expect(result.success).toBe(false);
expect(result.error).toContain('权限被拒绝');
});
it('需要确认时返回提示', async () => {
vi.mocked(getPermissionManager).mockReturnValue({
checkGitPermission: vi.fn().mockResolvedValue({
allowed: false,
action: 'ask',
needsConfirmation: true,
}),
} as any);
const result = await gitLogTool.execute({});
expect(result.success).toBe(false);
expect(result.error).toContain('需要用户确认');
});
it('Git 命令失败返回错误', async () => {
// 恢复权限检查
vi.mocked(getPermissionManager).mockReturnValue({
checkGitPermission: vi.fn().mockResolvedValue({ allowed: true }),
} as any);
mockExecAsyncResult = Object.assign(
new Error('Command failed'),
{ stdout: '', stderr: 'fatal: not a git repository' }
);
const result = await gitLogTool.execute({});
expect(result.success).toBe(false);
expect(result.error).toContain('not a git repository');
});
});
});
+163
View File
@@ -0,0 +1,163 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
// 定义一个可控的 mock
let mockExecAsyncResult: { stdout: string; stderr: string } | Error = {
stdout: 'Already up to date.',
stderr: '',
};
// 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().mockResolvedValue({
allowed: true,
action: 'allow',
}),
})),
}));
// Mock loadDescription
vi.mock('../../../../src/tools/load_description.js', () => ({
loadDescription: vi.fn(() => '从远程仓库拉取更新'),
}));
import { gitPullTool } from '../../../../src/tools/git/git_pull.js';
import { getPermissionManager } from '../../../../src/permission/index.js';
describe('gitPullTool - Git Pull 工具', () => {
beforeEach(() => {
vi.clearAllMocks();
mockExecAsyncResult = {
stdout: 'Already up to date.',
stderr: '',
};
});
describe('工具定义', () => {
it('有正确的名称', () => {
expect(gitPullTool.name).toBe('git_pull');
});
it('有正确的元数据', () => {
expect(gitPullTool.metadata.category).toBe('git');
expect(gitPullTool.metadata.keywords).toContain('pull');
expect(gitPullTool.metadata.keywords).toContain('fetch');
});
it('所有参数都是可选的', () => {
expect(gitPullTool.parameters.remote.required).toBe(false);
expect(gitPullTool.parameters.branch.required).toBe(false);
expect(gitPullTool.parameters.rebase.required).toBe(false);
});
});
describe('execute - 执行', () => {
it('成功拉取更新', async () => {
const result = await gitPullTool.execute({});
expect(result.success).toBe(true);
expect(result.output).toContain('Already up to date');
});
it('指定远程仓库和分支', async () => {
const result = await gitPullTool.execute({
remote: 'upstream',
branch: 'develop',
});
expect(result.success).toBe(true);
});
it('使用 rebase 模式', async () => {
const result = await gitPullTool.execute({ rebase: true });
expect(result.success).toBe(true);
});
it('合并冲突时返回友好错误', async () => {
mockExecAsyncResult = Object.assign(
new Error('Command failed'),
{ stdout: '', stderr: 'CONFLICT (content): Merge conflict in file.txt' }
);
const result = await gitPullTool.execute({});
expect(result.success).toBe(false);
expect(result.error).toContain('合并冲突');
expect(result.error).toContain('手动解决');
});
it('本地变更时返回友好错误', async () => {
mockExecAsyncResult = Object.assign(
new Error('Command failed'),
{ stdout: '', stderr: 'error: Your local changes would be overwritten by merge' }
);
const result = await gitPullTool.execute({});
expect(result.success).toBe(false);
expect(result.error).toContain('未提交的变更');
});
it('权限被拒绝时返回错误', async () => {
vi.mocked(getPermissionManager).mockReturnValue({
checkGitPermission: vi.fn().mockResolvedValue({
allowed: false,
action: 'deny',
reason: '不允许拉取',
}),
} as any);
const result = await gitPullTool.execute({});
expect(result.success).toBe(false);
expect(result.error).toContain('权限被拒绝');
});
it('需要确认时返回提示', async () => {
vi.mocked(getPermissionManager).mockReturnValue({
checkGitPermission: vi.fn().mockResolvedValue({
allowed: false,
action: 'ask',
needsConfirmation: true,
}),
} as any);
const result = await gitPullTool.execute({});
expect(result.success).toBe(false);
expect(result.error).toContain('需要用户确认');
});
it('Git 命令失败返回错误', async () => {
// 恢复权限检查(因为之前的测试可能修改了 mock)
vi.mocked(getPermissionManager).mockReturnValue({
checkGitPermission: vi.fn().mockResolvedValue({ allowed: true }),
} as any);
mockExecAsyncResult = Object.assign(
new Error('Command failed'),
{ stdout: '', stderr: 'fatal: not a git repository' }
);
const result = await gitPullTool.execute({});
expect(result.success).toBe(false);
expect(result.error).toContain('not a git repository');
});
});
});
+195
View File
@@ -0,0 +1,195 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
// Mock child_process
const mockExec = vi.fn();
vi.mock('child_process', () => ({
exec: (cmd: string, opts: any, cb?: Function) => {
if (typeof opts === 'function') {
cb = opts;
}
setImmediate(() => {
const result = mockExec(cmd);
if (result.error) {
const err = result.error;
err.stdout = result.stdout || '';
err.stderr = result.stderr || '';
cb?.(err, result.stdout || '', result.stderr || '');
} else {
cb?.(null, result.stdout || '', result.stderr || '');
}
});
},
}));
// Mock permission manager
vi.mock('../../../../src/permission/index.js', () => ({
getPermissionManager: vi.fn(() => ({
checkGitPermission: vi.fn().mockResolvedValue({
allowed: true,
action: 'allow',
}),
})),
}));
// Mock loadDescription
vi.mock('../../../../src/tools/load_description.js', () => ({
loadDescription: vi.fn(() => '推送 Git 变更到远程仓库'),
}));
import { gitPushTool } from '../../../../src/tools/git/git_push.js';
import { getPermissionManager } from '../../../../src/permission/index.js';
describe('gitPushTool - Git Push 工具', () => {
beforeEach(() => {
vi.clearAllMocks();
mockExec.mockReturnValue({
stdout: '',
stderr: 'Everything up-to-date',
});
});
describe('工具定义', () => {
it('有正确的名称', () => {
expect(gitPushTool.name).toBe('git_push');
});
it('有正确的元数据', () => {
expect(gitPushTool.metadata.category).toBe('git');
expect(gitPushTool.metadata.keywords).toContain('push');
expect(gitPushTool.metadata.keywords).toContain('upload');
});
it('所有参数都是可选的', () => {
expect(gitPushTool.parameters.remote.required).toBe(false);
expect(gitPushTool.parameters.branch.required).toBe(false);
expect(gitPushTool.parameters.force.required).toBe(false);
expect(gitPushTool.parameters.set_upstream.required).toBe(false);
expect(gitPushTool.parameters.tags.required).toBe(false);
});
});
describe('execute - 执行', () => {
it('成功推送', async () => {
const result = await gitPushTool.execute({});
expect(result.success).toBe(true);
// 源代码: stdout || stderr || '推送成功'
expect(result.output).toContain('推送成功');
expect(mockExec).toHaveBeenCalledWith(expect.stringContaining('git push origin'));
});
it('指定远程仓库和分支', async () => {
const result = await gitPushTool.execute({
remote: 'upstream',
branch: 'develop',
});
expect(result.success).toBe(true);
expect(mockExec).toHaveBeenCalledWith(expect.stringContaining('git push upstream develop'));
});
it('设置上游分支', async () => {
const result = await gitPushTool.execute({ set_upstream: true });
expect(result.success).toBe(true);
expect(mockExec).toHaveBeenCalledWith(expect.stringContaining('git push -u'));
});
it('强制推送', async () => {
const result = await gitPushTool.execute({ force: true });
expect(result.success).toBe(true);
expect(mockExec).toHaveBeenCalledWith(expect.stringContaining('git push --force'));
});
it('推送标签', async () => {
const result = await gitPushTool.execute({ tags: true });
expect(result.success).toBe(true);
expect(mockExec).toHaveBeenCalledWith(expect.stringContaining('git push --tags'));
});
it('推送被拒绝时返回友好错误', async () => {
// 确保权限通过
vi.mocked(getPermissionManager).mockReturnValue({
checkGitPermission: vi.fn().mockResolvedValue({ allowed: true }),
} as any);
mockExec.mockReturnValue({
error: new Error('Command failed'),
stdout: '',
stderr: '! [rejected] main -> main (fetch first)',
});
const result = await gitPushTool.execute({});
expect(result.success).toBe(false);
expect(result.error).toContain('推送被拒绝');
expect(result.error).toContain('git_pull');
});
it('没有上游分支时返回友好错误', async () => {
// 确保权限通过
vi.mocked(getPermissionManager).mockReturnValue({
checkGitPermission: vi.fn().mockResolvedValue({ allowed: true }),
} as any);
mockExec.mockReturnValue({
error: new Error('Command failed'),
stdout: '',
stderr: 'fatal: The current branch has no upstream branch',
});
const result = await gitPushTool.execute({});
expect(result.success).toBe(false);
expect(result.error).toContain('没有设置上游分支');
expect(result.error).toContain('set_upstream: true');
});
it('权限被拒绝时返回错误', async () => {
vi.mocked(getPermissionManager).mockReturnValue({
checkGitPermission: vi.fn().mockResolvedValue({
allowed: false,
action: 'deny',
reason: '不允许推送',
}),
} as any);
const result = await gitPushTool.execute({});
expect(result.success).toBe(false);
expect(result.error).toContain('权限被拒绝');
});
it('需要确认时返回提示', async () => {
vi.mocked(getPermissionManager).mockReturnValue({
checkGitPermission: vi.fn().mockResolvedValue({
allowed: false,
action: 'ask',
needsConfirmation: true,
}),
} as any);
const result = await gitPushTool.execute({});
expect(result.success).toBe(false);
expect(result.error).toContain('需要用户确认');
});
it('Git 命令失败返回错误', async () => {
// 恢复权限检查
vi.mocked(getPermissionManager).mockReturnValue({
checkGitPermission: vi.fn().mockResolvedValue({ allowed: true }),
} as any);
mockExec.mockReturnValue({
error: new Error('Command failed'),
stdout: '',
stderr: 'fatal: not a git repository',
});
const result = await gitPushTool.execute({});
expect(result.success).toBe(false);
expect(result.error).toContain('not a git repository');
});
});
});
+224
View File
@@ -0,0 +1,224 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
// Mock child_process
const mockExec = vi.fn();
vi.mock('child_process', () => ({
exec: (cmd: string, opts: any, cb?: Function) => {
if (typeof opts === 'function') {
cb = opts;
}
setImmediate(() => {
const result = mockExec(cmd);
if (result.error) {
const err = result.error;
err.stdout = result.stdout || '';
err.stderr = result.stderr || '';
cb?.(err, result.stdout || '', result.stderr || '');
} else {
cb?.(null, result.stdout || '', result.stderr || '');
}
});
},
}));
// Mock permission manager
vi.mock('../../../../src/permission/index.js', () => ({
getPermissionManager: vi.fn(() => ({
checkGitPermission: vi.fn().mockResolvedValue({
allowed: true,
action: 'allow',
}),
})),
}));
// Mock loadDescription
vi.mock('../../../../src/tools/load_description.js', () => ({
loadDescription: vi.fn(() => '暂存工作区变更'),
}));
import { gitStashTool } from '../../../../src/tools/git/git_stash.js';
import { getPermissionManager } from '../../../../src/permission/index.js';
describe('gitStashTool - Git Stash 工具', () => {
beforeEach(() => {
vi.clearAllMocks();
mockExec.mockReturnValue({ stdout: 'Saved working directory', stderr: '' });
});
describe('工具定义', () => {
it('有正确的名称', () => {
expect(gitStashTool.name).toBe('git_stash');
});
it('有正确的元数据', () => {
expect(gitStashTool.metadata.category).toBe('git');
expect(gitStashTool.metadata.keywords).toContain('stash');
expect(gitStashTool.metadata.keywords).toContain('save');
});
it('所有参数都是可选的', () => {
expect(gitStashTool.parameters.action.required).toBe(false);
expect(gitStashTool.parameters.message.required).toBe(false);
expect(gitStashTool.parameters.index.required).toBe(false);
expect(gitStashTool.parameters.include_untracked.required).toBe(false);
});
});
describe('execute - 执行', () => {
it('默认 push 操作暂存变更', async () => {
const result = await gitStashTool.execute({});
expect(result.success).toBe(true);
expect(mockExec).toHaveBeenCalledWith(expect.stringContaining('git stash push'));
});
it('带消息暂存', async () => {
const result = await gitStashTool.execute({
message: 'WIP: feature',
});
expect(result.success).toBe(true);
expect(mockExec).toHaveBeenCalledWith(expect.stringContaining('-m'));
});
it('包含未跟踪文件', async () => {
const result = await gitStashTool.execute({
include_untracked: true,
});
expect(result.success).toBe(true);
expect(mockExec).toHaveBeenCalledWith(expect.stringContaining('-u'));
});
it('列出暂存', async () => {
mockExec.mockReturnValue({
stdout: 'stash@{0}: WIP on main\nstash@{1}: feature',
stderr: '',
});
const result = await gitStashTool.execute({ action: 'list' });
expect(result.success).toBe(true);
expect(mockExec).toHaveBeenCalledWith(expect.stringContaining('git stash list'));
});
it('恢复暂存 (pop)', async () => {
mockExec.mockReturnValue({ stdout: '', stderr: '' });
const result = await gitStashTool.execute({ action: 'pop' });
expect(result.success).toBe(true);
expect(result.output).toContain('暂存已恢复并删除');
expect(mockExec).toHaveBeenCalledWith(expect.stringContaining('git stash pop'));
});
it('应用暂存 (apply)', async () => {
mockExec.mockReturnValue({ stdout: '', stderr: '' });
const result = await gitStashTool.execute({ action: 'apply' });
expect(result.success).toBe(true);
expect(result.output).toContain('暂存已恢复');
expect(mockExec).toHaveBeenCalledWith(expect.stringContaining('git stash apply'));
});
it('删除暂存 (drop)', async () => {
mockExec.mockReturnValue({ stdout: '', stderr: '' });
const result = await gitStashTool.execute({ action: 'drop', index: 1 });
expect(result.success).toBe(true);
expect(mockExec).toHaveBeenCalledWith(expect.stringContaining('git stash drop stash@{1}'));
});
it('清除所有暂存', async () => {
mockExec.mockReturnValue({ stdout: '', stderr: '' });
const result = await gitStashTool.execute({ action: 'clear' });
expect(result.success).toBe(true);
expect(result.output).toContain('所有暂存已清除');
expect(mockExec).toHaveBeenCalledWith(expect.stringContaining('git stash clear'));
});
it('显示暂存内容', async () => {
mockExec.mockReturnValue({ stdout: 'diff --git a/file.ts', stderr: '' });
const result = await gitStashTool.execute({ action: 'show' });
expect(result.success).toBe(true);
expect(mockExec).toHaveBeenCalledWith(expect.stringContaining('git stash show -p'));
});
it('未知操作返回错误', async () => {
const result = await gitStashTool.execute({ action: 'unknown' });
expect(result.success).toBe(false);
expect(result.error).toContain('未知操作');
});
it('没有变更时返回友好错误', async () => {
// 确保权限通过
vi.mocked(getPermissionManager).mockReturnValue({
checkGitPermission: vi.fn().mockResolvedValue({ allowed: true }),
} as any);
mockExec.mockReturnValue({
error: new Error('Command failed'),
stdout: '',
stderr: 'No local changes to save',
});
const result = await gitStashTool.execute({});
expect(result.success).toBe(false);
expect(result.error).toContain('没有需要暂存的变更');
});
it('恢复冲突时返回友好错误', async () => {
// 确保权限通过
vi.mocked(getPermissionManager).mockReturnValue({
checkGitPermission: vi.fn().mockResolvedValue({ allowed: true }),
} as any);
mockExec.mockReturnValue({
error: new Error('Command failed'),
stdout: '',
stderr: 'CONFLICT (content): Merge conflict',
});
const result = await gitStashTool.execute({ action: 'pop' });
expect(result.success).toBe(false);
expect(result.error).toContain('冲突');
});
it('权限被拒绝时返回错误', async () => {
vi.mocked(getPermissionManager).mockReturnValue({
checkGitPermission: vi.fn().mockResolvedValue({
allowed: false,
action: 'deny',
reason: '不允许暂存操作',
}),
} as any);
const result = await gitStashTool.execute({});
expect(result.success).toBe(false);
expect(result.error).toContain('权限被拒绝');
});
it('需要确认时返回提示', async () => {
vi.mocked(getPermissionManager).mockReturnValue({
checkGitPermission: vi.fn().mockResolvedValue({
allowed: false,
action: 'ask',
needsConfirmation: true,
}),
} as any);
const result = await gitStashTool.execute({});
expect(result.success).toBe(false);
expect(result.error).toContain('需要用户确认');
});
});
});
+139
View File
@@ -0,0 +1,139 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
// 定义一个可控的 mock
let mockExecAsyncResult: { stdout: string; stderr: string } | Error = {
stdout: 'On branch main\nnothing to commit',
stderr: '',
};
// 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().mockResolvedValue({
allowed: true,
action: 'allow',
}),
})),
}));
// Mock loadDescription
vi.mock('../../../../src/tools/load_description.js', () => ({
loadDescription: vi.fn(() => '查看 Git 仓库状态'),
}));
import { gitStatusTool } from '../../../../src/tools/git/git_status.js';
import { getPermissionManager } from '../../../../src/permission/index.js';
describe('gitStatusTool - Git 状态工具', () => {
beforeEach(() => {
vi.clearAllMocks();
mockExecAsyncResult = {
stdout: 'On branch main\nnothing to commit',
stderr: '',
};
});
describe('工具定义', () => {
it('有正确的名称', () => {
expect(gitStatusTool.name).toBe('git_status');
});
it('有正确的元数据', () => {
expect(gitStatusTool.metadata.category).toBe('git');
expect(gitStatusTool.metadata.keywords).toContain('git');
expect(gitStatusTool.metadata.keywords).toContain('status');
});
it('定义了可选参数', () => {
expect(gitStatusTool.parameters.short).toBeDefined();
expect(gitStatusTool.parameters.short.required).toBe(false);
expect(gitStatusTool.parameters.branch).toBeDefined();
expect(gitStatusTool.parameters.branch.required).toBe(false);
});
});
describe('execute - 执行', () => {
it('成功获取状态', async () => {
const result = await gitStatusTool.execute({});
expect(result.success).toBe(true);
expect(result.output).toContain('On branch main');
});
it('权限被拒绝时返回错误', async () => {
vi.mocked(getPermissionManager).mockReturnValue({
checkGitPermission: vi.fn().mockResolvedValue({
allowed: false,
action: 'deny',
reason: '操作不被允许',
}),
} as any);
const result = await gitStatusTool.execute({});
expect(result.success).toBe(false);
expect(result.error).toContain('权限被拒绝');
});
it('需要确认时返回提示', async () => {
vi.mocked(getPermissionManager).mockReturnValue({
checkGitPermission: vi.fn().mockResolvedValue({
allowed: false,
action: 'ask',
needsConfirmation: true,
}),
} as any);
const result = await gitStatusTool.execute({});
expect(result.success).toBe(false);
expect(result.error).toContain('需要用户确认');
});
it('非 git 仓库返回错误', async () => {
vi.mocked(getPermissionManager).mockReturnValue({
checkGitPermission: vi.fn().mockResolvedValue({
allowed: true,
action: 'allow',
}),
} as any);
mockExecAsyncResult = Object.assign(
new Error('Command failed'),
{ stderr: 'fatal: not a git repository', stdout: '' }
);
const result = await gitStatusTool.execute({});
expect(result.success).toBe(false);
expect(result.error).toContain('not a git repository');
});
it('包含 stderr 输出', async () => {
mockExecAsyncResult = {
stdout: 'main output',
stderr: 'warning message',
};
const result = await gitStatusTool.execute({});
expect(result.success).toBe(true);
expect(result.output).toContain('main output');
expect(result.output).toContain('warning message');
});
});
});
+360
View File
@@ -0,0 +1,360 @@
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);
}
});
});
+278
View File
@@ -0,0 +1,278 @@
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);
}
});
});
});
+183
View File
@@ -0,0 +1,183 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
// 定义一个可控的 mock
let mockExecAsyncResult: { stdout: string; stderr: string } | Error = {
stdout: 'command output',
stderr: '',
};
// 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(() => ({
checkBashPermission: vi.fn().mockResolvedValue({
allowed: true,
action: 'allow',
}),
})),
}));
// Mock loadDescription
vi.mock('../../../../src/tools/load_description.js', () => ({
loadDescription: vi.fn(() => '执行 shell 命令'),
}));
import { bashTool } from '../../../../src/tools/shell/bash.js';
import { getPermissionManager } from '../../../../src/permission/index.js';
describe('bashTool - Bash 命令工具', () => {
beforeEach(() => {
vi.clearAllMocks();
mockExecAsyncResult = {
stdout: 'command output',
stderr: '',
};
});
describe('工具定义', () => {
it('有正确的名称', () => {
expect(bashTool.name).toBe('bash');
});
it('有正确的元数据', () => {
expect(bashTool.metadata.category).toBe('shell');
expect(bashTool.metadata.keywords).toContain('bash');
expect(bashTool.metadata.keywords).toContain('command');
expect(bashTool.metadata.keywords).toContain('terminal');
});
it('定义了必需的 command 参数', () => {
expect(bashTool.parameters.command).toBeDefined();
expect(bashTool.parameters.command.required).toBe(true);
});
it('定义了可选的 cwd 参数', () => {
expect(bashTool.parameters.cwd).toBeDefined();
expect(bashTool.parameters.cwd.required).toBe(false);
});
});
describe('execute - 执行', () => {
it('成功执行命令', async () => {
const result = await bashTool.execute({ command: 'ls -la' });
expect(result.success).toBe(true);
expect(result.output).toContain('command output');
});
it('包含 stderr 输出', async () => {
mockExecAsyncResult = {
stdout: 'output',
stderr: 'warning message',
};
const result = await bashTool.execute({ command: 'some_cmd' });
expect(result.success).toBe(true);
expect(result.output).toContain('output');
expect(result.output).toContain('STDERR');
expect(result.output).toContain('warning message');
});
it('权限被拒绝时返回错误', async () => {
vi.mocked(getPermissionManager).mockReturnValue({
checkBashPermission: vi.fn().mockResolvedValue({
allowed: false,
action: 'deny',
reason: '命令不被允许执行',
}),
} as any);
const result = await bashTool.execute({ command: 'rm -rf /' });
expect(result.success).toBe(false);
expect(result.error).toContain('权限被拒绝');
});
it('需要确认时返回提示', async () => {
vi.mocked(getPermissionManager).mockReturnValue({
checkBashPermission: vi.fn().mockResolvedValue({
allowed: false,
action: 'ask',
needsConfirmation: true,
reason: '需要确认',
patterns: ['rm *'],
}),
} as any);
const result = await bashTool.execute({ command: 'rm file.txt' });
expect(result.success).toBe(false);
expect(result.error).toContain('需要用户确认');
expect(result.error).toContain('rm file.txt');
});
it('命令执行失败时返回错误', async () => {
vi.mocked(getPermissionManager).mockReturnValue({
checkBashPermission: vi.fn().mockResolvedValue({
allowed: true,
action: 'allow',
}),
} as any);
mockExecAsyncResult = Object.assign(
new Error('Command failed'),
{ stdout: '', stderr: 'command not found', message: 'Command failed' }
);
const result = await bashTool.execute({ command: 'nonexistent_cmd' });
expect(result.success).toBe(false);
expect(result.error).toContain('command not found');
});
it('保留失败命令的 stdout', async () => {
vi.mocked(getPermissionManager).mockReturnValue({
checkBashPermission: vi.fn().mockResolvedValue({
allowed: true,
action: 'allow',
}),
} as any);
mockExecAsyncResult = Object.assign(
new Error('Command failed'),
{ stdout: 'partial output', stderr: 'error occurred', message: 'Command failed' }
);
const result = await bashTool.execute({ command: 'failing_cmd' });
expect(result.success).toBe(false);
expect(result.output).toBe('partial output');
expect(result.error).toContain('error occurred');
});
it('传递正确的参数给权限检查', async () => {
const mockCheck = vi.fn().mockResolvedValue({
allowed: true,
action: 'allow',
});
vi.mocked(getPermissionManager).mockReturnValue({
checkBashPermission: mockCheck,
} as any);
await bashTool.execute({ command: 'ls -la', cwd: '/home/user' });
expect(mockCheck).toHaveBeenCalledWith({
command: 'ls -la',
workdir: '/home/user',
});
});
});
});
+307
View File
@@ -0,0 +1,307 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
// 使用可变的引用对象来绕过 hoisting 问题
const mockState = {
execute: vi.fn(),
};
// Mock agent registry 和 AgentExecutor
vi.mock('../../../../src/agent/index.js', () => {
// 在 mock 工厂内部定义类
return {
agentRegistry: {
listSubagents: vi.fn(() => [
{ name: 'explore', description: '代码探索', mode: 'subagent' },
{ name: 'code-reviewer', description: '代码审查', mode: 'subagent' },
]),
get: vi.fn(),
},
AgentExecutor: class {
execute(...args: any[]) {
return mockState.execute(...args);
}
},
};
});
// Mock tool registry
vi.mock('../../../../src/tools/registry.js', () => ({
toolRegistry: {},
}));
// Mock session manager
vi.mock('../../../../src/session/index.js', () => ({
SessionManager: vi.fn(),
}));
import { taskTool, initTaskContext, updateTaskDescription } from '../../../../src/tools/task/task.js';
import { agentRegistry } from '../../../../src/agent/index.js';
describe('taskTool - Task 工具', () => {
beforeEach(() => {
vi.clearAllMocks();
// 重置上下文为 null
initTaskContext(null as any, null as any);
// 重置 mock
mockState.execute.mockResolvedValue({
success: true,
text: '任务完成',
steps: 3,
});
});
describe('工具定义', () => {
it('有正确的名称', () => {
expect(taskTool.name).toBe('task');
});
it('有正确的元数据', () => {
expect(taskTool.metadata.category).toBe('agent');
expect(taskTool.metadata.keywords).toContain('task');
expect(taskTool.metadata.keywords).toContain('subagent');
});
it('定义了必需的参数', () => {
expect(taskTool.parameters.description.required).toBe(true);
expect(taskTool.parameters.prompt.required).toBe(true);
expect(taskTool.parameters.subagent_type.required).toBe(true);
});
});
describe('initTaskContext - 初始化上下文', () => {
it('设置上下文不报错', () => {
const mockConfig = { model: 'test' };
const mockSession = {
getSessionId: vi.fn(() => 'session-id'),
createChildSession: vi.fn(() => ({ id: 'child', messages: [] })),
saveChildSession: vi.fn(),
};
expect(() => initTaskContext(mockConfig as any, mockSession as any)).not.toThrow();
});
});
describe('updateTaskDescription - 更新描述', () => {
it('更新工具描述', () => {
vi.mocked(agentRegistry.listSubagents).mockReturnValue([
{ name: 'explore', description: '代码探索', mode: 'subagent' },
{ name: 'code-reviewer', description: '代码审查', mode: 'subagent' },
]);
updateTaskDescription();
expect(taskTool.description).toContain('explore');
expect(taskTool.description).toContain('code-reviewer');
});
it('无子 Agent 时显示提示', () => {
vi.mocked(agentRegistry.listSubagents).mockReturnValue([]);
updateTaskDescription();
expect(taskTool.description).toContain('没有可用');
});
});
describe('execute - 执行', () => {
it('未初始化上下文时返回错误', async () => {
// 确保上下文为 null
initTaskContext(null as any, null as any);
vi.mocked(agentRegistry.get).mockReturnValue(undefined);
const result = await taskTool.execute({
description: 'test task',
prompt: 'do something',
subagent_type: 'explore',
});
expect(result.success).toBe(false);
// 可能是未初始化或未找到 Agent
expect(result.error).toBeDefined();
});
it('成功执行子任务', async () => {
const mockSession = {
getSessionId: vi.fn(() => 'parent-session'),
createChildSession: vi.fn(() => ({
id: 'child-session',
messages: [],
})),
saveChildSession: vi.fn(),
};
initTaskContext({ model: 'test' } as any, mockSession as any);
vi.mocked(agentRegistry.get).mockReturnValue({
name: 'explore',
description: '探索 Agent',
mode: 'subagent',
prompt: '你是探索助手',
});
const result = await taskTool.execute({
description: 'search code',
prompt: 'find all API routes',
subagent_type: 'explore',
});
expect(result.success).toBe(true);
expect(result.output).toContain('任务完成');
});
it('未找到 Agent 时返回错误', async () => {
const mockSession = {
getSessionId: vi.fn(() => 'parent-session'),
createChildSession: vi.fn(() => ({ id: 'child', messages: [] })),
saveChildSession: vi.fn(),
};
initTaskContext({ model: 'test' } as any, mockSession as any);
vi.mocked(agentRegistry.get).mockReturnValue(undefined);
vi.mocked(agentRegistry.listSubagents).mockReturnValue([
{ name: 'explore', description: '探索', mode: 'subagent' },
]);
const result = await taskTool.execute({
description: 'test',
prompt: 'test',
subagent_type: 'nonexistent',
});
expect(result.success).toBe(false);
expect(result.error).toContain('未找到 Agent');
});
it('primary 模式 Agent 不能作为子任务', async () => {
const mockSession = {
getSessionId: vi.fn(() => 'parent-session'),
createChildSession: vi.fn(() => ({ id: 'child', messages: [] })),
saveChildSession: vi.fn(),
};
initTaskContext({ model: 'test' } as any, mockSession as any);
vi.mocked(agentRegistry.get).mockReturnValue({
name: 'primary-agent',
description: '主 Agent',
mode: 'primary',
prompt: '你是主助手',
});
const result = await taskTool.execute({
description: 'test',
prompt: 'test',
subagent_type: 'primary-agent',
});
expect(result.success).toBe(false);
expect(result.error).toContain('primary 模式');
});
it('子任务失败时返回错误', async () => {
const mockSession = {
getSessionId: vi.fn(() => 'parent-session'),
createChildSession: vi.fn(() => ({ id: 'child', messages: [] })),
saveChildSession: vi.fn(),
};
initTaskContext({ model: 'test' } as any, mockSession as any);
vi.mocked(agentRegistry.get).mockReturnValue({
name: 'explore',
description: '探索 Agent',
mode: 'subagent',
prompt: '你是探索助手',
});
mockState.execute.mockResolvedValue({
success: false,
text: '',
error: '执行失败',
steps: 1,
});
const result = await taskTool.execute({
description: 'test',
prompt: 'test',
subagent_type: 'explore',
});
expect(result.success).toBe(false);
expect(result.error).toContain('执行失败');
});
it('返回元数据', async () => {
const mockSession = {
getSessionId: vi.fn(() => 'parent-session'),
createChildSession: vi.fn(() => ({
id: 'child-session-123',
messages: [],
})),
saveChildSession: vi.fn(),
};
initTaskContext({ model: 'test' } as any, mockSession as any);
vi.mocked(agentRegistry.get).mockReturnValue({
name: 'explore',
description: '探索 Agent',
mode: 'subagent',
prompt: '你是探索助手',
});
mockState.execute.mockResolvedValue({
success: true,
text: '完成',
steps: 5,
});
const result = await taskTool.execute({
description: 'test',
prompt: 'test',
subagent_type: 'explore',
});
expect(result.metadata).toBeDefined();
expect(result.metadata?.agent).toBe('explore');
expect(result.metadata?.sessionId).toBe('child-session-123');
expect(result.metadata?.steps).toBe(5);
});
it('保存子会话', async () => {
const saveChildSession = vi.fn();
const mockSession = {
getSessionId: vi.fn(() => 'parent-session'),
createChildSession: vi.fn(() => ({
id: 'child-session',
messages: [],
})),
saveChildSession,
};
initTaskContext({ model: 'test' } as any, mockSession as any);
vi.mocked(agentRegistry.get).mockReturnValue({
name: 'explore',
description: '探索 Agent',
mode: 'subagent',
prompt: '你是探索助手',
});
mockState.execute.mockResolvedValue({
success: true,
text: '任务结果',
steps: 2,
});
await taskTool.execute({
description: 'test',
prompt: 'test prompt',
subagent_type: 'explore',
});
expect(saveChildSession).toHaveBeenCalled();
});
});
});
+333
View File
@@ -0,0 +1,333 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
import type { Todo, TodoStatus } from '../../../src/session/types.js';
import type { SessionManager } from '../../../src/session/index.js';
// 需要单独导入 todoManager 因为它是单例
// 每个测试创建新的实例来避免状态污染
// 简单的 TodoManager 测试类(复制逻辑以测试)
class TestTodoManager {
private sessionManager: SessionManager | null = null;
setSessionManager(manager: SessionManager): void {
this.sessionManager = manager;
}
getTodos(): Todo[] {
if (!this.sessionManager) {
return [];
}
return this.sessionManager.getTodos();
}
async setTodos(todos: Todo[]): Promise<void> {
if (!this.sessionManager) {
return;
}
await this.sessionManager.setTodos(todos);
}
async addTodo(content: string, status: TodoStatus = 'pending'): Promise<Todo> {
const todos = this.getTodos();
const now = new Date().toISOString();
const newTodo: Todo = {
id: this.generateId(),
content,
status,
createdAt: now,
updatedAt: now,
};
todos.push(newTodo);
await this.setTodos(todos);
return newTodo;
}
async updateTodoStatus(id: string, status: TodoStatus): Promise<boolean> {
const todos = this.getTodos();
const todo = todos.find((t) => t.id === id);
if (!todo) return false;
todo.status = status;
todo.updatedAt = new Date().toISOString();
await this.setTodos(todos);
return true;
}
async deleteTodo(id: string): Promise<boolean> {
const todos = this.getTodos();
const index = todos.findIndex((t) => t.id === id);
if (index === -1) return false;
todos.splice(index, 1);
await this.setTodos(todos);
return true;
}
async clearTodos(): Promise<void> {
await this.setTodos([]);
}
private generateId(): string {
return Math.random().toString(36).substring(2, 10);
}
isInitialized(): boolean {
return this.sessionManager !== null;
}
}
// Mock SessionManager
function createMockSessionManager(): SessionManager {
let todos: Todo[] = [];
return {
getTodos: vi.fn(() => [...todos]),
setTodos: vi.fn(async (newTodos: Todo[]) => {
todos = [...newTodos];
}),
} as unknown as SessionManager;
}
describe('TodoManager - Todo 管理器', () => {
let todoManager: TestTodoManager;
let mockSessionManager: SessionManager;
beforeEach(() => {
todoManager = new TestTodoManager();
mockSessionManager = createMockSessionManager();
});
describe('初始化状态', () => {
it('未设置 sessionManager 时 isInitialized 返回 false', () => {
expect(todoManager.isInitialized()).toBe(false);
});
it('设置 sessionManager 后 isInitialized 返回 true', () => {
todoManager.setSessionManager(mockSessionManager);
expect(todoManager.isInitialized()).toBe(true);
});
it('未初始化时 getTodos 返回空数组', () => {
expect(todoManager.getTodos()).toEqual([]);
});
it('未初始化时 setTodos 不报错', async () => {
await expect(todoManager.setTodos([{
id: '1',
content: 'test',
status: 'pending',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
}])).resolves.not.toThrow();
});
});
describe('getTodos', () => {
beforeEach(() => {
todoManager.setSessionManager(mockSessionManager);
});
it('返回空数组当没有 todos', () => {
const todos = todoManager.getTodos();
expect(todos).toEqual([]);
});
it('返回 sessionManager 中的 todos', async () => {
await todoManager.addTodo('Task 1');
await todoManager.addTodo('Task 2');
const todos = todoManager.getTodos();
expect(todos).toHaveLength(2);
});
});
describe('setTodos', () => {
beforeEach(() => {
todoManager.setSessionManager(mockSessionManager);
});
it('设置 todos 列表', async () => {
const todos: Todo[] = [
{
id: '1',
content: 'Task 1',
status: 'pending',
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
},
];
await todoManager.setTodos(todos);
expect(todoManager.getTodos()).toHaveLength(1);
expect(todoManager.getTodos()[0].content).toBe('Task 1');
});
it('清空 todos', async () => {
await todoManager.addTodo('Task 1');
await todoManager.setTodos([]);
expect(todoManager.getTodos()).toHaveLength(0);
});
});
describe('addTodo', () => {
beforeEach(() => {
todoManager.setSessionManager(mockSessionManager);
});
it('添加新 todo 默认状态为 pending', async () => {
const todo = await todoManager.addTodo('New task');
expect(todo.content).toBe('New task');
expect(todo.status).toBe('pending');
expect(todo.id).toBeDefined();
expect(todo.createdAt).toBeDefined();
expect(todo.updatedAt).toBeDefined();
});
it('添加新 todo 指定状态', async () => {
const todo = await todoManager.addTodo('In progress task', 'in_progress');
expect(todo.status).toBe('in_progress');
});
it('添加的 todo 出现在列表中', async () => {
await todoManager.addTodo('Task 1');
await todoManager.addTodo('Task 2');
const todos = todoManager.getTodos();
expect(todos).toHaveLength(2);
expect(todos.some(t => t.content === 'Task 1')).toBe(true);
expect(todos.some(t => t.content === 'Task 2')).toBe(true);
});
it('每个 todo 有唯一 ID', async () => {
const todo1 = await todoManager.addTodo('Task 1');
const todo2 = await todoManager.addTodo('Task 2');
expect(todo1.id).not.toBe(todo2.id);
});
});
describe('updateTodoStatus', () => {
beforeEach(() => {
todoManager.setSessionManager(mockSessionManager);
});
it('更新存在的 todo 状态', async () => {
const todo = await todoManager.addTodo('Task');
const result = await todoManager.updateTodoStatus(todo.id, 'completed');
expect(result).toBe(true);
const updated = todoManager.getTodos().find(t => t.id === todo.id);
expect(updated?.status).toBe('completed');
});
it('更新不存在的 todo 返回 false', async () => {
const result = await todoManager.updateTodoStatus('non-existent-id', 'completed');
expect(result).toBe(false);
});
it('更新状态时更新 updatedAt', async () => {
const todo = await todoManager.addTodo('Task');
const originalUpdatedAt = todo.updatedAt;
// 等待一小段时间确保时间戳不同
await new Promise(resolve => setTimeout(resolve, 10));
await todoManager.updateTodoStatus(todo.id, 'in_progress');
const updated = todoManager.getTodos().find(t => t.id === todo.id);
expect(updated?.updatedAt).not.toBe(originalUpdatedAt);
});
});
describe('deleteTodo', () => {
beforeEach(() => {
todoManager.setSessionManager(mockSessionManager);
});
it('删除存在的 todo', async () => {
const todo = await todoManager.addTodo('Task to delete');
const result = await todoManager.deleteTodo(todo.id);
expect(result).toBe(true);
expect(todoManager.getTodos().find(t => t.id === todo.id)).toBeUndefined();
});
it('删除不存在的 todo 返回 false', async () => {
const result = await todoManager.deleteTodo('non-existent-id');
expect(result).toBe(false);
});
it('删除后列表长度减少', async () => {
await todoManager.addTodo('Task 1');
const todo2 = await todoManager.addTodo('Task 2');
await todoManager.addTodo('Task 3');
await todoManager.deleteTodo(todo2.id);
expect(todoManager.getTodos()).toHaveLength(2);
});
});
describe('clearTodos', () => {
beforeEach(() => {
todoManager.setSessionManager(mockSessionManager);
});
it('清空所有 todos', async () => {
await todoManager.addTodo('Task 1');
await todoManager.addTodo('Task 2');
await todoManager.addTodo('Task 3');
await todoManager.clearTodos();
expect(todoManager.getTodos()).toHaveLength(0);
});
it('清空空列表不报错', async () => {
await expect(todoManager.clearTodos()).resolves.not.toThrow();
});
});
});
describe('Todo 状态流转', () => {
let todoManager: TestTodoManager;
let mockSessionManager: SessionManager;
beforeEach(() => {
todoManager = new TestTodoManager();
mockSessionManager = createMockSessionManager();
todoManager.setSessionManager(mockSessionManager);
});
it('pending -> in_progress -> completed', async () => {
const todo = await todoManager.addTodo('Task');
expect(todo.status).toBe('pending');
await todoManager.updateTodoStatus(todo.id, 'in_progress');
let updated = todoManager.getTodos().find(t => t.id === todo.id);
expect(updated?.status).toBe('in_progress');
await todoManager.updateTodoStatus(todo.id, 'completed');
updated = todoManager.getTodos().find(t => t.id === todo.id);
expect(updated?.status).toBe('completed');
});
it('直接从 pending 到 completed', async () => {
const todo = await todoManager.addTodo('Quick task');
await todoManager.updateTodoStatus(todo.id, 'completed');
const updated = todoManager.getTodos().find(t => t.id === todo.id);
expect(updated?.status).toBe('completed');
});
});
+103
View File
@@ -0,0 +1,103 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
// 使用可变的引用对象来绕过 hoisting 问题
const mockState = {
isInitialized: vi.fn().mockReturnValue(true),
getTodos: vi.fn().mockReturnValue([]),
};
vi.mock('../../../../src/tools/todo/todo-manager.js', () => ({
todoManager: {
isInitialized: () => mockState.isInitialized(),
getTodos: () => mockState.getTodos(),
},
}));
import { todoReadTool } from '../../../../src/tools/todo/todoread.js';
describe('todoReadTool - Todo 读取工具', () => {
beforeEach(() => {
vi.clearAllMocks();
mockState.isInitialized.mockReturnValue(true);
mockState.getTodos.mockReturnValue([]);
});
describe('工具定义', () => {
it('有正确的名称', () => {
expect(todoReadTool.name).toBe('todoread');
});
it('有正确的元数据', () => {
expect(todoReadTool.metadata.category).toBe('core');
expect(todoReadTool.metadata.keywords).toContain('todo');
expect(todoReadTool.metadata.keywords).toContain('task');
expect(todoReadTool.metadata.keywords).toContain('list');
});
it('无必需参数', () => {
expect(Object.keys(todoReadTool.parameters)).toHaveLength(0);
});
});
describe('execute - 执行', () => {
it('成功读取空列表', async () => {
const result = await todoReadTool.execute({});
expect(result.success).toBe(true);
expect(result.output).toBe('[]');
expect(result.metadata?.totalCount).toBe(0);
expect(result.metadata?.pendingCount).toBe(0);
});
it('成功读取待办列表', async () => {
const todos = [
{ id: '1', content: '任务1', status: 'pending', createdAt: '2024-01-01', updatedAt: '2024-01-01' },
{ id: '2', content: '任务2', status: 'in_progress', createdAt: '2024-01-01', updatedAt: '2024-01-01' },
{ id: '3', content: '任务3', status: 'completed', createdAt: '2024-01-01', updatedAt: '2024-01-01' },
];
mockState.getTodos.mockReturnValue(todos);
const result = await todoReadTool.execute({});
expect(result.success).toBe(true);
expect(result.metadata?.totalCount).toBe(3);
expect(result.metadata?.pendingCount).toBe(2); // pending + in_progress
});
it('返回 JSON 格式输出', async () => {
const todos = [
{ id: '1', content: '任务1', status: 'pending', createdAt: '2024-01-01', updatedAt: '2024-01-01' },
];
mockState.getTodos.mockReturnValue(todos);
const result = await todoReadTool.execute({});
const parsed = JSON.parse(result.output);
expect(parsed).toHaveLength(1);
expect(parsed[0].content).toBe('任务1');
});
it('未初始化时返回错误', async () => {
mockState.isInitialized.mockReturnValue(false);
const result = await todoReadTool.execute({});
expect(result.success).toBe(false);
expect(result.error).toContain('会话管理器未初始化');
});
it('返回正确的元数据', async () => {
const todos = [
{ id: '1', content: '任务1', status: 'pending' },
{ id: '2', content: '任务2', status: 'completed' },
];
mockState.getTodos.mockReturnValue(todos);
const result = await todoReadTool.execute({});
expect(result.metadata?.todos).toEqual(todos);
expect(result.metadata?.pendingCount).toBe(1);
expect(result.metadata?.totalCount).toBe(2);
});
});
});
+171
View File
@@ -0,0 +1,171 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
// 使用可变的引用对象来绕过 hoisting 问题
const mockState = {
isInitialized: vi.fn().mockReturnValue(true),
getTodos: vi.fn().mockReturnValue([]),
setTodos: vi.fn().mockResolvedValue(undefined),
};
vi.mock('../../../../src/tools/todo/todo-manager.js', () => ({
todoManager: {
isInitialized: () => mockState.isInitialized(),
getTodos: () => mockState.getTodos(),
setTodos: (todos: any) => mockState.setTodos(todos),
},
}));
import { todoWriteTool } from '../../../../src/tools/todo/todowrite.js';
describe('todoWriteTool - Todo 写入工具', () => {
beforeEach(() => {
vi.clearAllMocks();
mockState.isInitialized.mockReturnValue(true);
mockState.getTodos.mockReturnValue([]);
});
describe('工具定义', () => {
it('有正确的名称', () => {
expect(todoWriteTool.name).toBe('todowrite');
});
it('有正确的元数据', () => {
expect(todoWriteTool.metadata.category).toBe('core');
expect(todoWriteTool.metadata.keywords).toContain('todo');
expect(todoWriteTool.metadata.keywords).toContain('task');
expect(todoWriteTool.metadata.keywords).toContain('write');
});
it('定义了必需的 todos 参数', () => {
expect(todoWriteTool.parameters.todos.required).toBe(true);
});
});
describe('execute - 执行', () => {
it('成功创建待办列表', async () => {
const result = await todoWriteTool.execute({
todos: [
{ content: '任务1', status: 'pending' },
{ content: '任务2', status: 'in_progress' },
],
});
expect(result.success).toBe(true);
expect(result.output).toContain('待办事项已更新');
expect(mockState.setTodos).toHaveBeenCalled();
});
it('返回正确的统计信息', async () => {
const result = await todoWriteTool.execute({
todos: [
{ content: '任务1', status: 'pending' },
{ content: '任务2', status: 'in_progress' },
{ content: '任务3', status: 'completed' },
],
});
expect(result.success).toBe(true);
// pendingCount 包含所有非 completed 状态的任务(pending + in_progress
expect(result.metadata?.pendingCount).toBe(2);
expect(result.metadata?.inProgressCount).toBe(1);
expect(result.metadata?.completedCount).toBe(1);
});
it('更新现有任务', async () => {
mockState.getTodos.mockReturnValue([
{ id: 'existing-1', content: '任务1', status: 'pending', createdAt: '2024-01-01', updatedAt: '2024-01-01' },
]);
const result = await todoWriteTool.execute({
todos: [
{ content: '任务1', status: 'completed' },
],
});
expect(result.success).toBe(true);
const savedTodos = mockState.setTodos.mock.calls[0][0];
expect(savedTodos[0].id).toBe('existing-1'); // 保留原有 ID
expect(savedTodos[0].status).toBe('completed');
});
it('未初始化时返回错误', async () => {
mockState.isInitialized.mockReturnValue(false);
const result = await todoWriteTool.execute({
todos: [{ content: '任务', status: 'pending' }],
});
expect(result.success).toBe(false);
expect(result.error).toContain('会话管理器未初始化');
});
it('todos 非数组返回错误', async () => {
const result = await todoWriteTool.execute({
todos: 'not an array',
});
expect(result.success).toBe(false);
expect(result.error).toContain('todos 参数必须是数组');
});
it('无效的待办项格式返回错误', async () => {
const result = await todoWriteTool.execute({
todos: [
{ content: '有效任务', status: 'pending' },
{ content: '', status: 'pending' }, // 空内容
],
});
expect(result.success).toBe(false);
expect(result.error).toContain('第 2 个待办事项格式无效');
});
it('无效的状态值返回错误', async () => {
const result = await todoWriteTool.execute({
todos: [
{ content: '任务', status: 'invalid_status' },
],
});
expect(result.success).toBe(false);
expect(result.error).toContain('格式无效');
});
it('缺少 content 返回错误', async () => {
const result = await todoWriteTool.execute({
todos: [
{ status: 'pending' },
],
});
expect(result.success).toBe(false);
expect(result.error).toContain('格式无效');
});
it('为新任务生成 ID', async () => {
const result = await todoWriteTool.execute({
todos: [
{ content: '新任务', status: 'pending' },
],
});
expect(result.success).toBe(true);
const savedTodos = mockState.setTodos.mock.calls[0][0];
expect(savedTodos[0].id).toBeDefined();
expect(savedTodos[0].id.length).toBeGreaterThan(0);
});
it('设置创建和更新时间', async () => {
const result = await todoWriteTool.execute({
todos: [
{ content: '新任务', status: 'pending' },
],
});
expect(result.success).toBe(true);
const savedTodos = mockState.setTodos.mock.calls[0][0];
expect(savedTodos[0].createdAt).toBeDefined();
expect(savedTodos[0].updatedAt).toBeDefined();
});
});
});
+264
View File
@@ -0,0 +1,264 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
// Mock tavily
const mockExtract = vi.fn();
vi.mock('@tavily/core', () => ({
tavily: vi.fn(() => ({
extract: mockExtract,
})),
}));
// Mock config
vi.mock('../../../../src/utils/config.js', () => ({
getConfig: vi.fn(() => ({
tavilyApiKey: 'test-api-key',
})),
}));
// Mock permission manager
vi.mock('../../../../src/permission/index.js', () => ({
getPermissionManager: vi.fn(() => ({
checkWebPermission: vi.fn().mockResolvedValue({
allowed: true,
action: 'allow',
}),
})),
}));
// Mock loadDescription
vi.mock('../../../../src/tools/load_description.js', () => ({
loadDescription: vi.fn(() => '从网页URL提取内容'),
}));
import { webExtractTool } from '../../../../src/tools/web/web_extract.js';
import { getPermissionManager } from '../../../../src/permission/index.js';
import { getConfig } from '../../../../src/utils/config.js';
describe('webExtractTool - 网页内容提取工具', () => {
beforeEach(() => {
vi.clearAllMocks();
mockExtract.mockResolvedValue({
results: [
{
url: 'https://example.com',
rawContent: '# Hello World\n\nThis is example content.',
images: ['https://example.com/img1.png'],
},
],
failedResults: [],
responseTime: 1.5,
});
});
describe('工具定义', () => {
it('有正确的名称', () => {
expect(webExtractTool.name).toBe('web_extract');
});
it('有正确的元数据', () => {
expect(webExtractTool.metadata.category).toBe('web');
expect(webExtractTool.metadata.keywords).toContain('extract');
expect(webExtractTool.metadata.keywords).toContain('url');
expect(webExtractTool.metadata.keywords).toContain('scrape');
});
it('定义了必需的 urls 参数', () => {
expect(webExtractTool.parameters.urls.required).toBe(true);
});
it('定义了可选参数', () => {
expect(webExtractTool.parameters.extract_depth.required).toBe(false);
expect(webExtractTool.parameters.format.required).toBe(false);
expect(webExtractTool.parameters.include_images.required).toBe(false);
});
});
describe('execute - 执行', () => {
it('成功提取单个 URL 内容', async () => {
const result = await webExtractTool.execute({
urls: ['https://example.com'],
});
expect(result.success).toBe(true);
expect(result.output).toContain('网页内容提取');
expect(result.output).toContain('example.com');
expect(result.output).toContain('Hello World');
});
it('支持字符串格式的单个 URL', async () => {
const result = await webExtractTool.execute({
urls: 'https://example.com',
});
expect(result.success).toBe(true);
expect(mockExtract).toHaveBeenCalledWith(
['https://example.com'],
expect.any(Object)
);
});
it('支持多个 URL', async () => {
mockExtract.mockResolvedValue({
results: [
{ url: 'https://example1.com', rawContent: 'Content 1' },
{ url: 'https://example2.com', rawContent: 'Content 2' },
],
failedResults: [],
});
const result = await webExtractTool.execute({
urls: ['https://example1.com', 'https://example2.com'],
});
expect(result.success).toBe(true);
expect(result.output).toContain('example1.com');
expect(result.output).toContain('example2.com');
});
it('限制最多 20 个 URL', async () => {
const urls = Array.from({ length: 25 }, (_, i) => `https://example${i}.com`);
await webExtractTool.execute({ urls });
expect(mockExtract).toHaveBeenCalledWith(
expect.any(Array),
expect.any(Object)
);
const calledUrls = mockExtract.mock.calls[0][0];
expect(calledUrls.length).toBe(20);
});
it('使用 advanced 提取深度', async () => {
await webExtractTool.execute({
urls: ['https://example.com'],
extract_depth: 'advanced',
});
expect(mockExtract).toHaveBeenCalledWith(
expect.any(Array),
expect.objectContaining({ extractDepth: 'advanced' })
);
});
it('包含图片列表', async () => {
const result = await webExtractTool.execute({
urls: ['https://example.com'],
include_images: true,
});
expect(result.success).toBe(true);
expect(result.output).toContain('图片');
expect(result.output).toContain('img1.png');
});
it('显示失败的 URL', async () => {
mockExtract.mockResolvedValue({
results: [],
failedResults: [
{ url: 'https://failed.com', error: '404 Not Found' },
],
});
const result = await webExtractTool.execute({
urls: ['https://failed.com'],
});
expect(result.success).toBe(true);
expect(result.output).toContain('提取失败');
expect(result.output).toContain('failed.com');
expect(result.output).toContain('404');
});
it('显示响应时间', async () => {
const result = await webExtractTool.execute({
urls: ['https://example.com'],
});
expect(result.success).toBe(true);
expect(result.output).toContain('提取耗时');
});
it('截断过长的内容', async () => {
mockExtract.mockResolvedValue({
results: [
{
url: 'https://example.com',
rawContent: 'a'.repeat(6000), // 超过 5000 字符
},
],
failedResults: [],
});
const result = await webExtractTool.execute({
urls: ['https://example.com'],
});
expect(result.success).toBe(true);
expect(result.output).toContain('内容已截断');
});
it('无 API Key 返回错误', async () => {
vi.mocked(getConfig).mockReturnValue({} as any);
const originalEnv = process.env.TAVILY_API_KEY;
delete process.env.TAVILY_API_KEY;
const result = await webExtractTool.execute({
urls: ['https://example.com'],
});
expect(result.success).toBe(false);
expect(result.error).toContain('未配置 Tavily API Key');
process.env.TAVILY_API_KEY = originalEnv;
});
it('权限被拒绝时返回错误', async () => {
vi.mocked(getPermissionManager).mockReturnValue({
checkWebPermission: vi.fn().mockResolvedValue({
allowed: false,
action: 'deny',
reason: '提取不被允许',
}),
} as any);
const result = await webExtractTool.execute({
urls: ['https://example.com'],
});
expect(result.success).toBe(false);
expect(result.error).toContain('权限被拒绝');
});
it('需要确认时返回提示', async () => {
vi.mocked(getPermissionManager).mockReturnValue({
checkWebPermission: vi.fn().mockResolvedValue({
allowed: false,
action: 'ask',
needsConfirmation: true,
}),
} as any);
const result = await webExtractTool.execute({
urls: ['https://example.com'],
});
expect(result.success).toBe(false);
expect(result.error).toContain('需要用户确认');
});
it('提取失败返回错误', async () => {
vi.mocked(getPermissionManager).mockReturnValue({
checkWebPermission: vi.fn().mockResolvedValue({ allowed: true }),
} as any);
mockExtract.mockRejectedValue(new Error('API 调用失败'));
const result = await webExtractTool.execute({
urls: ['https://example.com'],
});
expect(result.success).toBe(false);
expect(result.error).toContain('提取失败');
expect(result.error).toContain('API 调用失败');
});
});
});
+207
View File
@@ -0,0 +1,207 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
// Mock tavily
const mockSearch = vi.fn();
vi.mock('@tavily/core', () => ({
tavily: vi.fn(() => ({
search: mockSearch,
})),
}));
// Mock config
vi.mock('../../../../src/utils/config.js', () => ({
getConfig: vi.fn(() => ({
tavilyApiKey: 'test-api-key',
})),
}));
// Mock permission manager
vi.mock('../../../../src/permission/index.js', () => ({
getPermissionManager: vi.fn(() => ({
checkWebPermission: vi.fn().mockResolvedValue({
allowed: true,
action: 'allow',
}),
})),
}));
// Mock loadDescription
vi.mock('../../../../src/tools/load_description.js', () => ({
loadDescription: vi.fn(() => '网络搜索'),
}));
import { webSearchTool } from '../../../../src/tools/web/web_search.js';
import { getPermissionManager } from '../../../../src/permission/index.js';
import { getConfig } from '../../../../src/utils/config.js';
describe('webSearchTool - 网络搜索工具', () => {
beforeEach(() => {
vi.clearAllMocks();
mockSearch.mockResolvedValue({
answer: '搜索摘要',
results: [
{ title: '结果1', url: 'https://example.com/1', content: '内容1' },
{ title: '结果2', url: 'https://example.com/2', content: '内容2' },
],
});
});
describe('工具定义', () => {
it('有正确的名称', () => {
expect(webSearchTool.name).toBe('web_search');
});
it('有正确的元数据', () => {
expect(webSearchTool.metadata.category).toBe('web');
expect(webSearchTool.metadata.keywords).toContain('search');
expect(webSearchTool.metadata.keywords).toContain('web');
});
it('定义了必需的 query 参数', () => {
expect(webSearchTool.parameters.query.required).toBe(true);
});
it('定义了可选参数', () => {
expect(webSearchTool.parameters.max_results.required).toBe(false);
expect(webSearchTool.parameters.search_depth.required).toBe(false);
expect(webSearchTool.parameters.topic.required).toBe(false);
expect(webSearchTool.parameters.include_answer.required).toBe(false);
});
});
describe('execute - 执行', () => {
it('成功搜索并返回结果', async () => {
const result = await webSearchTool.execute({ query: 'test query' });
expect(result.success).toBe(true);
expect(result.output).toContain('搜索结果');
expect(result.output).toContain('test query');
expect(result.output).toContain('搜索摘要');
expect(result.output).toContain('结果1');
expect(result.output).toContain('结果2');
});
it('无结果时显示提示', async () => {
mockSearch.mockResolvedValue({
answer: null,
results: [],
});
const result = await webSearchTool.execute({ query: 'no results' });
expect(result.success).toBe(true);
expect(result.output).toContain('未找到相关结果');
});
it('限制最大结果数量', async () => {
const result = await webSearchTool.execute({
query: 'test',
max_results: 100, // 超过限制
});
expect(result.success).toBe(true);
expect(mockSearch).toHaveBeenCalledWith(
'test',
expect.objectContaining({ maxResults: 20 }) // 最大 20
);
});
it('使用指定的搜索深度', async () => {
await webSearchTool.execute({
query: 'test',
search_depth: 'advanced',
});
expect(mockSearch).toHaveBeenCalledWith(
'test',
expect.objectContaining({ searchDepth: 'advanced' })
);
});
it('使用指定的主题', async () => {
await webSearchTool.execute({
query: 'test',
topic: 'news',
});
expect(mockSearch).toHaveBeenCalledWith(
'test',
expect.objectContaining({ topic: 'news' })
);
});
it('无 API Key 返回错误', async () => {
vi.mocked(getConfig).mockReturnValue({} as any);
const originalEnv = process.env.TAVILY_API_KEY;
delete process.env.TAVILY_API_KEY;
const result = await webSearchTool.execute({ query: 'test' });
expect(result.success).toBe(false);
expect(result.error).toContain('未配置 Tavily API Key');
process.env.TAVILY_API_KEY = originalEnv;
});
it('权限被拒绝时返回错误', async () => {
vi.mocked(getPermissionManager).mockReturnValue({
checkWebPermission: vi.fn().mockResolvedValue({
allowed: false,
action: 'deny',
reason: '搜索不被允许',
}),
} as any);
const result = await webSearchTool.execute({ query: 'test' });
expect(result.success).toBe(false);
expect(result.error).toContain('权限被拒绝');
});
it('需要确认时返回提示', async () => {
vi.mocked(getPermissionManager).mockReturnValue({
checkWebPermission: vi.fn().mockResolvedValue({
allowed: false,
action: 'ask',
needsConfirmation: true,
}),
} as any);
const result = await webSearchTool.execute({ query: 'test' });
expect(result.success).toBe(false);
expect(result.error).toContain('需要用户确认');
});
it('搜索失败返回错误', async () => {
vi.mocked(getPermissionManager).mockReturnValue({
checkWebPermission: vi.fn().mockResolvedValue({ allowed: true }),
} as any);
mockSearch.mockRejectedValue(new Error('API 调用失败'));
const result = await webSearchTool.execute({ query: 'test' });
expect(result.success).toBe(false);
expect(result.error).toContain('搜索失败');
expect(result.error).toContain('API 调用失败');
});
it('截断过长的内容', async () => {
mockSearch.mockResolvedValue({
answer: null,
results: [
{
title: '长内容结果',
url: 'https://example.com',
content: 'a'.repeat(500), // 超过 300 字符
},
],
});
const result = await webSearchTool.execute({ query: 'test' });
expect(result.success).toBe(true);
expect(result.output).toContain('...');
});
});
});