Files
kurihada 5e32375f0e feat: 重构为 Monorepo 架构并实现 HTTP Server
架构变更:
- 采用 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 - 配置管理
2025-12-12 10:42:20 +08:00

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();
});
});
});