/** * 未发送操作的本地抵消证明测试:完整创建到删除的封闭生命周期可以不产生请求,但必须保存身份高水位。 * 抵消需证明操作连续、没有观察者遗漏且没有可能执行的网络证据;资源仍可由撤销历史持有。 */ import { test } from 'node:test'; import assert from 'node:assert/strict'; import { createDocument, createRegistry, builtinComponents, applyChanges } from '../../src/lib/canvas-core/index.ts'; import { createLedger } from '../../src/lib/canvas-runtime/ledger.ts'; import { elisionBatchLimit, findElision, proveElision, validateElisions, withLocalLifecycles } from '../../src/lib/canvas-runtime/elision.ts'; import { replayPending } from '../../src/lib/canvas-runtime/reconcile.ts'; import { resourcePins } from '../../src/lib/canvas-runtime/resources.ts'; // 共享只读空基线与内置定义;各用例从该基线创建新工作文档,不修改基线本身。 const registry=createRegistry(builtinComponents()),base=createDocument('project',[{id:'canvas',parent:null}],[]); // 创建指定 ID、归属与 incarnation 的文本样本,用于生命周期及原型敏感名称检查。 const node=(id='new',ownerCanvasId='canvas',incarnation=1)=>({id,ownerCanvasId,incarnation,componentId:'text',dataVersion:1,structure:'node',data:{title:id,text:''},geometry:{x:0,y:0,width:100,height:100,rotation:0}}); // 分别构造元素创建和删除变化;前后快照为抵消证明提供完整身份及内容依据。 const create=element=>({kind:'element',id:element.id,before:null,after:element}),remove=element=>({kind:'element',id:element.id,before:element,after:null}); // 将分块变化登记为线性依赖链,并记录每个操作首次看到的对象存在性;proof 每次读取最新账本。 function fixture(chunks,document=base){ const ledger=createLedger(),intents=chunks.map((changes,index)=>({id:`op-${index}`,operationType:'document.change',operationSchemaVersion:1,sequence:index+1,origin:'user',canvasIds:['canvas'],changes,dependencies:index?[`op-${index-1}`]:[]})); for(const intent of intents){const presence=Object.create(null);for(const change of intent.changes)if(change.kind!=='canvas'){const id=change.kind==='field'?change.target.id:change.id;presence[id]??=change.kind==='element'&&!change.before?'absent':'exists';}ledger.add(intent,presence);} return{ledger,intents,proof:()=>findElision(intents,document,ledger.getSnapshot())}; } // 创建、改名、删除可整体抵消,不产生请求;仅在本地重放中保留 incarnation 墓碑,操作标记为 elided。 test('closed create/update/delete lifetime retires without a request and preserves only local high-water marks',()=>{ const original=node(),changed={...original,data:{...original.data,title:'changed'}},f=fixture([[create(original)],[{kind:'field',target:{id:original.id,incarnation:1},field:'data.title',before:'new',after:'changed'}],[remove(changed)]]),proof=f.proof(); assert.deepEqual(proof.operationIds,['op-0','op-1','op-2']);assert.deepEqual(proof.tombstones,{new:1});f.ledger.elide(proof);validateElisions(f.ledger.getSnapshot()); // 未发送的生命周期不需要服务回执;墓碑只进入本地重放,避免下一次重用相同第一代身份。 assert.equal(f.ledger.getSnapshot().requests.size,0);assert.equal(base.tombstones.size,0);assert.equal(replayPending(base,f.ledger.getSnapshot(),registry).tombstones.get('new'),1); for(const record of f.ledger.getSnapshot().operations.values()){assert.equal(record.resolution,'elided');assert.equal(record.requestId,undefined);} }); // 已有元素的后续修改不属于封闭生命周期:只抵消前两个操作,后续标题修改继续 pending 并可重放。 test('the longest balanced prefix can retire before a later existing-element edit',()=>{ const a=node('a'),document=createDocument('project',[{id:'canvas',parent:null}],[a]),n=node(),f=fixture([[create(n)],[remove(n)],[{kind:'field',target:{id:'a',incarnation:1},field:'data.title',before:'a',after:'edited'}]],document); const proof=f.proof();assert.deepEqual(proof.operationIds,['op-0','op-1']);f.ledger.elide(proof); const working=replayPending(document,f.ledger.getSnapshot(),registry);assert.equal(working.elements.get('a').data.title,'edited');assert.equal(working.tombstones.get('new'),1);assert.equal(f.ledger.getSnapshot().operations.get('op-2').phase,'pending'); }); // 分区入口、子 canvas 与成员一起创建再删除时可抵消;仍记录元素墓碑,不给服务端虚构 canvas 版本。 test('closed partitions retain entry/member lifecycles without inventing server canvas versions',()=>{ const frame={...node('frame'),componentId:'frame'},member=node('member','child'),canvas={id:'child',parent:{canvasId:'canvas',entryElementId:'frame'}},changes=[create(frame),{kind:'canvas',id:'child',before:null,after:canvas},create(member)],inverse=[remove(member),{kind:'canvas',id:'child',before:canvas,after:null},remove(frame)],f=fixture([changes,inverse]); f.ledger.elide(f.proof());const working=replayPending(base,f.ledger.getSnapshot(),registry);assert.equal(working.canvases.size,1);assert.deepEqual(Object.fromEntries(working.tombstones),{member:1,frame:1});assert.equal(base.canvases.has('child'),false); }); // 逐一注入未知基线、阻塞、冻结和已标记派发状态,任一条件都使本地抵消证明不可用。 test('unknown, blocked, frozen and possibly-applied creation cannot be elided',()=>{ const n=node(); for(const state of ['unknown','blocked','frozen','dispatch']){ const f=fixture([[create(n)],[remove(n)]]); if(state==='unknown'){const records=new Map(f.ledger.getSnapshot().operations);records.set('op-0',{...records.get('op-0'),basePresence:{new:'unknown'}});assert.equal(findElision(f.intents,base,{...f.ledger.getSnapshot(),operations:records}),undefined);continue;} if(state==='blocked')f.ledger.block('op-0',['resource']);else{f.ledger.freeze({id:'request',operationIds:['op-0'],canvasIds:['canvas'],expectedVersions:{canvas:1},payload:{}});if(state==='dispatch')f.ledger.markDispatch('request');} assert.equal(f.proof(),undefined); } }); // 伪造高水位、跳过中间观察者或复用同一代身份都被拒绝;失败不能提前解决原操作。 test('a skipped observer, forged tombstone or reused incarnation cannot acquire a local completion proof',()=>{ const n=node(),f=fixture([[create(n)],[remove(n)]]),proof=f.proof();assert.throws(()=>f.ledger.elide({...proof,tombstones:{new:2}}),/invalid_elision/);assert.equal(f.ledger.getSnapshot().operations.get('op-0').phase,'pending'); // observer 虽无 changes,仍占据中间操作序列;只挑创建/删除两端无法证明整个连续区间都可抵消。 const createIntent={...f.intents[0],sequence:1},deleteIntent={...f.intents[1],sequence:3},ledger=createLedger();ledger.add(createIntent,{new:'absent'});ledger.add({...createIntent,id:'observer',sequence:2,changes:[],dependencies:['op-0']},{});ledger.add(deleteIntent,{new:'exists'}); assert.throws(()=>ledger.elide(proveElision([createIntent,deleteIntent],{})),/elision_not_contiguous/); assert.throws(()=>proveElision([{...createIntent,changes:[create(n),remove(n),create(n),remove(n)]}],{}),/stale_incarnation/); }); // 篡改已保存证明的归属、墓碑和基线时恢复校验拒绝;未来证明版本单独报告缺少解释器。 test('persisted elision groups validate ownership, scope continuity, version and exact lifecycle evidence',()=>{ const n=node(),f=fixture([[create(n)],[remove(n)]]);f.ledger.elide(f.proof());const saved=f.ledger.getSnapshot(); for(const change of [record=>({...record,elisionId:'missing'}),record=>({...record,elision:{...record.elision,tombstones:{new:2}}}),record=>({...record,basePresence:{new:'unknown'}})]){ const operations=new Map(saved.operations);operations.set('op-0',change(operations.get('op-0')));assert.throws(()=>validateElisions({operations}),/invalid_elision/); } const operations=new Map(saved.operations),anchor=operations.get('op-0');operations.set('op-0',{...anchor,elision:{...anchor.elision,version:2}});assert.throws(()=>validateElisions({operations}),/elision_interpreter_unavailable/); }); // 即使文档操作已抵消,undo 历史仍引用图片;资源 pin 必须同时保留创建和删除历史的来源。 test('resource references remain pinned by undo history after the document operations retire',()=>{ const picture={...node('picture'),componentId:'image',data:{title:'picture',text:'',src:'asset:asset',assetId:'asset'}},f=fixture([[create(picture)],[remove(picture)]]);f.ledger.elide(f.proof()); // 抵消的是同步意图,不是用户撤销能力;创建/删除历史条目中的 assetId 仍分别持有资源。 const entries=new Map([['create',{id:'create',changes:[create(picture)],operationIds:['op-0']}],['delete',{id:'delete',changes:[remove(picture)],operationIds:['op-1']}]]),pins=resourcePins({document:base,history:{undo:['create','delete'],redo:[],entries}},f.ledger.getSnapshot(),registry,new Map()); assert.equal(pins.complete,true);assert.ok(pins.pins.get('asset').has('history:create'));assert.ok(pins.pins.get('asset').has('history:delete')); }); // 上一段本地生命周期留下的高水位仍有效,普通和原型敏感 ID 均不能以旧 incarnation 再次抵消,失败保持原快照。 test('a subsequent cancellation group cannot reuse an incarnation from a prior local lifetime',()=>{ for(const id of ['new','constructor','__proto__']){ const n=node(id),f=fixture([[create(n)],[remove(n)]]);f.ledger.elide(f.proof()); const intents=f.intents.map((intent,index)=>({...intent,id:`later-${index}`,sequence:index+3,dependencies:index?['later-0']:[]})); f.ledger.add(intents[0],{[id]:'absent'});f.ledger.add(intents[1],{[id]:'exists'});const before=f.ledger.getSnapshot(); assert.throws(()=>f.ledger.elide(proveElision(intents,{})),/stale_incarnation/);assert.equal(f.ledger.getSnapshot(),before); } }); // 真实的已有元素修改使前批在该处截断;后续封闭片段不能忽略外部依赖单独抵消,而独立封闭链可完整入批。 test('a preceding real edit is split from a later closed segment, including explicit preceding dependencies',()=>{ const a=node('a'),n=node(),document=createDocument('project',[{id:'canvas',parent:null}],[a]),f=fixture([[{kind:'field',target:{id:'a',incarnation:1},field:'data.title',before:'a',after:'edited'}],[create(n)],[remove(n)]],document); assert.equal(elisionBatchLimit(f.intents,document,f.ledger.getSnapshot()),1);assert.equal(findElision(f.intents.slice(1),document,f.ledger.getSnapshot()),undefined); const closed=fixture([[create(n)],[remove(n)]]);assert.equal(elisionBatchLimit(closed.intents,base,closed.ledger.getSnapshot()),2); });