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), readFile: vi.fn().mockRejectedValue(new Error('ENOENT: file not found')), })); // Mock permission manager const mockCheckFilePermission = vi.fn(); vi.mock('../../../../src/permission/index.js', () => ({ getPermissionManager: vi.fn(() => ({ checkFilePermission: mockCheckFilePermission, })), })); // Mock loadDescription vi.mock('../../../../src/tools/load_description.js', () => ({ loadDescription: vi.fn(() => '写入文件内容'), })); // Mock LSP const mockTouchFile = vi.fn(); const mockGetFormattedFileDiagnostics = vi.fn(); const mockIsLanguageSupported = vi.fn(); vi.mock('../../../../src/lsp/index.js', () => ({ touchFile: (...args: unknown[]) => mockTouchFile(...args), getFormattedFileDiagnostics: (...args: unknown[]) => mockGetFormattedFileDiagnostics(...args), isLanguageSupported: (...args: unknown[]) => mockIsLanguageSupported(...args), })); import * as fs from 'fs/promises'; describe('writeFileTool - 写入文件工具扩展测试', () => { beforeEach(() => { vi.clearAllMocks(); // 重置所有 mock 返回值 vi.mocked(fs.mkdir).mockResolvedValue(undefined); vi.mocked(fs.writeFile).mockResolvedValue(undefined); mockCheckFilePermission.mockResolvedValue({ allowed: true, action: 'allow', }); mockIsLanguageSupported.mockReturnValue(false); }); describe('LSP 集成', () => { it('支持的语言触发 LSP 诊断', async () => { mockIsLanguageSupported.mockReturnValue(true); mockTouchFile.mockResolvedValue(false); mockGetFormattedFileDiagnostics.mockResolvedValue(null); const result = await writeFileTool.execute({ path: './test.ts', content: 'const x = 1;', }); expect(result.success).toBe(true); expect(mockTouchFile).toHaveBeenCalled(); expect(mockGetFormattedFileDiagnostics).toHaveBeenCalled(); }); it('首次启动等待更长时间', async () => { mockIsLanguageSupported.mockReturnValue(true); mockTouchFile.mockResolvedValue(true); // 首次启动返回 true mockGetFormattedFileDiagnostics.mockResolvedValue(null); const startTime = Date.now(); await writeFileTool.execute({ path: './test.ts', content: 'const x = 1;', }); const elapsed = Date.now() - startTime; // 首次启动应该等待更长时间(2000ms vs 300ms) expect(elapsed).toBeGreaterThanOrEqual(1900); }); it('有诊断问题时在输出中显示', async () => { mockIsLanguageSupported.mockReturnValue(true); mockTouchFile.mockResolvedValue(false); mockGetFormattedFileDiagnostics.mockResolvedValue( '\n\n1:1 ERROR: Type error\n' ); const result = await writeFileTool.execute({ path: './test.ts', content: 'const x: string = 1;', }); expect(result.success).toBe(true); expect(result.output).toContain('代码检查发现问题'); expect(result.output).toContain('file_diagnostics'); }); it('LSP 错误不影响主流程', async () => { mockIsLanguageSupported.mockReturnValue(true); mockTouchFile.mockRejectedValue(new Error('LSP error')); const result = await writeFileTool.execute({ path: './test.ts', content: 'const x = 1;', }); // 即使 LSP 失败,文件写入仍然成功 expect(result.success).toBe(true); expect(result.output).toContain('文件已写入'); }); it('不支持的语言不触发 LSP', async () => { mockIsLanguageSupported.mockReturnValue(false); await writeFileTool.execute({ path: './test.txt', content: 'plain text', }); expect(mockTouchFile).not.toHaveBeenCalled(); expect(mockGetFormattedFileDiagnostics).not.toHaveBeenCalled(); }); }); describe('路径处理', () => { it('相对路径转换为绝对路径', async () => { await writeFileTool.execute({ path: './relative/path/file.txt', content: 'content', }); expect(fs.mkdir).toHaveBeenCalledWith( expect.stringMatching(/relative\/path$/), { recursive: true } ); }); it('绝对路径保持不变', async () => { await writeFileTool.execute({ path: '/absolute/path/file.txt', content: 'content', }); expect(fs.writeFile).toHaveBeenCalledWith( '/absolute/path/file.txt', 'content', 'utf-8' ); }); }); describe('权限检查详情', () => { it('权限检查包含正确的上下文', async () => { await writeFileTool.execute({ path: './test.txt', content: 'file content', }); expect(mockCheckFilePermission).toHaveBeenCalledWith( expect.objectContaining({ operation: 'write', newContent: 'file content', }) ); }); it('被拒绝时返回具体原因', async () => { mockCheckFilePermission.mockResolvedValue({ allowed: false, action: 'deny', reason: '系统文件不允许修改', }); const result = await writeFileTool.execute({ path: '/etc/passwd', content: 'malicious', }); expect(result.success).toBe(false); expect(result.error).toContain('权限被拒绝'); expect(result.error).toContain('系统文件不允许修改'); }); it('需要确认时返回具体原因', async () => { mockCheckFilePermission.mockResolvedValue({ allowed: false, action: 'ask', needsConfirmation: true, reason: '首次写入此文件', }); const result = await writeFileTool.execute({ path: './new-file.txt', content: 'content', }); expect(result.success).toBe(false); expect(result.error).toContain('需要用户确认'); expect(result.error).toContain('首次写入此文件'); }); }); describe('错误处理', () => { it('文件系统错误返回错误信息', async () => { vi.mocked(fs.writeFile).mockRejectedValue(new Error('EACCES: permission denied')); const result = await writeFileTool.execute({ path: './test.txt', content: 'content', }); expect(result.success).toBe(false); expect(result.error).toContain('EACCES'); }); it('目录创建失败返回错误信息', async () => { vi.mocked(fs.mkdir).mockImplementationOnce(() => { return Promise.reject(new Error('mkdir failed')); }); const result = await writeFileTool.execute({ path: './deep/nested/file.txt', content: 'content', }); expect(result.success).toBe(false); // The error is now wrapped by the editors module expect(result.error).toBeTruthy(); }); it('非 Error 对象也能正确处理', async () => { vi.mocked(fs.writeFile).mockRejectedValue('String error'); const result = await writeFileTool.execute({ path: './test.txt', content: 'content', }); expect(result.success).toBe(false); expect(result.error).toContain('String error'); }); }); describe('工具元数据', () => { it('包含完整的元数据', () => { expect(writeFileTool.metadata.category).toBe('filesystem'); expect(writeFileTool.metadata.keywords).toContain('write'); expect(writeFileTool.metadata.keywords).toContain('save'); expect(writeFileTool.metadata.keywords).toContain('create'); expect(writeFileTool.metadata.keywords).toContain('写入'); expect(writeFileTool.metadata.deferLoading).toBe(false); }); it('参数定义正确', () => { expect(writeFileTool.parameters.path.required).toBe(true); expect(writeFileTool.parameters.content.required).toBe(true); expect(writeFileTool.parameters.path.type).toBe('string'); expect(writeFileTool.parameters.content.type).toBe('string'); }); }); });