import { describe, it, expect, beforeEach, vi } from 'vitest'; // Mock fs/promises - still needed by editors module vi.mock('fs/promises', () => ({ readFile: vi.fn(), writeFile: vi.fn().mockResolvedValue(undefined), access: 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(() => '编辑文件'), })); // 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'); vi.mocked(fs.access).mockResolvedValue(undefined); }); 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')); vi.mocked(fs.access).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); // The error message may vary, just check it failed expect(result.error).toBeTruthy(); }); 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', }) ); }); }); });