test: 补充 task、copy_file、skill_search 工具测试

- task-extended.test.ts: 覆盖 Vision Agent、模型选择、后台运行
- copy_file-extended.test.ts: 覆盖递归复制、权限检查边界情况
- skill_search-extended.test.ts: 覆盖来源标记、未分类分组、参数显示

覆盖率提升:
- task.ts: 97.91%
- copy_file.ts: 100%
- skill_search.ts: 98.61%
This commit is contained in:
2025-12-11 20:33:14 +08:00
parent d0d0e8dbaf
commit f8b0cd4bec
3 changed files with 757 additions and 0 deletions
@@ -0,0 +1,192 @@
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([]),
}));
// 可变状态
let mockCheckResults: Array<{
allowed: boolean;
action?: string;
reason?: string;
needsConfirmation?: boolean;
}> = [];
let checkCallIndex = 0;
// Mock permission manager
vi.mock('../../../../src/permission/index.js', () => ({
getPermissionManager: () => ({
checkFilePermission: vi.fn(async () => {
const result = mockCheckResults[checkCallIndex] || { allowed: true };
checkCallIndex++;
return result;
}),
}),
}));
// 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';
describe('copyFileTool - 扩展测试', () => {
beforeEach(() => {
vi.clearAllMocks();
mockCheckResults = [{ allowed: true }, { allowed: true }];
checkCallIndex = 0;
// 重置 mock 默认值
vi.mocked(fs.stat).mockReset();
vi.mocked(fs.copyFile).mockReset().mockResolvedValue(undefined);
vi.mocked(fs.mkdir).mockReset().mockResolvedValue(undefined);
vi.mocked(fs.readdir).mockReset().mockResolvedValue([]);
});
describe('递归复制目录', () => {
it('递归复制包含文件的目录', async () => {
vi.mocked(fs.stat)
.mockResolvedValueOnce({ isDirectory: () => true } as any)
.mockRejectedValueOnce(new Error('ENOENT'))
.mockResolvedValueOnce({ isDirectory: () => false } as any);
vi.mocked(fs.readdir).mockResolvedValueOnce(['file1.txt'] as any);
const result = await copyFileTool.execute({
source: 'src_dir',
destination: 'dest_dir',
});
expect(result.success).toBe(true);
expect(fs.mkdir).toHaveBeenCalled();
expect(fs.copyFile).toHaveBeenCalled();
});
it('递归复制包含子目录的目录', async () => {
// 调用顺序:
// 1. execute: stat(source) - 检查源是否存在
// 2. execute: stat(dest) - 检查目标(ENOENT
// 3. copyRecursive: stat(source) - 判断是否是目录
// 4. copyRecursive: stat(source/subdir) - 递归判断子目录
vi.mocked(fs.stat)
.mockResolvedValueOnce({ isDirectory: () => true } as any) // source 存在且是目录
.mockRejectedValueOnce(new Error('ENOENT')) // dest 不存在
.mockResolvedValueOnce({ isDirectory: () => true } as any) // copyRecursive(source)
.mockResolvedValueOnce({ isDirectory: () => true } as any); // copyRecursive(source/subdir)
vi.mocked(fs.readdir)
.mockResolvedValueOnce(['subdir'] as any) // 第一层目录
.mockResolvedValueOnce([] as any); // 子目录为空
const result = await copyFileTool.execute({
source: 'src_dir',
destination: 'dest_dir',
});
expect(result.success).toBe(true);
expect(fs.mkdir).toHaveBeenCalled();
});
});
describe('目标位置权限', () => {
it('目标位置需要确认时返回错误', async () => {
mockCheckResults = [
{ allowed: true },
{ allowed: false, action: 'ask', needsConfirmation: true, reason: '首次复制到此位置' },
];
vi.mocked(fs.stat).mockResolvedValueOnce({ isDirectory: () => false } as any);
const result = await copyFileTool.execute({
source: 'src.txt',
destination: '/new/location/dest.txt',
});
expect(result.success).toBe(false);
expect(result.error).toContain('需要用户确认');
expect(result.error).toContain('首次复制到此位置');
});
});
describe('绝对路径处理', () => {
it('源和目标都是绝对路径', async () => {
vi.mocked(fs.stat)
.mockResolvedValueOnce({ isDirectory: () => false } as any)
.mockRejectedValueOnce(new Error('ENOENT'));
const result = await copyFileTool.execute({
source: '/absolute/source/file.txt',
destination: '/absolute/destination/file.txt',
});
expect(result.success).toBe(true);
expect(result.output).toContain('/absolute/source/file.txt');
expect(result.output).toContain('/absolute/destination/file.txt');
});
});
describe('权限检查细节', () => {
it('源文件权限被拒绝(无原因)', async () => {
mockCheckResults = [
{ allowed: false, action: 'deny' },
];
const result = await copyFileTool.execute({
source: 'src.txt',
destination: 'dest.txt',
});
expect(result.success).toBe(false);
expect(result.error).toContain('不允许读取此文件');
});
it('源文件需要确认(无原因)', async () => {
mockCheckResults = [
{ allowed: false, action: 'ask', needsConfirmation: true },
];
const result = await copyFileTool.execute({
source: 'src.txt',
destination: 'dest.txt',
});
expect(result.success).toBe(false);
expect(result.error).toContain('需要权限确认');
});
it('目标权限被拒绝(无原因)', async () => {
mockCheckResults = [
{ allowed: true },
{ allowed: false, action: 'deny' },
];
const result = await copyFileTool.execute({
source: 'src.txt',
destination: 'dest.txt',
});
expect(result.success).toBe(false);
expect(result.error).toContain('不允许复制到此位置');
});
it('目标需要确认(无原因)', async () => {
mockCheckResults = [
{ allowed: true },
{ allowed: false, action: 'ask', needsConfirmation: true },
];
const result = await copyFileTool.execute({
source: 'src.txt',
destination: 'dest.txt',
});
expect(result.success).toBe(false);
expect(result.error).toContain('需要权限确认');
});
});
});