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:
@@ -0,0 +1,346 @@
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
|
||||
// Mock functions
|
||||
const mockTouchFile = vi.fn();
|
||||
const mockGetDiagnostics = vi.fn();
|
||||
const mockGetFileDiagnostics = vi.fn();
|
||||
const mockShutdown = vi.fn();
|
||||
|
||||
// 创建 mock class
|
||||
class MockLSPClientManager {
|
||||
touchFile = mockTouchFile;
|
||||
getDiagnostics = mockGetDiagnostics;
|
||||
getFileDiagnostics = mockGetFileDiagnostics;
|
||||
shutdown = mockShutdown;
|
||||
|
||||
constructor(_rootPath?: string) {}
|
||||
}
|
||||
|
||||
// Mock client module
|
||||
vi.mock('../../../src/lsp/client.js', () => ({
|
||||
LSPClientManager: MockLSPClientManager,
|
||||
}));
|
||||
|
||||
// Mock language module
|
||||
vi.mock('../../../src/lsp/language.js', () => ({
|
||||
getLanguageId: vi.fn((path: string) => {
|
||||
if (path.endsWith('.ts')) return 'typescript';
|
||||
if (path.endsWith('.py')) return 'python';
|
||||
return undefined;
|
||||
}),
|
||||
isLanguageSupported: vi.fn((path: string) => {
|
||||
return path.endsWith('.ts') || path.endsWith('.py');
|
||||
}),
|
||||
getSupportedExtensions: vi.fn(() => ['.ts', '.js', '.py']),
|
||||
}));
|
||||
|
||||
// Mock server module
|
||||
vi.mock('../../../src/lsp/server.js', () => ({
|
||||
getServerConfig: vi.fn(),
|
||||
hasServerConfig: vi.fn(),
|
||||
getSupportedLanguages: vi.fn(() => []),
|
||||
}));
|
||||
|
||||
describe('LSP Index - LSP 模块入口', () => {
|
||||
let lspModule: typeof import('../../../src/lsp/index.js');
|
||||
|
||||
beforeEach(async () => {
|
||||
vi.clearAllMocks();
|
||||
// 重置模块缓存以获取干净的状态
|
||||
vi.resetModules();
|
||||
// 重新导入模块
|
||||
lspModule = await import('../../../src/lsp/index.js');
|
||||
});
|
||||
|
||||
describe('initLSP - 初始化 LSP', () => {
|
||||
it('初始化 LSP 管理器', () => {
|
||||
lspModule.initLSP('/test/project');
|
||||
|
||||
expect(lspModule.getLSPManager()).not.toBeNull();
|
||||
});
|
||||
|
||||
it('重复初始化不会创建新实例', () => {
|
||||
lspModule.initLSP('/test/project');
|
||||
const manager1 = lspModule.getLSPManager();
|
||||
|
||||
lspModule.initLSP('/another/project');
|
||||
const manager2 = lspModule.getLSPManager();
|
||||
|
||||
expect(manager1).toBe(manager2);
|
||||
});
|
||||
|
||||
it('默认使用 process.cwd()', () => {
|
||||
lspModule.initLSP();
|
||||
|
||||
expect(lspModule.getLSPManager()).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getLSPManager - 获取管理器', () => {
|
||||
it('未初始化时返回 null', async () => {
|
||||
// 重置模块获取干净状态
|
||||
vi.resetModules();
|
||||
const freshModule = await import('../../../src/lsp/index.js');
|
||||
|
||||
expect(freshModule.getLSPManager()).toBeNull();
|
||||
});
|
||||
|
||||
it('初始化后返回管理器实例', () => {
|
||||
lspModule.initLSP();
|
||||
|
||||
expect(lspModule.getLSPManager()).not.toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('touchFile - 通知文件变更', () => {
|
||||
it('未初始化时返回 false', async () => {
|
||||
vi.resetModules();
|
||||
const freshModule = await import('../../../src/lsp/index.js');
|
||||
|
||||
const result = await freshModule.touchFile('/test/file.ts');
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('不支持的语言返回 false', async () => {
|
||||
lspModule.initLSP();
|
||||
|
||||
const result = await lspModule.touchFile('/test/file.txt');
|
||||
|
||||
expect(result).toBe(false);
|
||||
});
|
||||
|
||||
it('支持的语言调用管理器', async () => {
|
||||
mockTouchFile.mockResolvedValue(true);
|
||||
lspModule.initLSP();
|
||||
|
||||
const result = await lspModule.touchFile('/test/file.ts');
|
||||
|
||||
expect(mockTouchFile).toHaveBeenCalledWith('/test/file.ts', false);
|
||||
expect(result).toBe(true);
|
||||
});
|
||||
|
||||
it('新文件传递 isNew 参数', async () => {
|
||||
mockTouchFile.mockResolvedValue(true);
|
||||
lspModule.initLSP();
|
||||
|
||||
await lspModule.touchFile('/test/file.ts', true);
|
||||
|
||||
expect(mockTouchFile).toHaveBeenCalledWith('/test/file.ts', true);
|
||||
});
|
||||
|
||||
it('touchFile 出错时静默返回 false', async () => {
|
||||
mockTouchFile.mockRejectedValue(new Error('LSP error'));
|
||||
lspModule.initLSP();
|
||||
|
||||
const consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
|
||||
const result = await lspModule.touchFile('/test/file.ts');
|
||||
|
||||
expect(result).toBe(false);
|
||||
consoleErrorSpy.mockRestore();
|
||||
});
|
||||
});
|
||||
|
||||
describe('getDiagnostics - 获取所有诊断', () => {
|
||||
it('未初始化时返回空 Map', async () => {
|
||||
vi.resetModules();
|
||||
const freshModule = await import('../../../src/lsp/index.js');
|
||||
|
||||
const result = freshModule.getDiagnostics();
|
||||
|
||||
expect(result).toBeInstanceOf(Map);
|
||||
expect(result.size).toBe(0);
|
||||
});
|
||||
|
||||
it('返回管理器的诊断信息', () => {
|
||||
const mockDiagnostics = new Map([
|
||||
['/test/file.ts', [{ file: '/test/file.ts', line: 1, column: 1, severity: 'error', message: 'Test error' }]],
|
||||
]);
|
||||
mockGetDiagnostics.mockReturnValue(mockDiagnostics);
|
||||
lspModule.initLSP();
|
||||
|
||||
const result = lspModule.getDiagnostics();
|
||||
|
||||
expect(result).toBe(mockDiagnostics);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getFileDiagnostics - 获取单文件诊断', () => {
|
||||
it('未初始化时返回空数组', async () => {
|
||||
vi.resetModules();
|
||||
const freshModule = await import('../../../src/lsp/index.js');
|
||||
|
||||
const result = freshModule.getFileDiagnostics('/test/file.ts');
|
||||
|
||||
expect(result).toEqual([]);
|
||||
});
|
||||
|
||||
it('返回指定文件的诊断', () => {
|
||||
const mockDiagnostics = [
|
||||
{ file: '/test/file.ts', line: 1, column: 1, severity: 'error' as const, message: 'Error' },
|
||||
];
|
||||
mockGetFileDiagnostics.mockReturnValue(mockDiagnostics);
|
||||
lspModule.initLSP();
|
||||
|
||||
const result = lspModule.getFileDiagnostics('/test/file.ts');
|
||||
|
||||
expect(result).toBe(mockDiagnostics);
|
||||
});
|
||||
});
|
||||
|
||||
describe('formatDiagnostics - 格式化诊断信息', () => {
|
||||
it('空诊断返回空字符串', () => {
|
||||
const result = lspModule.formatDiagnostics([]);
|
||||
|
||||
expect(result).toBe('');
|
||||
});
|
||||
|
||||
it('格式化单个诊断', () => {
|
||||
const diagnostics = [
|
||||
{
|
||||
file: '/test/file.ts',
|
||||
line: 10,
|
||||
column: 5,
|
||||
severity: 'error' as const,
|
||||
message: 'Type error',
|
||||
code: 'TS2345',
|
||||
source: 'typescript',
|
||||
},
|
||||
];
|
||||
|
||||
const result = lspModule.formatDiagnostics(diagnostics);
|
||||
|
||||
expect(result).toContain('10:5');
|
||||
expect(result).toContain('ERROR');
|
||||
expect(result).toContain('TS2345');
|
||||
expect(result).toContain('Type error');
|
||||
expect(result).toContain('typescript');
|
||||
});
|
||||
|
||||
it('格式化多个诊断', () => {
|
||||
const diagnostics = [
|
||||
{ file: '/test/file.ts', line: 1, column: 1, severity: 'error' as const, message: 'Error 1' },
|
||||
{ file: '/test/file.ts', line: 2, column: 2, severity: 'warning' as const, message: 'Warning 1' },
|
||||
];
|
||||
|
||||
const result = lspModule.formatDiagnostics(diagnostics);
|
||||
|
||||
expect(result).toContain('Error 1');
|
||||
expect(result).toContain('Warning 1');
|
||||
expect(result).toContain('ERROR');
|
||||
expect(result).toContain('WARNING');
|
||||
});
|
||||
|
||||
it('无 code 时不显示 code', () => {
|
||||
const diagnostics = [
|
||||
{ file: '/test/file.ts', line: 1, column: 1, severity: 'error' as const, message: 'Error' },
|
||||
];
|
||||
|
||||
const result = lspModule.formatDiagnostics(diagnostics);
|
||||
|
||||
expect(result).not.toContain('[');
|
||||
});
|
||||
|
||||
it('无 source 时不显示 source', () => {
|
||||
const diagnostics = [
|
||||
{ file: '/test/file.ts', line: 1, column: 1, severity: 'error' as const, message: 'Error' },
|
||||
];
|
||||
|
||||
const result = lspModule.formatDiagnostics(diagnostics);
|
||||
|
||||
expect(result).not.toContain('(');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getFormattedFileDiagnostics - 获取格式化的文件诊断', () => {
|
||||
it('无错误和警告时返回空字符串', async () => {
|
||||
mockGetFileDiagnostics.mockReturnValue([
|
||||
{ file: '/test/file.ts', line: 1, column: 1, severity: 'info', message: 'Info' },
|
||||
{ file: '/test/file.ts', line: 2, column: 2, severity: 'hint', message: 'Hint' },
|
||||
]);
|
||||
lspModule.initLSP();
|
||||
|
||||
const result = await lspModule.getFormattedFileDiagnostics('/test/file.ts');
|
||||
|
||||
expect(result).toBe('');
|
||||
});
|
||||
|
||||
it('只显示错误和警告', async () => {
|
||||
mockGetFileDiagnostics.mockReturnValue([
|
||||
{ file: '/test/file.ts', line: 1, column: 1, severity: 'error', message: 'Error' },
|
||||
{ file: '/test/file.ts', line: 2, column: 2, severity: 'warning', message: 'Warning' },
|
||||
{ file: '/test/file.ts', line: 3, column: 3, severity: 'info', message: 'Info' },
|
||||
]);
|
||||
lspModule.initLSP();
|
||||
|
||||
const result = await lspModule.getFormattedFileDiagnostics('/test/file.ts');
|
||||
|
||||
expect(result).toContain('Error');
|
||||
expect(result).toContain('Warning');
|
||||
expect(result).not.toContain('Info');
|
||||
});
|
||||
|
||||
it('包含 file_diagnostics 标签', async () => {
|
||||
mockGetFileDiagnostics.mockReturnValue([
|
||||
{ file: '/test/file.ts', line: 1, column: 1, severity: 'error', message: 'Error' },
|
||||
]);
|
||||
lspModule.initLSP();
|
||||
|
||||
const result = await lspModule.getFormattedFileDiagnostics('/test/file.ts');
|
||||
|
||||
expect(result).toContain('<file_diagnostics');
|
||||
expect(result).toContain('/test/file.ts');
|
||||
expect(result).toContain('</file_diagnostics>');
|
||||
});
|
||||
|
||||
it('显示错误和警告数量', async () => {
|
||||
mockGetFileDiagnostics.mockReturnValue([
|
||||
{ file: '/test/file.ts', line: 1, column: 1, severity: 'error', message: 'Error 1' },
|
||||
{ file: '/test/file.ts', line: 2, column: 2, severity: 'error', message: 'Error 2' },
|
||||
{ file: '/test/file.ts', line: 3, column: 3, severity: 'warning', message: 'Warning' },
|
||||
]);
|
||||
lspModule.initLSP();
|
||||
|
||||
const result = await lspModule.getFormattedFileDiagnostics('/test/file.ts');
|
||||
|
||||
expect(result).toContain('2 个错误');
|
||||
expect(result).toContain('1 个警告');
|
||||
});
|
||||
});
|
||||
|
||||
describe('shutdownLSP - 关闭 LSP', () => {
|
||||
it('关闭管理器', async () => {
|
||||
lspModule.initLSP();
|
||||
|
||||
await lspModule.shutdownLSP();
|
||||
|
||||
expect(mockShutdown).toHaveBeenCalled();
|
||||
expect(lspModule.getLSPManager()).toBeNull();
|
||||
});
|
||||
|
||||
it('未初始化时安全调用', async () => {
|
||||
vi.resetModules();
|
||||
const freshModule = await import('../../../src/lsp/index.js');
|
||||
|
||||
await expect(freshModule.shutdownLSP()).resolves.not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('导出', () => {
|
||||
it('导出 getLanguageId', () => {
|
||||
expect(lspModule.getLanguageId).toBeDefined();
|
||||
});
|
||||
|
||||
it('导出 isLanguageSupported', () => {
|
||||
expect(lspModule.isLanguageSupported).toBeDefined();
|
||||
});
|
||||
|
||||
it('导出 getSupportedExtensions', () => {
|
||||
expect(lspModule.getSupportedExtensions).toBeDefined();
|
||||
});
|
||||
|
||||
it('导出 LSPClientManager', () => {
|
||||
expect(lspModule.LSPClientManager).toBeDefined();
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user