docs(tests): clarify import loading and package validation boundaries

This commit is contained in:
2026-11-12 18:17:43 +08:00
parent 11735d3d78
commit 48ef0dc896
3 changed files with 14 additions and 3 deletions
+4
View File
@@ -2,6 +2,7 @@
* 独立导入 runner 的解析、依赖分组、发布和持久化状态机测试。每个已提交前缀可见,坏记录及取消不默认回滚全部导入。
* 依赖循环作为强连通组原子提交;稳定 ID、待处理定位、源身份和 generation 支持恢复并隔离迟到异步准备。
* fixture 用内存文档与克隆 cut 验证端口调用顺序;引擎/真实存储集成另有测试,不由此模拟持久层事务保证。
* 文中的“可见”指当前文档已包含元素,未挂载 renderer;dependencies 仅用于调度,不在此 fixture 中创建真实父子/连线引用。
*/
import {test} from 'node:test';
import assert from 'node:assert/strict';
@@ -89,6 +90,7 @@ test('dependency backpressure pauses with locators intact and a larger budget re
});
// 未来 job 版本、重复目标 ID 和缺乏结束条件的 completed cut 均在校验阶段拒绝。
// 本例直接调用 validateImportJob,不创建 runner;验证的是记录合同,未执行或计数源读取。
test('future interpreter versions and malformed job cuts fail before any source read',()=>{
const job=createImportJob({id:'i',projectId:'p',sourceFingerprint:'s',parserVersion:1});assert.throws(()=>validateImportJob({...job,version:4}),/import_interpreter_unavailable/);assert.throws(()=>validateImportJob({...job,stableIds:{a:'same',b:'same'}}),/invalid_import_job/);assert.throws(()=>validateImportJob({...job,status:'completed'}),/invalid_import_job/);
});
@@ -121,6 +123,7 @@ test('a source I/O failure is reported after publishing all already prepared rea
});
// A/B/C 循环即使 maxBatch=1 仍作为一个完整原子组提交一次,不能用软批量限制拆开依赖环。
// maxBatch 是调度的软上限,强连通依赖组优先保持完整;这里断言一个 commit 接收三项,而不是三次单项发布。
test('a reference cycle commits every member in one root even when the adaptive batch budget is one',async()=>{
const f=fixture([{id:'A',dependencies:['B']},{id:'B',dependencies:['C']},{id:'C',dependencies:['A']}],{ports:{maxBatch:1}});await f.runner.start();assert.equal(f.root.job.status,'completed');assert.deepEqual(f.calls,[['A','B','C']]);assert.equal(f.commits.length,1);assert.equal(f.root.document.elements.size,3);validateImportJob(f.root.job);
});
@@ -151,6 +154,7 @@ test('optimistic local checkpoints use host backpressure without declaring compl
});
// 最终内容 cut 或 completed 元数据保存失败均 paused;修复后续跑不重复 committed key 或 ID,仅补完持久化终态。
// 第二次 start 复用当前内存 root;此例证明失败后原地重试,不模拟进程崩溃后仅从最后 durable cut 重开。
test('both final content and completion-metadata storage failures pause resumably without replaying committed keys',async()=>{
for(const phase of ['content','metadata']){
const f=fixture([{id:'one'},{id:'two'}],{ports:{optimisticLocal:true,awaitDurabilityBudget(){}}}),persist=f.ports.persist;let broken=true;
+7 -3
View File
@@ -123,7 +123,8 @@ test('permitted command cannot hide a reference write inside a feature handler',
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 请求均在恢复校验拒绝。
// 把 v1 标记改为 v2 却不补计数证书、错误 received 数、空声明范围或伪造 frozen 请求,均在恢复校验拒绝。
// v2 本身是支持的解释器;第一种损坏验证升级后的必需字段,不能把它解释为“不支持未来版本”。
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));}
@@ -209,6 +210,7 @@ test('v2 admits path edits in a certified connection owner while another scope i
});
// v3 完整 child 的修改可先同步,root 编辑继续等待;原流捕获版本和新 ACK 分开,旧后缀/最终完成不覆盖已确认 child 编辑。
// loadingRead 保留捕获时的 child=1,用来核对后续源片段;runtime.versions 的 child=2 用于下一次写入,二者不能相互覆盖。
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{
@@ -338,7 +340,8 @@ test('v5 copy rejects an incomplete ancestor or a missing declared child before
// 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.
// 此场景多一个 other canvas,打开时显式提供三范围版本向量,不能沿用默认仅 root/child 的 versions。
// 同选 F 和其后代 C 时复用同一映射;上级 A→F 保留并新增 A→F′,未加载的无关 V 不进入复制范围。
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();}
@@ -382,7 +385,8 @@ function assetPort(f){const uploads=[],receipts=new Map();let lose=false;const a
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 不重复上传,旧源完成保留资源。
// v6 图片立即进入工作文档,完整 child 允许持久化后上传并同步,未完成 root 拒绝;恢复校验准入证据,undo/redo 不重复上传,旧源完成保留资源。
// 此例检查模型和上传顺序,不挂载图片 renderer;后面的配额失败场景另用 displayResource 读取本地 Blob。
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);
+3
View File
@@ -56,6 +56,7 @@ try {
for(const name of ['window','document','navigator','indexedDB','Worker'])Object.defineProperty(globalThis,name,{configurable:true,get(){throw Error('Browser global: '+name);}});
`);
// 在独立进程运行打包 consumer.mjs;portable、重开和元素数由消费者真实执行结果断言,不从源码文本推断。
// 全局与路径守卫只覆盖该消费者实际加载和执行的路径,不会遍历包的全部导出或所有延迟分支。
const report=JSON.parse(run(process.execPath,['--import',join(temporary,'guard.mjs'),join(packageRoot,'consumer.mjs')],{cwd:temporary}));
assert.equal(report.status,'ok');assert.equal(report.portableBindings,true);assert.equal(report.reopened,true);assert.equal(report.elements,2);
// 不加载 DOM 类型库,验证 portable 端口的公开声明没有泄漏浏览器专属类型。
@@ -88,6 +89,7 @@ try {
return openProjectEngine({scope,storage,registry,document:createDocument(scope.projectId,[{id:scope.projectId,parent:null}],[]),versions:new Map([[scope.projectId,1]]),persistence:createFoundryPersistence({registry,transport,rootId:scope.projectId,workspaceId:scope.workspaceId})});
}
`);
// 模板中的导出函数仅参加类型检查,不在此处运行;运行时行为仍由上面的 consumer.mjs 和其它场景验证。
// 仅使用 ES2022 与 Node 类型进行 noEmit 检查,防止 portable 端口意外依赖浏览器声明。
run(process.execPath,[join(root,'node_modules/typescript/bin/tsc'),'--noEmit','--strict','--target','ES2022','--module','NodeNext','--moduleResolution','NodeNext','--lib','ES2022','--types','node','--typeRoots',join(root,'node_modules/@types'),join(packageRoot,'consumer-types.mts')],{cwd:temporary});
// 框架 peers 由第二个消费者显式接入;上面的路径守卫保持严格,/bindings 不能依赖这些 peers。
@@ -112,6 +114,7 @@ try {
const html=renderToStaticMarkup(React.createElement(CanvasRendererCommandsContext.Provider,{value:commands},React.createElement(renderers.nodeTypes.fixture)));
assert.equal(html,'<span>peer renderer</span>');assert.strictEqual(seen,commands);seen.dispatch({type:'element.data',target:{id:'fixture',incarnation:1},patch:{title:'command'}});assert.equal(dispatched,1);console.log('PASS explicit React peer renderer/context consumer');
`);
// SSR 验证首次读取及 context 传递,不运行客户端 effect、订阅更新或指针交互;命令计数来自显式 seen.dispatch。
// 运行实际解包后的 React 入口,显示该独立消费者输出。
const react=run(process.execPath,[join(packageRoot,'react-consumer.mjs')],{cwd:temporary});process.stdout.write(react);
// 生成框架 TS 消费者:Hooks 检查选择器、actions 和资源/操作视图,Card/Link 分别验证公开节点及边投影类型。