Files
DCTS/dashboard/test/polling.test.js
T
fmq d16b3d3cdc feat(all): 数据库模块化拆分与版本化迁移、任务引擎命名体系收敛、物理输出校验加固与用户配置接通
- server/db: 拆 4929 行 db.rs 单体为 db/ 目录,migrations.rs 引入 PRAGMA user_version
    版本化迁移运行器(M1~M13)
  - 任务引擎 Phase 6/7b/7c 改名收敛:EngineStageConfig→PhaseConfig、StagePolicy→ResumePolicy、
    Converged→Completed、删除 task_type 列、success_method 拆 tlusty_/synspec_ 双列、
    新增 tlusty_status/synspec_status 半失败阶段守卫
  - 科学正确性加固:conv_check 任意行 NaN/Inf/溢出判无效(0 行容忍)、新增 spec_is_valid
    校验 SYNSPEC 脏谱、itek_history 逐次迭代全量保真、fmt_abn powf 溢出饱和
  - 用户配置真正接通:tlusty_chain/tlusty_input 由死字段经 调度器→TaskSpec→executor→runner
    透传生效;config 加载期 validate + deny_unknown_fields + 解析失败记 warn
  - 调度修复:H1 活锁(pending_strategies 跳过已失败策略)、种子查找错误不再静默降级冷启动
  - dashboard: 阶段配置面板 tlusty_stage/synspec_stage、"已完成"标签、迭代诊断展示
  - docs: 新增 database_refactor_design.md,同步 database/api/PIPELINE/workflow_detail
2026-08-06 20:51:21 +08:00

