From 93f14af667c84ddafbecc292746b0f81ef8e1295 Mon Sep 17 00:00:00 2001 From: Codex Bot Date: Thu, 12 Nov 2026 17:55:19 +0800 Subject: [PATCH] docs(tests): explain editing leases and conditional wire compaction --- tests/headless/batch-projection.test.mjs | 4 ++++ tests/headless/bindings.test.mjs | 3 +++ 2 files changed, 7 insertions(+) diff --git a/tests/headless/batch-projection.test.mjs b/tests/headless/batch-projection.test.mjs index c575874..ef4a4b9 100644 --- a/tests/headless/batch-projection.test.mjs +++ b/tests/headless/batch-projection.test.mjs @@ -30,6 +30,7 @@ const values=document=>({elements:[...document.elements],canvases:[...document.c // original→长中间值→final可合成一个字段修改,但两逻辑ID都在;比较wire前后ledger及完整apply结果。 test('wire projection composes long field edits, preserves every original logical ID and leaves ledger/history inputs untouched',()=>{ const f=fixture([[field('original',long)],[field(long,'final')]]),before=JSON.stringify([...f.ledger.getSnapshot().operations]),wire=f.project(); + // changes 可以集中到末个 wire 操作,但每个逻辑操作 ID 都须保留,便于回执完整覆盖原 ledger。 assert.deepEqual(wire.map(op=>op.id),f.intents.map(op=>op.id)); assert.deepEqual(wire.at(-1).changes,[field('original','final','data.text',{afterPresent:true})]); assert.ok(JSON.stringify(wire).length({id,changes}))).length); @@ -46,6 +47,7 @@ test('position-only compaction keeps title, opaque data and the original before }); // new创建/更新/删除最终无节点但有墓碑;wire仍留创建与删除,不把生命周期错误压成无操作。 +// 这里是保留条件与生命周期的 wire 压缩;整体本地抵消须由 elision 另行证明,不能直接套用普通 create→delete 合并。 test('created records absorb checked updates while create/delete keeps tombstone and lifecycle evidence',()=>{ const created={...node('new'),data:{title:'new',text:long}},updated={...created,data:{...created.data,text:'final'}}; const f=fixture([[{kind:'element',id:'new',before:null,after:created}],[{...field(long,'final'),target:{id:'new',incarnation:1}}],[{kind:'element',id:'new',before:updated,after:null}]]),wire=f.project(); @@ -57,6 +59,7 @@ test('created records absorb checked updates while create/delete keeps tombstone // 可选字段从absent到值再absent,净值虽零仍保留前置存在性检查,apply结果与逐项操作相同。 test('net-zero field changes retain conditional presence checks; omitted true flags compose with explicit true flags',()=>{ const changes=[field(null,long,'data.optional',{beforePresent:false}),field(long,null,'data.optional',{beforePresent:true,afterPresent:false})]; + // 值最终仍缺失,不等于可以删除条件:beforePresent=false 还能阻止在已有 optional 的文档上误应用。 const compressed=compressChanges(changes,{preserveChecks:true,preserveLifecycle:true});assert.equal(compressed.length,1);assert.equal(compressed[0].beforePresent,false);assert.equal(compressed[0].afterPresent,false); assert.deepEqual(values(applyChanges(document,compressed)),values(applyChanges(document,changes))); const f=fixture([[changes[0]],[changes[1]]]);assert.equal(f.project().at(-1).changes[0].beforePresent,false); @@ -105,6 +108,7 @@ test('projection checks only affected records and leaves a large untouched docum test('wire cost uses UTF-8 bytes, so a Unicode guard cannot turn a shorter JS string into a larger request',()=>{ const a={...node('a',child),data:{title:'😀'.repeat(50),text:'😀'.repeat(50),blob:'x'}}; const base=createDocument(root,[{id:root,parent:null},{id:child,parent:{canvasId:root,entryElementId:'frame'}}],[{...node('frame'),componentId:'frame'},a]); + // 待压缩字段是很短的 blob,而参与范围护栏需携带含 emoji 的元素;UTF-8 成本会抵消压缩收益。 const middle='m'.repeat(150),f=fixture([[field('x',middle,'data.blob')],[field(middle,'final','data.blob')]],base),wire=f.project([child]); assert.deepEqual(wire,f.intents.map(({id,changes})=>({id,changes}))); }); diff --git a/tests/headless/bindings.test.mjs b/tests/headless/bindings.test.mjs index 115501c..305f245 100644 --- a/tests/headless/bindings.test.mjs +++ b/tests/headless/bindings.test.mjs @@ -50,6 +50,7 @@ test('connection drag owns the project gesture, previews without history, commit const path = { type: 'bezier-chain', knots: [] }; for (let i = 0; i < 20; i++) sessions.updateConnection('one', { ...path, startControl: { x: i, y: 0 } }); assert.equal(editor.getSnapshot().history.undo.length, 0); assert.equal(editor.getSnapshot().operations.size, 0); assert.equal(editor.getSnapshot().document.elements.get('edge').path, null); + // 20 次预览只改会话;commit 将最后一次 x=19 的控制向量提交成一个操作及一个撤销单元。 assert.equal(sessions.commitConnection('one').status, 'applied'); assert.equal(editor.getSnapshot().history.undo.length, 1); assert.equal(editor.getSnapshot().operations.size, 1); assert.equal(editor.getSnapshot().document.elements.get('edge').path.startControl.x, 19); sessions.beginConnection('one', target); sessions.updateConnection('one', { ...path, startControl: { x: 99, y: 0 } }); detach(); @@ -94,6 +95,7 @@ test('workspace retains a failed close for retry and stops edits before releasin await assert.rejects(() => workspace.close(), /workspace_close_failed/); assert.equal(leader.active, true); assert.equal(closed, false);assert.equal(digestClosed,false); assert.equal(a.editor.dispatch({ type: 'element.data', target: { id: 'outside', incarnation: 1 }, patch: { title: 'late' } }).status, 'rejected'); assert.equal(workspace.getProjects().size, 1); + // 第一次关闭失败后 workspace 仍保留 project 和 leader,供同一 owner 重试保存;成功后才关闭共享资源和存储。 broken = false; await workspace.close(); assert.equal(leader.active, false); assert.equal(closed, true);assert.equal(digestClosed,true); }); @@ -117,6 +119,7 @@ test('a canceled pointer lease cannot update, finish or cancel a newer gesture i const lease = () => { const token = sessions.getSnapshot().gesture.token; return guardEditingLease(() => sessions.getSnapshot().gesture?.token === token, { preview: patch => sessions.update('view', target, patch), commit: () => sessions.commit('view'), cancel: () => sessions.cancel('pointer_cancel') }); }; sessions.begin('view', [target]); const first = lease(); first.preview({ x: 100 }); first.cancel(); sessions.begin('view', [target]); const second = lease(); second.preview({ x: 200 }); + // 同一 view/element 的第二场手势有新 token;旧 lease 的三个入口都应无效,不能只防止重复 commit。 first.preview({ x: 500 }); first.commit(); first.cancel(); assert.equal(sessions.getSnapshot().gesture.preview.get('outside').x, 200); assert.equal(editor.getSnapshot().history.undo.length, 0); second.commit(); assert.equal(editor.getSnapshot().document.elements.get('outside').geometry.x, 200); assert.equal(editor.getSnapshot().history.undo.length, 1);