迭代19-21收尾: 指示灯自适应探测(R-022/R-023)+ 列设置拖拽排序(R-024)+ Tab设置间隙线统一(R-025)+ 需求池归档(R-014/15/16/17/19 → 已完成)

This commit is contained in:
2026-09-10 14:04:51 +08:00
parent 8782aa9c50
commit 3dfa39b8bf
29 changed files with 1216 additions and 72 deletions
+126
View File
@@ -0,0 +1,126 @@
/**
* QmtHealthMonitor 自适应探测 回归测试(R-022 / 迭代 192026-09-09
*
* 运行:node scripts/test-health-monitor.mjs
* 隔离:纯内存 mockmock 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 scoperesolveActiveBaseUrl 依赖 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 mockbridgeUp=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);