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('需要权限确认'); }); }); });