Files
Endoleap/tests/headless/model-preview.test.mjs

56 lines
6.9 KiB
JavaScript

import {test} from 'node:test';
import assert from 'node:assert/strict';
import {createRegistry,builtinComponents,createDocument,validateDocument} from '../../src/lib/canvas-core/index.ts';
import {createModelPreview} from '../../src/lib/canvas-runtime/model-preview.ts';
const registry=createRegistry(builtinComponents()),root='root',child='child';
const node=(id,ownerCanvasId=root)=>({id,incarnation:1,ownerCanvasId,componentId:'text',dataVersion:1,structure:'node',data:{title:id,text:''},geometry:{x:0,y:0,width:100,height:80,rotation:0}});
const frame={...node('F'),componentId:'frame',data:{title:'F',text:'',ports:[{id:'p',nodeId:'A',direction:'output',handle:'right'}]}};
const a=node('A',child),b=node('B'),edge={id:'E',incarnation:1,ownerCanvasId:root,componentId:'connection',dataVersion:1,structure:'connection',data:{},source:{elementId:'A'},target:{elementId:'B'},path:null};
const canvases=[{id:root,parent:null},{id:child,parent:{canvasId:root,entryElementId:'F'}}],versions=new Map([[root,1],[child,1]]);
const head=(total=4)=>({status:'receiving',projectId:root,canvases,elements:[],tombstones:new Map(),versions,receivedElements:0,totalElements:total});
const delta=(elements,received,total=4)=>({...head(total),canvases:[],elements,receivedElements:received});
test('partial model waits for entry/owner/port cycles and edge endpoints; publishes one closed legal prefix',()=>{
const preview=createModelPreview(registry);const first=preview.update(head());validateDocument(first.document,registry);assert.equal(first.document.canvases.size,1);
const second=preview.update(delta([a,edge],2));assert.equal(second.document.elements.size,0);assert.equal(second.waitingElements,2);assert.strictEqual(second.document,first.document);
const third=preview.update(delta([b],3));assert.deepEqual([...third.document.elements.keys()],['B']);assert.equal(third.waitingElements,2);
const fourth=preview.update(delta([frame],4));validateDocument(fourth.document,registry);assert.equal(fourth.document.elements.size,4);assert.equal(fourth.document.canvases.size,2);assert.equal(fourth.waitingElements,0);assert.equal(fourth.document.elements.get('E').ownerCanvasId,root);
assert.equal(first.document.elements.size,0);assert.equal(third.document.elements.size,1);assert.equal(fourth.complete,false);preview.close();assert.throws(()=>preview.update(delta([],4)),{code:'model_preview_closed'});
});
test('declared external reference waits; unsupported component data remains an opaque read-only candidate',()=>{
const preview=createModelPreview(registry);preview.update(head(2));const future={...node('future'),componentId:'plugin.future',dataVersion:99,data:{opaque:{ref:'absent',assetId:'secret'}}};
const shown=preview.update(delta([future],1,2));assert.deepEqual(shown.document.elements.get('future'),future);assert.equal(registry.resolve(shown.document.elements.get('future')),'unknown-component');
const delayed={...node('delayed'),componentId:'frame',data:{title:'delayed',text:'',ports:[{nodeId:'absent'}]}};
const next=preview.update(delta([delayed],2,2));assert.equal(next.document.elements.size,1);assert.equal(next.waitingElements,1);
});
test('invalid batches do not replace the last legal prefix or publish a partial batch',()=>{
for(const bad of [{...node('bad'),geometry:{...node('bad').geometry,width:-1}},{...node('bad'),incarnation:0},{...node('bad'),ownerCanvasId:'foreign'}]){
const preview=createModelPreview(registry),first=preview.update(head(2));assert.throws(()=>preview.update(delta([node('good'),bad],2,2)));assert.strictEqual(preview.getSnapshot(),first);assert.equal(first.document.elements.size,0);
}
const preview=createModelPreview(registry),first=preview.update(head(2));assert.throws(()=>preview.update(delta([node('same'),node('same')],2,2)));assert.strictEqual(preview.getSnapshot(),first);
});
test('bad topology and version/source switches cannot silently alter a preview',()=>{
for(const topology of [[{id:root,parent:{canvasId:child,entryElementId:'F'}},{id:child,parent:{canvasId:root,entryElementId:'G'}}],[{id:root,parent:null},{id:child,parent:null}]])assert.throws(()=>createModelPreview(registry).update({...head(),canvases:topology}));
const preview=createModelPreview(registry);preview.update(head());assert.throws(()=>preview.update({...delta([b],1),projectId:'other'}));
});
test('owned preview records do not freeze or retain mutable caller payloads; final completion must match',()=>{
const preview=createModelPreview(registry),header=structuredClone(head(1)),element=node('one');preview.update(header);const state=preview.update(delta([element],1,1));
element.data.text='changed by caller';header.canvases[0].id='changed';assert.equal(state.document.elements.get('one').data.text,'');assert.equal(state.document.canvases.has(root),true);
const final=createDocument(root,[{id:root,parent:null},{id:child,parent:{canvasId:root,entryElementId:'F'}}],[element]);assert.throws(()=>preview.update({status:'ready',document:final,versions}),{code:'model_preview_changed'});
});
test('published identities cannot be repeated in later chunks and invalid input closes the projection',()=>{
const preview=createModelPreview(registry);preview.update({...head(2),canvases:[{id:root,parent:null}]});preview.update(delta([node('one')],1,2));assert.throws(()=>preview.update(delta([node('one')],2,2)));assert.throws(()=>preview.update(delta([node('two')],2,2)),{code:'model_preview_closed'});
});
test('scope certificates count received records without treating dependency-blocked members as published',()=>{
const preview=createModelPreview(registry);const counts={[root]:3,[child]:1};preview.update({...head(),canvasElementCounts:counts});counts[child]=0;
const waiting=preview.update(delta([a,edge],2));assert.equal(waiting.document.canvases.has(child),false);assert.equal(waiting.canvasElementCounts[child],1);assert.equal(waiting.waitingElements,2);
const shown=preview.update(delta([frame,b],4));assert.equal(shown.document.elements.size,4);assert.deepEqual(shown.canvasElementCounts,{[root]:3,[child]:1});
const invalid=createModelPreview(registry),first=invalid.update({...head(1),canvasElementCounts:{[root]:0,[child]:1}});assert.throws(()=>invalid.update(delta([b],1,1)),{code:'invalid_model_coverage'});assert.strictEqual(invalid.getSnapshot(),first);
});
test('custom loader identity manifests are owned and checked before a receiving batch can publish',()=>{
const preview=createModelPreview(registry),elementOwners={F:root,E:root,B:root,A:child};const first=preview.update({...head(),canvasElementCounts:{[root]:3,[child]:1},elementOwners});elementOwners.A=root;assert.equal(first.elementOwners.A,child);assert.deepEqual(first.sourceCanvases,head().canvases);assert.equal(first.document.canvases.has(child),false);assert.ok(Object.isFrozen(first.sourceCanvases));const before=preview.getSnapshot();assert.throws(()=>preview.update(delta([node('not-in-capture')],1)),{code:'invalid_model_identity'});assert.strictEqual(preview.getSnapshot(),before);
});