Files
one_divine_lot/scripts/test-mcp-status.mjs
T

150 lines
5.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* QmtMcpManager 状态自适应探测 回归测试(R-023 / 迭代 192026-09-09
*
* 运行:node scripts/test-mcp-status.mjs
* 说明:SDK 握手(initialize/tools.list)依赖真实网络端点,无法纯内存 mock;
* 本测试守护的是【调度机制】——probe 结果驱动 setTimeout 自适应链:
* connected → intervalMs 稳态;非 connected → retryMs 快速重试;
* 无激活连接不调度;dispose 停止调度。
* 用 stub probe(记录调用 + 翻转状态 + 触达真实 _scheduleNext)验证自动恢复数据流;
* 无激活连接场景用真实 probe(早退路径不触 SDK)。
*/
let passed = 0;
let failed = 0;
function ok(cond, name) {
if (cond) { passed++; console.log(' ✓ ' + name); }
else { failed++; console.error(' ✗ ' + name); }
}
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
const { QmtMcpManager } = await import('../src/mcp/QmtMcpManager.js');
const RETRY_MS = 50;
const INTERVAL_MS = 350;
const BRIDGE_URL = 'http://bridge.test:8610';
/** settings scopebaseUrl 传 '' = 无激活连接 */
function mockSettings(baseUrl) {
return {
get: () => ({
qmtConnections: {
list: baseUrl ? [{ id: 'c1', name: 'bridge', baseUrl, order: 1 }] : [],
activeId: baseUrl ? 'c1' : '',
defaultId: '',
},
}),
};
}
const quietLogger = { info() {}, warn() {}, debug() {} };
console.log('[1] 无激活连接:置 disabled 且不调度(真实 probe 早退,不触 SDK');
{
const mgr = new QmtMcpManager({
ctx: {}, runtime: { settings: mockSettings(''), dataSource: {} },
logger: quietLogger, intervalMs: INTERVAL_MS, retryMs: RETRY_MS,
});
let probeCalls = 0;
const orig = mgr.probe.bind(mgr);
mgr.probe = async (u) => { probeCalls++; return orig(u); };
await mgr.start();
ok(mgr.getStatus().state === 'disabled', '无激活连接 → 状态 disabled');
await sleep(RETRY_MS * 4);
ok(probeCalls === 1, '无激活连接无定时探测(仅 start 一次)');
await mgr.dispose();
}
console.log('[2] 异常 → 快速重试自动恢复(核心回归:MCP 服务恢复后灯 ≤ 重试间隔回绿)');
{
const mgr = new QmtMcpManager({
ctx: {}, runtime: { settings: mockSettings(BRIDGE_URL), dataSource: {} },
logger: quietLogger, intervalMs: INTERVAL_MS, retryMs: RETRY_MS,
});
let bridgeUp = false;
let probeCalls = 0;
mgr.probe = async () => {
probeCalls++;
mgr.status = bridgeUp
? { state: 'connected', serverName: 'QMT_Bridge_MCP', url: BRIDGE_URL + '/mcp', mounted: true, toolCount: 9, error: null, latencyMs: 3, checkedAt: Date.now() }
: { state: 'error', serverName: 'QMT_Bridge_MCP', url: BRIDGE_URL + '/mcp', mounted: true, toolCount: 0, error: 'ECONNREFUSED', latencyMs: 3, checkedAt: Date.now() };
mgr._scheduleNext(); // 等价真实 probe 尾部
return mgr.status;
};
await mgr.probe(); // 启动首次探测(Bridge 未启动)
ok(mgr.getStatus().state === 'error', 'Bridge 未启动 → 状态 error');
await sleep(RETRY_MS * 3);
ok(probeCalls >= 3, '异常态按 retryMs 快速重试(' + probeCalls + ' 次 ≥ 3');
bridgeUp = true; // 模拟 MCP 服务启动
await sleep(RETRY_MS * 3);
ok(mgr.getStatus().state === 'connected', '恢复后被自动探测到 → connected(灯将回绿,未等 5 分钟稳态间隔)');
await mgr.dispose();
}
console.log('[3] 已连接稳态:回落到长间隔,不空转');
{
const mgr = new QmtMcpManager({
ctx: {}, runtime: { settings: mockSettings(BRIDGE_URL), dataSource: {} },
logger: quietLogger, intervalMs: INTERVAL_MS, retryMs: RETRY_MS,
});
let probeCalls = 0;
mgr.probe = async () => {
probeCalls++;
mgr.status = { state: 'connected', serverName: 'QMT_Bridge_MCP', url: BRIDGE_URL + '/mcp', mounted: true, toolCount: 9, error: null, latencyMs: 3, checkedAt: Date.now() };
mgr._scheduleNext();
return mgr.status;
};
await mgr.probe();
ok(probeCalls === 1 && mgr.getStatus().state === 'connected', '首探测 connected');
await sleep(RETRY_MS * 3); // 小于稳态间隔、大于重试节奏
ok(probeCalls === 1, '已连接态在稳态间隔内不按重试节奏空转');
await sleep(INTERVAL_MS + RETRY_MS * 2); // 跨过稳态间隔
ok(probeCalls === 2, '跨过稳态间隔后又探测 1 次');
await mgr.dispose();
}
console.log('[4] dispose() 停止自动调度');
{
const mgr = new QmtMcpManager({
ctx: {}, runtime: { settings: mockSettings(BRIDGE_URL), dataSource: {} },
logger: quietLogger, intervalMs: INTERVAL_MS, retryMs: RETRY_MS,
});
let bridgeUp = false;
let probeCalls = 0;
mgr.probe = async () => {
probeCalls++;
mgr.status = bridgeUp
? { state: 'connected', serverName: 'QMT_Bridge_MCP', url: BRIDGE_URL + '/mcp', mounted: true, toolCount: 9, error: null, latencyMs: 3, checkedAt: Date.now() }
: { state: 'error', serverName: 'QMT_Bridge_MCP', url: BRIDGE_URL + '/mcp', mounted: true, toolCount: 0, error: 'ECONNREFUSED', latencyMs: 3, checkedAt: Date.now() };
mgr._scheduleNext();
return mgr.status;
};
await mgr.probe();
await sleep(RETRY_MS * 2);
const before = probeCalls;
await mgr.dispose();
bridgeUp = true;
await sleep(RETRY_MS * 4);
ok(probeCalls === before, 'dispose 后无自动探测');
}
console.log('[5] _nextDelay 节奏选择');
{
const mgr = new QmtMcpManager({
ctx: {}, runtime: { settings: mockSettings(BRIDGE_URL), dataSource: {} },
logger: quietLogger, intervalMs: INTERVAL_MS, retryMs: RETRY_MS,
});
mgr.status = { state: 'connected' };
ok(mgr._nextDelay() === INTERVAL_MS, 'connected → intervalMs');
mgr.status = { state: 'error' };
ok(mgr._nextDelay() === RETRY_MS, 'error → retryMs');
mgr.status = mgr._disabledStatus();
ok(mgr._nextDelay() === RETRY_MS, 'disabled → retryMs(无连接时 probe 早退不调度)');
}
console.log('----');
console.log(passed + ' passed, ' + failed + ' failed');
if (failed > 0) process.exit(1);