test: 补充单元测试提升代码覆盖率

新增测试文件:
- 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
This commit is contained in:
2025-12-11 20:37:03 +08:00
parent f8b0cd4bec
commit bca19b7741
24 changed files with 6347 additions and 4 deletions
@@ -0,0 +1,258 @@
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');
});
});
});