迭代19-21收尾: 指示灯自适应探测(R-022/R-023)+ 列设置拖拽排序(R-024)+ Tab设置间隙线统一(R-025)+ 需求池归档(R-014/15/16/17/19 → 已完成)
This commit is contained in:
@@ -0,0 +1,126 @@
|
||||
/**
|
||||
* QmtHealthMonitor 自适应探测 回归测试(R-022 / 迭代 19,2026-09-09)
|
||||
*
|
||||
* 运行:node scripts/test-health-monitor.mjs
|
||||
* 隔离:纯内存 mock(mock fetch + mock settings scope),用短间隔参数测真实 setTimeout 调度。
|
||||
* 背景:原实现固定 5 分钟探测,QMT Bridge 恢复后指示灯要等满 5 分钟(老师反馈「自动检查没生效」);
|
||||
* 本测试守护「异常/未知 → 快速重试,健康 → 稳态间隔」的自适应节奏。
|
||||
*/
|
||||
|
||||
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 { QmtHealthMonitor } = await import('../src/data-source/QmtHealthMonitor.js');
|
||||
|
||||
/** 可编程 settings scope(resolveActiveBaseUrl 依赖 scope.get() → qmtConnections) */
|
||||
function mockSettings(baseUrl) {
|
||||
return {
|
||||
get: () => ({
|
||||
qmtConnections: {
|
||||
list: [{ id: 'c1', name: 'bridge', baseUrl, order: 1 }],
|
||||
activeId: 'c1',
|
||||
defaultId: '',
|
||||
},
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
const BRIDGE_URL = 'http://bridge.test:8610';
|
||||
|
||||
/** 可编程 fetch mock:bridgeUp=false 时抛连接错误 */
|
||||
function mockFetch() {
|
||||
const calls = [];
|
||||
let up = true;
|
||||
globalThis.fetch = async (url) => {
|
||||
calls.push({ url: String(url), t: Date.now() });
|
||||
if (!up) throw new Error('fetch failed: ECONNREFUSED bridge down');
|
||||
return { ok: true, status: 200 };
|
||||
};
|
||||
return {
|
||||
calls,
|
||||
set up(v) { up = v; },
|
||||
get up() { return up; },
|
||||
};
|
||||
}
|
||||
|
||||
const RETRY_MS = 40;
|
||||
const INTERVAL_MS = 300;
|
||||
|
||||
console.log('[1] 启动即探测 + 缓存读取(健康)');
|
||||
{
|
||||
const fetch = mockFetch();
|
||||
const monitor = new QmtHealthMonitor({
|
||||
runtime: { settings: mockSettings(BRIDGE_URL), dataSource: { baseUrl: BRIDGE_URL } },
|
||||
intervalMs: INTERVAL_MS, retryMs: RETRY_MS,
|
||||
});
|
||||
await monitor.probe(); // 等价 start() 的立即探测
|
||||
ok(fetch.calls.length === 1, '启动探测发起 1 次');
|
||||
ok(fetch.calls[0].url === BRIDGE_URL + '/health', '探测地址 = 激活 baseUrl + /health');
|
||||
const st = monitor.getStatus();
|
||||
ok(st.healthy === true && st.fromCache === true, '缓存 healthy=true 且 fromCache=true');
|
||||
await monitor.stop();
|
||||
}
|
||||
|
||||
console.log('[2] 异常 → 快速重试自动恢复(核心回归:Bridge 启动后灯 ≤ 重试间隔回绿)');
|
||||
{
|
||||
const fetch = mockFetch();
|
||||
fetch.up = false;
|
||||
const monitor = new QmtHealthMonitor({
|
||||
runtime: { settings: mockSettings(BRIDGE_URL), dataSource: { baseUrl: BRIDGE_URL } },
|
||||
intervalMs: INTERVAL_MS, retryMs: RETRY_MS,
|
||||
});
|
||||
monitor.start();
|
||||
await sleep(RETRY_MS * 3); // 快速重试窗口内应已多次探测
|
||||
ok(fetch.calls.length >= 2, '异常态多次快速重试(' + fetch.calls.length + ' 次 ≥ 2)');
|
||||
ok(monitor.getStatus().healthy === false, '异常缓存 healthy=false');
|
||||
|
||||
fetch.up = true; // 模拟 QMT Bridge 启动
|
||||
const before = fetch.calls.length;
|
||||
await sleep(RETRY_MS * 4); // 等下一个快速重试命中
|
||||
const st = monitor.getStatus();
|
||||
ok(fetch.calls.length > before, '恢复后被自动重探测到');
|
||||
ok(st.healthy === true, '恢复后缓存 healthy=true(灯将回绿,未等 5 分钟稳态间隔)');
|
||||
await monitor.stop();
|
||||
}
|
||||
|
||||
console.log('[3] 健康稳态:回落到长间隔,不空转');
|
||||
{
|
||||
const fetch = mockFetch();
|
||||
const monitor = new QmtHealthMonitor({
|
||||
runtime: { settings: mockSettings(BRIDGE_URL), dataSource: { baseUrl: BRIDGE_URL } },
|
||||
intervalMs: INTERVAL_MS, retryMs: RETRY_MS,
|
||||
});
|
||||
monitor.start();
|
||||
await sleep(RETRY_MS * 4); // 小于稳态间隔:健康态不应按重试节奏空转
|
||||
ok(fetch.calls.length === 1, '健康态在稳态间隔内只探测 1 次(无快速空转)');
|
||||
await sleep(INTERVAL_MS + RETRY_MS * 2); // 跨过稳态间隔
|
||||
ok(fetch.calls.length === 2, '跨过稳态间隔后又探测 1 次(共 2 次)');
|
||||
await monitor.stop();
|
||||
}
|
||||
|
||||
console.log('[4] stop() 后停止自动调度');
|
||||
{
|
||||
const fetch = mockFetch();
|
||||
fetch.up = false;
|
||||
const monitor = new QmtHealthMonitor({
|
||||
runtime: { settings: mockSettings(BRIDGE_URL), dataSource: { baseUrl: BRIDGE_URL } },
|
||||
intervalMs: INTERVAL_MS, retryMs: RETRY_MS,
|
||||
});
|
||||
monitor.start();
|
||||
await sleep(RETRY_MS * 2);
|
||||
const before = fetch.calls.length;
|
||||
monitor.stop();
|
||||
fetch.up = true;
|
||||
await sleep(RETRY_MS * 4);
|
||||
ok(fetch.calls.length === before, 'stop 后无自动探测');
|
||||
}
|
||||
|
||||
console.log('----');
|
||||
console.log(passed + ' passed, ' + failed + ' failed');
|
||||
if (failed > 0) process.exit(1);
|
||||
@@ -0,0 +1,149 @@
|
||||
/**
|
||||
* QmtMcpManager 状态自适应探测 回归测试(R-023 / 迭代 19,2026-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 scope:baseUrl 传 '' = 无激活连接 */
|
||||
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);
|
||||
Reference in New Issue
Block a user