diff --git a/tests/headless/scheduler.test.mjs b/tests/headless/scheduler.test.mjs index d47e0fb..c0ca154 100644 --- a/tests/headless/scheduler.test.mjs +++ b/tests/headless/scheduler.test.mjs @@ -12,6 +12,7 @@ const deferred = () => { let resolve, reject; const promise = new Promise((yes, // 推进有限轮微任务供调度泵运行;不推进墙上时间,也不替代 scheduler.settle 的收敛等待。 const tick = async () => { for (let i = 0; i < 20; i++) await Promise.resolve(); }; // 建立可替换端口的调度环境;mutate 使用账本副本发布状态,persist 仅记录快照,默认服务立即成功。 +// changes 中的 canvas 创建只是调度参与者标记;此夹具不构建/校验业务文档,create/delete 名称不代表实际节点增删。 function setup(overrides = {}, initial) { let ledger = createLedger(initial), time = 1000, next = 0, timerId = 0; const timers = new Map(), calls = [], persisted = [], errors = []; @@ -89,7 +90,7 @@ test('storage failure never sends and keeps claims; explicit resume retries pers state.add('create'); state.scheduler.start(); await state.scheduler.settle(); assert.equal(sent.length, 0); assert.equal(state.errors[0].message, 'quota'); assert.equal(state.scheduler.getClaims().size, 1); broken = false; await state.scheduler.resume(); await state.scheduler.settle(); - // Dispatch evidence means query first, even if the failed barrier preceded the network call. + // 账本已有 dispatch-marked 风险证据,即使本测试知道网络尚未进入,恢复仍保守先查询,不能依赖进程外无法证明的观察。 assert.equal(state.calls[0][0], 'query'); assert.equal(sent.length, 0); assert.equal(state.scheduler.getClaims().size, 0); state.scheduler.stop(); }); @@ -234,6 +235,7 @@ test('batching retains independent lanes and does not cross an external dependen test('unknown batch preserves all IDs, claims, and exact bytes across query and retry; later edits stay mutable', async () => { const sent = [], state = setup({ compileBatch, submit: async request => { sent.push(request); return { outcome: 'unresolved' }; }, query: async () => ({ outcome: 'unresolved' }) }); state.add('one'); state.add('two', ['canvas'], ['one']); state.scheduler.start(); await state.scheduler.settle(); + // 冻结后加入的 three 仍是独立可变操作,不能为了节省请求而并入原未知批次的 body。 const original = sent[0]; state.add('three', ['canvas'], ['two']); await state.scheduler.settle(); assert.equal(sent.length, 1); assert.equal(state.ledger().getSnapshot().operations.get('three').phase, 'pending'); await state.advance(100); await state.advance(100); @@ -256,7 +258,6 @@ test('a fenced pump reports failed state publication without throwing or sending const state=setup({mutate(){throw Error('storage_leader_lost');}});state.scheduler.start();state.add('fenced');await tick();await state.scheduler.settle();assert.equal(state.calls.length,0);assert.equal(state.ledger().getSnapshot().requests.size,0);assert.equal(state.ledger().getSnapshot().operations.get('fenced').phase,'pending');assert.ok(state.errors.some(error=>error.message==='storage_leader_lost'));state.scheduler.stop(); }); -// Wall time can change independently of the elapsed time used by browser timers. // 分离墙上时间与定时器经过时间:可单独跳动 wall,或推进 elapsed 并返回旧回调来模拟迟到触发。 function adjustableClock(initial=1000){ let wall=initial,elapsed=0,next=0;const timers=new Map(); @@ -273,6 +274,7 @@ test('wall-clock jumps recheck retry deadlines without releasing unknown claims }); state.add('first');state.add('dependent',['canvas'],['first']);state.scheduler.start();await state.scheduler.settle(); const frozen=state.ledger().getSnapshot().requests.get('request-1');assert.equal(frozen.retryAt,1100); + // 定时器经过 100ms 不代表 wall 已到 retryAt;墙钟从 1000 回退到 800 后,还须重新等待 300。 clock.setWall(800);clock.fireAfter(100);await state.scheduler.settle(); assert.equal(calls.length,1);assert.equal(state.scheduler.getClaims().get('canvas'),frozen.id); assert.equal(clock.timers.size,1);assert.equal([...clock.timers.values()][0].delay,300); @@ -281,6 +283,7 @@ test('wall-clock jumps recheck retry deadlines without releasing unknown claims clock.setWall(5000);const staleCallbacks=clock.fireAfter(300);await tick(); assert.equal(calls.at(-1)[0],'query');assert.equal(calls.at(-1)[1].body,frozen.body);assert.equal(calls.at(-1)[1].id,frozen.id); assert.equal(state.scheduler.getClaims().get('canvas'),frozen.id);assert.equal(state.ledger().getSnapshot().operations.get('dependent').phase,'pending'); + // 手工重复执行已出队回调,模拟取消无法阻止迟到触发;请求仍在查询中,不能并发再查一次。 for(const callback of staleCallbacks)callback();await tick();assert.equal(calls.length,3); receipt.resolve({outcome:'applied'});await state.scheduler.settle(); assert.deepEqual(calls.map(([action,request])=>[action,request.operationIds[0]]),[['submit','first'],['submit','independent'],['query','first'],['submit','dependent']]);