From 05a41813f3f31a5017de787b61d17e8d3fcfd4b6 Mon Sep 17 00:00:00 2001 From: Codex Bot Date: Thu, 12 Nov 2026 17:57:51 +0800 Subject: [PATCH] docs(tests): explain size history traversal and recovery capture semantics --- tests/headless/creation-size-plan.test.mjs | 4 +++- tests/headless/recovery-encoding.test.mjs | 6 ++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/tests/headless/creation-size-plan.test.mjs b/tests/headless/creation-size-plan.test.mjs index 7e1cc42..6c82c55 100644 --- a/tests/headless/creation-size-plan.test.mjs +++ b/tests/headless/creation-size-plan.test.mjs @@ -22,6 +22,7 @@ function setup(options = {}) { return { editor, entry, propose() { const { document, history } = editor.getSnapshot(); return planCreationSize(document, history, registry, entry, target, { width: 260, height: 130 }); } }; } /** 从plan当前状态逆序撤销到空,再按undo顺序重做,对照最终图片;只适用于不含额外初始元素的调用。 */ +// 直接应用历史 changes/invert 来检查候选前后值连续性;不走 editor.history,也不模拟其恢复对象时的 incarnation 分配。 function traverse(plan) { let document = plan.document; for (const id of [...plan.history.undo].reverse()) document = applyChanges(document, invert(plan.history.entries.get(id).changes)); @@ -82,7 +83,7 @@ test('an undone creation cannot be enriched', () => { // 删除再恢复使生命周期变化,即使ID相同,旧创建尺寸任务也不能修改新对象。 test('a restored element has a new incarnation and rejects the old asynchronous task', () => { const { editor, propose } = setup(); editor.dispatch({ type: 'element.remove', target }); editor.history.undo(); - // Undo of removal creates a new incarnation: an old asynchronous task is stale. + // 删除撤销会恢复为新 incarnation,原 target 仍绑定第一代,因此旧尺寸任务必须失效。 assert.throws(propose, { code: 'creation_size_target_changed' }); }); // 用已有historyTransition格式描述plan,JSON往返后正反回放仍精确相等,entries不暴露set。 @@ -138,6 +139,7 @@ test('a future resize followed by movement retains the user size through both re editor.dispatch({ type: 'element.geometry', target, patch: { height: 300 } }); editor.dispatch({ type: 'element.geometry', target, patch: { x: 75 } }); editor.history.undo(); editor.history.undo(); + // redo 栈顶在尾端,故正向重做倒序取项;回卷则按原数组顺序应用逆变化。 const plan = propose(); let document = plan.document; for (const id of [...plan.history.redo].reverse()) document = applyChanges(document, plan.history.entries.get(id).changes); assert.equal(document.elements.get('image').geometry.height, 300); diff --git a/tests/headless/recovery-encoding.test.mjs b/tests/headless/recovery-encoding.test.mjs index 4c4c72e..100d201 100644 --- a/tests/headless/recovery-encoding.test.mjs +++ b/tests/headless/recovery-encoding.test.mjs @@ -61,7 +61,7 @@ test('cooperative sealing captures unknown values before yielding and retains ex assert.deepEqual(calls,['getter','left','right']); mutable.title='outside';current='outside'; const image=await pending; - // A second uncached table below exercises the work ceiling with a coarse clock. + // 下方另造未预热的大表,用静止时钟隔离工作量上限;即使时间预算不增长也必须让出。 assert.ok(yields>0);assert.deepEqual(image,expected);assert.equal(isSealedRecoveryImage(image),true); assert.equal(sealedRecoveryByteLength(image),new TextEncoder().encode(JSON.stringify(image)).byteLength); const beforeCoarse=yields; @@ -82,6 +82,7 @@ test('cooperative preparation evaluates every unsafe occurrence and rejects erro await assert.rejects(sealRecoveryImageAsync(17,1,1,dataArray(Array.from({length:1000},(_,i)=>i)),{now:()=>ticks++,frameBudgetMs:1,yieldTask:async()=>{throw Error('yield_failed');}}),/yield_failed/); // 异步深度检查使用内部 dataRecord 包装;省略的对象字段和变成 null 的数组项占用深度不同。 const nested=(n,leaf)=>{for(let i=0;i0);assert.equal(image.payload.left,image.payload.right); assert.equal(image.payload.left.length,1000);assert.equal(image.payload.left[999].index,999); assert.equal(sealedRecoveryByteLength(image),new TextEncoder().encode(JSON.stringify(image)).byteLength); - // Unsafe descendants still need independent values for every occurrence. + // 相同容器身份不代表后代 getter 安全;每次出现仍需独立捕获,递增值必须依次为 1、2。 let n=0;const unsafe=dataRecord({getValue:Object.freeze({get value(){return ++n;}})}); const captured=await sealRecoveryImageAsync(17,1,1,dataArray([unsafe,unsafe])); assert.deepEqual(captured.payload,[{getValue:{value:1}},{getValue:{value:2}}]); @@ -171,6 +172,7 @@ test('unknown callbacks invalidate the detached JSON environment without changin // left 先捕获,bridge getter 改写数组 toJSON,right 后捕获;同一 image 内顺序变化必须与同步 codec 一致。 const make=async()=>dataRecord({left:await ownedRecord(),bridge:{get value(){Object.defineProperty(Array.prototype,'toJSON',{configurable:true,value(){return ['changed-by-callback'];}});return 'bridge';}},right:await ownedRecord()}); try{ + // 同步参照也会改写原型,先 cleanup 再构造异步输入,保证两次捕获从相同环境开始。 const expected=sealRecoveryImage(17,1,1,await make());cleanup(); const payload=await make();let tick=0,yields=0; const pending=sealRecoveryImageAsync(17,1,1,payload,{now:()=>tick++,frameBudgetMs:1,yieldTask:async()=>{yields++;}});