42 lines
5.2 KiB
JavaScript
42 lines
5.2 KiB
JavaScript
/**
|
||
* 持久复制源的引用依赖与批次选择测试。映射必须和声明的引用路径使用同一遍历规则,不能仍指向原来的子对象。
|
||
* 外部保留引用还需校验目标生命周期;循环依赖要作为完整强连通组加入,不能靠拆边绕过依赖或容量。
|
||
*/
|
||
import {test} from 'node:test';
|
||
import assert from 'node:assert/strict';
|
||
import {createDocument,createRegistry,builtinComponents,planDuplicate,defineComponent} from '../../src/lib/canvas-core/index.ts';
|
||
import {encodeDuplicateSource,prepareDuplicateImport,assertDuplicateExternal} from '../../src/lib/canvas-runtime/import-duplicate.ts';
|
||
import {selectImportGroups} from '../../src/lib/canvas-runtime/import-groups.ts';
|
||
// 可指定类型/归属/数据的第一代节点样本,用于 frame 端口及自定义对象数组引用。
|
||
const node=(id,owner='P',componentId='text',data={})=>({id,incarnation:1,structure:'node',ownerCanvasId:owner,componentId,dataVersion:1,data:{title:id,text:'',...data},geometry:{x:0,y:0,width:100,height:100,rotation:0}});
|
||
// 使用内置复制 schema,frame.ports 中的 nodeId 被识别为元素引用,端口局部 ID 仍是普通数据。
|
||
const registry=createRegistry(builtinComponents());
|
||
|
||
// 复制 frame 的持久源须声明子节点依赖并使用新 nodeId;篡改为旧引用或删掉依赖都在导入准备时拒绝。
|
||
test('durable copy source includes mapped port dependencies and refuses an old source still targeting original children',async()=>{
|
||
const document=createDocument('P',[{id:'P',parent:null},{id:'S',parent:{canvasId:'P',entryElementId:'F'}}],[node('F','P','frame',{ports:[{nodeId:'A',id:'in',direction:'input',handle:'left'}]}),node('A','S')]);let next=0;
|
||
const plan=planDuplicate(document,registry,[{id:'F',incarnation:1}],{createId:()=>`new-${++next}`}),source=await encodeDuplicateSource(document,registry,plan,new AbortController().signal),rows=(await source.text()).trim().split('\n').map(row=>JSON.parse(row)),value=rows.find(row=>row.key==='element:F');
|
||
assert.ok(value.dependencies.includes('element:A'));assert.equal(value.change.after.data.ports[0].nodeId,plan.elementIds.A);
|
||
const stableIds=Object.fromEntries([...Object.entries(plan.elementIds).map(([id,mapped])=>[`element:${id}`,mapped]),...Object.entries(plan.canvasIds).map(([id,mapped])=>[`canvas:${id}`,mapped])]),job={source:{kind:'duplicate-ndjson'},stableIds};
|
||
const line={value};assert.equal(prepareDuplicateImport(line,job,key=>stableIds[key],registry).key,'element:F');
|
||
const stale=structuredClone(value);stale.change.after.data.ports[0].nodeId='A';stale.dependencies=stale.dependencies.filter(key=>key!=='element:A');assert.throws(()=>prepareDuplicateImport({value:stale},job,key=>stableIds[key],registry),/copy_external_reference/);
|
||
const missing=structuredClone(value);missing.dependencies=missing.dependencies.filter(key=>key!=='element:A');assert.throws(()=>prepareDuplicateImport({value:missing},job,key=>stableIds[key],registry),/invalid_duplicate_source/);
|
||
});
|
||
|
||
// 自定义 rows.*.node 外部引用保持原目标并记录 incarnation;目标后来换代,晚期 guard 必须拒绝旧引用。
|
||
test('object-array external references share the same traversal for mapping and late lifecycle guards',async()=>{
|
||
const linked=defineComponent({...builtinComponents()[0],id:'linked',copy:{references:[{path:['rows','*','node'],target:'element',external:'preserve'}],omit:[]}}),r=createRegistry([...builtinComponents(),linked]),document=createDocument('P',[{id:'P',parent:null}],[node('C'),node('L','P','linked',{rows:[{node:'C',literal:'C'}]})]);
|
||
const plan=planDuplicate(document,r,[{id:'L',incarnation:1}],{createId:()=> 'new-L'}),rows=(await(await encodeDuplicateSource(document,r,plan,new AbortController().signal)).text()).trim().split('\n').map(row=>JSON.parse(row)),job={source:{kind:'duplicate-ndjson'},stableIds:{'element:L':'new-L'}};
|
||
assert.equal(rows[0].externalElements.find(row=>row.id==='C').incarnation,1);assertDuplicateExternal({value:rows[0]},job,document,r);
|
||
const changed=createDocument('P',[...document.canvases.values()],[...document.elements.values()].map(row=>row.id==='C'?{...row,incarnation:2}:row));assert.throws(()=>assertDuplicateExternal({value:rows[0]},job,changed,r),/copy_external_changed/);
|
||
});
|
||
|
||
// 构造待加入记录的位置描述:依赖键、编码字节数及变化数供 SCC 分组和硬容量检查。
|
||
const locator=(key,dependencies,byteLength=100)=>({key,dependencies,byteLength,changeCount:1});
|
||
// A/B 循环还依赖 outside:外部未完成时不选,已完成时整组可超过软 units=1,但字节或 changes 硬上限不足应抛错。
|
||
test('SCC selection waits for missing external dependencies and enforces hard group capacity',()=>{
|
||
const pending=[locator('A',['B']),locator('B',['A','outside'])];assert.deepEqual(selectImportGroups(pending,new Set(),{units:1,bytes:1000,changes:100}),[]);
|
||
assert.deepEqual(selectImportGroups(pending,new Set(['outside']),{units:1,bytes:1000,changes:100}).flat().map(row=>row.key),['A','B']);
|
||
assert.throws(()=>selectImportGroups(pending,new Set(['outside']),{units:32,bytes:199,changes:100}),/import_dependency_group_capacity/);assert.throws(()=>selectImportGroups(pending,new Set(['outside']),{units:32,bytes:1000,changes:1}),/import_dependency_group_capacity/);
|
||
});
|