- 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
171 lines
6.0 KiB
JavaScript
171 lines
6.0 KiB
JavaScript
/* 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 stub:node 环境无 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 不并发执行 fn(inFlight 守卫)', 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(); // 触发首次 schedule(fn 在途,停在 await)
|
||
// 让 start 的 schedule 进入 fn()——通过一次微任务边界。
|
||
await Promise.resolve();
|
||
await Promise.resolve();
|
||
// 此时 fn 在途(calls=1,active=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 不得并发触发第二次 fn(inFlight 守卫)');
|
||
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();
|
||
});
|