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
+356
View File
@@ -0,0 +1,356 @@
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import {
listServers,
printServerList,
installServer,
installAllServers,
showServerInfo,
type ServerStatus,
} from '../../../src/lsp/cli.js';
// Mock child_process
vi.mock('child_process', () => ({
execSync: vi.fn(),
spawnSync: vi.fn(),
}));
// Mock server module
vi.mock('../../../src/lsp/server.js', () => ({
getUniqueServers: vi.fn(() => [
{
id: 'typescript-language-server',
languages: ['typescript', 'javascript'],
config: {
displayName: 'TypeScript Language Server',
description: 'TypeScript/JavaScript 语言服务器',
command: 'typescript-language-server',
install: {
npm: 'typescript-language-server typescript',
},
},
},
{
id: 'pylsp',
languages: ['python'],
config: {
displayName: 'Python LSP Server',
description: 'Python 语言服务器',
command: 'pylsp',
install: {
pip: 'python-lsp-server',
manual: '也可以使用: pip3 install python-lsp-server',
},
},
},
{
id: 'gopls',
languages: ['go'],
config: {
displayName: 'Go Language Server',
description: 'Go 语言服务器',
command: 'gopls',
install: {
go: 'golang.org/x/tools/gopls@latest',
},
},
},
]),
}));
import { execSync, spawnSync } from 'child_process';
describe('LSP CLI - LSP 命令行工具', () => {
let consoleLogSpy: ReturnType<typeof vi.spyOn>;
let consoleErrorSpy: ReturnType<typeof vi.spyOn>;
beforeEach(() => {
vi.clearAllMocks();
consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {});
consoleErrorSpy = vi.spyOn(console, 'error').mockImplementation(() => {});
// 默认命令存在
vi.mocked(execSync).mockImplementation((cmd: string) => {
if (cmd.includes('which npm')) return Buffer.from('/usr/bin/npm');
if (cmd.includes('which pip3')) return Buffer.from('/usr/bin/pip3');
if (cmd.includes('which go')) return Buffer.from('/usr/bin/go');
throw new Error('Command not found');
});
});
afterEach(() => {
consoleLogSpy.mockRestore();
consoleErrorSpy.mockRestore();
});
describe('listServers - 列出服务器', () => {
it('返回所有服务器状态', () => {
const servers = listServers();
expect(servers).toHaveLength(3);
expect(servers[0].id).toBe('typescript-language-server');
expect(servers[1].id).toBe('pylsp');
expect(servers[2].id).toBe('gopls');
});
it('检测已安装的服务器', () => {
// 只有 typescript-language-server 安装了
vi.mocked(execSync).mockImplementation((cmd: string) => {
if (cmd === 'which typescript-language-server') return Buffer.from('/usr/bin/tsc');
throw new Error('not found');
});
const servers = listServers();
expect(servers[0].installed).toBe(true);
expect(servers[1].installed).toBe(false);
expect(servers[2].installed).toBe(false);
});
it('包含服务器详细信息', () => {
const servers = listServers();
const tsServer = servers[0];
expect(tsServer.displayName).toBe('TypeScript Language Server');
expect(tsServer.description).toContain('TypeScript');
expect(tsServer.command).toBe('typescript-language-server');
expect(tsServer.languages).toContain('typescript');
expect(tsServer.install).toBeDefined();
});
});
describe('printServerList - 打印服务器列表', () => {
it('输出格式化的服务器列表', () => {
printServerList();
expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('语言服务器状态'));
expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('状态'));
expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('服务器'));
});
it('显示已安装数量统计', () => {
printServerList();
const calls = consoleLogSpy.mock.calls.flat().join('\n');
expect(calls).toContain('已安装');
});
});
describe('installServer - 安装服务器', () => {
it('服务器不存在时返回 false', async () => {
const result = await installServer('non-existent');
expect(result).toBe(false);
expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining('未找到服务器'));
});
it('服务器已安装时返回 true', async () => {
vi.mocked(execSync).mockImplementation((cmd: string) => {
if (cmd === 'which typescript-language-server') return Buffer.from('/usr/bin/tsc');
if (cmd.includes('which npm')) return Buffer.from('/usr/bin/npm');
throw new Error('not found');
});
const result = await installServer('typescript-language-server');
expect(result).toBe(true);
expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('已安装'));
});
it('使用 npm 安装 TypeScript 服务器', async () => {
vi.mocked(execSync).mockImplementation((cmd: string) => {
if (cmd === 'which typescript-language-server') throw new Error('not found');
if (cmd.includes('which npm')) return Buffer.from('/usr/bin/npm');
throw new Error('not found');
});
vi.mocked(spawnSync).mockReturnValue({ status: 0 } as any);
const result = await installServer('typescript-language-server');
expect(result).toBe(true);
expect(spawnSync).toHaveBeenCalledWith(
expect.stringContaining('npm install -g'),
expect.any(Object)
);
});
it('安装失败时返回 false', async () => {
vi.mocked(execSync).mockImplementation((cmd: string) => {
if (cmd === 'which typescript-language-server') throw new Error('not found');
if (cmd.includes('which npm')) return Buffer.from('/usr/bin/npm');
throw new Error('not found');
});
vi.mocked(spawnSync).mockReturnValue({ status: 1 } as any);
const result = await installServer('typescript-language-server');
expect(result).toBe(false);
expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining('安装失败'));
});
it('按显示名称查找服务器', async () => {
vi.mocked(execSync).mockImplementation((cmd: string) => {
if (cmd === 'which typescript-language-server') return Buffer.from('/usr/bin/tsc');
if (cmd.includes('which npm')) return Buffer.from('/usr/bin/npm');
throw new Error('not found');
});
const result = await installServer('TypeScript Language Server');
expect(result).toBe(true);
});
it('spawnSync 抛出异常时返回 false', async () => {
vi.mocked(execSync).mockImplementation((cmd: string) => {
if (cmd === 'which typescript-language-server') throw new Error('not found');
if (cmd.includes('which npm')) return Buffer.from('/usr/bin/npm');
throw new Error('not found');
});
vi.mocked(spawnSync).mockImplementation(() => {
throw new Error('spawn error');
});
const result = await installServer('typescript-language-server');
expect(result).toBe(false);
expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining('安装出错'));
});
it('无法自动安装时显示手动说明', async () => {
vi.mocked(execSync).mockImplementation((cmd: string) => {
// pylsp 未安装,且没有可用的 pip
if (cmd === 'which pylsp') throw new Error('not found');
throw new Error('not found');
});
const result = await installServer('pylsp');
expect(result).toBe(false);
expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining('无法自动安装'));
});
});
describe('installAllServers - 安装所有服务器', () => {
it('所有服务器都已安装时显示完成', async () => {
vi.mocked(execSync).mockReturnValue(Buffer.from('/usr/bin/cmd'));
await installAllServers();
expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('都已安装'));
});
it('安装未安装的服务器', async () => {
vi.mocked(execSync).mockImplementation((cmd: string) => {
// 所有服务器都未安装,但有 npm
if (cmd.includes('which npm')) return Buffer.from('/usr/bin/npm');
throw new Error('not found');
});
vi.mocked(spawnSync).mockReturnValue({ status: 0 } as any);
await installAllServers();
expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('将安装'));
expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('安装完成'));
});
});
describe('showServerInfo - 显示服务器信息', () => {
it('显示已安装服务器的信息', () => {
vi.mocked(execSync).mockImplementation((cmd: string) => {
if (cmd === 'which typescript-language-server') return Buffer.from('/usr/bin/tsc');
if (cmd.includes('which npm')) return Buffer.from('/usr/bin/npm');
throw new Error('not found');
});
showServerInfo('typescript-language-server');
const calls = consoleLogSpy.mock.calls.flat().join('\n');
expect(calls).toContain('TypeScript Language Server');
expect(calls).toContain('已安装');
});
it('显示未安装服务器的安装命令', () => {
vi.mocked(execSync).mockImplementation((cmd: string) => {
if (cmd === 'which typescript-language-server') throw new Error('not found');
if (cmd.includes('which npm')) return Buffer.from('/usr/bin/npm');
throw new Error('not found');
});
showServerInfo('typescript-language-server');
const calls = consoleLogSpy.mock.calls.flat().join('\n');
expect(calls).toContain('未安装');
expect(calls).toContain('安装命令');
expect(calls).toContain('npm install');
});
it('服务器不存在时显示错误', () => {
showServerInfo('non-existent');
expect(consoleErrorSpy).toHaveBeenCalledWith(expect.stringContaining('未找到服务器'));
});
it('按显示名称查找服务器', () => {
vi.mocked(execSync).mockReturnValue(Buffer.from('/usr/bin/cmd'));
showServerInfo('Python LSP Server');
const calls = consoleLogSpy.mock.calls.flat().join('\n');
expect(calls).toContain('Python LSP Server');
});
});
describe('getInstallCommand - 安装命令选择', () => {
it('pip 作为 Python 服务器的安装方式', async () => {
vi.mocked(execSync).mockImplementation((cmd: string) => {
if (cmd === 'which pylsp') throw new Error('not found');
if (cmd.includes('which pip3')) return Buffer.from('/usr/bin/pip3');
throw new Error('not found');
});
vi.mocked(spawnSync).mockReturnValue({ status: 0 } as any);
await installServer('pylsp');
expect(spawnSync).toHaveBeenCalledWith(
expect.stringContaining('pip3 install'),
expect.any(Object)
);
});
it('go install 作为 Go 服务器的安装方式', async () => {
vi.mocked(execSync).mockImplementation((cmd: string) => {
if (cmd === 'which gopls') throw new Error('not found');
if (cmd.includes('which go')) return Buffer.from('/usr/bin/go');
throw new Error('not found');
});
vi.mocked(spawnSync).mockReturnValue({ status: 0 } as any);
await installServer('gopls');
expect(spawnSync).toHaveBeenCalledWith(
expect.stringContaining('go install'),
expect.any(Object)
);
});
});
});
describe('ServerStatus 类型', () => {
it('包含所有必要字段', () => {
const status: ServerStatus = {
id: 'test-server',
displayName: 'Test Server',
description: 'A test server',
command: 'test-cmd',
installed: true,
languages: ['typescript'],
install: { npm: 'test-package' },
};
expect(status.id).toBeDefined();
expect(status.displayName).toBeDefined();
expect(status.command).toBeDefined();
expect(status.installed).toBeDefined();
expect(status.languages).toBeDefined();
expect(status.install).toBeDefined();
});
});
+357
View File
@@ -0,0 +1,357 @@
import { describe, it, expect, beforeEach, vi } from 'vitest';
import { LSPClientManager } from '../../../src/lsp/client.js';
import { DiagnosticSeverity } from 'vscode-languageserver-protocol';
// Mock child_process
const mockSpawn = vi.fn();
const mockExecSync = vi.fn();
vi.mock('child_process', () => ({
spawn: (...args: unknown[]) => mockSpawn(...args),
execSync: (...args: unknown[]) => mockExecSync(...args),
}));
// Mock vscode-jsonrpc
const mockConnection = {
listen: vi.fn(),
sendRequest: vi.fn().mockResolvedValue({}),
sendNotification: vi.fn(),
onNotification: vi.fn(),
dispose: vi.fn(),
};
vi.mock('vscode-jsonrpc/node.js', () => ({
createMessageConnection: vi.fn(() => mockConnection),
StreamMessageReader: vi.fn(),
StreamMessageWriter: vi.fn(),
}));
// Mock fs/promises
vi.mock('fs/promises', () => ({
readFile: vi.fn().mockResolvedValue('file content'),
}));
// 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;
}),
}));
// Mock server module
vi.mock('../../../src/lsp/server.js', () => ({
getServerConfig: vi.fn((languageId: string) => {
if (languageId === 'typescript') {
return {
command: 'typescript-language-server',
args: ['--stdio'],
env: {},
initializationOptions: {},
};
}
if (languageId === 'python') {
return {
command: 'pylsp',
args: [],
env: {},
};
}
return null;
}),
}));
import { getLanguageId } from '../../../src/lsp/language.js';
describe('LSPClientManager - 扩展测试', () => {
let manager: LSPClientManager;
beforeEach(() => {
vi.clearAllMocks();
manager = new LSPClientManager('/test/project');
// 默认命令存在
mockExecSync.mockReturnValue(Buffer.from(''));
// 模拟 spawn 返回的进程
mockSpawn.mockReturnValue({
stdin: { on: vi.fn(), write: vi.fn() },
stdout: { on: vi.fn(), pipe: vi.fn() },
stderr: { on: vi.fn() },
on: vi.fn(),
kill: vi.fn(),
pid: 12345,
});
});
describe('getClient - 启动客户端', () => {
it('重复获取同一语言返回相同客户端', async () => {
const client1 = await manager.getClient('typescript');
const client2 = await manager.getClient('typescript');
expect(client1).toBe(client2);
});
it('成功启动时设置初始化状态', async () => {
const client = await manager.getClient('typescript');
if (client) {
expect(client.initialized).toBe(true);
expect(client.languageId).toBe('typescript');
}
});
});
describe('touchFile - 文件变更通知', () => {
it('相对路径转换为绝对路径', async () => {
vi.mocked(getLanguageId).mockReturnValue('typescript');
// 先获取客户端
await manager.getClient('typescript');
// touchFile 应该能处理相对路径
const result = await manager.touchFile('relative/file.ts');
// 由于客户端已初始化,应该调用通知
expect(typeof result).toBe('boolean');
});
it('首次启动返回 true', async () => {
vi.mocked(getLanguageId).mockReturnValue('typescript');
const result = await manager.touchFile('/test/file.ts');
// 首次启动服务器应返回 true
expect(result).toBe(true);
});
it('后续调用返回 false', async () => {
vi.mocked(getLanguageId).mockReturnValue('typescript');
// 首次调用
await manager.touchFile('/test/file.ts');
// 第二次调用
const result = await manager.touchFile('/test/file.ts');
// 已经在运行,返回 false
expect(result).toBe(false);
});
});
describe('closeFile - 关闭文件', () => {
it('正常关闭打开的文件', async () => {
vi.mocked(getLanguageId).mockReturnValue('typescript');
// 先打开文件
await manager.touchFile('/test/file.ts');
// 关闭文件
await manager.closeFile('/test/file.ts');
expect(mockConnection.sendNotification).toHaveBeenCalledWith(
'textDocument/didClose',
expect.any(Object)
);
});
});
describe('诊断信息转换', () => {
it('正确转换诊断严重性', async () => {
// 通过 onNotification 回调模拟诊断
let diagnosticsCallback: ((params: unknown) => void) | null = null;
mockConnection.onNotification.mockImplementation((method: string, callback: (params: unknown) => void) => {
if (method === 'textDocument/publishDiagnostics') {
diagnosticsCallback = callback;
}
});
vi.mocked(getLanguageId).mockReturnValue('typescript');
await manager.getClient('typescript');
// 模拟收到诊断
if (diagnosticsCallback) {
diagnosticsCallback({
uri: 'file:///test/file.ts',
diagnostics: [
{
range: { start: { line: 0, character: 0 }, end: { line: 0, character: 10 } },
severity: DiagnosticSeverity.Error,
message: 'Test error',
source: 'typescript',
code: 'TS2345',
},
{
range: { start: { line: 1, character: 0 }, end: { line: 1, character: 5 } },
severity: DiagnosticSeverity.Warning,
message: 'Test warning',
},
{
range: { start: { line: 2, character: 0 }, end: { line: 2, character: 5 } },
severity: DiagnosticSeverity.Information,
message: 'Test info',
},
{
range: { start: { line: 3, character: 0 }, end: { line: 3, character: 5 } },
severity: DiagnosticSeverity.Hint,
message: 'Test hint',
},
],
});
}
// 获取诊断
const diagnostics = manager.getFileDiagnostics('/test/file.ts');
expect(diagnostics.length).toBe(4);
expect(diagnostics[0].severity).toBe('error');
expect(diagnostics[1].severity).toBe('warning');
expect(diagnostics[2].severity).toBe('info');
expect(diagnostics[3].severity).toBe('hint');
});
it('转换行列号(从 0 开始转为 1 开始)', async () => {
let diagnosticsCallback: ((params: unknown) => void) | null = null;
mockConnection.onNotification.mockImplementation((method: string, callback: (params: unknown) => void) => {
if (method === 'textDocument/publishDiagnostics') {
diagnosticsCallback = callback;
}
});
vi.mocked(getLanguageId).mockReturnValue('typescript');
await manager.getClient('typescript');
if (diagnosticsCallback) {
diagnosticsCallback({
uri: 'file:///test/file.ts',
diagnostics: [
{
range: { start: { line: 9, character: 4 }, end: { line: 9, character: 20 } },
severity: DiagnosticSeverity.Error,
message: 'Error',
},
],
});
}
const diagnostics = manager.getFileDiagnostics('/test/file.ts');
expect(diagnostics[0].line).toBe(10); // 9 + 1
expect(diagnostics[0].column).toBe(5); // 4 + 1
expect(diagnostics[0].endLine).toBe(10);
expect(diagnostics[0].endColumn).toBe(21);
});
});
describe('shutdown - 关闭所有客户端', () => {
it('关闭所有运行中的客户端', async () => {
vi.mocked(getLanguageId).mockImplementation((path: string) => {
if (path.endsWith('.ts')) return 'typescript';
if (path.endsWith('.py')) return 'python';
return undefined;
});
// 启动多个客户端
await manager.getClient('typescript');
await manager.getClient('python');
expect(manager.getRunningServers().length).toBe(2);
// 关闭
await manager.shutdown();
expect(manager.getRunningServers().length).toBe(0);
});
it('处理关闭时的错误', async () => {
vi.mocked(getLanguageId).mockReturnValue('typescript');
await manager.getClient('typescript');
// 模拟 dispose 抛出错误
mockConnection.dispose.mockImplementation(() => {
throw new Error('Dispose error');
});
// 应该不抛出错误
await expect(manager.shutdown()).resolves.not.toThrow();
});
});
describe('进程事件处理', () => {
it('进程退出时清理客户端', async () => {
let exitCallback: (() => void) | null = null;
mockSpawn.mockReturnValue({
stdin: { on: vi.fn(), write: vi.fn() },
stdout: { on: vi.fn(), pipe: vi.fn() },
stderr: { on: vi.fn() },
on: vi.fn((event: string, callback: () => void) => {
if (event === 'exit') {
exitCallback = callback;
}
}),
kill: vi.fn(),
pid: 12345,
});
vi.mocked(getLanguageId).mockReturnValue('typescript');
await manager.getClient('typescript');
expect(manager.isServerRunning('typescript')).toBe(true);
// 模拟进程退出
if (exitCallback) {
exitCallback();
}
expect(manager.isServerRunning('typescript')).toBe(false);
});
});
describe('getDiagnostics - 获取所有诊断', () => {
it('可以过滤指定文件的诊断', async () => {
let diagnosticsCallback: ((params: unknown) => void) | null = null;
mockConnection.onNotification.mockImplementation((method: string, callback: (params: unknown) => void) => {
if (method === 'textDocument/publishDiagnostics') {
diagnosticsCallback = callback;
}
});
vi.mocked(getLanguageId).mockReturnValue('typescript');
await manager.getClient('typescript');
// 为两个文件添加诊断
if (diagnosticsCallback) {
diagnosticsCallback({
uri: 'file:///test/file1.ts',
diagnostics: [
{
range: { start: { line: 0, character: 0 }, end: { line: 0, character: 10 } },
severity: DiagnosticSeverity.Error,
message: 'Error in file1',
},
],
});
diagnosticsCallback({
uri: 'file:///test/file2.ts',
diagnostics: [
{
range: { start: { line: 0, character: 0 }, end: { line: 0, character: 10 } },
severity: DiagnosticSeverity.Warning,
message: 'Warning in file2',
},
],
});
}
// 获取所有诊断
const allDiagnostics = manager.getDiagnostics();
expect(allDiagnostics.size).toBe(2);
// 只获取 file1 的诊断
const file1Diagnostics = manager.getDiagnostics('/test/file1.ts');
expect(file1Diagnostics.size).toBe(1);
expect(file1Diagnostics.has('/test/file1.ts')).toBe(true);
});
});
});
+346
View File
@@ -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();
});
});
});