35 lines
5.8 KiB
JavaScript
35 lines
5.8 KiB
JavaScript
/**
|
|
* 资源引用扫描测试:工作文档、ACK、历史及显式 owner 都可能持有资源;无法解释的数据必须让扫描 complete=false。
|
|
* 缓存只复用不可变历史范围的结构事实,不能缓存外部可变表成员,也不能忽略当前 registry 的资源字段定义。
|
|
*/
|
|
import {test} from 'node:test';
|
|
import assert from 'node:assert/strict';
|
|
import {createDocument,createRegistry,builtinComponents,describeHistoryTransition} from '../../src/lib/canvas-core/index.ts';
|
|
import {createResourcePinScan,resourcePins} from '../../src/lib/canvas-runtime/resources.ts';
|
|
import {resourceRetentionPins} from '../../src/lib/canvas-runtime/resource-retention.ts';
|
|
// 准备内置定义、空历史及图片/文本工厂;doc 总是建立独立文档,用于区分 working 与 ACK owner。
|
|
const registry=createRegistry(builtinComponents()),empty={undo:[],redo:[],entries:new Map()},node=(id,asset)=>({id,incarnation:1,ownerCanvasId:'root',componentId:asset?'image':'text',dataVersion:1,structure:'node',geometry:{x:0,y:0,width:100,height:100,rotation:0},data:{title:id,text:'',...(asset?{assetId:asset}:{})}}),doc=(records=[])=>createDocument('p',[{id:'root',parent:null}],records);
|
|
// 用 Proxy 计数证明不可变历史范围只准备一次;working/ACK 分别扫描,最终 pin 保留各自来源而不混淆。
|
|
test('immutable history ranges are reused across retention passes while working and ACK keep separate owners',()=>{
|
|
const entry={id:'h',label:'h',changes:[{kind:'element',id:'past',before:null,after:node('past','historical')}],operationIds:[]},raw=describeHistoryTransition(empty,{undo:['h'],redo:[],entries:new Map([['h',entry]])},1);let reads=0;const transition=new Proxy(raw,{get(target,key,receiver){if(key==='entries')reads++;return Reflect.get(target,key,receiver);}}),ledger={operations:new Map([['o',{phase:'queued',intent:{id:'o',sequence:1,operationType:'document.change',changes:[],historyTransition:transition}}]])},scan=createResourcePinScan(empty,ledger,registry),preparationReads=reads;assert.ok(preparationReads>0);scan(doc([node('text')]));scan(doc([node('remote','ack')]));assert.equal(reads,preparationReads);
|
|
const snapshot={document:doc([node('text')]),history:empty,runtime:{ledger,acknowledged:doc([node('remote','ack')]),resourcePins:new Map(),resources:new Map(),runs:new Map()}};reads=0;const result=resourceRetentionPins(snapshot,registry);assert.equal(reads,0);assert.equal(result.complete,true);assert.deepEqual([...result.pins.get('historical')],['history-transition:o']);assert.deepEqual([...result.pins.get('ack')],['acknowledged:element:remote:1']);
|
|
});
|
|
// 历史字段修改可从已解决创建记录推断类型,并按 history/显式 owner 顺序保留资源;缺少类型证据时改为不完整扫描。
|
|
test('historical data fields retain schemas from resolved creations, conservative fallback pins and explicit owner order',()=>{
|
|
const field={kind:'field',target:{id:'deleted',incarnation:1},field:'data.assetId',before:'old',after:'new'},history={undo:['h'],redo:[],entries:new Map([['h',{id:'h',label:'h',changes:[field],operationIds:[]}]])},ledger={operations:new Map([['resolved',{phase:'resolved',intent:{id:'resolved',sequence:1,operationType:'document.change',changes:[{kind:'element',id:'deleted',before:null,after:node('deleted','unreferenced')}]} }]])},snapshot={document:doc(),history},result=resourcePins(snapshot,ledger,registry,new Map([['explicit',['old','extra']]]));assert.equal(result.complete,true);assert.deepEqual([...result.pins].map(([id,owners])=>[id,[...owners]]),[['old',['history:h','explicit']],['new',['history:h']],['extra',['explicit']]]);ledger.operations.clear();assert.equal(resourcePins(snapshot,ledger,registry,new Map()).complete,false);
|
|
});
|
|
// 新扫描必须看到可变 history.entries 后加的成员;即使已解决操作没有资源字段,损坏的 historyTransition 也不能跳过校验。
|
|
test('new passes reread mutable input membership and still reject malformed resolved evidence with no resource fields',()=>{
|
|
const history={undo:[],redo:[],entries:new Map()},ledger={operations:new Map()},snapshot={document:doc(),history};assert.equal(resourcePins(snapshot,ledger,registry,new Map()).pins.size,0);history.entries.set('h',{id:'h',label:'h',changes:[{kind:'element',id:'image',before:null,after:node('image','late')}],operationIds:[]});assert.ok(resourcePins(snapshot,ledger,registry,new Map()).pins.has('late'));ledger.operations.set('bad',{phase:'resolved',intent:{id:'bad',sequence:1,changes:[],historyTransition:{version:1,rootRevision:-1,before:{undo:[],redo:[]},after:{undo:[],redo:[]},entries:[]}}});assert.throws(()=>resourcePins(snapshot,ledger,registry,new Map()),/invalid_history_transition/);
|
|
});
|
|
|
|
|
|
// 同一缓存历史范围配合不同 registry 扫描时,分别使用当前 image.resourceFields,不能沿用上次 registry 的资源提取结果。
|
|
test('cached immutable transition ranges still use each current registry resource definition',()=>{
|
|
const element={...node('historical','first'),data:{...node('historical','first').data,otherAsset:'second'}},entry={id:'h',label:'h',changes:[{kind:'element',id:element.id,before:null,after:element}],operationIds:[]};
|
|
const transition=describeHistoryTransition(empty,{undo:['h'],redo:[],entries:new Map([['h',entry]])},1),ledger={operations:new Map([['o',{phase:'queued',intent:{id:'o',sequence:1,operationType:'document.change',changes:[],historyTransition:transition}}]])};
|
|
const otherRegistry=createRegistry(builtinComponents().map(component=>component.id==='image'?{...component,resourceFields:['otherAsset']}:component));
|
|
const first=createResourcePinScan(empty,ledger,registry)(doc()),second=createResourcePinScan(empty,ledger,otherRegistry)(doc());
|
|
assert.deepEqual([...first.pins.keys()],['first']);assert.deepEqual([...second.pins.keys()],['second']);assert.equal(first.complete,true);assert.equal(second.complete,true);
|
|
});
|