171 lines
6.0 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.
/* DCTS 轮询器单测(node:test,零依赖)
*
* utils/polling.js 的 createPoller:指数退避、failCount 重置/增长、stop 清理。
* 通过 stubbing setTimeout / clearTimeout 捕获调度延迟,不真等 5s+。
*
* 时序说明:start() 触发 schedule() 时 fn 是同步调用的(calls 立即生效),
* 后续的 failCount 更新与 setTimeout 排期发生在微任务里——`await p.start()`
* 之后测试续体排在其后,故断言时调度已完成(确定性)。
*/
import { test } from 'node:test';
import assert from 'node:assert/strict';
import { createPoller } from '../src/utils/polling.js';
// 最小 document stubnode 环境无 DOM,轮询器只在 visibilitychange 处触碰 document。
function installDocStub() {
const doc = { hidden: false, addEventListener() {}, removeEventListener() {} };
const prev = globalThis.document;
globalThis.document = doc;
return { doc, restore() { globalThis.document = prev; } };
}
// 捕获 setTimeout 调用。fire(n) 手动执行第 n 次已排定的回调(await 其完整完成)。
function captureTimers() {
const realSet = globalThis.setTimeout;
const realClear = globalThis.clearTimeout;
const scheduled = [];
globalThis.setTimeout = (fn, ms) => {
const id = { fn, ms };
scheduled.push(id);
return id;
};
globalThis.clearTimeout = (id) => {
const i = scheduled.indexOf(id);
if (i >= 0) scheduled.splice(i, 1);
};
return {
delays: () => scheduled.map(t => t.ms),
fire: async (n) => { await scheduled[n].fn(); },
restore() {
globalThis.setTimeout = realSet;
globalThis.clearTimeout = realClear;
},
};
}
test('start() 立即执行一次 fn', async () => {
const stub = installDocStub();
const timers = captureTimers();
let calls = 0;
const p = createPoller(() => { calls++; return true; }, { baseMs: 5000 });
await p.start();
assert.equal(calls, 1); // 立即跑了一次(fn 同步调用)
assert.equal(p.isRunning(), true);
// 成功后下一轮间隔为 baseMs
assert.deepEqual(timers.delays(), [5000]);
p.stop();
timers.restore();
stub.restore();
});
test('fn 返回 false 触发指数退避,成功后重置', async () => {
const stub = installDocStub();
const timers = captureTimers();
let ok = false;
const p = createPoller(() => ok, { baseMs: 5000, maxMs: 60000 });
await p.start();
// 第一次失败 → failCount=1 → 5k*2^1
assert.deepEqual(timers.delays(), [10000]);
await timers.fire(0); // 第二轮仍失败 → failCount=2
assert.deepEqual(timers.delays().slice(-1), [20000]);
ok = true;
await timers.fire(0); // 第三轮成功 → failCount 重置
assert.deepEqual(timers.delays().slice(-1), [5000]);
p.stop();
timers.restore();
stub.restore();
});
test('fn 抛错同样计为失败', async () => {
const stub = installDocStub();
const timers = captureTimers();
const p = createPoller(() => { throw new Error('boom'); }, { baseMs: 5000 });
await p.start();
assert.deepEqual(timers.delays().slice(-1), [10000]);
p.stop();
timers.restore();
stub.restore();
});
test('stop() 清除挂起定时器且不再调度', async () => {
const stub = installDocStub();
const timers = captureTimers();
let calls = 0;
const p = createPoller(() => { calls++; return true; }, { baseMs: 5000 });
await p.start(); // calls=1,已排下一轮
assert.equal(timers.delays().length, 1);
p.stop();
assert.equal(timers.delays().length, 0); // 挂起定时器已被清除
assert.equal(calls, 1); // fn 未再被调用
assert.equal(p.isRunning(), false);
timers.restore();
stub.restore();
});
test('triggerNow() 立即触发一次并续排', async () => {
const stub = installDocStub();
const timers = captureTimers();
let calls = 0;
const p = createPoller(() => { calls++; return true; }, { baseMs: 5000 });
await p.start(); // calls=1
await p.triggerNow(); // calls=2
assert.equal(calls, 2);
assert.equal(p.isRunning(), true);
p.stop();
timers.restore();
stub.restore();
});
test('triggerNow() 与在途 schedule 不并发执行 fninFlight 守卫)', async () => {
const stub = installDocStub();
const timers = captureTimers();
let active = 0; // 当前正在执行的 fn 数(应恒 ≤1)
let maxActive = 0;
let calls = 0;
// fn 是慢异步:用 gate 控制其完成时机,确保 triggerNow 落在它在途时。
let resolveFn = null;
const fn = () => {
active++;
maxActive = Math.max(maxActive, active);
calls++;
return new Promise((resolve) => { resolveFn = resolve; });
};
const p = createPoller(fn, { baseMs: 5000 });
const startP = p.start(); // 触发首次 schedulefn 在途,停在 await
// 让 start 的 schedule 进入 fn()——通过一次微任务边界。
await Promise.resolve();
await Promise.resolve();
// 此时 fn 在途(calls=1active=1)。triggerNow 应受 inFlight 守卫直接 return
// 不再起一个新的 schedule 并发跑 fn。
const triggerP = p.triggerNow();
await Promise.resolve();
await Promise.resolve();
// 完成 fn(在途的 schedule 才会推进)。
resolveFn(true);
await Promise.all([startP, triggerP]);
active = 0; // fn 已完成
assert.equal(calls, 1, 'triggerNow 不得并发触发第二次 fninFlight 守卫)');
assert.equal(maxActive, 1, 'fn 不得并发执行(maxActive 应为 1');
p.stop();
timers.restore();
stub.restore();
});
test('退避上限 maxMs 生效(长时间连续失败不无限增长)', async () => {
const stub = installDocStub();
const timers = captureTimers();
const p = createPoller(() => false, { baseMs: 5000, maxMs: 60000, maxFails: 4 });
await p.start();
let maxDelay = 0;
for (let i = 0; i < 10; i++) {
maxDelay = Math.max(maxDelay, timers.delays()[timers.delays().length - 1] || 0);
await timers.fire(0);
}
assert.ok(maxDelay <= 60000, `maxDelay=${maxDelay} 不应超过 60s`);
assert.equal(maxDelay, 60000); // 5k*2^4 clamp 到 60s
p.stop();
timers.restore();
stub.restore();
});