Files
Endoleap/tests/headless/elision.test.mjs

81 lines
8.3 KiB
JavaScript

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}],[]);
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});
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())};
}
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);}
});
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');
});
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');
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/);
});
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());
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'));
});
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);
});