/**
* 端到端测试 - 完整流程验证
*/
import { MCPServer } from '@/mcp/MCPServer';
import { SerialService } from '@/services/SerialService';
import { SerialEngine } from '@/core/SerialEngine';
import { EventBus } from '@/core/EventBus';
import { SerialPortAdapter } from '@/adapters/SerialPortAdapter';
import { PlatformAdapter } from '@/adapters/PlatformAdapter';
import { SerialError } from '@/types';
// Mock NotificationManager to avoid hanging tests
jest.mock('@/mcp/NotificationManager', () => {
return {
NotificationManager: jest.fn().mockImplementation(() => ({
subscribe: jest.fn(),
unsubscribe: jest.fn(),
sendNotification: jest.fn(),
getSubscriptions: jest.fn().mockReturnValue([]),
destroy: jest.fn()
}))
};
});
// Mock PlatformAdapter to avoid real hardware access
jest.mock('@/adapters/PlatformAdapter', () => {
return {
PlatformAdapter: jest.fn().mockImplementation(() => ({
listPorts: jest.fn().mockResolvedValue([
{
path: 'COM1',
manufacturer: 'Virtual Device',
serialNumber: 'TEST123',
pnpId: 'PNP123',
locationId: 'LOC123',
vendorId: '0x1234',
productId: '0x5678'
},
{
path: 'COM2',
manufacturer: 'Virtual Device 2',
serialNumber: 'TEST456',
pnpId: 'PNP456',
locationId: 'LOC456',
vendorId: '0x5678',
productId: '0x9ABC'
}
]),
normalizePortPath: jest.fn((path) => path),
validatePortPath: jest.fn(() => true),
isPortInUse: jest.fn().mockResolvedValue(false),
getDeviceType: jest.fn().mockReturnValue('Virtual COM Port'),
getRecommendedConfig: jest.fn().mockReturnValue({
baudrate: 9600,
dataBits: 8,
parity: 'none',
stopBits: 1,
flowControl: 'none'
})
}))
};
});
// Mock SerialPortAdapter to avoid real hardware access
jest.mock('@/adapters/SerialPortAdapter', () => {
return {
SerialPortAdapter: jest.fn().mockImplementation(() => {
let isOpen = false;
return {
open: jest.fn().mockImplementation(async (config: any) => {
isOpen = true;
// 模拟打开延迟
await new Promise(resolve => setTimeout(resolve, 100));
}),
close: jest.fn().mockImplementation(async () => {
isOpen = false;
await new Promise(resolve => setTimeout(resolve, 50));
}),
write: jest.fn().mockImplementation(async (data: Buffer) => {
if (!isOpen) {
throw new Error('Port not open');
}
// 模拟写入延迟
await new Promise(resolve => setTimeout(resolve, 50));
}),
drain: jest.fn().mockResolvedValue(undefined),
flush: jest.fn().mockResolvedValue(undefined),
set: jest.fn().mockResolvedValue(undefined),
get: jest.fn().mockResolvedValue({
cts: true,
dsr: false,
dcd: true
}),
on: jest.fn(),
off: jest.fn(),
isOpen: jest.fn().mockReturnValue(() => isOpen)
};
})
};
});
// Mock SerialEngine to avoid health check intervals
jest.mock('@/core/SerialEngine', () => {
return {
SerialEngine: jest.fn().mockImplementation(() => {
const ports = new Map();
return {
initialize: jest.fn().mockResolvedValue(undefined),
openPort: jest.fn().mockImplementation(async (config: any) => {
ports.set(config.port, {
path: config.port,
isOpen: true,
config,
lastActivity: new Date(),
stats: {
writeCount: 0,
readCount: 0,
errorCount: 0
}
});
return {
path: config.port,
manufacturer: 'Virtual Device',
serialNumber: 'TEST123'
};
}),
closePort: jest.fn().mockImplementation(async (port: string) => {
ports.delete(port);
}),
writeAndRead: jest.fn().mockImplementation(async (port: string, data: Buffer, timeout: number, sessionId: string) => {
// 模拟读取延迟
await new Promise(resolve => setTimeout(resolve, Math.random() * 100 + 50));
// 根据数据内容返回不同的响应
const hexData = data.toString('hex');
if (hexData === '48656c6c6f') { // "Hello"
return {
status: 'ok',
sessionId,
timeoutMs: timeout,
rawHex: '48656c6c6f',
text: 'Hello',
partial: false,
urcDetected: false
};
} else if (hexData === '415445415441') { // "ATDA"
return {
status: 'ok',
sessionId,
timeoutMs: timeout,
rawHex: '4f4b',
text: 'OK',
partial: false,
urcDetected: true,
filteredUrc: ['+CSQ: 23,99']
};
}
// 默认返回空响应
return {
status: 'ok',
sessionId,
timeoutMs: timeout,
rawHex: '',
text: '',
partial: false,
urcDetected: false
};
}),
getPortStatus: jest.fn().mockImplementation((port: string) => {
const portInstance = ports.get(port);
if (!portInstance) {
throw new SerialError('NOT_OPEN', 'Port is not open', port);
}
return {
port,
isOpen: portInstance.isOpen,
config: portInstance.config,
lastActivity: portInstance.lastActivity.toISOString()
};
}),
subscribe: jest.fn().mockReturnValue('subscription-id'),
unsubscribe: jest.fn(),
closeAllPorts: jest.fn().mockResolvedValue(undefined),
dispose: jest.fn(),
getEngineStats: jest.fn().mockReturnValue({
totalPorts: ports.size,
openPorts: Array.from(ports.values()).filter(p => p.isOpen).length,
totalRequests: 0,
totalErrors: 0
}),
resetPort: jest.fn().mockResolvedValue(undefined),
batchClosePorts: jest.fn().mockResolvedValue(undefined),
getPortDetails: jest.fn().mockResolvedValue({
signals: { cts: true, dsr: false, dcd: true }
})
};
})
};
});
describe('端到端测试', () => {
let eventBus: EventBus;
let platformAdapter: PlatformAdapter;
let serialEngine: SerialEngine;
let serialService: SerialService;
let mcpServer: MCPServer;
beforeEach(async () => {
// 初始化各层
eventBus = new EventBus();
platformAdapter = new PlatformAdapter();
serialEngine = new SerialEngine(eventBus);
serialService = new SerialService(serialEngine, platformAdapter);
mcpServer = new MCPServer(
{
name: 'E2E Test Server',
version: '1.0.0',
maxConnections: 10
},
{
eventBus,
serialService
}
);
// 启动服务器
await mcpServer.start();
});
afterEach(async () => {
// 清理资源
if (mcpServer) {
await mcpServer.stop();
}
if (serialEngine) {
await serialEngine.closeAllPorts();
}
if (serialService) {
serialService.dispose();
}
});
describe('基本功能流程', () => {
test('应该能够列出可用串口', async () => {
const request = {
jsonrpc: '2.0' as const,
id: 1,
method: 'mcp.serial.list',
params: {}
};
const response = await mcpServer.handleRequest('test-conn', request);
expect(response.result).toBeDefined();
expect(response.result.status).toBe('ok');
expect(Array.isArray(response.result.ports)).toBe(true);
expect(response.result.ports.length).toBe(2);
});
test('应该能够打开和关闭串口', async () => {
// 打开端口
const openRequest = {
jsonrpc: '2.0' as const,
id: 1,
method: 'mcp.serial.open',
params: {
port: 'COM1',
baudrate: 9600,
data_bits: 8,
parity: 'none',
stop_bits: 1,
flow_control: 'none'
}
};
const openResponse = await mcpServer.handleRequest('test-conn', openRequest);
expect(openResponse.result).toBeDefined();
expect(openResponse.result.status).toBe('ok');
expect(openResponse.result.port).toBe('COM1');
// 关闭端口
const closeRequest = {
jsonrpc: '2.0' as const,
id: 2,
method: 'mcp.serial.close',
params: {
port: 'COM1'
}
};
const closeResponse = await mcpServer.handleRequest('test-conn', closeRequest);
expect(closeResponse.result).toBeDefined();
expect(closeResponse.result.status).toBe('ok');
expect(closeResponse.result.port).toBe('COM1');
});
test('应该能够写入数据并读取响应', async () => {
// 先打开端口
await mcpServer.handleRequest('test-conn', {
jsonrpc: '2.0' as const,
id: 1,
method: 'mcp.serial.open',
params: {
port: 'COM1',
baudrate: 9600,
data_bits: 8,
parity: 'none',
stop_bits: 1,
flow_control: 'none'
}
});
// 写入并读取数据
const writeRequest = {
jsonrpc: '2.0' as const,
id: 2,
method: 'mcp.serial.write',
params: {
port: 'COM1',
data: '48656c6c6f', // "Hello" in hex
timeout_ms: 5000,
filter_urc: true
}
};
const writeResponse = await mcpServer.handleRequest('test-conn', writeRequest);
expect(writeResponse.result).toBeDefined();
expect(writeResponse.result.status).toBe('ok');
expect(writeResponse.result.text).toBe('Hello');
expect(writeResponse.result.raw_hex).toBe('48656c6c6f');
expect(writeResponse.result.urc_detected).toBe(false);
});
test('应该能够处理URC检测', async () => {
// 先打开端口
await mcpServer.handleRequest('test-conn', {
jsonrpc: '2.0' as const,
id: 1,
method: 'mcp.serial.open',
params: {
port: 'COM1',
baudrate: 9600,
data_bits: 8,
parity: 'none',
stop_bits: 1,
flow_control: 'none'
}
});
// 发送AT命令触发URC
const writeRequest = {
jsonrpc: '2.0' as const,
id: 2,
method: 'mcp.serial.write',
params: {
port: 'COM1',
data: '415445415441', // "ATDA" in hex
timeout_ms: 5000,
filter_urc: true
}
};
const writeResponse = await mcpServer.handleRequest('test-conn', writeRequest);
expect(writeResponse.result).toBeDefined();
expect(writeResponse.result.status).toBe('ok');
expect(writeResponse.result.text).toBe('OK');
expect(writeResponse.result.urc_detected).toBe(true);
expect(writeResponse.result.filtered_urc).toEqual(['+CSQ: 23,99']);
});
test('应该能够获取端口状态', async () => {
// 先打开端口
await mcpServer.handleRequest('test-conn', {
jsonrpc: '2.0' as const,
id: 1,
method: 'mcp.serial.open',
params: {
port: 'COM1',
baudrate: 9600,
data_bits: 8,
parity: 'none',
stop_bits: 1,
flow_control: 'none'
}
});
// 获取端口状态
const statusRequest = {
jsonrpc: '2.0' as const,
id: 2,
method: 'mcp.serial.status',
params: {
port: 'COM1'
}
};
const statusResponse = await mcpServer.handleRequest('test-conn', statusRequest);
expect(statusResponse.result).toBeDefined();
expect(statusResponse.result.status).toBe('ok');
expect(statusResponse.result.port).toBe('COM1');
expect(statusResponse.result.isOpen).toBe(true);
});
});
describe('错误处理流程', () => {
test('应该处理打开不存在的端口', async () => {
const request = {
jsonrpc: '2.0' as const,
id: 1,
method: 'mcp.serial.open',
params: {
port: 'INVALID_PORT',
baudrate: 9600,
data_bits: 8,
parity: 'none',
stop_bits: 1,
flow_control: 'none'
}
};
const response = await mcpServer.handleRequest('test-conn', request);
expect(response.result).toBeDefined();
expect(response.result.status).toBe('error');
expect(response.result.error_code).toBe('PORT_NOT_FOUND');
});
test('应该处理无效的串口配置', async () => {
const request = {
jsonrpc: '2.0' as const,
id: 1,
method: 'mcp.serial.open',
params: {
port: 'COM1',
baudrate: -1, // 无效波特率
data_bits: 8,
parity: 'none',
stop_bits: 1,
flow_control: 'none'
}
};
const response = await mcpServer.handleRequest('test-conn', request);
expect(response.result).toBeDefined();
expect(response.result.status).toBe('error');
expect(response.result.error_code).toBe('INVALID_CONFIG');
});
test('应该处理无效的十六进制数据', async () => {
// 先打开端口
await mcpServer.handleRequest('test-conn', {
jsonrpc: '2.0' as const,
id: 1,
method: 'mcp.serial.open',
params: {
port: 'COM1',
baudrate: 9600,
data_bits: 8,
parity: 'none',
stop_bits: 1,
flow_control: 'none'
}
});
const request = {
jsonrpc: '2.0' as const,
id: 2,
method: 'mcp.serial.write',
params: {
port: 'COM1',
data: 'invalid-hex', // 无效的十六进制
timeout_ms: 5000,
filter_urc: true
}
};
const response = await mcpServer.handleRequest('test-conn', request);
expect(response.result).toBeDefined();
expect(response.result.status).toBe('error');
expect(response.result.error_code).toBe('INVALID_CONFIG');
});
});
describe('连接管理流程', () => {
test('应该支持多个并发连接', async () => {
const requests = [];
// 创建多个并发请求
for (let i = 0; i < 3; i++) {
requests.push({
jsonrpc: '2.0' as const,
id: i + 1,
method: 'mcp.server.info',
params: {}
});
}
// 并发执行所有请求
const responses = await Promise.all(
requests.map(req => mcpServer.handleRequest(`conn-${req.id}`, req))
);
// 验证所有响应都成功
responses.forEach((response, index) => {
expect(response.result).toBeDefined();
expect(response.result.name).toBe('E2E Test Server');
expect(response.id).toBe(index + 1);
});
});
test('应该正确关闭连接', async () => {
const request = {
jsonrpc: '2.0' as const,
id: 1,
method: 'mcp.server.info',
params: {}
};
await mcpServer.handleRequest('test-conn', request);
// 获取连接统计
const stats = mcpServer.getConnectionStats();
expect(stats.total).toBe(1);
// 关闭连接
await mcpServer.closeConnection('test-conn');
const statsAfterClose = mcpServer.getConnectionStats();
expect(statsAfterClose.total).toBe(0);
});
test('应该强制更新服务器信息', async () => {
const request = {
jsonrpc: '2.0' as const,
id: 1,
method: 'mcp.server.info',
params: {}
};
const response = await mcpServer.handleRequest('test-conn', request);
expect(response.result).toBeDefined();
expect(response.result.name).toBe('E2E Test Server');
// 获取服务器信息
const serverInfo = mcpServer.getServerInfo();
expect(serverInfo.name).toBe('E2E Test Server');
expect(serverInfo.version).toBe('1.0.0');
});
});
describe('性能测试', () => {
test('应该能够处理高频请求', async () => {
const requestCount = 50;
const requests = [];
// 创建多个并发请求
for (let i = 0; i < requestCount; i++) {
requests.push({
jsonrpc: '2.0' as const,
id: i + 1,
method: 'mcp.serial.list',
params: {}
});
}
const startTime = Date.now();
// 并发执行所有请求
const responses = await Promise.all(
requests.map(req => mcpServer.handleRequest(`perf-conn-${req.id}`, req))
);
const endTime = Date.now();
const duration = endTime - startTime;
// 验证所有响应都成功
responses.forEach(response => {
expect(response.result).toBeDefined();
expect(response.result.status).toBe('ok');
});
// 验证性能(应该在合理时间内完成)
expect(duration).toBeLessThan(5000); // 5秒内完成50个请求
console.log(`处理 ${requestCount} 个请求耗时: ${duration}ms`);
});
test('应该能够处理大数据量', async () => {
// 先打开端口
await mcpServer.handleRequest('test-conn', {
jsonrpc: '2.0' as const,
id: 1,
method: 'mcp.serial.open',
params: {
port: 'COM1',
baudrate: 9600,
data_bits: 8,
parity: 'none',
stop_bits: 1,
flow_control: 'none'
}
});
// 创建大数据(1KB)
const largeData = '48'.repeat(512); // 1KB of 'H' in hex
const request = {
jsonrpc: '2.0' as const,
id: 2,
method: 'mcp.serial.write',
params: {
port: 'COM1',
data: largeData,
timeout_ms: 10000,
filter_urc: true
}
};
const startTime = Date.now();
const response = await mcpServer.handleRequest('test-conn', request);
const endTime = Date.now();
const duration = endTime - startTime;
expect(response.result).toBeDefined();
expect(response.result.status).toBe('ok');
expect(response.result.raw_hex.length).toBe(1024); // 1KB
expect(duration).toBeLessThan(2000); // 2秒内完成
});
});
describe('健康检查流程', () => {
test('应该能够获取服务器健康状态', async () => {
const request = {
jsonrpc: '2.0' as const,
id: 1,
method: 'mcp.health.status',
params: {}
};
const response = await mcpServer.handleRequest('health-check', request);
expect(response.result).toBeDefined();
expect(response.result.status).toBe('ok');
expect(response.result.uptime).toBeGreaterThanOrEqual(0);
});
test('应该能够获取性能指标', async () => {
const request = {
jsonrpc: '2.0' as const,
id: 1,
method: 'mcp.health.metrics',
params: {}
};
const response = await mcpServer.handleRequest('health-check', request);
expect(response.result).toBeDefined();
expect(response.result.status).toBe('ok');
expect(response.result.metrics).toBeDefined();
});
});
describe('日志查询流程', () => {
test('应该能够获取日志', async () => {
const request = {
jsonrpc: '2.0' as const,
id: 1,
method: 'mcp.trace.get_log',
params: {
limit_kb: 10
}
};
const response = await mcpServer.handleRequest('log-check', request);
expect(response.result).toBeDefined();
expect(response.result.status).toBe('ok');
});
});
});