Files
Endoleap/tests/headless/loading-engine.test.mjs
T

417 lines
76 KiB
JavaScript
Raw 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.
/**
* 部分模型上的可编辑 ProjectEngine 集成测试。loading v1–v6 是加载状态解释器版本,逐步加入范围证书、提前同步、结构/任务/资源准入。
* working、原始 loadingRead 与最新 ACK 各有不同用途:后续旧流不能覆盖已确认编辑,未知请求也不能被新完整读当作未执行。
* 局部允许的操作仍要验证实际 changes、引用闭包及持久化证据,不能仅按命令名放行;完整接纳后才移除加载边界。
* fixture 使用内存存储和 Foundry persistence adapter 加假 transport,验证协调与协议检查,不证明真实服务或浏览器表现。
*/
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';
// 内置定义与 root/child UUID,满足 Foundry 协议的作用域校验。
const registry=createRegistry(builtinComponents()),root='10000000-0000-4000-8000-000000000001',child='10000000-0000-4000-8000-000000000002';
// 固定账户、workspace 与项目,使恢复读取命中同一持久化状态。
const scope={accountId:'10000000-0000-4000-8000-000000000003',workspaceId:'10000000-0000-4000-8000-000000000004',projectId:root};
// 第一代文本节点工厂,可指定 owner,基线正文 original 便于区分本地和远端改动。
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}});
// root 的 A/B/F 与 child 的 C,通过共同 root 的边 E 形成加载期引用闭包。
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};
// prefix 只含 root/A,full 含五元素和 child;partial engine 不能因为某对象未到就推定其不存在。
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]);
// 捕获源版本、loading v1 元数据和 A 的条件身份;后续协议版本在用例中显式补上证书。
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};
// 建立可重开的 image、文件、leader 与假服务端;extra 可覆盖加载状态、初始文档、时钟或资源端口。
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=[];
// 内存原子文件接口:校验 owner,broken 时在修改前失败,成功保存 image/文件和已保存快照列表。
const storage={broken:false,saved:[],
// 同一 fixture 只有一个存储 owner,release 清空 leader 并使旧 handle inactive;不模拟跨标签竞选。
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;},
// 返回 image 克隆,让重开真正走恢复解码,不复用旧引擎对象。
async recover(){return structuredClone(image);},
// 先验证 owner 和配额,再保存根/文件与删除列表;saved 记录实际提交内容,真实存储回滚不由此内存实现证明。
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};},
// 按 ID 读项目文件,供加载期导入与资源上传使用。
async readFile(_leader,id){return files.get(id);},files,
// 内存存储没有外部连接需要关闭,leader 生命周期由引擎归还。
close(){},
// 向故障注入/恢复断言交出克隆,避免检查代码意外改动当前已保存 image。
image:()=>structuredClone(image)};
// 把指定参与 canvas 的服务端内容编码为 canonical 模型;回执是局部模型,完整读取默认包含所有范围。
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))});
// GET 返回完整模型或保存回执;POST 检查 expected_versions,并按 requestId 幂等应用批次、递增参与版本。
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))};
// 只计文档写请求,先验证每个参与 canvas 的 ACK 基线;已有 receipt 的同 ID 请求不得重复应用 changes。
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);
// 假服务先验证版本,再检查同 ID 是否已应用;只覆盖本文件的协调路径,不模拟完整生产幂等协议。
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)};
};
// 使用真实 Foundry persistence 编解码,只有底层 transport 与存储被替换为测试实现。
const persistence=createFoundryPersistence({workspaceId:scope.workspaceId,rootId:root,registry,transport});
// 暴露服务端推进及版本副本供冲突注入;open 默认从 prefix/捕获版本/初级 loading 状态开始。
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})};
}
// v1 可对已加载 A 编辑和 undo/redo 并保存,但不冻结请求;重开/接纳前缀不覆盖本地值,full 完成后才同步并保留历史。
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();}
});
// 不完整范围拒绝结构、资源引用、移动和危险 presentation 写;公共入口不能删 loading 标记,stageFiles/copy 也不能绕过。
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();}
});
// 完整基线接纳必须先持久化才能释放全局发送 hold;配额失败仍无冻结请求,retry 保存后才发送原编辑。
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();}
});
// 来源 token 不匹配或已捕获前缀改变时拒绝且 root 不变;显式 fresh 完整读取可补同版本新记录,完成后旧 accept 不得降级状态。
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();}
});
// 格式 7 的恢复 image 必须携带合法 loading 标记,降级格式或删标记都拒绝,不能误当普通完整文档打开。
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();}
});
// fresh 全量读与本地字段冲突时保留原 partial 编辑且不发请求;通过用户审阅的 keep-local 计划协调后才结束加载。
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();}
});
// 重开虽传入 full,持久化 loading 仍需保留;显式 refresh 协调完整基线后保留原历史并同步编辑。
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();}
});
// 命令表面只改标题,恶意 feature handler 实际写声明引用;检查生成 changes 后仍以 model_loading 拒绝整次发布。
test('permitted command cannot hide a reference write inside a feature handler',async()=>{
const {defineComponent,defineFeature}=await import('../../src/lib/canvas-core/index.ts');
// 故意声明一个会把 content.edit 转成引用字段修改的 feature,验证安全门检查最终 changes 而非命令表面 patch。
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();}
});
// 未来 loading 解释器、错误 received 数、空声明范围或伪造 frozen 请求均在恢复校验拒绝。
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();}
});
// 新完整读带远端标题,本地几何重放不覆盖它;之后 undo 几何也不回滚远端无关字段。
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();}
});
// 身份失效后编辑、历史遍历、前缀接纳和 refresh 都拒绝且保持同一 root,不发送迟到工作。
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();}
});
// 多次 loading.resume 共用一次后台完整读取;完成保留本地编辑,之后旧流不能降级,也不再次启动多余读取。
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();}
});
// 关闭项目中止正在等待的背景源并保存 partial 编辑;下一 owner 重开仍保留内容且未发送。
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();}
});
// Workspace 并发 openPartial 返回同一 engine;背景同步已完成后,旧前台完整读不替换 root 或制造冲突。
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();}
});
// 背景 offline 上报一次并保留 partial cut;身份退役后的 resume 报 identity_changed,不继续向旧观察者上报源错误。
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();}
});
// v2 计数证书可证明 child 成员完整但 root/关联边仍不完整;覆盖元数据与内容原子发布,重开保留,最终覆盖完成不虚增文档 revision。
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{
// 先保存旧查询视图;后续新覆盖发布后,旧视图仍描述原来的 loaded/unloaded 状态。
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);
// 这里仅覆盖状态完成,文档内容已齐;root 可变化,但文档 revision 不应为元数据变化而虚增。
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();}
});
// 旧 v1 前缀即使收齐五元素也不能猜测某范围完整;同一读取中临时换成不同 count manifest 必须拒绝。
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();}
});
// 零成员 child 也须其入口依赖已发布才标记 members complete;项目仍加载中,未知 ID 仍是 unloaded。
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();}
});
// v2 允许完整连线 owner 内改 path,即使另一个 canvas 未完成;undo/redo 和格式 8 恢复保留路径,但 owner 不完整时禁用。
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();}
});
// v3 完整 child 的修改可先同步,root 编辑继续等待;原流捕获版本和新 ACK 分开,旧后缀/最终完成不覆盖已确认 child 编辑。
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();}
});
// v3 请求丢 ACK 时 fresh 完整接纳先等回执;重开查询原请求而不新发,保留 body/key,并拒绝篡改 loadingRead/旧流内容。
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();}
});
// 背景完整读取在网络 lane 外等待未知回执;retry 先解决原请求再读 fresh 模型,不能占住网络位置阻止查询。
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();}
});
// fresh 背景读期间 hold 新网络准入,但仍保留当时提交的本地编辑;读完成后重放并只发送一次。
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();}
});
// v3 范围虽完整,源及编辑尚未落盘时仍不能 freeze;配额恢复后 retry 唤醒同一逻辑操作。
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();}
});
// 关闭回执等待取消订阅、保存未知请求,不额外发完整读取;格式 9 重开仍带原请求证据。
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);
});
// 捕获源全量 ID→owner 清单,区分尚未加载的源 ID 与确实未占用的新 ID。
const identities=Object.fromEntries([...full.elements.values()].map(element=>[element.id,element.ownerCanvasId]));
// loading v4 在范围计数外携带身份清单,允许完整 owner 内的安全结构修改。
const structuralLoading={...state,version:4,receivedElements:3,canvasElementCounts:{[root]:4,[child]:1},elementOwners:identities};
// v4 身份清单证明 fresh ID 未占用,允许完整 child 内创建/确认/undo/redo;未加载 B 的 ID 仍禁复用,旧流完成保留第二代新元素。
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();}
});
// v4 frame/canvas 一次事务是一组历史,新 canvas 有独立版本;重开 undo/redo 后原流不删除新增范围或回退其版本。
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();}
});
// v4 原对象删除先等捕获引用闭包完整;本地新生节点/连线可成组创建删除,最终原流到齐后删除 A 也包含 E。
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();}
});
// v4 普通改名保留源 AI 元数据;缺失/错归属身份清单及旧 image 格式不能伪装恢复。
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();}
});
// v4 完全未发送的创建/删除可抵消且零请求;undo 恢复同 ID 的第二代,后续旧流不使它回到旧生命周期。
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();}
});
// v4 新生节点换 owner 要求源/目标范围均可用;禁止移入未完成 root,允许 child/newCanvas 间移动并保留 undo。
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();}
});
// 源清单已声明 A 但它尚未加载时,即使存在第一代墓碑仍应是 unloaded;加载第二代后才能条件删除并变 absent。
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();}
});
// fresh 完整读可包含已确认新生对象,但不能在版本未变的不完整源范围凭空加入成员;正确后台读取保留新对象。
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();}
});
// loading v5 再附源 canvas 树,让导入/复制能校验完整子树及其上级入边范围。
const jobLoading={...structuralLoading,version:5,sourceCanvases:canvases};
// 把原始元素封装为 native 导入行,测试任务准入与目标 owner 映射。
const ndjson=records=>new Blob(records.map(record=>JSON.stringify({kind:'element',key:record.id,record})+'\n'));
// v5 只允许导入到完整 child,root 未完整时准入就拒绝;合法任务在其他尾部仍加载时可发布/同步并保存 loadingAdmission。
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();}
});
// v5 复制涉及的上级范围或声明子分区尚未完整,canDuplicate 和实际 duplicate 都拒绝,不能先分配任务/源文件。
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();}
});
// v5 无关第三 canvas 可继续加载,只要被复制子树及上级入边范围闭合;暂停/重开保留去重映射与 C 的单副本,不吸入未加载 V。
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();}
});
// v5 paused 导入源重开仍用原 ID;loadingAdmission/token/范围/序号必须可验证,完成后 undo 的删除不会被源 full 重新复活。
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();}
});
// v5 cancelled job 重开/resume 仍取消,源 full 完成只补原始五元素,不生成被取消任务的内容。
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();}
});
// v5 已知入口但 child 尚未发布时,不可把它解释为空分区并允许深复制,任务表保持空。
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();}
});
// 动作可用性先检查目标范围、选择非空/身份新鲜度及策略;不可对 UI 声称可执行而到提交才发现明显缺项。
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();}
});
// loading v6 为完整 owner 中的文件暂存/上传和已有共享资源接纳增加明确准入证据。
const assetLoading={...jobLoading,version:6};
// 可记录上传和丢 ACK 的资源端口;上传前核对原 Blob 与 dispatch 证据已保存,成功资产可按原请求查询。
function assetPort(f){const uploads=[],receipts=new Map();let lose=false;const assets={sourceURL:id=>`https://assets.example/${id}`,displayURL:id=>`/assets/${id}`,
// 上传先核对原 Blob 大小及持久化 dispatch 标记;保存资产回执后 lose 开关可抛错模拟服务成功、客户端丢 ACK。
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};},
// 按原 requestId 查保存的上传结果;未知保持 unresolved,不能用新上传掩盖未知请求。
async query(request){return receipts.has(request.id)?{outcome:'applied',canonical:receipts.get(request.id)}:{outcome:'unresolved'};},
// 把 canonical 元数据交给引擎做身份验证与依赖解锁,此处不模拟服务端二进制上传本身。
accept:(_request,status)=>status.canonical};return{assets,uploads,
// 从此让上传回执丢失,用于重开后必须走查询且不重新传字节的场景。
lose(){lose=true;}};}
// 在 stageFiles 的同步事务内创建图片,返回资源句柄与提交结果,owner 可切换以验证范围拒绝。
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};}
// v6 图片立即显示,完整 child 允许持久化后上传并同步,未完成 root 拒绝;恢复校验准入证据,undo/redo 不重复上传,旧源完成保留资源。
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();}
});
// v6 上传服务成功但 ACK 丢失,复制图片可先本地复用待确认资源;重开查询原上传后释放两份图片文档,不重传字节或改 body。
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();}
});
// v6 可替换完整原始 owner 中的图片引用;undo/redo 保留新旧选择且只上传一次,不因其他范围未加载而丢资源。
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();}
});
// v6 Blob/root 配额失败时本地仍可显示字节,但上传和文档都不发;retry 完成保存后发送一次并保留单一创建历史。
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();}
});
// v6 已授权共享图片可接纳到完整 child,不创建上传任务;binding 和准入证据需恢复验证,重开显示/undo/redo 及旧流完成均保持资源身份。
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();}
});