bca19b7741
新增测试文件: - agent/executor-extended.test.ts, presets/ - context/manager-extended.test.ts - core/agent.test.ts, providers.test.ts - lsp/cli.test.ts, client-extended.test.ts, index.test.ts - permission/file-prompt.test.ts, prompt.test.ts - skills/builtin/ - tools/filesystem/write_file-extended.test.ts - tools/git/git_commit-extended.test.ts - tools/load_description.test.ts - tools/todo/todo-manager.test.ts - tools/tool-search.test.ts - types/ - utils/config-extended.test.ts, diff-extended.test.ts 修改现有测试: - agent/manager.test.ts - tools/skill/skill.test.ts - utils/config.test.ts, diff.test.ts, image.test.ts
259 lines
7.9 KiB
TypeScript
259 lines
7.9 KiB
TypeScript
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
|
||
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<file_diagnostics>\n1:1 ERROR: Type error\n</file_diagnostics>'
|
||
);
|
||
|
||
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).mockRejectedValue(new Error('ENOENT'));
|
||
|
||
const result = await writeFileTool.execute({
|
||
path: './deep/nested/file.txt',
|
||
content: 'content',
|
||
});
|
||
|
||
expect(result.success).toBe(false);
|
||
expect(result.error).toContain('ENOENT');
|
||
});
|
||
|
||
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');
|
||
});
|
||
});
|
||
});
|