5e32375f0e
架构变更: - 采用 pnpm workspaces 实现 Monorepo 结构 - 将现有代码迁移到 packages/core - 新增 packages/server HTTP 服务层 Server 功能: - REST API: 会话管理、工具管理、配置管理 - WebSocket: 实时双向通信支持 - SSE: 服务端事件推送 - Hono + Bun 作为运行时 API 端点: - GET/POST /api/sessions - 会话 CRUD - GET/POST /api/sessions/:id/messages - 消息管理 - GET /api/sessions/:id/events - SSE 事件流 - WS /api/ws/:sessionId - WebSocket 连接 - GET/POST /api/tools - 工具管理 - GET/PUT /api/config - 配置管理
347 lines
10 KiB
TypeScript
347 lines
10 KiB
TypeScript
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();
|
|
});
|
|
});
|
|
});
|