322 lines
65 KiB
JavaScript
322 lines
65 KiB
JavaScript
import {test} from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import {randomUUID} from 'node:crypto';
|
|
import {createDocument,applyChanges,createRegistry,builtinComponents} from '../../src/lib/canvas-core/index.ts';
|
|
import {openProjectEngine} from '../../src/lib/canvas-runtime/engine.ts';
|
|
import {createFoundryPersistence} from '../../src/lib/canvas-service/foundry.ts';
|
|
import {decodeRecovery} from '../../src/lib/canvas-runtime/recovery.ts';
|
|
const registry=createRegistry(builtinComponents()),root='10000000-0000-4000-8000-000000000001',child='10000000-0000-4000-8000-000000000002';
|
|
const scope={accountId:'10000000-0000-4000-8000-000000000003',workspaceId:'10000000-0000-4000-8000-000000000004',projectId:root};
|
|
const node=(id,ownerCanvasId=root)=>({id,incarnation:1,ownerCanvasId,componentId:'text',dataVersion:1,structure:'node',data:{title:id,text:'original'},geometry:{x:0,y:0,width:100,height:80,rotation:0}});
|
|
const a=node('A'),b=node('B'),frame={...node('F'),componentId:'frame'},c=node('C',child),edge={id:'E',incarnation:1,ownerCanvasId:root,componentId:'connection',dataVersion:1,structure:'connection',data:{},source:{elementId:'A'},target:{elementId:'C'},path:null};
|
|
const canvases=[{id:root,parent:null},{id:child,parent:{canvasId:root,entryElementId:'F'}}],prefix=createDocument(root,[canvases[0]],[a]),full=createDocument(root,canvases,[a,b,frame,c,edge]);
|
|
const versions=new Map([[root,1],[child,1]]),state={version:1,sourceToken:'captured-read',declaredCanvasIds:[root,child],receivedElements:1,totalElements:5},target={id:'A',incarnation:1};
|
|
function fixture(initial=full){
|
|
let image=null,leader,server=initial;const files=new Map();const serverVersions=new Map([...initial.canvases.keys()].map(id=>[id,1])),calls=[],receipts=new Map(),errors=[];
|
|
const storage={broken:false,saved:[],async claim(scope){assert.equal(leader,undefined);const held={scope,fence:1,active:true,async release(){leader=undefined;held.active=false;}};leader=held;return held;},async recover(){return structuredClone(image);},async commit(held,batch){assert.strictEqual(held,leader);if(storage.broken)throw Error('quota');image=structuredClone(batch.image);for(const file of batch.files??[])files.set(file.id,file.bytes);for(const id of batch.deleteFiles??[])files.delete(id);storage.saved.push(image);return{durableRootRevision:image.rootRevision};},async readFile(_leader,id){return files.get(id);},files,close(){},image:()=>structuredClone(image)};
|
|
const model=(participants=[...serverVersions.keys()],complete=true)=>({documentFormatVersion:1,projectId:root,complete,canvases:[...server.canvases.values()].filter(item=>participants.includes(item.id)),elements:[...server.elements.values()].filter(item=>participants.includes(item.ownerCanvasId)),tombstones:Object.fromEntries(server.tombstones),versions:Object.fromEntries(participants.map(id=>[id,serverVersions.get(id)])),removedCanvasIds:participants.filter(id=>!server.canvases.has(id))});
|
|
const transport=async input=>{
|
|
if(input.method==='GET')return input.path.endsWith('/model')?{status:200,body:{model:model()}}:{status:200,body:receipts.get(input.path.split('/').at(-1))};
|
|
calls.push(input);const body=JSON.parse(input.body),participants=Object.keys(body.expected_versions);for(const id of participants)assert.equal(body.expected_versions[id],serverVersions.get(id)??0);
|
|
if(!receipts.has(input.requestId)){server=applyChanges(server,body.operations.flatMap(item=>item.changes));for(const id of participants)serverVersions.set(id,(serverVersions.get(id)??0)+1);receipts.set(input.requestId,{protocol_version:2,request_id:input.requestId,status:'applied',operation_ids:body.operations.map(item=>item.id),model:model(participants,false)});}
|
|
return{status:200,body:receipts.get(input.requestId)};
|
|
};
|
|
const persistence=createFoundryPersistence({workspaceId:scope.workspaceId,rootId:root,registry,transport});
|
|
return{storage,persistence,calls,errors,server:()=>server,advance(changes){server=applyChanges(server,changes);serverVersions.set(root,serverVersions.get(root)+1);},currentVersions:()=>new Map(serverVersions),open:extra=>openProjectEngine({scope,registry,document:prefix,versions,loading:state,storage,persistence,createId:randomUUID,onError:error=>errors.push(error),...extra})};
|
|
}
|
|
|
|
test('loaded fields edit and undo/redo durably while no request freezes; full source adds tail and sends each edit once',async()=>{
|
|
const f=fixture();let engine=await f.open();try{
|
|
assert.equal(engine.editor.dispatch({type:'element.data',target,patch:{text:'local'}}).status,'applied');assert.equal(engine.editor.dispatch({type:'element.geometry',target,patch:{x:25}}).status,'applied');await engine.settle();
|
|
assert.equal(f.calls.length,0);assert.equal(engine.getSnapshot().runtime.ledger.requests.size,0);assert.equal(f.storage.image().formatVersion,7);decodeRecovery(f.storage.image(),registry);
|
|
assert.equal(engine.editor.history.undo().status,'applied');assert.equal(engine.editor.history.redo().status,'applied');await engine.settle();const history=engine.getSnapshot().history;
|
|
await engine.close();engine=await f.open();await engine.settle();assert.equal(f.calls.length,0);assert.equal(engine.getSnapshot().document.elements.get('A').data.text,'local');assert.equal(engine.getSnapshot().document.elements.get('A').geometry.x,25);
|
|
const next=createDocument(root,[canvases[0]],[a,b]);assert.equal(engine.loading.accept(next,versions,{...state,receivedElements:2}).status,'applied');assert.equal(engine.getSnapshot().document.elements.get('A').data.text,'local');assert.equal(engine.getSnapshot().document.elements.size,2);assert.deepEqual(engine.getSnapshot().history.undo,history.undo);
|
|
assert.equal((await engine.loading.complete(full,versions,{sourceToken:state.sourceToken})).status,'applied');await engine.settle();assert.equal(engine.loading.getState(),undefined);assert.equal(engine.getSnapshot().document.elements.size,5);assert.equal(f.server().elements.get('A').data.text,'local');assert.equal(f.server().elements.get('A').geometry.x,25);assert.ok(f.calls.length>0);assert.equal(engine.getSnapshot().history.undo.length,2);assert.ok([...engine.getSnapshot().runtime.ledger.operations.values()].every(record=>record.phase==='resolved'));assert.equal(f.errors.length,0);
|
|
}finally{await engine.close();}
|
|
});
|
|
test('incomplete scopes block structure/resources/references and untrusted marker removal without changing the root',async()=>{
|
|
const f=fixture(),engine=await f.open();try{
|
|
for(const command of [{type:'element.remove',target},{type:'element.create',element:node('new')},{type:'element.move',target,canvasId:child},{type:'element.data',target,patch:{assetId:'asset'}},{type:'element.data',target,patch:{presentation:{scale:2}}}]){const before=engine.getSnapshot();assert.equal(engine.editor.dispatch(command).code,'model_loading');assert.strictEqual(engine.getSnapshot(),before);}
|
|
const before=engine.getSnapshot();assert.equal(engine.editor.updateRuntime(previous=>({...previous,loading:undefined})).code,'loading_state_owned');assert.strictEqual(engine.getSnapshot(),before);
|
|
assert.throws(()=>engine.stageFiles([{name:'x',bytes:new Blob(['x'])}],()=>{}),{code:'model_loading'});await assert.rejects(engine.imports.duplicate([target]),{code:'model_loading'});assert.equal(f.calls.length,0);
|
|
}finally{await engine.close();}
|
|
});
|
|
test('full baseline must be durable before the hold releases; quota retry never sends from the partial cut',async()=>{
|
|
const f=fixture(),engine=await f.open();try{
|
|
engine.editor.dispatch({type:'element.data',target,patch:{title:'saved edit'}});await engine.settle();f.storage.broken=true;
|
|
await assert.rejects(engine.loading.complete(full,versions,{sourceToken:state.sourceToken}),/quota/);assert.equal(f.calls.length,0);assert.equal(f.storage.image().formatVersion,7);assert.ok(engine.getSnapshot().runtime.ledger.requests.size===0);
|
|
f.storage.broken=false;await engine.retry();await engine.settle();assert.equal(f.server().elements.get('A').data.title,'saved edit');assert.ok(f.calls.length>0);assert.notEqual(f.storage.image().formatVersion,7);
|
|
}finally{f.storage.broken=false;await engine.close();}
|
|
});
|
|
test('late or changed prefix is rejected; a fresh full read preserves local edits and same-version new records',async()=>{
|
|
const f=fixture(),engine=await f.open();try{
|
|
engine.editor.dispatch({type:'element.data',target,patch:{text:'local'}});await engine.settle();const before=engine.getSnapshot();
|
|
assert.equal(engine.loading.accept(prefix,versions,{...state,sourceToken:'another'}).code,'loading_source_mismatch');assert.strictEqual(engine.getSnapshot(),before);
|
|
const changed=createDocument(root,[canvases[0]],[{...a,data:{...a.data,title:'remote'}}]);assert.equal(engine.loading.accept(changed,versions,state).code,'loading_source_mismatch');assert.strictEqual(engine.getSnapshot(),before);
|
|
assert.equal((await engine.loading.complete(full,versions,{fresh:true})).status,'applied');await engine.settle();const done=engine.getSnapshot();assert.equal(engine.loading.accept(prefix,versions,state).code,'loading_not_active');assert.strictEqual(engine.getSnapshot(),done);
|
|
}finally{await engine.close();}
|
|
});
|
|
test('v7 cannot lose its marker or downgrade, and recovery retains partial edit/history without requests',async()=>{
|
|
const f=fixture(),engine=await f.open();try{engine.editor.dispatch({type:'element.data',target,patch:{text:'local'}});await engine.settle();const image=f.storage.image();assert.equal(decodeRecovery(image,registry).runtime.loading.sourceToken,state.sourceToken);
|
|
for(const formatVersion of [1,2,3,4,5,6])assert.throws(()=>decodeRecovery({...image,formatVersion},registry),{code:'unsupported_recovery_version'});
|
|
const missing=structuredClone(image);delete missing.payload.runtime.loading;assert.throws(()=>decodeRecovery(missing,registry),{code:'invalid_loading_state'});
|
|
}finally{await engine.close();}
|
|
});
|
|
|
|
test('a fresh conflicting completion keeps the original partial edit and resolves through the existing reviewed recovery path',async()=>{
|
|
const f=fixture(),engine=await f.open();try{
|
|
const edit=engine.editor.dispatch({type:'element.data',target,patch:{text:'local'}});await engine.settle();
|
|
f.advance([{kind:'field',target,field:'data.text',before:'original',after:'remote',beforePresent:true,afterPresent:true}]);
|
|
assert.equal((await engine.loading.complete(f.server(),f.currentVersions(),{fresh:true})).status,'rejected');assert.ok(engine.loading.getState());assert.equal(engine.getSnapshot().document.elements.get('A').data.text,'local');assert.equal(f.calls.length,0);
|
|
const session=await engine.prepareRecovery();assert.ok(session.conflict);const plan=session.plan(new Map([[edit.operationIds[0],'keep-local']]));assert.equal((await session.resolve(plan)).status,'applied');await engine.settle();assert.equal(engine.loading.getState(),undefined);assert.equal(f.server().elements.get('A').data.text,'local');assert.equal(f.errors.length,0);
|
|
}finally{await engine.close();}
|
|
});
|
|
test('reopening a partial image with a complete fresh model finishes through refresh without replacing history',async()=>{
|
|
const f=fixture();let engine=await f.open();try{engine.editor.dispatch({type:'element.data',target,patch:{title:'retained'}});await engine.settle();const history=engine.getSnapshot().history;await engine.close();engine=await f.open({document:full,loading:undefined});assert.ok(engine.loading.getState());assert.equal(engine.refresh(full,versions).status,'applied');await engine.settle();assert.equal(f.server().elements.get('A').data.title,'retained');assert.deepEqual(engine.getSnapshot().history.undo,history.undo);}finally{await engine.close();}
|
|
});
|
|
|
|
test('permitted command cannot hide a reference write inside a feature handler',async()=>{
|
|
const {defineComponent,defineFeature}=await import('../../src/lib/canvas-core/index.ts');
|
|
const custom=defineComponent({id:'unsafe-handler',dataVersion:1,structure:'node',validate:()=>true,resourceFields:[],copy:{references:[{path:['link'],target:'element',external:'preserve'}],omit:[]},features:[defineFeature('side-effect',{'content.edit':({element})=>[{kind:'field',target:{id:element.id,incarnation:element.incarnation},field:'data.link',before:'old',after:'new',beforePresent:true,afterPresent:true}]})]});
|
|
const customRegistry=createRegistry([...builtinComponents(),custom]),record={...a,componentId:custom.id,data:{title:'A',link:'old'}},document=createDocument(root,[canvases[0]],[record]),f=fixture(),engine=await f.open({registry:customRegistry,document});
|
|
try{const before=engine.getSnapshot();assert.equal(engine.editor.dispatch({type:'element.data',target,patch:{title:'allowed title'}}).code,'model_loading');assert.strictEqual(engine.getSnapshot(),before);assert.equal(f.calls.length,0);}finally{await engine.close();}
|
|
});
|
|
test('future loading interpreters, changed capture counts and forged frozen requests fail closed on recovery',async()=>{
|
|
const f=fixture(),engine=await f.open();try{await engine.persist();const source=f.storage.image();
|
|
for(const mutate of [image=>image.payload.runtime.loading.version=2,image=>image.payload.runtime.loading.receivedElements=99,image=>image.payload.runtime.loading.declaredCanvasIds=[],image=>image.payload.runtime.ledger.requests.push(['fake',{}])]){const image=structuredClone(source);mutate(image);assert.throws(()=>decodeRecovery(image,registry));}
|
|
}finally{await engine.close();}
|
|
});
|
|
|
|
test('fresh full read merges unrelated remote fields and geometry undo does not roll them back',async()=>{
|
|
const f=fixture(),engine=await f.open();try{
|
|
engine.editor.dispatch({type:'element.geometry',target,patch:{x:25}});await engine.settle();f.advance([{kind:'field',target,field:'data.title',before:'A',after:'remote title',beforePresent:true,afterPresent:true}]);
|
|
assert.equal((await engine.loading.complete(f.server(),f.currentVersions(),{fresh:true})).status,'applied');await engine.settle();assert.equal(engine.getSnapshot().document.elements.get('A').data.title,'remote title');assert.equal(f.server().elements.get('A').geometry.x,25);
|
|
assert.equal(engine.editor.history.undo().status,'applied');await engine.settle();assert.equal(f.server().elements.get('A').geometry.x,0);assert.equal(f.server().elements.get('A').data.title,'remote title');
|
|
}finally{await engine.close();}
|
|
});
|
|
|
|
test('identity fencing rejects edits, history traversal and late prefix publication without changing the root',async()=>{
|
|
const f=fixture();let current=true;const engine=await f.open({isCurrentIdentity:()=>current});try{engine.editor.dispatch({type:'element.data',target,patch:{text:'saved'}});await engine.settle();current=false;const before=engine.getSnapshot();assert.equal(engine.editor.dispatch({type:'element.data',target,patch:{text:'late'}}).code,'identity_changed');assert.equal(engine.editor.history.undo().code,'identity_changed');assert.equal(engine.loading.accept(prefix,versions,state).code,'identity_changed');assert.equal(engine.refresh(full,versions).code,'identity_changed');assert.strictEqual(engine.getSnapshot(),before);assert.equal(f.calls.length,0);}finally{await engine.close();}
|
|
});
|
|
|
|
test('background full reads are shared, preserve edits and cannot downgrade after completion',async()=>{
|
|
let release;const gate=new Promise(resolve=>release=resolve);const f=fixture();let reads=0;const persistence={...f.persistence,async read(){reads++;await gate;return{document:f.server(),versions:f.currentVersions()};}},engine=await f.open({persistence});
|
|
try{engine.editor.dispatch({type:'element.data',target,patch:{text:'background edit'}});await engine.persist();const first=engine.loading.resume(),second=engine.loading.resume();assert.strictEqual(first,second);while(!reads)await new Promise(resolve=>setImmediate(resolve));assert.equal(reads,1);release();assert.equal((await first).status,'applied');await engine.settle();assert.equal(f.server().elements.get('A').data.text,'background edit');assert.equal(engine.loading.accept(prefix,versions,state).code,'loading_not_active');assert.equal((await engine.loading.resume()).status,'applied');assert.equal(reads,1);}finally{release();await engine.close();}
|
|
});
|
|
test('project close aborts a background read and persists its partial edits for the next owner',async()=>{
|
|
const f=fixture();let aborted=false,started;const reading=new Promise(resolve=>started=resolve);const persistence={...f.persistence,read:signal=>new Promise((resolve,reject)=>{started();signal.addEventListener('abort',()=>{aborted=true;reject(signal.reason);},{once:true});})};let engine=await f.open({persistence});
|
|
engine.editor.dispatch({type:'element.data',target,patch:{text:'survives source close'}});await engine.persist();const rejected=assert.rejects(engine.loading.resume());await reading;await engine.close();await rejected;assert.equal(aborted,true);assert.equal(f.storage.image().formatVersion,7);engine=await f.open();try{assert.equal(engine.getSnapshot().document.elements.get('A').data.text,'survives source close');assert.equal(f.calls.length,0);}finally{await engine.close();}
|
|
});
|
|
test('Workspace opens one partial owner and ignores an older foreground result after background synchronization',async()=>{
|
|
const {createWorkspaceRuntime}=await import('../../src/lib/canvas-runtime/workspace.ts'),f=fixture();const workspace=createWorkspaceRuntime({accountId:scope.accountId,workspaceId:scope.workspaceId,authEpoch:1,registry,storage:f.storage,isCurrentIdentity:()=>true,adapters:()=>({persistence:f.persistence})});
|
|
try{const [first,second]=await Promise.all([workspace.openPartial(prefix,versions,state),workspace.openPartial(prefix,versions,state)]);assert.strictEqual(first,second);first.editor.dispatch({type:'element.data',target,patch:{title:'kept background'}});await first.loading.resume();await first.settle();const before=first.getSnapshot();assert.strictEqual(await workspace.open(full,versions),first);assert.strictEqual(first.getSnapshot(),before);assert.equal(first.getSnapshot().runtime.conflicts.size,0);assert.equal(workspace.getProjects().size,1);}finally{await workspace.close();}
|
|
});
|
|
|
|
test('Workspace reports a background source failure while preserving the pending partial cut and fences retired reports',async()=>{
|
|
const {createWorkspaceRuntime}=await import('../../src/lib/canvas-runtime/workspace.ts'),f=fixture(),reports=[];let current=true;const workspace=createWorkspaceRuntime({accountId:scope.accountId,workspaceId:scope.workspaceId,authEpoch:1,registry,storage:f.storage,isCurrentIdentity:()=>current,onError:error=>reports.push(error),adapters:()=>({persistence:{...f.persistence,async read(){throw Error('offline');}}})});
|
|
try{const engine=await workspace.openPartial(prefix,versions,state);engine.editor.dispatch({type:'element.data',target,patch:{text:'held while offline'}});await engine.persist();await assert.rejects(workspace.resumeLoading(root),/offline/);assert.equal(reports.length,1);assert.ok(engine.loading.getState());assert.equal(f.calls.length,0);assert.equal(f.storage.image().formatVersion,7);current=false;await assert.rejects(workspace.resumeLoading(root),/identity_changed/);assert.equal(reports.length,1);}finally{await workspace.close();}
|
|
});
|
|
|
|
test('certified scopes distinguish a complete child from an incomplete parent and publish coverage atomically',async()=>{
|
|
const loading={...state,version:2,canvasElementCounts:{[root]:4,[child]:1}},f=fixture();let engine=await f.open({loading});
|
|
try{
|
|
const first=engine.editor.getQueries();assert.equal(first.lookupElement('B').status,'unloaded');assert.equal(first.lookupElement('A').status,'present');assert.equal(first.membersResult(root).complete,false);assert.equal(first.hitNodes(root,{x:50,y:40}).complete,false);
|
|
const next=createDocument(root,canvases,[a,frame,c]);assert.equal(engine.loading.accept(next,versions,{...loading,receivedElements:3}).status,'applied');
|
|
const queries=engine.editor.getQueries();assert.equal(queries.membersResult(child).complete,true);assert.deepEqual(queries.membersResult(child).ids,['C']);assert.equal(queries.membersResult(root).complete,false);assert.equal(queries.childCanvasesResult(root).complete,true);assert.equal(queries.incidentResult('C').complete,false);assert.equal(queries.relations.referenceComplete,false);assert.equal(queries.coverage.projectComplete,false);assert.equal(first.lookupElement('C').status,'unloaded');
|
|
engine.editor.dispatch({type:'element.data',target:{id:'C',incarnation:1},patch:{text:'child edit'}});await engine.persist();assert.equal(f.storage.image().formatVersion,8);assert.equal(f.calls.length,0);decodeRecovery(f.storage.image(),registry);
|
|
const source=f.storage.image();for(const formatVersion of [1,2,3,4,5,6,7])assert.throws(()=>decodeRecovery({...source,formatVersion},registry),{code:'unsupported_recovery_version'});
|
|
for(const mutate of [image=>delete image.payload.runtime.loading.canvasElementCounts,image=>image.payload.runtime.loading.version=1,image=>image.payload.runtime.loading.version=99,image=>image.payload.runtime.loading.canvasElementCounts[child]=0]){const image=structuredClone(source);mutate(image);assert.throws(()=>decodeRecovery(image,registry));}
|
|
await engine.close();engine=await f.open({loading});assert.equal(engine.editor.getQueries().membersResult(child).complete,true);assert.equal(engine.getSnapshot().document.elements.get('C').data.text,'child edit');
|
|
assert.equal(engine.loading.accept(full,versions,{...loading,receivedElements:5}).status,'applied');const before=engine.getSnapshot(),oldQueries=engine.editor.getQueries();assert.equal(oldQueries.lookupElement('missing').status,'unloaded');assert.equal(oldQueries.membersResult(root).complete,true);
|
|
const completion=await engine.loading.complete(full,versions,{sourceToken:loading.sourceToken});assert.equal(completion.status,'applied');assert.equal(completion.revision,before.revision,'coverage-only completion must not invent a document revision');assert.equal(engine.editor.getQueries().lookupElement('missing').status,'absent');assert.equal(engine.editor.getQueries().coverage.projectComplete,true);assert.equal(oldQueries.lookupElement('missing').status,'unloaded');await engine.settle();assert.equal(f.server().elements.get('C').data.text,'child edit');assert.equal(f.storage.image().formatVersion<7,true);
|
|
}finally{await engine.close();}
|
|
});
|
|
test('legacy prefixes do not infer scope completion; a changed count manifest cannot replace the published root',async()=>{
|
|
const f=fixture(),engine=await f.open();try{
|
|
assert.equal(engine.loading.accept(full,versions,{...state,receivedElements:5}).status,'applied');assert.equal(engine.editor.getQueries().membersResult(child).complete,false);
|
|
const before=engine.getSnapshot();assert.equal(engine.loading.accept(full,versions,{...state,version:2,canvasElementCounts:{[root]:4,[child]:1},receivedElements:5}).code,'loading_source_mismatch');assert.strictEqual(engine.getSnapshot(),before);
|
|
}finally{await engine.close();}
|
|
});
|
|
test('zero-member scopes are complete only once their entry dependencies publish',async()=>{
|
|
const f=fixture(),document=createDocument(root,canvases,[a,frame]),loading={...state,version:2,totalElements:2,receivedElements:2,canvasElementCounts:{[root]:2,[child]:0}},engine=await f.open({document,loading});
|
|
try{assert.deepEqual(engine.editor.getQueries().membersResult(child),{ids:[],complete:true});assert.equal(engine.editor.getQueries().lookupElement('not-here').status,'unloaded');}finally{await engine.close();}
|
|
});
|
|
|
|
test('v2 admits path edits in a certified connection owner while another scope is incomplete, preserving undo and v8 recovery',async()=>{
|
|
const f=fixture(),loading={...state,version:2,receivedElements:5,totalElements:6,canvasElementCounts:{[root]:4,[child]:2}},engine=await f.open({document:full,loading}),path={type:'bezier-chain',knots:[{id:'k',mode:'corner',position:{x:50,y:25},in:{x:0,y:0},out:{x:0,y:0}}]};
|
|
try{
|
|
assert.equal(engine.editor.getQueries().membersResult(root).complete,true);assert.equal(engine.editor.getQueries().membersResult(child).complete,false);
|
|
const command={type:'connection.path',target:{id:'E',incarnation:1},path};assert.equal(engine.editor.can(command).enabled,true);assert.equal(engine.editor.dispatch(command).status,'applied');assert.equal(engine.getSnapshot().document.elements.get('E').path.knots[0].position.x,50);
|
|
assert.equal(engine.editor.history.undo().status,'applied');assert.equal(engine.getSnapshot().document.elements.get('E').path,null);assert.equal(engine.editor.history.redo().status,'applied');await engine.persist();assert.equal(f.calls.length,0);assert.equal(f.storage.image().formatVersion,8);assert.equal(decodeRecovery(f.storage.image(),registry).document.elements.get('E').path.knots[0].position.x,50);
|
|
const before=engine.getSnapshot();assert.equal(engine.loading.accept(full,versions,{...loading,canvasElementCounts:{[root]:5,[child]:1}}).code,'loading_source_mismatch');assert.strictEqual(engine.getSnapshot(),before);
|
|
}finally{await engine.close();}
|
|
const other=fixture(),incomplete=await other.open({document:full,loading:{...loading,canvasElementCounts:{[root]:5,[child]:1}}});try{assert.equal(incomplete.editor.can({type:'connection.path',target:{id:'E',incarnation:1},path}).enabled,false);}finally{await incomplete.close();}
|
|
});
|
|
|
|
test('v3 sends a certified child before the parent tail, retains its ACK through old chunks and completes the original source',async()=>{
|
|
const f=fixture(),document=createDocument(root,canvases,[a,frame,c]),loading={...state,version:3,receivedElements:3,canvasElementCounts:{[root]:4,[child]:1}},engine=await f.open({document,loading});
|
|
try{
|
|
assert.equal(engine.editor.dispatch({type:'element.data',target,patch:{title:'parent waits'}}).status,'applied');const edit=engine.editor.dispatch({type:'element.data',target:{id:'C',incarnation:1},patch:{text:'child acknowledged before tail'}});assert.equal(edit.status,'applied');await engine.settle();
|
|
assert.equal(f.calls.length,1,JSON.stringify(f.errors));assert.equal(f.server().elements.get('C').data.text,'child acknowledged before tail');assert.equal(engine.loading.getState().version,3);assert.equal(engine.getSnapshot().runtime.versions.get(child),2);assert.equal(engine.getSnapshot().runtime.loadingRead.versions.get(child),1);assert.equal(f.storage.image().formatVersion,9);decodeRecovery(f.storage.image(),registry);
|
|
assert.equal(engine.editor.dispatch({type:'element.data',target,patch:{title:'parent waits'}}).status,'applied');await engine.settle();assert.equal(f.calls.length,1,'incomplete owner froze a request');
|
|
assert.equal(engine.loading.accept(full,versions,{...loading,receivedElements:5}).status,'applied');assert.equal(engine.getSnapshot().document.elements.get('C').data.text,'child acknowledged before tail');await engine.settle();assert.equal(f.calls.length,2);assert.equal(f.server().elements.get('A').data.title,'parent waits');assert.equal(engine.loading.getState().version,3);
|
|
assert.equal((await engine.loading.complete(full,versions,{sourceToken:loading.sourceToken})).status,'applied');await engine.settle();assert.equal(engine.getSnapshot().runtime.loadingRead,undefined);assert.equal(engine.getSnapshot().document.elements.get('C').data.text,'child acknowledged before tail');assert.equal(engine.getSnapshot().document.elements.get('A').data.title,'parent waits');assert.equal(engine.getSnapshot().runtime.versions.get(root),2);assert.equal(engine.getSnapshot().runtime.versions.get(child),2);assert.equal(engine.getSnapshot().history.undo.length,2);assert.equal(f.errors.length,0);
|
|
assert.equal(engine.editor.history.undo().status,'applied');await engine.settle();assert.equal(f.server().elements.get('A').data.title,'parent waits');assert.equal(f.server().elements.get('C').data.text,'original');assert.equal(engine.editor.history.undo().status,'applied');await engine.settle();assert.equal(f.server().elements.get('A').data.title,'A');
|
|
}finally{await engine.close();}
|
|
});
|
|
test('v3 unknown request reopens with original body/key, queries its receipt and rejects source tampering',async()=>{
|
|
let time=0;const f=fixture(),document=createDocument(root,canvases,[a,frame,c]),loading={...state,version:3,receivedElements:3,canvasElementCounts:{[root]:4,[child]:1}},clock={now:()=>time,setTimer:()=>0,clearTimer(){}},persistence={...f.persistence,async submit(request,signal){await f.persistence.submit(request,signal);throw Error('lost ACK');}};
|
|
let engine=await f.open({document,loading,persistence,clock});try{
|
|
engine.editor.dispatch({type:'element.data',target:{id:'C',incarnation:1},patch:{text:'applied unknown'}});await engine.settle();const request=[...engine.getSnapshot().runtime.ledger.requests.values()][0];assert.ok(request);assert.equal(request.phase,'waiting');assert.equal(f.server().elements.get('C').data.text,'applied unknown');
|
|
const beforeFresh=engine.getSnapshot();assert.equal((await engine.loading.complete(f.server(),f.currentVersions(),{fresh:true})).code,'loading_waiting_for_receipts');assert.strictEqual(engine.getSnapshot(),beforeFresh);assert.ok(engine.loading.getState());await engine.persist();const saved=f.storage.image();assert.equal(saved.formatVersion,9);decodeRecovery(saved,registry);
|
|
for(const mutate of [image=>delete image.payload.runtime.loadingRead,image=>image.payload.runtime.loadingRead.versions.find(([id])=>id===child)[1]=99,image=>image.formatVersion=8,image=>delete image.payload.runtime.ledger.requests[0][1].expectedVersions[child]]){const broken=structuredClone(saved);mutate(broken);assert.throws(()=>decodeRecovery(broken,registry));}
|
|
await engine.close();time=1000;engine=await f.open({document,loading,clock});await engine.settle();const restored=engine.getSnapshot().runtime.ledger.requests.get(request.id);assert.equal(restored.body,request.body);assert.equal(restored.phase,'resolved');assert.equal(f.calls.length,1,'reopening submitted a new write instead of querying the old receipt');assert.equal(engine.getSnapshot().runtime.versions.get(child),2);
|
|
const before=engine.getSnapshot(),tampered=createDocument(root,canvases,[a,b,frame,{...c,data:{...c.data,text:'changed old stream'}},edge]);assert.equal(engine.loading.accept(tampered,versions,{...loading,receivedElements:5}).code,'loading_source_mismatch');assert.strictEqual(engine.getSnapshot(),before);
|
|
assert.equal((await engine.loading.complete(full,versions,{sourceToken:loading.sourceToken})).status,'applied');await engine.settle();assert.equal(engine.getSnapshot().document.elements.get('C').data.text,'applied unknown');
|
|
}finally{await engine.close();}
|
|
});
|
|
|
|
test('background completion waits outside the network lane for a v3 unknown receipt, then reads the fresh full model',async()=>{
|
|
let time=0,reads=0;const f=fixture(),document=createDocument(root,canvases,[a,frame,c]),loading={...state,version:3,receivedElements:3,canvasElementCounts:{[root]:4,[child]:1}},clock={now:()=>time,setTimer:()=>0,clearTimer(){}},persistence={...f.persistence,async read(signal){reads++;return f.persistence.read(signal);},async submit(request,signal){await f.persistence.submit(request,signal);throw Error('lost ACK');}},engine=await f.open({document,loading,persistence,clock});
|
|
try{engine.editor.dispatch({type:'element.data',target:{id:'C',incarnation:1},patch:{text:'wait for original receipt'}});await engine.settle();const completion=engine.loading.resume();await new Promise(resolve=>setImmediate(resolve));assert.equal(reads,0);time=1000;await engine.retry();assert.equal((await completion).status,'applied');await engine.settle();assert.equal(reads,1);assert.equal(f.calls.length,1);assert.equal(engine.getSnapshot().document.elements.get('C').data.text,'wait for original receipt');assert.equal(engine.loading.getState(),undefined);}finally{await engine.close();}
|
|
});
|
|
|
|
test('a fresh v3 background read fences new admissions while preserving edits made during the read',async()=>{
|
|
let release,started;const gate=new Promise(resolve=>release=resolve),reading=new Promise(resolve=>started=resolve),f=fixture(),document=createDocument(root,canvases,[a,frame,c]),loading={...state,version:3,receivedElements:3,canvasElementCounts:{[root]:4,[child]:1}},engine=await f.open({document,loading,persistence:{...f.persistence,async read(signal){started();await gate;return f.persistence.read(signal);}}});
|
|
try{const completion=engine.loading.resume();await reading;engine.editor.dispatch({type:'element.data',target:{id:'C',incarnation:1},patch:{text:'edited during fresh read'}});await engine.settle();assert.equal(f.calls.length,0);release();assert.equal((await completion).status,'applied');await engine.settle();assert.equal(f.server().elements.get('C').data.text,'edited during fresh read');assert.equal(f.calls.length,1);}finally{release();await engine.close();}
|
|
});
|
|
|
|
test('a v3 certified owner cannot freeze until its source and edit are durably saved; retry wakes the same logical intent',async()=>{
|
|
const f=fixture(),document=createDocument(root,canvases,[a,frame,c]),loading={...state,version:3,receivedElements:3,canvasElementCounts:{[root]:4,[child]:1}},engine=await f.open({document,loading});
|
|
try{f.storage.broken=true;const edit=engine.editor.dispatch({type:'element.data',target:{id:'C',incarnation:1},patch:{text:'durability first'}});await assert.rejects(engine.persist(),/quota/);await new Promise(resolve=>setImmediate(resolve));assert.equal(engine.getSnapshot().runtime.ledger.requests.size,0);assert.equal(f.calls.length,0);f.storage.broken=false;await engine.retry();await engine.settle();assert.equal(f.calls.length,1);assert.equal(f.server().elements.get('C').data.text,'durability first');assert.equal(engine.getSnapshot().runtime.ledger.operations.get(edit.operationIds[0]).phase,'resolved');}finally{f.storage.broken=false;await engine.close();}
|
|
});
|
|
|
|
test('closing a v3 background receipt wait unsubscribes and saves the unknown request without issuing a fresh read',async()=>{
|
|
let reads=0;const f=fixture(),document=createDocument(root,canvases,[a,frame,c]),loading={...state,version:3,receivedElements:3,canvasElementCounts:{[root]:4,[child]:1}},engine=await f.open({document,loading,clock:{now:()=>0,setTimer:()=>0,clearTimer(){}},persistence:{...f.persistence,async read(signal){reads++;return f.persistence.read(signal);},async submit(request,signal){await f.persistence.submit(request,signal);throw Error('lost ACK');}}});
|
|
engine.editor.dispatch({type:'element.data',target:{id:'C',incarnation:1},patch:{text:'retain on close'}});await engine.settle();const waiting=assert.rejects(engine.loading.resume());await new Promise(resolve=>setImmediate(resolve));await engine.close();await waiting;assert.equal(reads,0);assert.equal(f.calls.length,1);assert.equal(f.storage.image().formatVersion,9);assert.equal(decodeRecovery(f.storage.image(),registry).runtime.ledger.requests.size,1);
|
|
});
|
|
|
|
const identities=Object.fromEntries([...full.elements.values()].map(element=>[element.id,element.ownerCanvasId]));
|
|
const structuralLoading={...state,version:4,receivedElements:3,canvasElementCounts:{[root]:4,[child]:1},elementOwners:identities};
|
|
test('v4 creates, acknowledges, undoes and restores a fresh element in a complete owner before an unrelated tail',async()=>{
|
|
const f=fixture(),document=createDocument(root,canvases,[a,frame,c]),engine=await f.open({document,loading:structuralLoading}),created=node('fresh',child);
|
|
try{
|
|
const before=engine.getSnapshot();assert.equal(engine.editor.getQueries().lookupElement('B').status,'unloaded');assert.equal(engine.editor.getQueries().lookupElement('fresh').status,'absent');assert.equal(engine.editor.dispatch({type:'element.create',element:{...b,ownerCanvasId:child}}).code,'model_loading');assert.strictEqual(engine.getSnapshot(),before,'an unloaded source ID was reused');
|
|
assert.equal(engine.editor.dispatch({type:'element.create',element:created}).status,'applied');await engine.settle();assert.equal(f.server().elements.get('fresh').incarnation,1,JSON.stringify(f.errors));assert.equal(f.calls.length,1);assert.equal(f.storage.image().formatVersion,10);decodeRecovery(f.storage.image(),registry);
|
|
assert.equal(engine.editor.history.undo().status,'applied');await engine.settle();assert.equal(f.server().elements.has('fresh'),false);assert.equal(engine.editor.history.redo().status,'applied');await engine.settle();assert.equal(f.server().elements.get('fresh').incarnation,2);assert.equal(engine.getSnapshot().runtime.loadingRead.document.elements.has('fresh'),false);
|
|
assert.equal(engine.loading.accept(full,versions,{...structuralLoading,receivedElements:5}).status,'applied');assert.equal((await engine.loading.complete(full,versions,{sourceToken:state.sourceToken})).status,'applied');await engine.settle();assert.equal(engine.getSnapshot().document.elements.get('fresh').incarnation,2);assert.equal(engine.getSnapshot().runtime.versions.get(child),4);assert.equal(f.calls.length,3);assert.equal(f.errors.length,0);
|
|
}finally{await engine.close();}
|
|
});
|
|
test('v4 frame/canvas creation is one atomic history unit and new scope versions recover independently of the old stream',async()=>{
|
|
const f=fixture(),document=createDocument(root,canvases,[a,frame,c]),newCanvas='10000000-0000-4000-8000-000000000009',newFrame={...node('new-frame',child),componentId:'frame'};let engine=await f.open({document,loading:structuralLoading});
|
|
try{
|
|
assert.equal(engine.editor.transaction(tx=>{tx.dispatch({type:'element.create',element:newFrame});tx.dispatch({type:'canvas.create',canvas:{id:newCanvas,parent:{canvasId:child,entryElementId:newFrame.id}}});}).status,'applied');await engine.settle();assert.equal(f.server().canvases.has(newCanvas),true,JSON.stringify(f.errors));assert.equal(engine.getSnapshot().runtime.versions.get(newCanvas),1);assert.equal(engine.getSnapshot().history.undo.length,1);decodeRecovery(f.storage.image(),registry);
|
|
await engine.close();engine=await f.open({document,loading:structuralLoading});assert.equal(engine.editor.getQueries().membersResult(newCanvas).complete,true);assert.equal(engine.editor.history.undo().status,'applied');await engine.settle();assert.equal(f.server().canvases.has(newCanvas),false);assert.equal(engine.editor.history.redo().status,'applied');await engine.settle();assert.equal(f.server().elements.get(newFrame.id).incarnation,2);assert.equal(engine.getSnapshot().runtime.versions.get(newCanvas),3);
|
|
assert.equal((await engine.loading.complete(full,versions,{sourceToken:state.sourceToken})).status,'applied');await engine.settle();assert.equal(engine.getSnapshot().document.canvases.has(newCanvas),true);assert.equal(f.errors.length,0);
|
|
}finally{await engine.close();}
|
|
});
|
|
|
|
test('v4 defers original deletions until the captured reference closure is loaded, while born links and nodes can be removed atomically',async()=>{
|
|
const f=fixture(),document=createDocument(root,canvases,[a,frame,c]),engine=await f.open({document,loading:structuralLoading});
|
|
try{const before=engine.getSnapshot();assert.equal(engine.editor.dispatch({type:'element.remove',target}).code,'model_loading');assert.strictEqual(engine.getSnapshot(),before);
|
|
const added=node('new-child',child),link={...edge,id:'new-edge',ownerCanvasId:child,source:{elementId:'C'},target:{elementId:added.id}};assert.equal(engine.editor.transaction(tx=>{tx.dispatch({type:'element.create',element:added});tx.dispatch({type:'element.create',element:link});}).status,'applied');await engine.settle();assert.equal(engine.editor.dispatch({type:'element.remove',target:{id:added.id,incarnation:1}}).status,'applied');await engine.settle();assert.equal(f.server().elements.has(added.id),false);assert.equal(f.server().elements.has(link.id),false);assert.equal(f.server().elements.has('C'),true);
|
|
assert.equal(engine.loading.accept(full,versions,{...structuralLoading,receivedElements:5}).status,'applied');assert.equal(engine.editor.dispatch({type:'element.remove',target}).status,'applied');await engine.settle();assert.equal(f.server().elements.has('A'),false);assert.equal(f.server().elements.has('E'),false);assert.equal((await engine.loading.complete(full,versions,{sourceToken:state.sourceToken})).status,'applied');await engine.settle();assert.equal(engine.getSnapshot().document.elements.has('A'),false);assert.equal(f.errors.length,0);
|
|
}finally{await engine.close();}
|
|
});
|
|
test('v4 identity proof preserves source AI metadata during ordinary edits and rejects forged membership recovery',async()=>{
|
|
const old={...c,data:{...c.data,ai:{enabled:true}}},f=fixture(createDocument(root,canvases,[a,b,frame,old,edge])),document=createDocument(root,canvases,[a,frame,old]),engine=await f.open({document,loading:structuralLoading});
|
|
try{assert.equal(engine.editor.dispatch({type:'element.data',target:{id:'C',incarnation:1},patch:{title:'still editable'}}).status,'applied');await engine.settle();assert.equal(f.errors.length,0);assert.equal(engine.getSnapshot().document.elements.get('C').data.ai.enabled,true);const saved=f.storage.image();decodeRecovery(saved,registry);for(const mutate of [image=>delete image.payload.runtime.loading.elementOwners,image=>image.payload.runtime.loading.elementOwners.C=root,image=>image.formatVersion=9]){const broken=structuredClone(saved);mutate(broken);assert.throws(()=>decodeRecovery(broken,registry));}}finally{await engine.close();}
|
|
});
|
|
|
|
test('v4 elides an unsent birth/deletion and restores the same ID with a fresh lifecycle before the source tail',async()=>{
|
|
const f=fixture(),document=createDocument(root,canvases,[a,frame,c]),engine=await f.open({document,loading:structuralLoading}),created=node('unsent-birth',child);
|
|
try{assert.equal(engine.editor.dispatch({type:'element.create',element:created}).status,'applied');assert.equal(engine.editor.dispatch({type:'element.remove',target:{id:created.id,incarnation:1}}).status,'applied');await engine.settle();assert.equal(f.calls.length,0);assert.equal(engine.editor.history.undo().status,'applied');await engine.settle();assert.equal(f.server().elements.get(created.id).incarnation,2);decodeRecovery(f.storage.image(),registry);assert.equal((await engine.loading.complete(full,versions,{sourceToken:state.sourceToken})).status,'applied');await engine.settle();assert.equal(f.server().elements.get(created.id).incarnation,2);assert.equal(f.calls.length,1);}finally{await engine.close();}
|
|
});
|
|
test('v4 born-node ownership changes require both scopes and preserve their history while another source owner is incomplete',async()=>{
|
|
const f=fixture(),document=createDocument(root,canvases,[a,frame,c]),engine=await f.open({document,loading:structuralLoading}),newCanvas='10000000-0000-4000-8000-000000000019',newFrame={...node('move-frame',child),componentId:'frame'},created=node('moving-birth',newCanvas);
|
|
try{assert.equal(engine.editor.transaction(tx=>{tx.dispatch({type:'element.create',element:newFrame});tx.dispatch({type:'canvas.create',canvas:{id:newCanvas,parent:{canvasId:child,entryElementId:newFrame.id}}});tx.dispatch({type:'element.create',element:created});}).status,'applied');await engine.settle();assert.equal(engine.editor.can({type:'element.move',target:{id:created.id,incarnation:1},canvasId:root}).enabled,false);assert.equal(engine.editor.dispatch({type:'element.move',target:{id:created.id,incarnation:1},canvasId:child}).status,'applied');await engine.settle();assert.equal(f.server().elements.get(created.id).ownerCanvasId,child);assert.equal(engine.editor.history.undo().status,'applied');await engine.settle();assert.equal(f.server().elements.get(created.id).ownerCanvasId,newCanvas);assert.equal(engine.loading.getState().version,4);assert.equal(f.errors.length,0);}finally{await engine.close();}
|
|
});
|
|
|
|
test('v4 lookup does not confuse an unloaded restored element with its older tombstone',async()=>{
|
|
const restored={...a,incarnation:2},dead=new Map([['A',1]]),server=createDocument(root,canvases,[restored,b,frame,c,edge],dead),f=fixture(server),document=createDocument(root,canvases,[frame,c],dead),engine=await f.open({document,loading:{...structuralLoading,receivedElements:2}});
|
|
try{assert.equal(engine.editor.getQueries().lookupElement('A').status,'unloaded');assert.equal(engine.editor.getQueries().lookupElement('not-in-capture').status,'absent');assert.equal(engine.loading.accept(server,versions,{...structuralLoading,receivedElements:5}).status,'applied');assert.equal(engine.editor.getQueries().lookupElement('A').element.incarnation,2);assert.equal(engine.editor.dispatch({type:'element.remove',target:{id:'A',incarnation:2}}).status,'applied');await engine.settle();assert.equal(engine.editor.getQueries().lookupElement('A').status,'absent');}finally{await engine.close();}
|
|
});
|
|
|
|
test('v4 fresh completion accepts acknowledged births but rejects an invented member in an unchanged incomplete source scope',async()=>{
|
|
const f=fixture(),document=createDocument(root,canvases,[a,frame,c]),engine=await f.open({document,loading:structuralLoading}),created=node('fresh-before-background',child);
|
|
try{engine.editor.dispatch({type:'element.create',element:created});await engine.settle();const malformed=createDocument(root,canvases,[...f.server().elements.values(),node('invented')]);assert.equal((await engine.loading.complete(malformed,f.currentVersions(),{fresh:true})).code,'canonical_version_mismatch');assert.equal(engine.getSnapshot().document.elements.has(created.id),true);assert.equal((await engine.loading.resume()).status,'applied');await engine.settle();assert.equal(engine.getSnapshot().document.elements.get(created.id).incarnation,1);assert.equal(engine.getSnapshot().runtime.loadingRead,undefined);assert.equal(f.calls.length,1);}finally{await engine.close();}
|
|
});
|
|
|
|
const jobLoading={...structuralLoading,version:5,sourceCanvases:canvases};
|
|
const ndjson=records=>new Blob(records.map(record=>JSON.stringify({kind:'element',key:record.id,record})+'\n'));
|
|
test('v5 native import publishes and synchronizes into a complete owner while another captured owner remains incomplete',async()=>{
|
|
const f=fixture(),document=createDocument(root,canvases,[a,frame,c]),engine=await f.open({document,loading:jobLoading});
|
|
try{assert.equal(engine.imports.canStart(root).enabled,false);assert.equal(engine.imports.canStart(child).enabled,true);assert.throws(()=>engine.imports.startNdjson(ndjson([node('source')]),root),{code:'loading_scope_incomplete'});assert.equal(engine.imports.getJobs().size,0);assert.equal(f.storage.files.size,0);
|
|
const id=engine.imports.startNdjson(ndjson([{...node('source','$target'),data:{title:'Imported during loading',text:'hello'}}]),child);await engine.settle();const job=engine.imports.getJobs().get(id);assert.equal(job.status,'completed',JSON.stringify({job,errors:f.errors}));const mapped=job.stableIds['element:source'];assert.equal(f.server().elements.get(mapped).data.title,'Imported during loading');assert.equal(engine.loading.getState().version,5);assert.equal(f.storage.image().formatVersion,17);assert.equal(job.source.loadingAdmission.sourceToken,state.sourceToken);assert.deepEqual(job.source.loadingAdmission.requiredCanvasIds,[child]);decodeRecovery(f.storage.image(),registry);
|
|
assert.equal((await engine.loading.complete(full,versions,{sourceToken:state.sourceToken})).status,'applied');await engine.settle();assert.equal(engine.getSnapshot().document.elements.get(mapped).data.text,'hello');assert.equal(f.errors.length,0);
|
|
}finally{await engine.close();}
|
|
});
|
|
test('v5 copy rejects an incomplete ancestor or a missing declared child before allocating task bytes',async()=>{
|
|
const f=fixture(),document=createDocument(root,canvases,[a,frame,c]),engine=await f.open({document,loading:jobLoading});
|
|
try{const before=engine.getSnapshot(),selection=[{id:'C',incarnation:1}];assert.equal(engine.imports.canDuplicate(selection,{viewCanvasId:child}).enabled,false);await assert.rejects(engine.imports.duplicate(selection,{viewCanvasId:child}),{code:'loading_scope_incomplete'});assert.strictEqual(engine.getSnapshot(),before);assert.equal(engine.imports.getJobs().size,0);assert.equal(f.storage.files.size,0);}finally{await engine.close();}
|
|
});
|
|
test('v5 deep copy includes upper incoming edges and a closed subtree while unrelated canvas contents are still loading',async()=>{
|
|
const other='10000000-0000-4000-8000-000000000029',g={...node('G'),componentId:'frame'},incoming={...edge,source:{elementId:'A'},target:{elementId:'F'}},topology=[...canvases,{id:other,parent:{canvasId:root,entryElementId:'G'}}],server=createDocument(root,topology,[a,frame,g,incoming,c,node('U',other),node('V',other)]),document=createDocument(root,topology,[a,frame,g,incoming,c,node('U',other)]),f=fixture(server),serverVersions=new Map([[root,1],[child,1],[other,1]]),loading={version:5,sourceToken:'closed-copy',declaredCanvasIds:[root,child,other],sourceCanvases:topology,receivedElements:6,totalElements:7,canvasElementCounts:{[root]:4,[child]:1,[other]:2},elementOwners:Object.fromEntries([...server.elements.values()].map(element=>[element.id,element.ownerCanvasId]))};
|
|
// The fixture's initial version vector follows the server's active scopes.
|
|
let engine=await f.open({document,versions:serverVersions,loading});
|
|
try{const selection=[{id:'F',incarnation:1},{id:'C',incarnation:1}];assert.equal(engine.imports.canDuplicate(selection,{viewCanvasId:root}).enabled,true);const id=await engine.imports.duplicate(selection,{viewCanvasId:root});await engine.imports.pause(id);await engine.settle();assert.equal(engine.imports.getJobs().get(id).status,'paused');decodeRecovery(f.storage.image(),registry);await engine.close();engine=await f.open({document,versions:serverVersions,loading});await engine.imports.resume(id);await engine.settle();const job=engine.imports.getJobs().get(id);assert.equal(job.status,'completed',JSON.stringify({job,errors:f.errors}));const copied=job.stableIds['element:F'],copiedChild=job.stableIds['element:C'];assert.ok(copied&&copiedChild);assert.equal([...engine.getSnapshot().document.elements.values()].filter(element=>element.id===copiedChild).length,1);assert.ok([...engine.getSnapshot().document.elements.values()].some(element=>element.structure==='connection'&&element.source.elementId==='A'&&element.target.elementId===copied));assert.ok([...engine.getSnapshot().document.elements.values()].some(element=>element.id==='E'&&element.target.elementId==='F'));assert.equal(engine.getSnapshot().document.elements.has('V'),false);assert.equal(engine.loading.getState().version,5);assert.deepEqual(job.source.loadingAdmission.requiredCanvasIds,[root,child].sort());assert.equal(f.errors.length,0);decodeRecovery(f.storage.image(),registry);
|
|
}finally{await engine.close();}
|
|
});
|
|
|
|
test('v5 paused native source survives reopen, resumes with stable IDs, and undo does not revive after completion',async()=>{
|
|
const f=fixture(),document=createDocument(root,canvases,[a,frame,c]);let engine=await f.open({document,loading:jobLoading});
|
|
try{const id=engine.imports.startNdjson(ndjson([node('paused-source','$target')]),child);await engine.imports.pause(id);await engine.settle();const paused=engine.imports.getJobs().get(id);assert.equal(paused.status,'paused');assert.equal(paused.committedSourceKeys.length,0);assert.equal(f.calls.length,0);assert.equal(f.storage.files.size,1);decodeRecovery(f.storage.image(),registry);for(const mutate of [image=>image.payload.runtime.imports[0][1].source.loadingAdmission.sourceToken='other-cut',image=>image.payload.runtime.imports[0][1].source.loadingAdmission.requiredCanvasIds=[root],image=>image.payload.runtime.imports[0][1].source.loadingAdmission.operationSequence=999,image=>delete image.payload.runtime.loading.sourceCanvases,image=>image.formatVersion=10]){const image=f.storage.image();mutate(image);assert.throws(()=>decodeRecovery(image,registry));}
|
|
await engine.close();engine=await f.open({document,loading:jobLoading});assert.equal(engine.imports.getJobs().get(id).status,'paused');await engine.imports.resume(id);await engine.settle();const job=engine.imports.getJobs().get(id);assert.equal(job.status,'completed');const mapped=job.stableIds['element:paused-source'];assert.ok(f.server().elements.has(mapped));assert.equal(f.storage.files.size,1);
|
|
await engine.close();engine=await f.open({document,loading:jobLoading});assert.equal(engine.imports.getJobs().get(id).stableIds['element:paused-source'],mapped);assert.equal(engine.editor.history.undo().status,'applied');await engine.settle();assert.equal(f.server().elements.has(mapped),false);assert.equal((await engine.loading.complete(full,versions,{sourceToken:state.sourceToken})).status,'applied');await engine.settle();assert.equal(engine.getSnapshot().document.elements.has(mapped),false);assert.equal(f.errors.length,0);
|
|
}finally{await engine.close();}
|
|
});
|
|
test('v5 cancelled native job stays cancelled through reopening and final source completion',async()=>{
|
|
const f=fixture(),document=createDocument(root,canvases,[a,frame,c]);let engine=await f.open({document,loading:jobLoading});
|
|
try{const id=engine.imports.startNdjson(ndjson([node('cancelled-source','$target')]),child);await engine.imports.cancel(id);await engine.settle();await engine.close();engine=await f.open({document,loading:jobLoading});await engine.imports.resume(id);await engine.settle();assert.equal(engine.imports.getJobs().get(id).status,'cancelled');assert.equal(f.calls.length,0);assert.equal((await engine.loading.complete(full,versions,{sourceToken:state.sourceToken})).status,'applied');await engine.settle();assert.equal(engine.getSnapshot().document.elements.size,5);assert.equal(f.errors.length,0);}finally{await engine.close();}
|
|
});
|
|
test('v5 copy cannot interpret a declared but unpublished child as an empty partition',async()=>{
|
|
const f=fixture(),document=createDocument(root,[canvases[0]],[a,b,frame]),loading={...jobLoading,receivedElements:3,canvasElementCounts:{[root]:3,[child]:2},elementOwners:{A:root,B:root,F:root,C:child,E:child}};
|
|
const engine=await f.open({document,loading});try{assert.equal(engine.editor.getQueries().membersResult(root).complete,true);assert.equal(engine.getSnapshot().document.canvases.has(child),false);assert.equal(engine.imports.canDuplicate([{id:'F',incarnation:1}],{viewCanvasId:root}).enabled,false);await assert.rejects(engine.imports.duplicate([{id:'F',incarnation:1}],{viewCanvasId:root}),{code:'loading_scope_incomplete'});assert.equal(engine.imports.getJobs().size,0);}finally{await engine.close();}
|
|
});
|
|
test('import availability rejects stale selection, missing target and policy denial before offering an action',async()=>{
|
|
const f=fixture();let engine=await f.open({document:full,loading:undefined});try{assert.equal(engine.imports.canStart('missing').code,'canvas_not_loaded');assert.equal(engine.imports.canDuplicate([],{viewCanvasId:root}).code,'empty_copy_selection');assert.equal(engine.imports.canDuplicate([{id:'A',incarnation:2}]).code,'stale_incarnation');}finally{await engine.close();}
|
|
engine=await f.open({document:full,loading:undefined,policy:()=>({enabled:false,code:'permission_denied'})});try{assert.equal(engine.imports.canStart(root).code,'permission_denied');assert.equal(engine.imports.canDuplicate([target]).code,'permission_denied');}finally{await engine.close();}
|
|
});
|
|
|
|
const assetLoading={...jobLoading,version:6};
|
|
function assetPort(f){const uploads=[],receipts=new Map();let lose=false;const assets={sourceURL:id=>`https://assets.example/${id}`,displayURL:id=>`/assets/${id}`,async upload(request,bytes){const value=JSON.parse(request.body);assert.equal(f.storage.files.get(value.resource_id).size,bytes.size);assert.ok(f.storage.image().payload.runtime.ledger.requests.some(([id,row])=>id===request.id&&row.phase==='dispatch-marked'));uploads.push(request);const stored={id:value.resource_id,url:assets.sourceURL(value.resource_id),bytes:bytes.size,width:20,height:10,mime_type:bytes.type};receipts.set(request.id,stored);if(lose)throw Error('lost upload ACK');return{outcome:'applied',canonical:stored};},async query(request){return receipts.has(request.id)?{outcome:'applied',canonical:receipts.get(request.id)}:{outcome:'unresolved'};},accept:(_request,status)=>status.canonical};return{assets,uploads,lose(){lose=true;}};}
|
|
function stageLoadingImage(engine,owner=child,id='partial-image'){let ref;const result=engine.stageFiles([{name:'picture.png',bytes:new Blob(['image bytes'],{type:'image/png'})}],([resource],tx)=>{ref=resource;tx.dispatch({type:'element.create',element:{...node(id,owner),componentId:'image',data:{title:'Picture',text:'',assetId:ref.id,src:ref.src}}});});return{result,ref};}
|
|
test('v6 displays, persists and uploads a scoped image before another owner tail; undo/redo and old completion preserve its resource',async()=>{
|
|
const f=fixture(),port=assetPort(f),document=createDocument(root,canvases,[a,frame,c]),engine=await f.open({document,loading:assetLoading,assets:port.assets});try{assert.equal(engine.canStageFiles(child).enabled,true);assert.equal(engine.canStageFiles(root).enabled,false);const before=engine.getSnapshot();assert.equal(stageLoadingImage(engine,root,'denied-image').result.status,'rejected');assert.strictEqual(engine.getSnapshot(),before);assert.equal(f.storage.files.size,0);
|
|
const {result,ref}=stageLoadingImage(engine);assert.equal(result.status,'applied');assert.equal(engine.getSnapshot().document.elements.get('partial-image').data.assetId,ref.id);assert.equal(port.uploads.length,0);await engine.settle();assert.equal(port.uploads.length,1);assert.equal(f.server().elements.get('partial-image').data.assetId,ref.id);assert.equal(f.storage.image().formatVersion,12);decodeRecovery(f.storage.image(),registry);for(const mutate of [image=>image.payload.runtime.resources[0][1].loadingAdmission.sourceToken='other-source',image=>image.payload.runtime.resources[0][1].loadingAdmission.canvasIds=[root],image=>image.payload.runtime.resources[0][1].loadingAdmission.sourceURL='https://wrong.example/',image=>image.payload.runtime.resources[0][1].loadingAdmission.version=2,image=>{const row=image.payload.runtime.ledger.requests.find(([,row])=>row.kind==='asset.upload')[1];const body=JSON.parse(row.body);body.name='changed';row.body=JSON.stringify(body);},image=>image.formatVersion=11]){const image=f.storage.image();mutate(image);assert.throws(()=>decodeRecovery(image,registry));}assert.equal(engine.loading.getState().version,6);assert.equal(engine.editor.history.undo().status,'applied');await engine.settle();assert.equal(f.server().elements.has('partial-image'),false);assert.equal(engine.editor.history.redo().status,'applied');await engine.settle();assert.equal(f.server().elements.get('partial-image').incarnation,2);assert.equal(port.uploads.length,1);assert.equal((await engine.loading.complete(full,versions,{sourceToken:state.sourceToken})).status,'applied');await engine.settle();assert.equal(engine.getSnapshot().runtime.resources.get(ref.id).stored.id,ref.id);assert.equal(engine.getSnapshot().document.elements.get('partial-image').incarnation,2);assert.equal(f.errors.length,0);
|
|
}finally{await engine.close();}
|
|
});
|
|
test('v6 queries a lost upload ACK after reopening and never resubmits bytes or loses the pending image to the source tail',async()=>{
|
|
let now=0;const f=fixture(),port=assetPort(f),document=createDocument(root,canvases,[a,frame,c]),clock={now:()=>now,setTimer:()=>0,clearTimer(){}};port.lose();let engine=await f.open({document,loading:assetLoading,assets:port.assets,clock});try{const {ref}=stageLoadingImage(engine);await engine.settle();assert.equal(port.uploads.length,1);assert.equal(f.calls.length,0);const request=port.uploads[0];assert.equal(engine.getSnapshot().runtime.ledger.requests.get(request.id).phase,'waiting');const copyId=await engine.imports.duplicate([{id:'partial-image',incarnation:1}],{viewCanvasId:child});await engine.settle();const copy=engine.imports.getJobs().get(copyId);assert.equal(copy.status,'completed');const copied=copy.stableIds['element:partial-image'];assert.equal(engine.getSnapshot().document.elements.get(copied).data.assetId,ref.id);assert.equal(f.calls.length,0);decodeRecovery(f.storage.image(),registry);await engine.close();now=10_000;engine=await f.open({document,loading:assetLoading,assets:port.assets,clock});await engine.settle();assert.equal(port.uploads.length,1);assert.equal(engine.getSnapshot().runtime.ledger.requests.get(request.id).body,request.body);assert.equal(engine.getSnapshot().runtime.ledger.requests.get(request.id).phase,'resolved');assert.equal(f.server().elements.get(copied).data.assetId,ref.id);assert.equal(f.server().elements.get('partial-image').data.assetId,ref.id);assert.equal((await engine.loading.complete(full,versions,{sourceToken:state.sourceToken})).status,'applied');await engine.settle();assert.equal(port.uploads.length,1);}finally{await engine.close();}
|
|
});
|
|
test('v6 replaces an image in a complete original owner and preserves both resource choices through undo/redo',async()=>{
|
|
const image={...c,componentId:'image',data:{title:'Original image',text:'',assetId:'old-asset',src:'https://assets.example/old-asset'}},server=createDocument(root,canvases,[a,b,frame,image,edge]),f=fixture(server),port=assetPort(f),document=createDocument(root,canvases,[a,frame,image]),engine=await f.open({document,loading:assetLoading,assets:port.assets});try{const result=engine.stageFiles([{name:'replacement.png',bytes:new Blob(['replacement'],{type:'image/png'})}],([ref],tx)=>tx.dispatch({type:'element.data',target:{id:'C',incarnation:1},patch:{assetId:ref.id,src:ref.src}}));assert.equal(result.status,'applied');await engine.settle();const replacement=f.server().elements.get('C').data.assetId;assert.notEqual(replacement,'old-asset');assert.equal(engine.editor.history.undo().status,'applied');await engine.settle();assert.equal(f.server().elements.get('C').data.assetId,'old-asset');assert.equal(engine.editor.history.redo().status,'applied');await engine.settle();assert.equal(f.server().elements.get('C').data.assetId,replacement);assert.equal(port.uploads.length,1);decodeRecovery(f.storage.image(),registry);assert.equal(f.errors.length,0);}finally{await engine.close();}
|
|
});
|
|
test('v6 failed Blob/root persistence keeps the local picture visible and sends neither upload nor document until retry',async()=>{
|
|
const f=fixture(),port=assetPort(f),document=createDocument(root,canvases,[a,frame,c]),engine=await f.open({document,loading:assetLoading,assets:port.assets});try{await engine.persist();f.storage.broken=true;const {result,ref}=stageLoadingImage(engine);assert.equal(result.status,'applied');const display=await engine.displayResource(ref.id);assert.equal(await(await fetch(display.url)).text(),'image bytes');display.release();await assert.rejects(engine.persist(),/quota/);await new Promise(resolve=>setTimeout(resolve,10));assert.equal(port.uploads.length,0);assert.equal(f.calls.length,0);assert.equal(engine.getSnapshot().document.elements.get('partial-image').data.assetId,ref.id);f.storage.broken=false;await engine.retry();await engine.settle();assert.equal(port.uploads.length,1);assert.equal(f.server().elements.get('partial-image').data.assetId,ref.id);assert.equal(engine.getSnapshot().history.undo.length,1);}finally{f.storage.broken=false;await engine.close();}
|
|
});
|
|
|
|
test('v6 adopts an authorized shared image into a complete owner without an upload and restores its binding before the unread tail',async()=>{
|
|
const f=fixture(),document=createDocument(root,canvases,[a,frame,c]),bytes=new Blob(['shared bytes'],{type:'image/png'}),digest=[...new Uint8Array(await crypto.subtle.digest('SHA-256',await bytes.arrayBuffer()))].map(n=>n.toString(16).padStart(2,'0')).join(''),id='shared-adopted',authority='https://assets.example';
|
|
const cache=new Map(),commit=f.storage.commit;f.storage.assetDepot={async read(_leader,_authority,id){return cache.get(id);},async collect(){throw Error('unexpected GC');}};f.storage.commit=async(leader,batch)=>{const result=await commit(leader,batch);for(const write of batch.depot?.writes??[])cache.set(write.ref.asset.id,{ref:write.ref,bytes:write.bytes??cache.get(write.ref.asset.id)?.bytes});return result;};
|
|
const assets={authority:{scope:authority,async resolve(id){return{version:1,authority,digest,asset:{id,url:`${authority}/${id}`,width:2,height:1,bytes:bytes.size,mime_type:'image/png'}};},async read(){return bytes;}},sourceURL:id=>`${authority}/${id}`,displayURL:id=>`/assets/${id}`,async upload(){throw Error('adoption must not upload');},async query(){throw Error('adoption must not query upload');},accept(){throw Error('adoption must not accept upload');}};
|
|
const create=(handle,tx,owner=child)=>tx.dispatch({type:'element.create',element:{...node('adopted-image',owner),componentId:'image',data:{title:'adopted',text:'',assetId:handle.id,src:handle.src}}});let engine=await f.open({document,loading:assetLoading,assets});
|
|
try{
|
|
assert.equal((await engine.adoptResource(id,(handle,tx)=>create(handle,tx,root))).status,'rejected');assert.equal(engine.getSnapshot().runtime.assetBindings,undefined);
|
|
assert.equal((await engine.adoptResource(id,create)).status,'applied');assert.equal(engine.getSnapshot().document.elements.get('adopted-image').data.assetId,id);await engine.settle();assert.equal(f.server().elements.get('adopted-image').data.assetId,id);assert.equal(engine.getSnapshot().runtime.resources.size,0);assert.equal(f.storage.image().formatVersion,13);assert.equal(f.storage.saved.at(-1).payload.runtime.loading.version,6);decodeRecovery(f.storage.image(),registry);
|
|
const saved=f.storage.image();for(const change of [image=>image.formatVersion=12,image=>image.payload.runtime.assetBindings[0][1].loadingAdmission.sourceToken='other',image=>image.payload.runtime.assetBindings[0][1].loadingAdmission.operationId='other',image=>image.payload.runtime.assetBindings[0][1].loadingAdmission.canvasIds=[root]]){const broken=structuredClone(saved);change(broken);assert.throws(()=>decodeRecovery(broken,registry));}
|
|
await engine.close();engine=await f.open({document,loading:assetLoading,assets});const display=await engine.displayResource(id);assert.equal(await(await fetch(display.url)).text(),'shared bytes');display.release();assert.equal(engine.editor.history.undo().status,'applied');await engine.settle();assert.equal(engine.editor.history.redo().status,'applied');await engine.settle();assert.equal(f.server().elements.get('adopted-image').incarnation,2);assert.equal((await engine.loading.complete(full,versions,{sourceToken:assetLoading.sourceToken})).status,'applied');await engine.settle();assert.equal(engine.getSnapshot().runtime.assetBindings.get(id).asset.id,id);assert.equal(f.errors.length,0);
|
|
}finally{await engine.close();}
|
|
});
|