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 - 配置管理
357 lines
12 KiB
TypeScript
357 lines
12 KiB
TypeScript
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();
|
|
});
|
|
});
|