Files
one_divine_lot/scripts/test-health-monitor.mjs
T

127 lines
4.5 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.
/**
* 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);