chore: initialize qiming workspace repository

This commit is contained in:
Codex
2026-05-29 14:22:48 +08:00
commit bfd67a0f2c
10750 changed files with 1885711 additions and 0 deletions

View File

@@ -0,0 +1,483 @@
/**
* Smoke tests: PersistentMcpBridge
*
* Verifies the bridge lifecycle (start/stop), HTTP routing, tool proxying,
* and auto-restart behavior using the same mock MCP server fixture.
*/
import { describe, it, expect, afterEach } from 'vitest';
import * as path from 'path';
import * as net from 'net';
import { PersistentMcpBridge } from '../src/bridge.js';
import type { BridgeLogger } from '../src/bridge.js';
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
/**
* 动态占用一个随机可用端口,返回端口号和释放函数。
* 用于模拟端口冲突,测试 start() 的容错清理逻辑。
*/
function occupyRandomPort(): Promise<{ port: number; release: () => Promise<void> }> {
return new Promise((resolve, reject) => {
const server = net.createServer();
server.listen(0, '127.0.0.1', () => {
const addr = server.address() as net.AddressInfo;
resolve({
port: addr.port,
release: () => new Promise<void>((res) => server.close(() => res())),
});
});
server.on('error', reject);
});
}
const MOCK_SERVER = path.resolve('tests/fixtures/mock-mcp-server.mjs');
/** Silent logger that captures messages for assertions */
function createTestLogger(): BridgeLogger & { messages: string[] } {
const messages: string[] = [];
return {
messages,
info: (...args: unknown[]) => messages.push(`INFO: ${args.map(String).join(' ')}`),
warn: (...args: unknown[]) => messages.push(`WARN: ${args.map(String).join(' ')}`),
error: (...args: unknown[]) => messages.push(`ERROR: ${args.map(String).join(' ')}`),
};
}
describe('PersistentMcpBridge', () => {
let bridge: PersistentMcpBridge;
afterEach(async () => {
if (bridge) {
await bridge.stop();
}
});
// ---- Construction & initial state ----
it('isRunning() returns false before start()', () => {
bridge = new PersistentMcpBridge();
expect(bridge.isRunning()).toBe(false);
});
it('getBridgeUrl() returns null before start()', () => {
bridge = new PersistentMcpBridge();
expect(bridge.getBridgeUrl('anything')).toBeNull();
});
it('isServerHealthy() returns false for unknown server', () => {
bridge = new PersistentMcpBridge();
expect(bridge.isServerHealthy('nonexistent')).toBe(false);
});
it('accepts a custom logger', () => {
const logger = createTestLogger();
bridge = new PersistentMcpBridge(logger);
expect(bridge.isRunning()).toBe(false);
});
it('stop() is safe to call before start()', async () => {
bridge = new PersistentMcpBridge();
await expect(bridge.stop()).resolves.toBeUndefined();
});
// ---- Lifecycle with real mock server ----
it('start() spawns server and exposes HTTP bridge', async () => {
const logger = createTestLogger();
bridge = new PersistentMcpBridge(logger);
await bridge.start({
'mock': {
command: 'node',
args: [MOCK_SERVER, '--tools', '["bridge-tool-a","bridge-tool-b"]', '--name', 'bridge-mock'],
},
});
expect(bridge.isRunning()).toBe(true);
expect(bridge.isServerHealthy('mock')).toBe(true);
const url = bridge.getBridgeUrl('mock');
expect(url).not.toBeNull();
expect(url).toMatch(/^http:\/\/127\.0\.0\.1:\d+\/mcp\/mock$/);
});
it('getBridgeUrl() returns null for unknown server after start', async () => {
bridge = new PersistentMcpBridge();
await bridge.start({
'mock': {
command: 'node',
args: [MOCK_SERVER],
},
});
expect(bridge.getBridgeUrl('nonexistent')).toBeNull();
});
it('stop() cleans up and sets isRunning to false', async () => {
bridge = new PersistentMcpBridge();
await bridge.start({
'mock': {
command: 'node',
args: [MOCK_SERVER],
},
});
expect(bridge.isRunning()).toBe(true);
await bridge.stop();
expect(bridge.isRunning()).toBe(false);
expect(bridge.isServerHealthy('mock')).toBe(false);
expect(bridge.getBridgeUrl('mock')).toBeNull();
});
// ---- HTTP bridge integration ----
it('proxies tools via HTTP bridge (list + call)', async () => {
bridge = new PersistentMcpBridge();
await bridge.start({
'mock': {
command: 'node',
args: [MOCK_SERVER, '--tools', '["alpha"]', '--name', 'proxy-test'],
},
});
const url = bridge.getBridgeUrl('mock')!;
expect(url).toBeTruthy();
// Connect as an MCP client via StreamableHTTP
const transport = new StreamableHTTPClientTransport(new URL(url));
const client = new Client({ name: 'test-client', version: '1.0.0' });
await client.connect(transport);
try {
// List tools
const { tools } = await client.listTools();
expect(tools).toHaveLength(1);
expect(tools[0].name).toBe('alpha');
// Call tool
const result = await client.callTool({ name: 'alpha', arguments: {} });
expect(result.content).toEqual([
{ type: 'text', text: 'proxy-test:alpha' },
]);
} finally {
await client.close();
await transport.close();
}
});
it('returns 404 for invalid HTTP paths', async () => {
bridge = new PersistentMcpBridge();
await bridge.start({
'mock': { command: 'node', args: [MOCK_SERVER] },
});
const url = bridge.getBridgeUrl('mock')!;
// Extract base URL (without /mcp/mock)
const baseUrl = url.replace('/mcp/mock', '');
const res = await fetch(`${baseUrl}/invalid`);
expect(res.status).toBe(404);
});
it('returns 503 for unknown server ID', async () => {
bridge = new PersistentMcpBridge();
await bridge.start({
'mock': { command: 'node', args: [MOCK_SERVER] },
});
const url = bridge.getBridgeUrl('mock')!;
const baseUrl = url.replace('/mcp/mock', '');
const res = await fetch(`${baseUrl}/mcp/nonexistent`, { method: 'POST' });
expect(res.status).toBe(503);
});
// ---- Multiple servers (parallel start/stop) ----
/**
* 验证 3 个 server 并行启动start() 返回后全部健康,且各自 URL 正确路由。
* 如果内部退化为串行,此测试仍应通过(正确性不变),但性能会下降。
*/
it('starts 3 servers in parallel and all become healthy', async () => {
bridge = new PersistentMcpBridge();
await bridge.start({
'svc-alpha': {
command: 'node',
args: [MOCK_SERVER, '--tools', '["alpha-tool"]', '--name', 'alpha'],
},
'svc-beta': {
command: 'node',
args: [MOCK_SERVER, '--tools', '["beta-tool"]', '--name', 'beta'],
},
'svc-gamma': {
command: 'node',
args: [MOCK_SERVER, '--tools', '["gamma-tool"]', '--name', 'gamma'],
},
});
// 全部健康
expect(bridge.isServerHealthy('svc-alpha')).toBe(true);
expect(bridge.isServerHealthy('svc-beta')).toBe(true);
expect(bridge.isServerHealthy('svc-gamma')).toBe(true);
// URL 路由各自独立
expect(bridge.getBridgeUrl('svc-alpha')).toMatch(/\/mcp\/svc-alpha$/);
expect(bridge.getBridgeUrl('svc-beta')).toMatch(/\/mcp\/svc-beta$/);
expect(bridge.getBridgeUrl('svc-gamma')).toMatch(/\/mcp\/svc-gamma$/);
// 每个 server 只暴露自己的工具
for (const [serverId, toolName, namePrefix] of [
['svc-alpha', 'alpha-tool', 'alpha'],
['svc-beta', 'beta-tool', 'beta'],
['svc-gamma', 'gamma-tool', 'gamma'],
] as const) {
const url = bridge.getBridgeUrl(serverId)!;
const transport = new StreamableHTTPClientTransport(new URL(url));
const client = new Client({ name: 'parallel-test', version: '1.0.0' });
await client.connect(transport);
try {
const { tools } = await client.listTools();
expect(tools).toHaveLength(1);
expect(tools[0].name).toBe(toolName);
const result = await client.callTool({ name: toolName, arguments: {} });
expect(result.content).toEqual([{ type: 'text', text: `${namePrefix}:${toolName}` }]);
} finally {
await client.close();
await transport.close();
}
}
});
it('stops all servers and bridge becomes not running', async () => {
bridge = new PersistentMcpBridge();
await bridge.start({
'stop-a': { command: 'node', args: [MOCK_SERVER, '--name', 'stop-a'] },
'stop-b': { command: 'node', args: [MOCK_SERVER, '--name', 'stop-b'] },
'stop-c': { command: 'node', args: [MOCK_SERVER, '--name', 'stop-c'] },
});
expect(bridge.isRunning()).toBe(true);
await bridge.stop();
// 并行停止后整体不再运行
expect(bridge.isRunning()).toBe(false);
// 每个 server 均已清理
expect(bridge.isServerHealthy('stop-a')).toBe(false);
expect(bridge.isServerHealthy('stop-b')).toBe(false);
expect(bridge.isServerHealthy('stop-c')).toBe(false);
expect(bridge.getBridgeUrl('stop-a')).toBeNull();
});
it('handles multiple concurrent servers', async () => {
bridge = new PersistentMcpBridge();
await bridge.start({
'server-a': {
command: 'node',
args: [MOCK_SERVER, '--tools', '["tool-a"]', '--name', 'a'],
},
'server-b': {
command: 'node',
args: [MOCK_SERVER, '--tools', '["tool-b"]', '--name', 'b'],
},
});
expect(bridge.isServerHealthy('server-a')).toBe(true);
expect(bridge.isServerHealthy('server-b')).toBe(true);
const urlA = bridge.getBridgeUrl('server-a')!;
const urlB = bridge.getBridgeUrl('server-b')!;
expect(urlA).toContain('/mcp/server-a');
expect(urlB).toContain('/mcp/server-b');
// Verify each proxies its own tools
for (const [url, expectedTool, expectedPrefix] of [
[urlA, 'tool-a', 'a:tool-a'],
[urlB, 'tool-b', 'b:tool-b'],
] as const) {
const transport = new StreamableHTTPClientTransport(new URL(url));
const client = new Client({ name: 'test', version: '1.0.0' });
await client.connect(transport);
try {
const { tools } = await client.listTools();
expect(tools[0].name).toBe(expectedTool);
const result = await client.callTool({ name: expectedTool, arguments: {} });
expect(result.content).toEqual([{ type: 'text', text: expectedPrefix }]);
} finally {
await client.close();
await transport.close();
}
}
});
// ---- Error handling ----
it('handles server spawn failure gracefully', async () => {
const logger = createTestLogger();
bridge = new PersistentMcpBridge(logger);
await bridge.start({
'bad': {
command: 'nonexistent-binary-that-does-not-exist',
args: [],
},
});
// Bridge should still be running (HTTP server started)
expect(bridge.isRunning()).toBe(true);
// But the server should not be healthy
expect(bridge.isServerHealthy('bad')).toBe(false);
expect(bridge.getBridgeUrl('bad')).toBeNull();
// Logger should capture the failure
const hasError = logger.messages.some((m) => m.includes('Failed to start server'));
expect(hasError).toBe(true);
});
/**
* 验证 start() 中 HTTP server 启动失败(端口冲突)时:
* 1. start() 应抛出异常
* 2. bridge 处于未运行状态isRunning = false
* 3. 所有 spawnAndConnect 完成后子进程被正确关闭(无孤儿进程)
* 4. bridge 实例回到可重用状态,可以重新正常启动
*/
it('start() throws and cleans up spawned processes when HTTP server fails to bind', async () => {
const { port, release } = await occupyRandomPort();
try {
const logger = createTestLogger();
bridge = new PersistentMcpBridge(logger);
// 使用被占用的端口 → startHttpServer 应抛 EADDRINUSE
await expect(
bridge.start(
{ 'mock': { command: 'node', args: [MOCK_SERVER, '--name', 'cleanup-test'] } },
{ port },
),
).rejects.toThrow();
// 失败后 bridge 必须处于未运行状态
expect(bridge.isRunning()).toBe(false);
expect(bridge.getBridgeUrl('mock')).toBeNull();
// 失败日志记录(来自 catch 块)
expect(logger.messages.some((m) => m.includes('启动失败'))).toBe(true);
} finally {
await release();
}
// 释放端口后,同一个 bridge 实例应能正常重新启动
await bridge.start({
'mock-retry': { command: 'node', args: [MOCK_SERVER, '--name', 'retry'] },
});
expect(bridge.isRunning()).toBe(true);
expect(bridge.isServerHealthy('mock-retry')).toBe(true);
});
/**
* 验证 stop() 并行关闭多个活跃 HTTP session
* 即使同时存在多个 sessionstop() 也能正确完成并清理全部 session。
*/
it('stop() closes multiple active HTTP sessions in parallel', async () => {
bridge = new PersistentMcpBridge();
await bridge.start({
'mock': {
command: 'node',
args: [MOCK_SERVER, '--tools', '["tool-x","tool-y"]', '--name', 'multi-session'],
},
});
const url = bridge.getBridgeUrl('mock')!;
// 建立 3 个独立 HTTP session
const connections: Array<{ client: Client; transport: StreamableHTTPClientTransport }> = [];
for (let i = 0; i < 3; i++) {
const transport = new StreamableHTTPClientTransport(new URL(url));
const client = new Client({ name: `multi-session-client-${i}`, version: '1.0.0' });
await client.connect(transport);
connections.push({ client, transport });
}
// 3 个 session 建立期间bridge 应保持健康
expect(bridge.isServerHealthy('mock')).toBe(true);
// 调用 stop(),验证多 session 并行关闭后 bridge 完全停止
await bridge.stop();
expect(bridge.isRunning()).toBe(false);
expect(bridge.isServerHealthy('mock')).toBe(false);
expect(bridge.getBridgeUrl('mock')).toBeNull();
// 清理客户端stop 后 transport 可能已关闭,忽略错误)
for (const { client, transport } of connections) {
try { await client.close(); } catch { /* ignore */ }
try { await transport.close(); } catch { /* ignore */ }
}
});
// ---- Logger ----
it('logs lifecycle events via injected logger', async () => {
const logger = createTestLogger();
bridge = new PersistentMcpBridge(logger);
await bridge.start({
'mock': { command: 'node', args: [MOCK_SERVER] },
});
await bridge.stop();
const hasStarting = logger.messages.some((m) => m.includes('Starting with'));
const hasReady = logger.messages.some((m) => m.includes('Bridge ready'));
const hasStopped = logger.messages.some((m) => m.includes('Stopped'));
expect(hasStarting).toBe(true);
expect(hasReady).toBe(true);
expect(hasStopped).toBe(true);
});
// ---- Explicit port ----
it('start() with explicit port listens on that port', async () => {
bridge = new PersistentMcpBridge();
await bridge.start(
{
'mock': {
command: 'node',
args: [MOCK_SERVER, '--tools', '["port-tool"]', '--name', 'port-test'],
},
},
{ port: 18199 },
);
expect(bridge.isRunning()).toBe(true);
const url = bridge.getBridgeUrl('mock');
expect(url).toBe('http://127.0.0.1:18199/mcp/mock');
// Verify it's actually listening on that port by connecting
const transport = new StreamableHTTPClientTransport(new URL(url!));
const client = new Client({ name: 'port-test-client', version: '1.0.0' });
await client.connect(transport);
try {
const { tools } = await client.listTools();
expect(tools).toHaveLength(1);
expect(tools[0].name).toBe('port-tool');
} finally {
await client.close();
await transport.close();
}
});
});