diff --git a/tests/headless/browser-migration.test.mjs b/tests/headless/browser-migration.test.mjs index 9ae7ac8..3fc5ee2 100644 --- a/tests/headless/browser-migration.test.mjs +++ b/tests/headless/browser-migration.test.mjs @@ -31,6 +31,7 @@ test('editable legacy views share a lease and exclude migration until every edit const f=fixture(),one=await claimLegacyCanvasEditing({...f.options,canvasId:root}),two=await claimLegacyCanvasEditing({...f.options,canvasId:root}); const migration=createBrowserCanvasMigration(f.options); await assert.rejects(()=>migration.resume(),/migration_busy/);await one.release();await assert.rejects(()=>migration.resume(),/migration_busy/); + // 所有 shared 租约释放后 resume 只取得独占检查并返回 idle;夹具里没有待迁移任务,不会发生迁移提交。 await two.release();assert.equal((await migration.resume()).status,'idle');assert.equal(f.held.size,0); assert.ok(f.calls.every(call=>call.name===canvasMigrationStorageKey(scope)+':editing'&&call.options.ifAvailable===true)); assert.deepEqual(f.calls.slice(0,2).map(call=>call.options.mode),['shared','shared']);assert.ok(f.calls.slice(2).every(call=>call.options.mode==='exclusive')); @@ -41,6 +42,7 @@ test('pending migration blocks affected old views and corrupt records cannot ena f.storage=f.options.storage;f.storage.setItem(key,JSON.stringify(job)); await assert.rejects(()=>claimLegacyCanvasEditing({...f.options,canvasId:root}),/migration_response_unknown/);assert.equal(f.held.size,0); const unrelated=await claimLegacyCanvasEditing({...f.options,canvasId:id(99)});await unrelated.release(); + // 用未来版本记录覆盖测试存储,拒绝后原字符串必须保留,不能把“无法解释”当作“没有任务”。 const saved=JSON.stringify({version:2,data:'retain'});f.storage.setItem(key,saved); await assert.rejects(()=>claimLegacyCanvasEditing({...f.options,canvasId:root}),/migration_interpreter_unavailable/);assert.equal(f.storage.getItem(key),saved);assert.equal(f.held.size,0); }); diff --git a/tests/headless/digest-worker-source.test.mjs b/tests/headless/digest-worker-source.test.mjs index 62dc035..7d80842 100644 --- a/tests/headless/digest-worker-source.test.mjs +++ b/tests/headless/digest-worker-source.test.mjs @@ -8,6 +8,7 @@ import {digestWorkerSource,nativeDigestByteLimit} from '../../src/lib/canvas-run // 空输入、Unicode 和二进制分别比较原生/流式结果,确认预算内原生路径只调用一次正确的 SHA-256 算法。 test('native Worker hashing and streaming fallback produce identical SHA256 for empty, UTF8 and binary sources',async()=>{ for(const bytes of [new Blob(),new Blob(['中文 😀']),new Blob([new Uint8Array([0,255,128,10])])]){ + // 注入的 native 只计数并委托 Node WebCrypto;这里执行 Worker 的算法函数,没有启动真实浏览器 Worker。 let calls=0;const native={digest(algorithm,input){calls++;assert.equal(algorithm,'SHA-256');assert.equal(input.byteLength,bytes.size);return crypto.subtle.digest(algorithm,input);}}; assert.equal(await digestWorkerSource(bytes,native),await digestWorkerSource(bytes,null));assert.equal(calls,1); } @@ -28,5 +29,6 @@ test('inputs above the native allocation budget use bounded chunks and never req // 伪 Blob 报告大尺寸,禁止整块 arrayBuffer,只允许 slice 返回有限大小的零字节块并记录峰值。 class ChunkOnly extends Blob{get size(){return size;}arrayBuffer(){throw Error('unbounded read');}slice(start=0,end=size){const count=Math.max(0,Math.min(size,end)-start);largest=Math.max(largest,count);return new Blob([new Uint8Array(count)]);}} for(let cursor=0;cursor{ const child=id(4),f=fixture({status:409,body:{error:'canonical_migration_required',root_id:board.id,document_format_version:1,source_versions:{[board.id]:3,[child]:7}}}); const result=await loadCanvasEntry(f.options); diff --git a/tests/headless/renderers.test.mjs b/tests/headless/renderers.test.mjs index 1a995d8..bcd66e8 100644 --- a/tests/headless/renderers.test.mjs +++ b/tests/headless/renderers.test.mjs @@ -20,6 +20,7 @@ test('renderer catalog captures exact version/structure mappings without React v }); // 组装仅在同一个 render 实现时共享别名包装;拒绝空 renderer、ID 冲突与模型契约不符,getter 只读取一次并保留 memo 对象。 test('React assembly preserves aliases only for the same component and rejects invalid renderers or model contracts',()=>{ + // render 返回 null,仅用作可比较的组件身份;此例检查包装与别名缓存,没有执行实际节点 UI。 const render=()=>null,assembly=createReactRenderers([{...descriptor,render}],registry);assert.equal(assembly.nodeTypes.meter.type,render);assert.ok(Object.isFrozen(assembly.nodeTypes));assert.equal(assembly.edgeTypes.meter,undefined); assert.throws(()=>createReactRenderers([{...descriptor,render:null}]),/invalid_react_renderer/);assert.throws(()=>createReactRenderers([{...descriptor,render},{...descriptor,componentId:'other',render:()=>null}]),/renderer_id_conflict/);assert.throws(()=>createReactRenderers([{...descriptor,dataVersion:2,render}],registry),/renderer_model_mismatch/); let reads=0;const getter=createReactRenderers([{...descriptor,get render(){reads++;return render;}}]);assert.equal(reads,1);assert.equal(getter.definitions[0].render,render);assert.equal(getter.nodeTypes.meter,assembly.nodeTypes.meter); @@ -29,6 +30,7 @@ test('React assembly preserves aliases only for the same component and rejects i test('installed node and edge renderers receive original records and transforms while absent schemas still fall back',()=>{ const customEdge=defineComponent({id:'plugin:edge',dataVersion:1,structure:'connection',validate:()=>true,features:[]}),r=createRegistry([...builtinComponents(),definition,customEdge]),other={...node,id:'second',geometry:{...node.geometry,x:300}},edge={id:'edge',incarnation:1,structure:'connection',componentId:'plugin:edge',dataVersion:1,ownerCanvasId:'root',source:{elementId:'meter',portId:'custom-out'},target:{elementId:'second',portId:'custom-in'},path:{type:'custom',retained:true},data:{path:{own:true},metric:99}},source=createDocument('project',[{id:'root',parent:null}],[node,other,edge]),catalog=createRendererCatalog([...builtinRendererDefinitions,descriptor,{...descriptor,componentId:'plugin:edge',structure:'connection',rendererId:'meter-edge'}]),p=projectCanvas(source,'root',r,new Set(),()=>undefined,null,catalog); assert.equal(p.nodes[0].type,'meter');assert.equal(p.nodes[0].data.__headlessFallback,undefined);assert.equal(p.nodes[0].data.__headlessRecord,source.elements.get('meter'));assert.deepEqual(p.nodes[0].data.__headlessRecord.data.title,{opaque:true});assert.equal(p.nodes[0].data.__headlessTransform.scale,1);assert.equal(p.edges[0].type,'meter-edge');assert.equal(p.edges[0].data.__headlessRecord,source.elements.get('edge'));assert.deepEqual(p.edges[0].data.__headlessRecord.path,{type:'custom',retained:true}); + // 已知模型但未装 renderer 与未来数据版本都走占位;前面的可信 __headlessRecord 必须覆盖用户同名字段。 const missing=projectCanvas(source,'root',r,new Set(),()=>undefined);assert.equal(missing.nodes[0].type,'unsupported');assert.equal(missing.edges[0].type,'unsupported');const future=createDocument('project',[{id:'root',parent:null}],[{...node,dataVersion:2}]);assert.equal(projectCanvas(future,'root',r,new Set(),()=>undefined,null,catalog).nodes[0].type,'unsupported'); }); // 加载前预览必须显式提供 preview,实现与正式 renderer 独立捕获;非法 preview 或不一致别名被拒绝。