Files
Endoleap/tests/headless/asset-adoption-browser-cases.mjs
T

39 lines
6.5 KiB
JavaScript

/**
* 供浏览器存储 runner 调用的真实 IndexedDB 接纳场景,不是 node:test 自动收集的文件。
* 先由源项目写入共享字节,再从目标项目授权复用,重开后验证 binding、显示、历史及关闭项目的持久化 pin。
*/
import {createBrowserStorage} from '../../src/lib/canvas-runtime/storage.ts';
import {createWorkspaceRuntime} from '../../src/lib/canvas-runtime/workspace.ts';
import {createDocument,createRegistry,builtinComponents,applyChanges} from '../../src/lib/canvas-core/index.ts';
// 每次使用独立数据库;assert 由浏览器 runner 注入,finally 释放 leader/runtime 并请求删库。
export async function assetAdoptionStorageCase(assert){
const name=`adoption-${crypto.randomUUID()}`,scope={accountId:'a',workspaceId:'w',projectId:'source'},authority='https://assets.test/w',bytes=new Blob(['shared original'],{type:'image/png'}),digest=[...new Uint8Array(await crypto.subtle.digest('SHA-256',await bytes.arrayBuffer()))].map(n=>n.toString(16).padStart(2,'0')).join(''),ref={version:1,authority,digest,asset:{id:'resource',url:`${authority}/resource`,width:2,height:1,bytes:bytes.size,mime_type:'image/png'}},registry=createRegistry(builtinComponents());
let storage=createBrowserStorage({name}),leader=await storage.claim(scope),runtime,engine,allowed=true,authorizations=0,reads=0,submits=0,version=1,document=createDocument('target',[{id:'canvas',parent:null}],[]);const receipts=new Map();
// 简化源项目 image 与 depot cut;目标引擎会自行生成完整的可恢复状态根。
const image=revision=>({formatVersion:1,rootRevision:revision,localRevision:revision,payload:{source:revision}}),cut=pins=>({version:1,authority,complete:true,pins,writes:[]});
// 源字节已经预热,authority 仍授权;任何再次下载或进入上传流程的调用都直接报错。
const assets={authority:{scope:authority,async resolve(){authorizations++;if(!allowed)throw Error('denied');return ref;},async read(){reads++;throw Error('cache miss should not download');}},sourceURL:id=>`${authority}/${id}`,displayURL:id=>`/assets/${id}`,async upload(){throw Error('existing bytes uploaded');},async query(){throw Error('upload queried');},accept(){throw Error('upload accepted');}};
// 简化目标文档服务:提交应用变化并保存 canonical 回执,版本逐次递增。
const persistence={compile(intent,id,snapshot){return{payload:{intent},canvasIds:intent.canvasIds,expectedVersions:{canvas:snapshot.runtime.versions.get('canvas')}};},async submit(request){submits++;document=applyChanges(document,JSON.parse(request.body).intent.changes);receipts.set(request.id,{document,versions:new Map([['canvas',++version]])});return{outcome:'applied'};},async query(){return{outcome:'unresolved'};},canonical:request=>receipts.get(request.id)};
// 重新装配 WorkspaceRuntime 并打开目标项目;用于证明数据来自新存储连接的恢复,而非旧内存状态。
const open=async()=>{runtime=createWorkspaceRuntime({accountId:'a',workspaceId:'w',authEpoch:1,registry,storage,isCurrentIdentity:()=>true,adapters:()=>({assets,persistence})});engine=await runtime.open(createDocument('target',[{id:'canvas',parent:null}],[]),new Map([['canvas',1]]));};
// 在接纳资源的同步事务内创建 image 元素,使 binding 和内容一起发布。
const adopt=id=>engine.adoptResource('resource',(handle,tx)=>tx.dispatch({type:'element.create',element:{id,incarnation:1,structure:'node',componentId:'image',dataVersion:1,ownerCanvasId:'canvas',geometry:{x:0,y:0,width:2,height:1,rotation:0},data:{title:id,text:'',assetId:handle.id,src:handle.src}}}));
try{
// 预写源项目共享字节和引用后释放源 leader,目标接纳必须从共享 depot 获取而不复制上传。
await storage.commit(leader,{image:image(0),depot:{...cut([['resource',['element:source']]]),writes:[{ref,bytes}]}});await leader.release();leader=undefined;await open();
assert((await adopt('picture')).status==='applied','adoption did not publish');await engine.settle();assert(authorizations===1&&reads===0&&submits===1,'adoption bypassed authorization, downloaded warm bytes or submitted twice');assert(engine.getSnapshot().runtime.resources.size===0&&engine.getSnapshot().runtime.assetBindings.size===1,'adoption invented upload metadata');
// 关闭整个 runtime 再创建新 storage,验证目标 binding 与原始字节已经持久化。
await runtime.close();runtime=undefined;storage=createBrowserStorage({name});await open();
const display=await engine.displayResource('resource');assert(display.url.startsWith('blob:')&&await(await fetch(display.url)).text()==='shared original','reopening lost durable binding or shared bytes');display.release();
// 已有缓存也不能绕过撤权;失败必须维持同一状态根,随后重新允许以继续历史检查。
allowed=false;const before=engine.getSnapshot();try{await adopt('denied');throw Error('revoked cache adoption succeeded');}catch(error){assert(error.message==='denied','unexpected authorization failure');}assert(engine.getSnapshot()===before,'denied authorization changed Root');allowed=true;
assert(engine.editor.history.undo().status==='applied','adopt undo failed');await engine.settle();assert(engine.editor.history.redo().status==='applied','adopt redo failed');await engine.settle();assert(engine.getSnapshot().document.elements.get('picture').incarnation===2,'redo did not create a fresh incarnation');
// Close every editor. GC must use target's durable manifest rather than an open engine map.
await engine.close();leader=await storage.claim(scope);await storage.commit(leader,{image:image(1),depot:cut([])});const collected=await storage.assetDepot.collect(leader,{authority,now:100,retentionMs:0,limit:128});assert(collected.complete&&collected.pinnedIds.includes('resource')&&!collected.purgedIds.length,'closed destination pins did not protect shared bytes');
// 直接扫描已关闭目标的项目文件表,确认共享资源没有被复制成目标项目本地文件。
const targetLeader=await storage.claim({...scope,projectId:'target'});try{const page=await storage.scanFiles(targetLeader,{expectedRootRevision:(await storage.recover(targetLeader)).rootRevision,limit:128});assert(page.files.length===0,'destination duplicated shared bytes into project files');}finally{await targetLeader.release();}
await leader.release();leader=undefined;await runtime.close();runtime=undefined;
}finally{await leader?.release();await runtime?.close();storage.close();indexedDB.deleteDatabase(name);}
}