Files
Endoleap/tests/headless/copy-reference-source.test.mjs

44 lines
5.5 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 持久复制源的引用依赖与批次选择测试。映射必须和声明的引用路径使用同一遍历规则,不能仍指向原来的子对象。
* 外部保留引用还需校验目标生命周期;循环依赖要作为完整强连通组加入,不能靠拆边绕过依赖或容量。
*/
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');
// 两次篡改各从合法行复制:stale 回指原节点,missing 保持新引用但删依赖,分别隔离两种拒绝原因。
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/);
});