leader=awaitstorage.claim(scope);assert(JSON.stringify(awaitstorage.recover(leader))===JSON.stringify(initial),'DB v4 legacy checkpoint changed during upgrade');constfirst=image(1);awaitstorage.commit(leader,{image:sealRecoveryImage(1,1,1,first.payload),files:[{id:'kept',bytes:newBlob(['kept bytes'])}]});
leader=awaitstorage.claim(scope);assert(JSON.stringify(awaitstorage.recover(leader))===JSON.stringify(initial),'DB v4 legacy checkpoint changed during upgrade');constfirst=image(1);awaitstorage.commit(leader,{image:sealRecoveryImage(1,1,1,first.payload),files:[{id:'kept',bytes:newBlob(['kept bytes'])}]});
// DB 应升级为 5;新 checkpoint 存 Blob 而非克隆文档对象,文本和 checksum 覆盖精确序列化字节。
raw=awaitread(indexedDB.open(name));assert(raw.version===5,'Blob cuts did not fence old DB clients');constsaved=awaitread(raw.transaction('checkpoints').objectStore('checkpoints').get([key,1]));assert(saved.checkpointVersion===1&&saved.bytesinstanceofBlob&&!saved.image,'full cut is still a cloned document object');assert(awaitsaved.bytes.text()===JSON.stringify(first),'Blob checkpoint changed serialized bytes');assert(saved.checksum===awaithash(saved.bytes),'Blob checksum did not cover exact source bytes');
raw=awaitread(indexedDB.open(name));assert(raw.version===5,'Blob cuts did not fence old DB clients');constsaved=awaitread(raw.transaction('checkpoints').objectStore('checkpoints').get([key,1]));assert(saved.checkpointVersion===1&&saved.bytesinstanceofBlob&&!saved.image,'full cut is still a cloned document object');assert(awaitsaved.bytes.text()===JSON.stringify(first),'Blob checkpoint changed serialized bytes');assert(saved.checksum===awaithash(saved.bytes),'Blob checksum did not cover exact source bytes');
// 旧 DB v4 客户端再打开必须得到 VersionError,防止老解释器接受新 Blob 行。
awaitwrite(damaged);letcode;try{awaitstorage.recover(leader);}catch(error){code=error.code;}assert(code===expected,`${kind} cut was accepted: ${code}`);if(kind==='future'){code=undefined;try{awaitstorage.commit(leader,{image:image(2)});}catch(error){code=error.code;}assert(code===expected,'writer replaced a future checkpoint');}awaitwrite(saved);
awaitwrite(damaged);letcode;try{awaitstorage.recover(leader);}catch(error){code=error.code;}assert(code===expected,`${kind} cut was accepted: ${code}`);if(kind==='future'){code=undefined;try{awaitstorage.commit(leader,{image:image(2)});}catch(error){code=error.code;}assert(code===expected,'writer replaced a future checkpoint');}awaitwrite(saved);
assert(injected&&error==='abort_blob_checkpoint','checkpoint fault was not injected after put');assert((awaitstorage.recover(leader)).rootRevision===1&&awaitstorage.readFile(leader,'orphan')===undefined,'aborted Blob cut leaked head or file bytes');
assert(injected&&error==='abort_blob_checkpoint','checkpoint fault was not injected after put');assert((awaitstorage.recover(leader)).rootRevision===1&&awaitstorage.readFile(leader,'orphan')===undefined,'aborted Blob cut leaked head or file bytes');
// 释放并重建适配器,检查 Blob 与同批文件在新 owner 下仍能恢复。
awaitleader.release();storage.close();storage=createBrowserStorage({name,checkpointEvery:1});leader=awaitstorage.claim(scope);assert(JSON.stringify(awaitstorage.recover(leader))===JSON.stringify(first),'fresh adapter lost Blob content');assert(await(awaitstorage.readFile(leader,'kept')).text()==='kept bytes','Blob reopening lost atomic file');
awaitleader.release();storage.close();storage=createBrowserStorage({name,checkpointEvery:1});leader=awaitstorage.claim(scope);assert(JSON.stringify(awaitstorage.recover(leader))===JSON.stringify(first),'fresh adapter lost Blob content');assert(await(awaitstorage.readFile(leader,'kept')).text()==='kept bytes','Blob reopening lost atomic file');
constlarge=sealRecoveryImage(1,2,2,{rows:Array.from({length:16000},(_,i)=>({id:i,text:'x'.repeat(100)}))});letfailure;constpending=storage.commit(leader,{image:large,files:[{id:'late',bytes:newBlob(['late'])}]}).catch(error=>{failure=error.code;});awaitPromise.resolve();awaitleader.release();awaitpending;assert(failure==='storage_leader_lost','released owner published or changed its cancellation error');leader=awaitstorage.claim(scope);assert((awaitstorage.recover(leader)).rootRevision===1&&awaitstorage.readFile(leader,'late')===undefined,'released owner advanced Blob checkpoint');
constlarge=sealRecoveryImage(1,2,2,{rows:Array.from({length:16000},(_,i)=>({id:i,text:'x'.repeat(100)}))});letfailure;constpending=storage.commit(leader,{image:large,files:[{id:'late',bytes:newBlob(['late'])}]}).catch(error=>{failure=error.code;});awaitPromise.resolve();awaitleader.release();awaitpending;assert(failure==='storage_leader_lost','released owner published or changed its cancellation error');leader=awaitstorage.claim(scope);assert((awaitstorage.recover(leader)).rootRevision===1&&awaitstorage.readFile(leader,'late')===undefined,'released owner advanced Blob checkpoint');
try{letcode;try{awaitstorage.claim(scope);}catch(error){code=error.code;}assert(code==='storage_upgrade_blocked','active old connection did not reject the upgrade');code=undefined;try{awaitstorage.claim(scope);}catch(error){code=error.code;}assert(code==='storage_upgrade_blocked','blocked adapter lost the original failure reason');raw.close();raw=awaitread(indexedDB.open(name));assert(raw.version===4,'abandoned upgrade changed database version after rejection');raw.close();storage.close();storage=createBrowserStorage({name});leader=awaitstorage.claim(scope);assert(awaitstorage.recover(leader)===null,'fresh adapter could not open after blocked request was retired');raw=awaitread(indexedDB.open(name));assert(raw.version===5,'fresh upgrade failed after abandoned open');}
try{letcode;try{awaitstorage.claim(scope);}catch(error){code=error.code;}assert(code==='storage_upgrade_blocked','active old connection did not reject the upgrade');code=undefined;try{awaitstorage.claim(scope);}catch(error){code=error.code;}assert(code==='storage_upgrade_blocked','blocked adapter lost the original failure reason');raw.close();raw=awaitread(indexedDB.open(name));assert(raw.version===4,'abandoned upgrade changed database version after rejection');raw.close();storage.close();storage=createBrowserStorage({name});leader=awaitstorage.claim(scope);assert(awaitstorage.recover(leader)===null,'fresh adapter could not open after blocked request was retired');raw=awaitread(indexedDB.open(name));assert(raw.version===5,'fresh upgrade failed after abandoned open');}
constresponse=await(awaitfetch(`/loader-release?key=${key}${fail?'&fail=1':''}`)).json();assert(response.reads===1,'subscribers fetched the same query twice');
constresponse=await(awaitfetch(`/loader-release?key=${key}${fail?'&fail=1':''}`)).json();assert(response.reads===1,'subscribers fetched the same query twice');
if(fail){awaitrejected;assert(b.getSnapshot().status==='failed-with-partial'&&b.getSnapshot().chunks.length===1,'invalid network tail discarded or completed the prefix');}
if(fail){awaitrejected;assert(b.getSnapshot().status==='failed-with-partial'&&b.getSnapshot().chunks.length===1,'invalid network tail discarded or completed the prefix');}
else{constresult=awaitb.ready;assert(result.status==='complete'&&result.chunks.length===2&&result.scope.navigationGeneration===2,'stream completion lost subscriber scope or chunks');assert(a.getSnapshot().status==='cancelled'&&a.getSnapshot().chunks.length===1,'cancelled subscriber observed its peer tail');}
else{constresult=awaitb.ready;assert(result.status==='complete'&&result.chunks.length===2&&result.scope.navigationGeneration===2,'stream completion lost subscriber scope or chunks');assert(a.getSnapshot().status==='cancelled'&&a.getSnapshot().chunks.length===1,'cancelled subscriber observed its peer tail');}
constwait=asyncpredicate=>{for(leti=0;i<100;i++){if(predicate())return;awaitnewPromise(resolve=>setTimeout(resolve,5));}throwError('shared display did not settle');};
constwait=asyncpredicate=>{for(leti=0;i<100;i++){if(predicate())return;awaitnewPromise(resolve=>setTimeout(resolve,5));}throwError('shared display did not settle');};
b.sync([['other-view',resource]]);awaitwait(()=>second.has('other-view'));assert(second.get('other-view').url===url&&reads===1&&created===1,'two views did not share one IndexedDB read and Blob URL');
b.sync([['other-view',resource]]);awaitwait(()=>second.has('other-view'));assert(second.get('other-view').url===url&&reads===1&&created===1,'two views did not share one IndexedDB read and Blob URL');
a.sync([['picture',resource],['copy',resource]]);awaitwait(()=>first.has('copy'));assert(first.get('copy').url===url&&reads===1&&created===1&&revoked===0,'adding an image reference replaced existing display handles');
a.sync([['picture',resource],['copy',resource]]);awaitwait(()=>first.has('copy'));assert(first.get('copy').url===url&&reads===1&&created===1&&revoked===0,'adding an image reference replaced existing display handles');
a.dispose();assert(revoked===0&&(await(awaitfetch(url)).arrayBuffer()).byteLength>0,'closing one view revoked another view image');
a.dispose();assert(revoked===0&&(await(awaitfetch(url)).arrayBuffer()).byteLength>0,'closing one view revoked another view image');
assert(engine.getResourcePins().pins.get(resource).has('display'),'remaining view lost its display pin');b.dispose();assert(revoked===1&&!engine.getResourcePins().pins.get(resource).has('display'),'last view did not release its URL and display pin');
assert(engine.getResourcePins().pins.get(resource).has('display'),'remaining view lost its display pin');b.dispose();assert(revoked===1&&!engine.getResourcePins().pins.get(resource).has('display'),'last view did not release its URL and display pin');
// 全部借用结束后不保留 idle URL,重新借用需新读取、新 URL,释放后再次撤销。
constfresh=awaitengine.displayResource(resource);assert(fresh.url!==url&&reads===2&&created===2,'released URL was retained as an idle cache');fresh.release();assert(revoked===2,'fresh handle did not revoke');
constfresh=awaitengine.displayResource(resource);assert(fresh.url!==url&&reads===2&&created===2,'released URL was retained as an idle cache');fresh.release();assert(revoked===2,'fresh handle did not revoke');
// 显示借用仅是资源生命周期,不应新增业务 Root 修订或向观察者报告错误。
assert(errors.length===0&&engine.getSnapshot().rootRevision===revision,'display caching changed the business Root or reported a reader failure');
assert(errors.length===0&&engine.getSnapshot().rootRevision===revision,'display caching changed the business Root or reported a reader failure');
assert(engine.getSnapshot().document.elements.get('picture').geometry.height===130&&engine.getSnapshot().history.undo.length===1,'decode did not enrich the original creation');
assert(engine.getSnapshot().document.elements.get('picture').geometry.height===130&&engine.getSnapshot().history.undo.length===1,'decode did not enrich the original creation');
constrequest=writes.find(request=>JSON.parse(request.body).intent.historyEffect);assert(!!request,'size change was never submitted');constbody=request.body;
constrequest=writes.find(request=>JSON.parse(request.body).intent.historyEffect);assert(!!request,'size change was never submitted');constbody=request.body;
awaitengine.close();constowner=awaitstorage.claim(scope),cut=awaitstorage.recover(owner);assert(cut.formatVersion===6,'size effect did not require recovery v6');decodeRecovery(cut,registry);awaitowner.release();
awaitengine.close();constowner=awaitstorage.claim(scope),cut=awaitstorage.recover(owner);assert(cut.formatVersion===6,'size effect did not require recovery v6');decodeRecovery(cut,registry);awaitowner.release();
assert(engine.editor.history.undo().status==='applied','sized creation could not undo after reopening');awaitengine.settle();assert(!server.elements.has('picture'),'undo left the server image behind');
assert(engine.editor.history.undo().status==='applied','sized creation could not undo after reopening');awaitengine.settle();assert(!server.elements.has('picture'),'undo left the server image behind');
assert(engine.editor.history.redo().status==='applied','sized creation could not redo after reopening');awaitengine.settle();
assert(engine.editor.history.redo().status==='applied','sized creation could not redo after reopening');awaitengine.settle();
assert(server.elements.get('picture').incarnation===2&&server.elements.get('picture').geometry.height===130,'redo lost size or reused a deleted incarnation');assert(uploads.length===1,'size recovery repeated the upload');
assert(server.elements.get('picture').incarnation===2&&server.elements.get('picture').geometry.height===130,'redo lost size or reused a deleted incarnation');assert(uploads.length===1,'size recovery repeated the upload');
constpool=createBrowserImageDecoder();try{constcanvas=newOffscreenCanvas(4,2);canvas.getContext('2d').fillRect(0,0,4,2);constbytes=awaitcanvas.convertToBlob({type:'image/png'}),results=awaitPromise.all([pool.decode(bytes),pool.decode(bytes),pool.decode(bytes)]);assert(results.every(size=>size.width===4&&size.height===2),'real bitmap decoder returned incorrect dimensions');assert(peak===2&&active===0&&created===3,'image worker concurrency/cleanup failed');letcode;try{awaitpool.decode(newBlob(['not an image'],{type:'image/png'}));}catch(error){code=error.code;}assert(code==='image_decode_failed'&&active===0,'invalid image did not fail or leaked a worker');constcancel=newAbortController(),pending=pool.decode(bytes,cancel.signal),cancelled=pending.then(()=>{throwError('cancelled image returned dimensions');},error=>assert(error.code==='image_decode_aborted','image cancellation code changed'));cancel.abort();awaitcancelled;assert(active===0,'cancelled image worker leaked');}finally{pool.close();globalThis.Worker=NativeWorker;}
constpool=createBrowserImageDecoder();try{constcanvas=newOffscreenCanvas(4,2);canvas.getContext('2d').fillRect(0,0,4,2);constbytes=awaitcanvas.convertToBlob({type:'image/png'}),results=awaitPromise.all([pool.decode(bytes),pool.decode(bytes),pool.decode(bytes)]);assert(results.every(size=>size.width===4&&size.height===2),'real bitmap decoder returned incorrect dimensions');assert(peak===2&&active===0&&created===3,'image worker concurrency/cleanup failed');letcode;try{awaitpool.decode(newBlob(['not an image'],{type:'image/png'}));}catch(error){code=error.code;}assert(code==='image_decode_failed'&&active===0,'invalid image did not fail or leaked a worker');constcancel=newAbortController(),pending=pool.decode(bytes,cancel.signal),cancelled=pending.then(()=>{throwError('cancelled image returned dimensions');},error=>assert(error.code==='image_decode_aborted','image cancellation code changed'));cancel.abort();awaitcancelled;assert(active===0,'cancelled image worker leaked');}finally{pool.close();globalThis.Worker=NativeWorker;}
cancel.abort();awaitcancelled;constresults=awaitPromise.all([a,b]);assert(results.every(value=>value===expected),'worker digest differs from native SHA256');assert(peak===2&&created===2&&active===0,'worker pool exceeded bounds or leaked a worker');assert(replies.length===2&&replies.every(reply=>reply.version===1&&reply.byteLength===bytes.size),'worker protocol did not actually run');
cancel.abort();awaitcancelled;constresults=awaitPromise.all([a,b]);assert(results.every(value=>value===expected),'worker digest differs from native SHA256');assert(peak===2&&created===2&&active===0,'worker pool exceeded bounds or leaked a worker');assert(replies.length===2&&replies.every(reply=>reply.version===1&&reply.byteLength===bytes.size),'worker protocol did not actually run');
constlate=pool.digest(bytes),closed=late.then(()=>{throwError('closed worker published');},error=>assert(error.code==='import_aborted','pool close did not cancel work'));awaitPromise.resolve();pool.close();awaitclosed;assert(active===0,'pool close retained a worker');
constlate=pool.digest(bytes),closed=late.then(()=>{throwError('closed worker published');},error=>assert(error.code==='import_aborted','pool close did not cancel work'));awaitPromise.resolve();pool.close();awaitclosed;assert(active===0,'pool close retained a worker');
assert((awaitstorage.recover(leader)).payload.nested.title==='captured','unowned image changed while checksum was pending');assert(await(awaitstorage.readFile(leader,'captured-file')).text()==='captured bytes','file descriptor changed while checksum was pending');assert(awaitstorage.readFile(leader,'changed-file')===undefined,'late descriptor identity leaked into storage');
assert((awaitstorage.recover(leader)).payload.nested.title==='captured','unowned image changed while checksum was pending');assert(await(awaitstorage.readFile(leader,'captured-file')).text()==='captured bytes','file descriptor changed while checksum was pending');assert(awaitstorage.readFile(leader,'changed-file')===undefined,'late descriptor identity leaked into storage');
constsealed=sealRecoveryImage(1,1,1,{nested:{title:'sealed'}});awaitstorage.commit(leader,{image:sealed});assert((awaitstorage.recover(leader)).payload.nested.title==='sealed','sealed image did not survive IndexedDB/checksum');
constsealed=sealRecoveryImage(1,1,1,{nested:{title:'sealed'}});awaitstorage.commit(leader,{image:sealed});assert((awaitstorage.recover(leader)).payload.nested.title==='sealed','sealed image did not survive IndexedDB/checksum');
assert(timerRan,'copy planning starved a real browser task');assert(plan.changes.length===1,'copy planning changed selection scope');assert(source.elements.size===1500,'copy planning mutated its source');
assert(timerRan,'copy planning starved a real browser task');assert(plan.changes.length===1,'copy planning changed selection scope');assert(source.elements.size===1500,'copy planning mutated its source');
try{letcode;try{awaitplanDuplicateAsync(source,registry,[{id:'source-0',incarnation:1}],{createId:()=>`unused-${++next}`},{signal:controller.signal,maxStepsPerSlice:128});}catch(error){code=error.code;}assert(code==='copy_aborted','browser cancellation did not stop planning');}finally{clearTimeout(cancel);}
try{letcode;try{awaitplanDuplicateAsync(source,registry,[{id:'source-0',incarnation:1}],{createId:()=>`unused-${++next}`},{signal:controller.signal,maxStepsPerSlice:128});}catch(error){code=error.code;}assert(code==='copy_aborted','browser cancellation did not stop planning');}finally{clearTimeout(cancel);}
// 默认明确拒绝写入;GET 提供当前完整基线,放行后逐参与版本检查并生成覆盖全部逻辑 ID 的回执。
consttransport=asyncrequest=>{calls.push(request);if(request.method==='GET')return{status:200,body:{model:model(true)}};constbody=JSON.parse(request.body);if(reject)return{status:409,body:{protocol_version:2,request_id:request.requestId,status:'not-applied',terminal:true,error:'version_conflict'}};for(const[id,version]ofObject.entries(body.expected_versions))assert(version===(versions.get(id)??0),'adoption consumer lost a version fence');for(constoperationofbody.operations)document=applyChanges(document,operation.changes);for(constidofObject.keys(body.expected_versions))versions.set(id,(versions.get(id)??0)+1);return{status:200,body:{protocol_version:2,request_id:request.requestId,status:'applied',operation_ids:body.operations.map(operation=>operation.id),model:model(false,Object.keys(body.expected_versions))}};};
consttransport=asyncrequest=>{calls.push(request);if(request.method==='GET')return{status:200,body:{model:model(true)}};constbody=JSON.parse(request.body);if(reject)return{status:409,body:{protocol_version:2,request_id:request.requestId,status:'not-applied',terminal:true,error:'version_conflict'}};for(const[id,version]ofObject.entries(body.expected_versions))assert(version===(versions.get(id)??0),'adoption consumer lost a version fence');for(constoperationofbody.operations)document=applyChanges(document,operation.changes);for(constidofObject.keys(body.expected_versions))versions.set(id,(versions.get(id)??0)+1);return{status:200,body:{protocol_version:2,request_id:request.requestId,status:'applied',operation_ids:body.operations.map(operation=>operation.id),model:model(false,Object.keys(body.expected_versions))}};};
if(structural){engine.editor.transaction(tx=>{tx.dispatch({type:'element.create',element:{...node('frame'),componentId:'frame'}});tx.dispatch({type:'canvas.create',canvas:{id:child,parent:{canvasId:root,entryElementId:'frame'}}});tx.dispatch({type:'element.create',element:node('member',child)});});awaitengine.settle();engine.editor.dispatch({type:'element.data',target:{id:'member',incarnation:1},patch:{title:'discard member edit'}});}
if(structural){engine.editor.transaction(tx=>{tx.dispatch({type:'element.create',element:{...node('frame'),componentId:'frame'}});tx.dispatch({type:'canvas.create',canvas:{id:child,parent:{canvasId:root,entryElementId:'frame'}}});tx.dispatch({type:'element.create',element:node('member',child)});});awaitengine.settle();engine.editor.dispatch({type:'element.data',target:{id:'member',incarnation:1},patch:{title:'discard member edit'}});}
if(!structural){engine.editor.dispatch({type:'element.data',target:{id:'a',incarnation:1},patch:{text:'after adoption'}},{groupId:'chain'});awaitengine.settle();document=applyChanges(document,[{kind:'field',target:{id:'a',incarnation:1},field:'data.title',before:'remote 1',after:'remote 2',beforePresent:true,afterPresent:true}]);versions.set(root,versions.get(root)+1);session=awaitengine.prepareRecovery();constthird=session.plan(selected());reject=false;awaitsession.resolve(third);awaitengine.settle();awaitreopen();assert(engine.getSnapshot().runtime.recoveries.size===3,'nested adoption audit lineage was lost');assert(document.elements.get('a').data.title==='two'&&document.elements.get('a').data.text==='after adoption','post-adoption native edit was lost');assert(engine.editor.history.undo().status==='applied','nested field recovery undo failed');awaitengine.settle();assert(document.elements.get('a').data.title==='remote 2'&&document.elements.get('a').data.text==='','undo did not restore newest reviewed server baseline');assert(engine.editor.history.redo().status==='applied','nested field recovery redo failed');awaitengine.settle();assert(calls.filter(call=>call.method==='POST').length===6,'nested field recovery did not batch its replacements');}
if(!structural){engine.editor.dispatch({type:'element.data',target:{id:'a',incarnation:1},patch:{text:'after adoption'}},{groupId:'chain'});awaitengine.settle();document=applyChanges(document,[{kind:'field',target:{id:'a',incarnation:1},field:'data.title',before:'remote 1',after:'remote 2',beforePresent:true,afterPresent:true}]);versions.set(root,versions.get(root)+1);session=awaitengine.prepareRecovery();constthird=session.plan(selected());reject=false;awaitsession.resolve(third);awaitengine.settle();awaitreopen();assert(engine.getSnapshot().runtime.recoveries.size===3,'nested adoption audit lineage was lost');assert(document.elements.get('a').data.title==='two'&&document.elements.get('a').data.text==='after adoption','post-adoption native edit was lost');assert(engine.editor.history.undo().status==='applied','nested field recovery undo failed');awaitengine.settle();assert(document.elements.get('a').data.title==='remote 2'&&document.elements.get('a').data.text==='','undo did not restore newest reviewed server baseline');assert(engine.editor.history.redo().status==='applied','nested field recovery redo failed');awaitengine.settle();assert(calls.filter(call=>call.method==='POST').length===6,'nested field recovery did not batch its replacements');}
else{assert(engine.getSnapshot().runtime.recoveries.size===2&&document.elements.get('member').data.title==='member','structural choice did not survive adoption');assert(engine.editor.history.undo().status==='applied','adopted structure undo failed');awaitengine.settle();assert(!document.canvases.has(child),'adopted structure undo left a child canvas');assert(engine.editor.history.redo().status==='applied','adopted structure redo failed');awaitengine.settle();assert(document.elements.get('member').incarnation===2&&document.elements.get('a').data.title==='remote 1','structural redo reused a life or erased remote data');assert(calls.filter(call=>call.method==='POST').length===5,'structural adoption repeated a request');}
else{assert(engine.getSnapshot().runtime.recoveries.size===2&&document.elements.get('member').data.title==='member','structural choice did not survive adoption');assert(engine.editor.history.undo().status==='applied','adopted structure undo failed');awaitengine.settle();assert(!document.canvases.has(child),'adopted structure undo left a child canvas');assert(engine.editor.history.redo().status==='applied','adopted structure redo failed');awaitengine.settle();assert(document.elements.get('member').incarnation===2&&document.elements.get('a').data.title==='remote 1','structural redo reused a life or erased remote data');assert(calls.filter(call=>call.method==='POST').length===5,'structural adoption repeated a request');}
// 最后再重开,逐个比对历次保存的 body,保证重规划没有改写任何历史请求。
awaitreopen();for(const[id,body]ofsaved)assert(engine.getSnapshot().runtime.ledger.requests.get(id).body===body,'original frozen request bytes changed after final reopen');
awaitreopen();for(const[id,body]ofsaved)assert(engine.getSnapshot().runtime.ledger.requests.get(id).body===body,'original frozen request bytes changed after final reopen');
consttransport=asyncrequest=>{calls.push(request);if(request.method==='GET')return{status:200,body:{model:model(true)}};constbody=JSON.parse(request.body);assert(!request.body.includes('historyReplay'),'local history evidence leaked into wire payload');if(reject)return{status:409,body:{protocol_version:2,request_id:request.requestId,status:'not-applied',terminal:true,error:'version_conflict'}};assert(body.expected_versions[root]===version,'adoption evidence changed the version fence');for(constoperationofbody.operations)document=applyChanges(document,operation.changes);version++;return{status:200,body:{protocol_version:2,request_id:request.requestId,status:'applied',operation_ids:body.operations.map(operation=>operation.id),model:model(false)}};};
consttransport=asyncrequest=>{calls.push(request);if(request.method==='GET')return{status:200,body:{model:model(true)}};constbody=JSON.parse(request.body);assert(!request.body.includes('historyReplay'),'local history evidence leaked into wire payload');if(reject)return{status:409,body:{protocol_version:2,request_id:request.requestId,status:'not-applied',terminal:true,error:'version_conflict'}};assert(body.expected_versions[root]===version,'adoption evidence changed the version fence');for(constoperationofbody.operations)document=applyChanges(document,operation.changes);version++;return{status:200,body:{protocol_version:2,request_id:request.requestId,status:'applied',operation_ids:body.operations.map(operation=>operation.id),model:model(false)}};};
// 从审计备份与 historyReplay 重建历史再比较;核对新逻辑 ID 共用一个请求,以及分组 undo 回到原宽度和标题。
constsnapshot=engine.getSnapshot(),audit=snapshot.runtime.recoveries.get(plan.id),backup=audit.backup.history,before={undo:backup.undo,redo:backup.redo,entries:newMap(backup.entries)},reconstructed=replayHistoryEvidence(before,audit.historyReplay,id=>snapshot.operations.has(id));assert(sameHistory(reconstructed,snapshot.history),'persisted adoption witness did not compose to its history');assert(audit.historyReplay.events.filter(event=>event.kind==='operation').length===2,'discarded contribution became a replay operation');assert(snapshot.runtime.ledger.requests.get(old.id).body===old.body,'adoption evidence changed old request bytes');assert(document.elements.get('a').geometry.width===250&&document.elements.get('a').data.title==='kept','adoption content or geometry changed');constreplacementRequests=calls.filter(call=>call.method==='POST'&&plan.operationIds.every(id=>JSON.parse(call.body).operations.some(operation=>operation.id===id)));assert(replacementRequests.length===1,'adopted logical IDs did not share one frozen request');assert(engine.editor.history.undo().status==='applied','geometry evidence broke undo');awaitengine.settle();assert(document.elements.get('a').geometry.width===200&&document.elements.get('a').data.title==='a','adopted group inverse is incorrect');assert(engine.editor.history.redo().status==='applied','geometry evidence broke redo');awaitengine.settle();assert(calls.filter(call=>call.method==='POST').length===4,'adoption evidence did not use one replacement batch');
constsnapshot=engine.getSnapshot(),audit=snapshot.runtime.recoveries.get(plan.id),backup=audit.backup.history,before={undo:backup.undo,redo:backup.redo,entries:newMap(backup.entries)},reconstructed=replayHistoryEvidence(before,audit.historyReplay,id=>snapshot.operations.has(id));assert(sameHistory(reconstructed,snapshot.history),'persisted adoption witness did not compose to its history');assert(audit.historyReplay.events.filter(event=>event.kind==='operation').length===2,'discarded contribution became a replay operation');assert(snapshot.runtime.ledger.requests.get(old.id).body===old.body,'adoption evidence changed old request bytes');assert(document.elements.get('a').geometry.width===250&&document.elements.get('a').data.title==='kept','adoption content or geometry changed');constreplacementRequests=calls.filter(call=>call.method==='POST'&&plan.operationIds.every(id=>JSON.parse(call.body).operations.some(operation=>operation.id===id)));assert(replacementRequests.length===1,'adopted logical IDs did not share one frozen request');assert(engine.editor.history.undo().status==='applied','geometry evidence broke undo');awaitengine.settle();assert(document.elements.get('a').geometry.width===200&&document.elements.get('a').data.title==='a','adopted group inverse is incorrect');assert(engine.editor.history.redo().status==='applied','geometry evidence broke redo');awaitengine.settle();assert(calls.filter(call=>call.method==='POST').length===4,'adoption evidence did not use one replacement batch');
consttransport=asyncrequest=>{calls.push(request);if(request.method==='GET')return{status:200,body:{model:model(true)}};constbody=JSON.parse(request.body);if(reject)return{status:409,body:{protocol_version:2,request_id:request.requestId,status:'not-applied',terminal:true,error:'version_conflict'}};for(const[id,version]ofObject.entries(body.expected_versions))assert(version===versions.get(id),'confirmed traversal lost its version condition');for(constoperationofbody.operations)document=applyChanges(document,operation.changes);for(constidofObject.keys(body.expected_versions))versions.set(id,versions.get(id)+1);return{status:200,body:{protocol_version:2,request_id:request.requestId,status:'applied',operation_ids:body.operations.map(operation=>operation.id),model:model(false,Object.keys(body.expected_versions))}};};
consttransport=asyncrequest=>{calls.push(request);if(request.method==='GET')return{status:200,body:{model:model(true)}};constbody=JSON.parse(request.body);if(reject)return{status:409,body:{protocol_version:2,request_id:request.requestId,status:'not-applied',terminal:true,error:'version_conflict'}};for(const[id,version]ofObject.entries(body.expected_versions))assert(version===versions.get(id),'confirmed traversal lost its version condition');for(constoperationofbody.operations)document=applyChanges(document,operation.changes);for(constidofObject.keys(body.expected_versions))versions.set(id,versions.get(id)+1);return{status:200,body:{protocol_version:2,request_id:request.requestId,status:'applied',operation_ids:body.operations.map(operation=>operation.id),model:model(false,Object.keys(body.expected_versions))}};};
// 丢弃未决前驱会违反已确认遍历顺序,必须拒绝;保留它后检查原请求字节与已确认 POST 次数不变。
constsession=awaitengine.prepareRecovery();letdenied=false;try{session.plan(newMap([[first.operationIds[0],'discard-local']]));}catch(error){denied=error.code==='recovery_history_traversal_order';}assert(denied,'confirmed traversal silently skipped a discarded predecessor');constplan=session.plan(newMap([[first.operationIds[0],'keep-local']]));awaitsession.resolve(plan);awaitengine.settle();awaitengine.close();engine=awaitopenProjectEngine(options);assert(engine.getSnapshot().runtime.ledger.requests.get(old.id).body===old.body&&engine.getSnapshot().runtime.ledger.requests.get(applied.id).body===applied.body,'history reconstruction rewrote original request facts');assert(calls.filter(call=>call.method==='POST'&&call.requestId===applied.id).length===1,'confirmed traversal was submitted again');
constsession=awaitengine.prepareRecovery();letdenied=false;try{session.plan(newMap([[first.operationIds[0],'discard-local']]));}catch(error){denied=error.code==='recovery_history_traversal_order';}assert(denied,'confirmed traversal silently skipped a discarded predecessor');constplan=session.plan(newMap([[first.operationIds[0],'keep-local']]));awaitsession.resolve(plan);awaitengine.settle();awaitengine.close();engine=awaitopenProjectEngine(options);assert(engine.getSnapshot().runtime.ledger.requests.get(old.id).body===old.body&&engine.getSnapshot().runtime.ledger.requests.get(applied.id).body===applied.body,'history reconstruction rewrote original request facts');assert(calls.filter(call=>call.method==='POST'&&call.requestId===applied.id).length===1,'confirmed traversal was submitted again');
// 后续继续操作更早/剩余历史,检查第二代再到第三代映射和各分支精确写次数,防止恢复重复派发。
if(direction==='undo'){assert(document.elements.get('a').incarnation===2&&document.elements.get('c').data.text==='','confirmed undo restoration was lost');assert(engine.editor.history.undo().status==='applied','older remapped history was stale');awaitengine.settle();assert(document.elements.get('a').data.text==='','older history inverse used the wrong lifecycle');assert(engine.editor.history.redo().status==='applied','older history redo failed');awaitengine.settle();assert(calls.filter(call=>call.method==='POST').length===8,'confirmed undo recovery duplicated a write');}
if(direction==='undo'){assert(document.elements.get('a').incarnation===2&&document.elements.get('c').data.text==='','confirmed undo restoration was lost');assert(engine.editor.history.undo().status==='applied','older remapped history was stale');awaitengine.settle();assert(document.elements.get('a').data.text==='','older history inverse used the wrong lifecycle');assert(engine.editor.history.redo().status==='applied','older history redo failed');awaitengine.settle();assert(calls.filter(call=>call.method==='POST').length===8,'confirmed undo recovery duplicated a write');}
else{assert(document.elements.get('new').incarnation===2,'confirmed redo incarnation was lost');assert(engine.editor.history.redo().status==='applied','remaining redo entry was not remapped');awaitengine.settle();for(leti=0;i<3;i++){assert(engine.editor.history.undo().status==='applied','post-recovery undo failed');awaitengine.settle();}for(leti=0;i<3;i++){assert(engine.editor.history.redo().status==='applied','post-recovery redo failed');awaitengine.settle();}assert(document.elements.get('new').incarnation===3&&document.elements.get('new').data.text==='edited new','later traversal reused an old lifecycle');assert(calls.filter(call=>call.method==='POST').length===16,'confirmed redo recovery duplicated a write');}
else{assert(document.elements.get('new').incarnation===2,'confirmed redo incarnation was lost');assert(engine.editor.history.redo().status==='applied','remaining redo entry was not remapped');awaitengine.settle();for(leti=0;i<3;i++){assert(engine.editor.history.undo().status==='applied','post-recovery undo failed');awaitengine.settle();}for(leti=0;i<3;i++){assert(engine.editor.history.redo().status==='applied','post-recovery redo failed');awaitengine.settle();}assert(document.elements.get('new').incarnation===3&&document.elements.get('new').data.text==='edited new','later traversal reused an old lifecycle');assert(calls.filter(call=>call.method==='POST').length===16,'confirmed redo recovery duplicated a write');}
// 完整读与条件写共用内存服务,按参与 canvas 推进版本,检查恢复没有重传已确认 c 的编辑。
consttransport=asyncrequest=>{calls.push(request);if(request.method==='GET')return{status:200,body:{model:model(true)}};constbody=JSON.parse(request.body);if(reject)return{status:409,body:{protocol_version:2,request_id:request.requestId,status:'not-applied',terminal:true,error:'version_conflict'}};for(const[id,version]ofObject.entries(body.expected_versions))assert(version===versions.get(id),'group recovery lost its version condition');for(constoperationofbody.operations)document=applyChanges(document,operation.changes);for(constidofObject.keys(body.expected_versions))versions.set(id,versions.get(id)+1);return{status:200,body:{protocol_version:2,request_id:request.requestId,status:'applied',operation_ids:body.operations.map(operation=>operation.id),model:model(false,Object.keys(body.expected_versions))}};};
consttransport=asyncrequest=>{calls.push(request);if(request.method==='GET')return{status:200,body:{model:model(true)}};constbody=JSON.parse(request.body);if(reject)return{status:409,body:{protocol_version:2,request_id:request.requestId,status:'not-applied',terminal:true,error:'version_conflict'}};for(const[id,version]ofObject.entries(body.expected_versions))assert(version===versions.get(id),'group recovery lost its version condition');for(constoperationofbody.operations)document=applyChanges(document,operation.changes);for(constidofObject.keys(body.expected_versions))versions.set(id,versions.get(id)+1);return{status:200,body:{protocol_version:2,request_id:request.requestId,status:'applied',operation_ids:body.operations.map(operation=>operation.id),model:model(false,Object.keys(body.expected_versions))}};};
// 恢复规划不能把已经成功的 c 当作 replacement 重新发送;恢复和下一次重开之间仍使用真实 IndexedDB。
constsession=awaitengine.prepareRecovery(),plan=session.plan(newMap([[first.operationIds[0],keep?'keep-local':'discard-local']]));awaitsession.resolve(plan);awaitengine.settle();assert(calls.filter(call=>call.method==='POST'&&JSON.parse(call.body).operations.some(operation=>operation.changes.some(change=>change.kind==='field'&&change.target.id==='c'))).length===1,'confirmed group contribution was sent again');awaitengine.close();engine=awaitopenProjectEngine(options);
constsession=awaitengine.prepareRecovery(),plan=session.plan(newMap([[first.operationIds[0],keep?'keep-local':'discard-local']]));awaitsession.resolve(plan);awaitengine.settle();assert(calls.filter(call=>call.method==='POST'&&JSON.parse(call.body).operations.some(operation=>operation.changes.some(change=>change.kind==='field'&&change.target.id==='c'))).length===1,'confirmed group contribution was sent again');awaitengine.close();engine=awaitopenProjectEngine(options);
// 组中必须保留 confirmed 的 operationId;undo/redo 组合选中贡献,丢弃 a 时不得把它重新带回。
consthistory=engine.getSnapshot().history;assert(history.undo.length===1&&history.entries.values().next().value.operationIds.includes(confirmed.operationIds[0]),'group lost its confirmed logical fact');assert(engine.getSnapshot().runtime.ledger.requests.get(old.id).body===old.body,'group replan changed rejected bytes');assert(document.elements.get('a').data.text===(keep?'pending first':''),'group prefix choice was ignored');assert(engine.editor.history.undo().status==='applied','group undo failed');awaitengine.settle();assert(document.elements.get('a').data.text===''&&document.elements.get('c').data.text==='','group undo did not restore both selected contributions');assert(engine.editor.history.redo().status==='applied','group redo failed');awaitengine.settle();assert(document.elements.get('a').data.text===(keep?'pending first':'')&&document.elements.get('c').data.text==='confirmed later','group redo lost selected content');assert(calls.filter(call=>call.method==='POST').length===(keep?5:4),'group recovery repeated a write');
consthistory=engine.getSnapshot().history;assert(history.undo.length===1&&history.entries.values().next().value.operationIds.includes(confirmed.operationIds[0]),'group lost its confirmed logical fact');assert(engine.getSnapshot().runtime.ledger.requests.get(old.id).body===old.body,'group replan changed rejected bytes');assert(document.elements.get('a').data.text===(keep?'pending first':''),'group prefix choice was ignored');assert(engine.editor.history.undo().status==='applied','group undo failed');awaitengine.settle();assert(document.elements.get('a').data.text===''&&document.elements.get('c').data.text==='','group undo did not restore both selected contributions');assert(engine.editor.history.redo().status==='applied','group redo failed');awaitengine.settle();assert(document.elements.get('a').data.text===(keep?'pending first':'')&&document.elements.get('c').data.text==='confirmed later','group redo lost selected content');assert(calls.filter(call=>call.method==='POST').length===(keep?5:4),'group recovery repeated a write');
consttransport=asyncrequest=>{calls.push(request);if(request.method==='GET')return{status:200,body:{model:model(true)}};constbody=JSON.parse(request.body);if(reject)return{status:409,body:{protocol_version:2,request_id:request.requestId,status:'not-applied',terminal:true,error:'version_conflict'}};for(const[id,version]ofObject.entries(body.expected_versions))assert(version===versions.get(id),'journal recovery lost its version condition');for(constoperationofbody.operations)document=applyChanges(document,operation.changes);for(constidofObject.keys(body.expected_versions))versions.set(id,versions.get(id)+1);return{status:200,body:{protocol_version:2,request_id:request.requestId,status:'applied',operation_ids:body.operations.map(operation=>operation.id),model:model(false,Object.keys(body.expected_versions))}};};
consttransport=asyncrequest=>{calls.push(request);if(request.method==='GET')return{status:200,body:{model:model(true)}};constbody=JSON.parse(request.body);if(reject)return{status:409,body:{protocol_version:2,request_id:request.requestId,status:'not-applied',terminal:true,error:'version_conflict'}};for(const[id,version]ofObject.entries(body.expected_versions))assert(version===versions.get(id),'journal recovery lost its version condition');for(constoperationofbody.operations)document=applyChanges(document,operation.changes);for(constidofObject.keys(body.expected_versions))versions.set(id,versions.get(id)+1);return{status:200,body:{protocol_version:2,request_id:request.requestId,status:'applied',operation_ids:body.operations.map(operation=>operation.id),model:model(false,Object.keys(body.expected_versions))}};};
// 先确认 a 的编辑,再拒绝其 undo,随后在 c 上创建已确认新分支,并核对 redo 已清空。
engine=awaitopenProjectEngine(options);engine.editor.dispatch({type:'element.data',target:{id:'a',incarnation:1},patch:{text:'edited'}});awaitengine.settle();reject=true;constundo=engine.editor.history.undo();awaitengine.settle();constold=[...engine.getSnapshot().runtime.ledger.requests.values()].find(request=>request.operationIds.includes(undo.operationIds[0]));reject=false;constbranch=engine.editor.dispatch({type:'element.data',target:{id:'c',incarnation:1},patch:{text:'confirmed branch'}});awaitengine.settle();assert(engine.getSnapshot().history.redo.length===0,'branch did not invalidate redo');awaitengine.close();engine=awaitopenProjectEngine(options);
engine=awaitopenProjectEngine(options);engine.editor.dispatch({type:'element.data',target:{id:'a',incarnation:1},patch:{text:'edited'}});awaitengine.settle();reject=true;constundo=engine.editor.history.undo();awaitengine.settle();constold=[...engine.getSnapshot().runtime.ledger.requests.values()].find(request=>request.operationIds.includes(undo.operationIds[0]));reject=false;constbranch=engine.editor.dispatch({type:'element.data',target:{id:'c',incarnation:1},patch:{text:'confirmed branch'}});awaitengine.settle();assert(engine.getSnapshot().history.redo.length===0,'branch did not invalidate redo');awaitengine.close();engine=awaitopenProjectEngine(options);
// 仅重规划未决 undo;重开后应是 a 已撤销、c 分支仍在,分支 ID 和旧请求 body 不变,再验证分支 undo/redo。
constsession=awaitengine.prepareRecovery(),plan=session.plan(newMap([[undo.operationIds[0],'keep-local']]));awaitsession.resolve(plan);awaitengine.settle();awaitengine.close();engine=awaitopenProjectEngine(options);assert(document.elements.get('a').data.text===''&&document.elements.get('c').data.text==='confirmed branch','confirmed branch was lost or pending undo was not restored');assert(engine.getSnapshot().history.undo.length===1&&engine.getSnapshot().history.redo.length===0,'journal recovery retained an invalidated redo');assert(engine.getSnapshot().history.entries.values().next().value.operationIds.includes(branch.operationIds[0]),'confirmed branch logical identity changed');assert(engine.getSnapshot().runtime.ledger.requests.get(old.id).body===old.body,'journal adoption changed the rejected request');assert(engine.editor.history.undo().status==='applied','confirmed branch undo failed');awaitengine.settle();assert(document.elements.get('c').data.text==='','branch inverse used an incorrect baseline');assert(engine.editor.history.redo().status==='applied','confirmed branch redo failed');awaitengine.settle();assert(calls.filter(call=>call.method==='POST').length===6,'confirmed content was sent twice during event replay');
constsession=awaitengine.prepareRecovery(),plan=session.plan(newMap([[undo.operationIds[0],'keep-local']]));awaitsession.resolve(plan);awaitengine.settle();awaitengine.close();engine=awaitopenProjectEngine(options);assert(document.elements.get('a').data.text===''&&document.elements.get('c').data.text==='confirmed branch','confirmed branch was lost or pending undo was not restored');assert(engine.getSnapshot().history.undo.length===1&&engine.getSnapshot().history.redo.length===0,'journal recovery retained an invalidated redo');assert(engine.getSnapshot().history.entries.values().next().value.operationIds.includes(branch.operationIds[0]),'confirmed branch logical identity changed');assert(engine.getSnapshot().runtime.ledger.requests.get(old.id).body===old.body,'journal adoption changed the rejected request');assert(engine.editor.history.undo().status==='applied','confirmed branch undo failed');awaitengine.settle();assert(document.elements.get('c').data.text==='','branch inverse used an incorrect baseline');assert(engine.editor.history.redo().status==='applied','confirmed branch redo failed');awaitengine.settle();assert(calls.filter(call=>call.method==='POST').length===6,'confirmed content was sent twice during event replay');
engine=awaitopenProjectEngine(options);engine.editor.dispatch({type:'element.data',target:{id:'a',incarnation:1},patch:{text:'first'}});awaitengine.settle();constfirst=engine.getSnapshot().history,undo=engine.editor.history.undo();awaitengine.settle();constbeforeBranch=engine.getSnapshot().history,branch=engine.editor.dispatch({type:'element.data',target:{id:'a',incarnation:1},patch:{text:'branch'}});awaitengine.settle();constold=[...engine.getSnapshot().runtime.ledger.requests.values()][0];assert(engine.getSnapshot().history.redo.length===0,'new branch did not clear redo');awaitengine.close();
engine=awaitopenProjectEngine(options);engine.editor.dispatch({type:'element.data',target:{id:'a',incarnation:1},patch:{text:'first'}});awaitengine.settle();constfirst=engine.getSnapshot().history,undo=engine.editor.history.undo();awaitengine.settle();constbeforeBranch=engine.getSnapshot().history,branch=engine.editor.dispatch({type:'element.data',target:{id:'a',incarnation:1},patch:{text:'branch'}});awaitengine.settle();constold=[...engine.getSnapshot().runtime.ledger.requests.values()][0];assert(engine.getSnapshot().history.redo.length===0,'new branch did not clear redo');awaitengine.close();
engine=awaitopenProjectEngine(options);constsnapshot=engine.getSnapshot(),transition=snapshot.operations.get(branch.operationIds[0]).historyTransition,restored=applyHistoryTransition(snapshot.history,transition,'reverse');assert(value(restored)===value(beforeBranch),'cleared redo evidence did not survive IndexedDB');constbaseline=applyHistoryTransition(restored,snapshot.operations.get(undo.operationIds[0]).historyTransition,'reverse');assert(value(baseline)===value(first),'undo stack transition could not be reconstructed after reopening');assert(snapshot.document.elements.get('a').data.text==='branch','metadata reconstruction mutated the document');assert(snapshot.runtime.ledger.requests.get(old.id).body===old.body&&calls.length===1,'evidence capture changed or repeated the old rejected request');assert(calls.every(call=>!String(call.body).includes('historyTransition')),'local evidence leaked into protocol 2');
engine=awaitopenProjectEngine(options);constsnapshot=engine.getSnapshot(),transition=snapshot.operations.get(branch.operationIds[0]).historyTransition,restored=applyHistoryTransition(snapshot.history,transition,'reverse');assert(value(restored)===value(beforeBranch),'cleared redo evidence did not survive IndexedDB');constbaseline=applyHistoryTransition(restored,snapshot.operations.get(undo.operationIds[0]).historyTransition,'reverse');assert(value(baseline)===value(first),'undo stack transition could not be reconstructed after reopening');assert(snapshot.document.elements.get('a').data.text==='branch','metadata reconstruction mutated the document');assert(snapshot.runtime.ledger.requests.get(old.id).body===old.body&&calls.length===1,'evidence capture changed or repeated the old rejected request');assert(calls.every(call=>!String(call.body).includes('historyTransition')),'local evidence leaked into protocol 2');
consttransport=asyncrequest=>{calls.push(request);if(request.method==='GET')return{status:200,body:{model:model(true)}};constbody=JSON.parse(request.body);if(reject)return{status:409,body:{protocol_version:2,request_id:request.requestId,status:'not-applied',terminal:true,error:'version_conflict'}};for(const[id,version]ofObject.entries(body.expected_versions))assert(version===versions.get(id),'traversal restoration lost an archive version fence');for(constoperationofbody.operations)document=applyChanges(document,operation.changes);for(constidofObject.keys(body.expected_versions))versions.set(id,versions.get(id)+1);return{status:200,body:{protocol_version:2,request_id:request.requestId,status:'applied',operation_ids:body.operations.map(operation=>operation.id),model:model(false,Object.keys(body.expected_versions))}};};
consttransport=asyncrequest=>{calls.push(request);if(request.method==='GET')return{status:200,body:{model:model(true)}};constbody=JSON.parse(request.body);if(reject)return{status:409,body:{protocol_version:2,request_id:request.requestId,status:'not-applied',terminal:true,error:'version_conflict'}};for(const[id,version]ofObject.entries(body.expected_versions))assert(version===versions.get(id),'traversal restoration lost an archive version fence');for(constoperationofbody.operations)document=applyChanges(document,operation.changes);for(constidofObject.keys(body.expected_versions))versions.set(id,versions.get(id)+1);return{status:200,body:{protocol_version:2,request_id:request.requestId,status:'applied',operation_ids:body.operations.map(operation=>operation.id),model:model(false,Object.keys(body.expected_versions))}};};
assert(engine.getSnapshot().history.redo.length===1&&engine.getSnapshot().history.undo.length===0,'recovered undo did not retain its stack transition');assert(document.elements.get('member').incarnation===2,'restoration used the deleted incarnation');assert(engine.getSnapshot().runtime.ledger.requests.get(old.id).body===old.body,'rejected undo bytes changed');
assert(engine.getSnapshot().history.redo.length===1&&engine.getSnapshot().history.undo.length===0,'recovered undo did not retain its stack transition');assert(document.elements.get('member').incarnation===2,'restoration used the deleted incarnation');assert(engine.getSnapshot().runtime.ledger.requests.get(old.id).body===old.body,'rejected undo bytes changed');
// redo 再归档,下一次 undo 用第三代成员;再次重开验证结构和历史处于同一持久化切点。
assert(engine.editor.history.redo().status==='applied','redo after recovered undo failed');awaitengine.settle();assert(!document.canvases.has(child),'redo did not archive the restored child');assert(engine.editor.history.undo().status==='applied','next lifecycle undo failed');awaitengine.settle();assert(document.elements.get('member').incarnation===3,'next undo did not advance incarnation');assert(document.elements.get('anchor').data.title==='remote','traversal recovery erased remote content');awaitengine.close();engine=awaitopenProjectEngine(options);assert(engine.getSnapshot().document.elements.get('member').incarnation===3&&engine.getSnapshot().history.redo.length===1,'restored document and history were not in one durable cut');assert(calls.filter(call=>call.method==='POST').length===5,'traversal recovery duplicated a write');
assert(engine.editor.history.redo().status==='applied','redo after recovered undo failed');awaitengine.settle();assert(!document.canvases.has(child),'redo did not archive the restored child');assert(engine.editor.history.undo().status==='applied','next lifecycle undo failed');awaitengine.settle();assert(document.elements.get('member').incarnation===3,'next undo did not advance incarnation');assert(document.elements.get('anchor').data.title==='remote','traversal recovery erased remote content');awaitengine.close();engine=awaitopenProjectEngine(options);assert(engine.getSnapshot().document.elements.get('member').incarnation===3&&engine.getSnapshot().history.redo.length===1,'restored document and history were not in one durable cut');assert(calls.filter(call=>call.method==='POST').length===5,'traversal recovery duplicated a write');
consttransport=asyncrequest=>{calls.push(request);if(request.method==='GET')return{status:200,body:{model:model(true)}};constbody=JSON.parse(request.body);if(reject)return{status:409,body:{protocol_version:2,request_id:request.requestId,status:'not-applied',terminal:true,error:'version_conflict'}};for(const[id,version]ofObject.entries(body.expected_versions))assert(version===(versions.get(id)??0),'mixed history lost its version fence');for(constoperationofbody.operations)document=applyChanges(document,operation.changes);for(constidofObject.keys(body.expected_versions))versions.set(id,(versions.get(id)??0)+1);return{status:200,body:{protocol_version:2,request_id:request.requestId,status:'applied',operation_ids:body.operations.map(operation=>operation.id),model:model(false,Object.keys(body.expected_versions))}};};
consttransport=asyncrequest=>{calls.push(request);if(request.method==='GET')return{status:200,body:{model:model(true)}};constbody=JSON.parse(request.body);if(reject)return{status:409,body:{protocol_version:2,request_id:request.requestId,status:'not-applied',terminal:true,error:'version_conflict'}};for(const[id,version]ofObject.entries(body.expected_versions))assert(version===(versions.get(id)??0),'mixed history lost its version fence');for(constoperationofbody.operations)document=applyChanges(document,operation.changes);for(constidofObject.keys(body.expected_versions))versions.set(id,(versions.get(id)??0)+1);return{status:200,body:{protocol_version:2,request_id:request.requestId,status:'applied',operation_ids:body.operations.map(operation=>operation.id),model:model(false,Object.keys(body.expected_versions))}};};
assert(engine.getSnapshot().history.undo.length===1,'mixed group split or lost its acknowledged prefix');assert(engine.getSnapshot().history.entries.values().next().value.operationIds.includes(acknowledged),'applied logical fact was replaced');assert(engine.getSnapshot().runtime.ledger.requests.get(old.id).body===old.body,'old rejected bytes changed');
assert(engine.getSnapshot().history.undo.length===1,'mixed group split or lost its acknowledged prefix');assert(engine.getSnapshot().history.entries.values().next().value.operationIds.includes(acknowledged),'applied logical fact was replaced');assert(engine.getSnapshot().runtime.ledger.requests.get(old.id).body===old.body,'old rejected bytes changed');
assert(engine.editor.history.undo().status==='applied','mixed structural undo failed after reopening');awaitengine.settle();assert(!document.elements.has('entry')&&!document.canvases.has(child),'mixed undo left its acknowledged entry');assert(document.elements.get('anchor').data.title==='remote','mixed undo erased remote data');
assert(engine.editor.history.undo().status==='applied','mixed structural undo failed after reopening');awaitengine.settle();assert(!document.elements.has('entry')&&!document.canvases.has(child),'mixed undo left its acknowledged entry');assert(document.elements.get('anchor').data.title==='remote','mixed undo erased remote data');
assert(engine.editor.history.redo().status==='applied','mixed structural redo failed');awaitengine.settle();assert(document.elements.get('entry').incarnation===2&&document.elements.get('member').incarnation===2,'mixed redo reused deleted identities');awaitengine.close();engine=awaitopenProjectEngine(options);assert(engine.getSnapshot().document.elements.get('member').incarnation===2,'mixed history restoration did not survive another reopen');assert(calls.filter(call=>call.method==='POST').length===5,'mixed recovery duplicated a write');
assert(engine.editor.history.redo().status==='applied','mixed structural redo failed');awaitengine.settle();assert(document.elements.get('entry').incarnation===2&&document.elements.get('member').incarnation===2,'mixed redo reused deleted identities');awaitengine.close();engine=awaitopenProjectEngine(options);assert(engine.getSnapshot().document.elements.get('member').incarnation===2,'mixed history restoration did not survive another reopen');assert(calls.filter(call=>call.method==='POST').length===5,'mixed recovery duplicated a write');
consttransport=asyncrequest=>{calls.push(request);constbody=JSON.parse(request.body);for(const[id,version]ofObject.entries(body.expected_versions))assert(version===versions.get(id),'scaled move lost version ordering');for(constoperationofbody.operations)document=applyChanges(document,operation.changes);constscopes=Object.keys(body.expected_versions);for(constidofscopes)versions.set(id,versions.get(id)+1);return{status:200,body:{protocol_version:2,request_id:request.requestId,status:'applied',operation_ids:body.operations.map(operation=>operation.id),model:{documentFormatVersion:1,projectId:root,complete:false,removedCanvasIds:[],canvases:[...document.canvases.values()].filter(canvas=>scopes.includes(canvas.id)),elements:[...document.elements.values()].filter(element=>scopes.includes(element.ownerCanvasId)),tombstones:{},versions:Object.fromEntries(scopes.map(id=>[id,versions.get(id)]))}}};};
consttransport=asyncrequest=>{calls.push(request);constbody=JSON.parse(request.body);for(const[id,version]ofObject.entries(body.expected_versions))assert(version===versions.get(id),'scaled move lost version ordering');for(constoperationofbody.operations)document=applyChanges(document,operation.changes);constscopes=Object.keys(body.expected_versions);for(constidofscopes)versions.set(id,versions.get(id)+1);return{status:200,body:{protocol_version:2,request_id:request.requestId,status:'applied',operation_ids:body.operations.map(operation=>operation.id),model:{documentFormatVersion:1,projectId:root,complete:false,removedCanvasIds:[],canvases:[...document.canvases.values()].filter(canvas=>scopes.includes(canvas.id)),elements:[...document.elements.values()].filter(element=>scopes.includes(element.ownerCanvasId)),tombstones:{},versions:Object.fromEntries(scopes.map(id=>[id,versions.get(id)]))}}};};
constcheck=()=>{for(constidof[child,deep]){consta=canvasTransform(engine.getSnapshot().document,id),b=canvasTransform(original,id);for(constkeyof['x','y','rotation','scale'])assert(Math.abs(a[key]-b[key])<1e-7,'descendant transform changed: '+key);}assert(engine.getSnapshot().document.elements.get('a').data.fontSize===17,'subtree typography data was rewritten');};
constcheck=()=>{for(constidof[child,deep]){consta=canvasTransform(engine.getSnapshot().document,id),b=canvasTransform(original,id);for(constkeyof['x','y','rotation','scale'])assert(Math.abs(a[key]-b[key])<1e-7,'descendant transform changed: '+key);}assert(engine.getSnapshot().document.elements.get('a').data.fontSize===17,'subtree typography data was rewritten');};
try{engine=awaitopenProjectEngine(options);assert(engine.editor.dispatch({type:'element.move',target:{id:'f',incarnation:1},canvasId:target}).status==='applied','scaled container move failed');awaitengine.settle();check();assert(Math.abs(engine.getSnapshot().document.elements.get('f').data.presentation.unitScale-.4)<1e-9,'entry unit ratio was not stored');awaitengine.close();engine=awaitopenProjectEngine(options);check();assert(engine.editor.history.undo().status==='applied','unit-normalized undo failed');awaitengine.settle();check();assert(engine.getSnapshot().document.elements.get('f').data.presentation.unitScale===undefined,'undo did not restore absent units');assert(engine.editor.history.redo().status==='applied','unit-normalized redo failed');awaitengine.settle();check();assert(calls.filter(call=>call.method==='POST').length===3,'unit conversion scheduled duplicate writes');awaitengine.close();engine=awaitopenProjectEngine(options);check();
try{engine=awaitopenProjectEngine(options);assert(engine.editor.dispatch({type:'element.move',target:{id:'f',incarnation:1},canvasId:target}).status==='applied','scaled container move failed');awaitengine.settle();check();assert(Math.abs(engine.getSnapshot().document.elements.get('f').data.presentation.unitScale-.4)<1e-9,'entry unit ratio was not stored');awaitengine.close();engine=awaitopenProjectEngine(options);check();assert(engine.editor.history.undo().status==='applied','unit-normalized undo failed');awaitengine.settle();check();assert(engine.getSnapshot().document.elements.get('f').data.presentation.unitScale===undefined,'undo did not restore absent units');assert(engine.editor.history.redo().status==='applied','unit-normalized redo failed');awaitengine.settle();check();assert(calls.filter(call=>call.method==='POST').length===3,'unit conversion scheduled duplicate writes');awaitengine.close();engine=awaitopenProjectEngine(options);check();
consttransport=asyncrequest=>{calls.push(request);if(request.method==='GET')return{status:200,body:{model:model(true)}};constbody=JSON.parse(request.body);if(reject){reject=false;versions.set(root,2);return{status:409,body:{protocol_version:2,request_id:request.requestId,status:'not-applied',terminal:true,error:'version_conflict'}};}for(const[id,version]ofObject.entries(body.expected_versions))assert(version===(versions.get(id)??0),'structural request version mismatch');for(constoperationofbody.operations)document=applyChanges(document,operation.changes);for(constidofObject.keys(body.expected_versions))versions.set(id,(versions.get(id)??0)+1);return{status:200,body:{protocol_version:2,request_id:request.requestId,status:'applied',operation_ids:body.operations.map(operation=>operation.id),model:model(false,Object.keys(body.expected_versions))}};};
consttransport=asyncrequest=>{calls.push(request);if(request.method==='GET')return{status:200,body:{model:model(true)}};constbody=JSON.parse(request.body);if(reject){reject=false;versions.set(root,2);return{status:409,body:{protocol_version:2,request_id:request.requestId,status:'not-applied',terminal:true,error:'version_conflict'}};}for(const[id,version]ofObject.entries(body.expected_versions))assert(version===(versions.get(id)??0),'structural request version mismatch');for(constoperationofbody.operations)document=applyChanges(document,operation.changes);for(constidofObject.keys(body.expected_versions))versions.set(id,(versions.get(id)??0)+1);return{status:200,body:{protocol_version:2,request_id:request.requestId,status:'applied',operation_ids:body.operations.map(operation=>operation.id),model:model(false,Object.keys(body.expected_versions))}};};
engine=awaitopenProjectEngine(options);assert(engine.getSnapshot().document.elements.get('member').ownerCanvasId===child,'structural replan did not recover its owner tree');assert(engine.getSnapshot().runtime.ledger.requests.get(old.id).body===old.body,'old structural request was changed');assert(engine.editor.history.undo().status==='applied','structural undo failed');awaitengine.settle();assert(!document.canvases.has(child),'undo left an orphan canvas');assert(engine.editor.history.redo().status==='applied','structural redo failed');awaitengine.settle();assert(document.elements.get('member').incarnation===2,'redo reused a deleted lifecycle');awaitengine.close();engine=awaitopenProjectEngine(options);assert(engine.getSnapshot().document.elements.get('member').incarnation===2,'restored tree/history lifecycle did not survive reopening');
engine=awaitopenProjectEngine(options);assert(engine.getSnapshot().document.elements.get('member').ownerCanvasId===child,'structural replan did not recover its owner tree');assert(engine.getSnapshot().runtime.ledger.requests.get(old.id).body===old.body,'old structural request was changed');assert(engine.editor.history.undo().status==='applied','structural undo failed');awaitengine.settle();assert(!document.canvases.has(child),'undo left an orphan canvas');assert(engine.editor.history.redo().status==='applied','structural redo failed');awaitengine.settle();assert(document.elements.get('member').incarnation===2,'redo reused a deleted lifecycle');awaitengine.close();engine=awaitopenProjectEngine(options);assert(engine.getSnapshot().document.elements.get('member').incarnation===2,'restored tree/history lifecycle did not survive reopening');
constbody=JSON.parse(request.body);assert(body.expected_versions[root]===version,'write did not use reviewed version');for(constoperationofbody.operations)server=applyChanges(server,operation.changes);version++;
constbody=JSON.parse(request.body);assert(body.expected_versions[root]===version,'write did not use reviewed version');for(constoperationofbody.operations)server=applyChanges(server,operation.changes);version++;
constrequest=[...engine.getSnapshot().runtime.ledger.requests.values()][0];assert(request.outcome==='applied'&&request.phase==='waiting','failed coordination lost the successful receipt');
constrequest=[...engine.getSnapshot().runtime.ledger.requests.values()][0];assert(request.outcome==='applied'&&request.phase==='waiting','failed coordination lost the successful receipt');
engine=awaitopenProjectEngine(options);constsession=awaitengine.prepareRecovery();assert(session.acknowledgedRequestIds.includes(request.id),'saved applied receipt was not recovered');assert(session.pendingOperationIds.length===1&&session.pendingOperationIds[0]===edit.operationIds[0],'already executed operation was offered for local re-execution');
engine=awaitopenProjectEngine(options);constsession=awaitengine.prepareRecovery();assert(session.acknowledgedRequestIds.includes(request.id),'saved applied receipt was not recovered');assert(session.pendingOperationIds.length===1&&session.pendingOperationIds[0]===edit.operationIds[0],'already executed operation was offered for local re-execution');
// 只替代待处理几何,修复解释器后一次解决成功回执与重规划;两次 POST 分别是原写和新替代写。
constplan=session.plan(newMap([[edit.operationIds[0],'keep-local']]));broken=false;awaitsession.resolve(plan);awaitengine.settle();assert(calls.filter(call=>call.method==='POST').length===2,'successful original operation was sent again');
constplan=session.plan(newMap([[edit.operationIds[0],'keep-local']]));broken=false;awaitsession.resolve(plan);awaitengine.settle();assert(calls.filter(call=>call.method==='POST').length===2,'successful original operation was sent again');
engine=awaitopenProjectEngine(options);assert(engine.getSnapshot().runtime.ledger.requests.get(request.id).phase==='resolved','acknowledgement and rebased document did not recover together');assert(engine.getSnapshot().document.elements.get('a').geometry.width===400,'old receipt rewound newer geometry');
engine=awaitopenProjectEngine(options);assert(engine.getSnapshot().runtime.ledger.requests.get(request.id).phase==='resolved','acknowledgement and rebased document did not recover together');assert(engine.getSnapshot().document.elements.get('a').geometry.width===400,'old receipt rewound newer geometry');
// 重开后 width 保留远程 400,undo 本地几何只回到审阅的 x=500。
assert(engine.editor.history.undo().status==='applied','rebased geometry history did not recover');awaitengine.settle();assert(engine.getSnapshot().document.elements.get('a').geometry.x===500,'undo did not return to the reviewed geometry');
assert(engine.editor.history.undo().status==='applied','rebased geometry history did not recover');awaitengine.settle();assert(engine.getSnapshot().document.elements.get('a').geometry.x===500,'undo did not return to the reviewed geometry');
// 恢复审计、旧 not-applied 和两份 body 都保留;总 POST 只应两次,undo 才另发回到 remote 的修改。
constrestored=engine.getSnapshot();assert(restored.runtime.recoveries.has(plan.id),'recovery audit disappeared');assert(restored.runtime.ledger.requests.get(old.id).body===old.body&&restored.runtime.ledger.requests.get(old.id).outcome==='not-applied','old rejected request was rewritten');
constrestored=engine.getSnapshot();assert(restored.runtime.recoveries.has(plan.id),'recovery audit disappeared');assert(restored.runtime.ledger.requests.get(old.id).body===old.body&&restored.runtime.ledger.requests.get(old.id).outcome==='not-applied','old rejected request was rewritten');
assert(restored.runtime.ledger.requests.get(replacement.id).body===replacement.body,'replacement identity changed after reopening');assert(calls.filter(call=>call.method==='POST').length===2,'recovery repeated a server write');
assert(restored.runtime.ledger.requests.get(replacement.id).body===replacement.body,'replacement identity changed after reopening');assert(calls.filter(call=>call.method==='POST').length===2,'recovery repeated a server write');
assert(engine.editor.history.undo().status==='applied','replanned history did not restore');awaitengine.settle();assert(engine.getSnapshot().document.elements.get('a').data.title==='remote','undo did not restore the reviewed server value');
assert(engine.editor.history.undo().status==='applied','replanned history did not restore');awaitengine.settle();assert(engine.getSnapshot().document.elements.get('a').data.title==='remote','undo did not restore the reviewed server value');
@@ -740,15 +865,20 @@ async function scopedAckRecoveryCase(name,assert,structural=false){
constrejects=async(callback,message)=>{try{awaitcallback();}catch(error){assert(error.message===message,`expected ${message}, received ${error.message}`);return;}thrownewError(`expected ${message}`);};
constrejects=async(callback,message)=>{try{awaitcallback();}catch(error){assert(error.message===message,`expected ${message}, received ${error.message}`);return;}thrownewError(`expected ${message}`);};
awaitdepotAdoptionRaceCase(assert);results.push('real IndexedDB GC and a gated older Root cannot consume or release bytes belonging to a newer adoption cut');
awaitdepotAdoptionRaceCase(assert);results.push('real IndexedDB GC and a gated older Root cannot consume or release bytes belonging to a newer adoption cut');
awaitdepotFilesCase(assert);results.push('stored uploads use shared-byte file pointers with original MIME, atomic promotion rollback, logical inventory and independent identity eviction/rehydration');
awaitdepotFilesCase(assert);results.push('stored uploads use shared-byte file pointers with original MIME, atomic promotion rollback, logical inventory and independent identity eviction/rehydration');
awaitblockedUpgradeCase(assert);results.push('blocked upgrades abort abandoned opens without upgrading after failure or leaking a connection');
awaitblockedUpgradeCase(assert);results.push('blocked upgrades abort abandoned opens without upgrading after failure or leaking a connection');
@@ -756,6 +886,7 @@ export async function run() {
awaitdepotFilesUpgradeCase(assert);results.push('DB v3 original files/checkpoints upgrade to v5 and lazily promote through an idempotent Root cut');
awaitdepotFilesUpgradeCase(assert);results.push('DB v3 original files/checkpoints upgrade to v5 and lazily promote through an idempotent Root cut');
awaitassetAdoptionStorageCase(assert);results.push('cross-project adoption uses fresh authorization, survives workspace/IndexedDB reopening and undo/redo, and preserves closed destination pins without project-local byte copies');
awaitassetAdoptionStorageCase(assert);results.push('cross-project adoption uses fresh authorization, survives workspace/IndexedDB reopening and undo/redo, and preserves closed destination pins without project-local byte copies');
awaitassetDepotStorageCase(assert);results.push('workspace depot atomically shares digest bytes, preserves closed-project pins, blocks unknown/stale coverage and rolls back failed Root cuts');
awaitassetDepotStorageCase(assert);results.push('workspace depot atomically shares digest bytes, preserves closed-project pins, blocks unknown/stale coverage and rolls back failed Root cuts');
results.push('project engine reopens real IndexedDB with one history unit and the original request; ACK resolves without duplicate edit');
results.push('project engine reopens real IndexedDB with one history unit and the original request; ACK resolves without duplicate edit');
awaitatomicResourceDeletionCase(name,assert);
awaitatomicResourceDeletionCase(name,assert);
@@ -833,6 +969,7 @@ export async function run() {
results.push('v12 scoped images save Blob/history/upload together, recover a lost ACK through IndexedDB without reupload, preserve undo/redo and survive old-source completion');
results.push('v12 scoped images save Blob/history/upload together, recover a lost ACK through IndexedDB without reupload, preserve undo/redo and survive old-source completion');
awaitloadingImportRecoveryCase(name,assert);
awaitloadingImportRecoveryCase(name,assert);
results.push('v11 partial import persists its source Blob through IndexedDB pause/reopen, preserves mapped IDs and undo while unrelated model records remain unloaded');
results.push('v11 partial import persists its source Blob through IndexedDB pause/reopen, preserves mapped IDs and undo while unrelated model records remain unloaded');
results.push('scoped loader shares one real streamed fetch, exposes the first chunk before tail release, keeps the other subscriber alive after cancellation and preserves partial data after a malformed tail');
results.push('scoped loader shares one real streamed fetch, exposes the first chunk before tail release, keeps the other subscriber alive after cancellation and preserves partial data after a malformed tail');
awaitdefaultImageSizeCase(name,assert);
awaitdefaultImageSizeCase(name,assert);
@@ -845,6 +982,7 @@ export async function run() {
results.push('owned immutable recovery images survive IndexedDB checksums; mutable images and file descriptors are captured before asynchronous work');
results.push('owned immutable recovery images survive IndexedDB checksums; mutable images and file descriptors are captured before asynchronous work');
awaitcooperativeCopyCase(assert);
awaitcooperativeCopyCase(assert);
results.push('cooperative duplicate planning yields to actual browser tasks and cancellation without mutating the source');
results.push('cooperative duplicate planning yields to actual browser tasks and cancellation without mutating the source');
results.push('source SHA256 detects same-length Blob tampering before recovery adds or sends a tail; exact bytes resume with the original prefix and IDs');
results.push('source SHA256 detects same-length Blob tampering before recovery adds or sends a tail; exact bytes resume with the original prefix and IDs');
awaitdeltaJournalCase(name,assert);
awaitdeltaJournalCase(name,assert);
@@ -867,6 +1005,7 @@ export async function run() {
results.push('protocol 2 service adapter recovers exact frozen bytes and reconciles a lost ACK through real IndexedDB');
results.push('protocol 2 service adapter recovers exact frozen bytes and reconciles a lost ACK through real IndexedDB');
awaitrunRecoveryCase(name,assert);
awaitrunRecoveryCase(name,assert);
results.push('AI admission and RunRecord recover through real IndexedDB; cursor and generated content share a durable cut without a second invocation');
results.push('AI admission and RunRecord recover through real IndexedDB; cursor and generated content share a durable cut without a second invocation');
// 最后一组检查规范刷新、冲突替代、已成功回执及复杂历史证据的真实 IndexedDB 重开。
awaitcanonicalRefreshCase(name,assert);
awaitcanonicalRefreshCase(name,assert);
results.push('quiescent canonical refresh preserves original requests and history; the new base/document/version cut survives IndexedDB reopening');
results.push('quiescent canonical refresh preserves original requests and history; the new base/document/version cut survives IndexedDB reopening');
awaitconflictReplanCase(name,assert);
awaitconflictReplanCase(name,assert);
@@ -893,6 +1032,7 @@ export async function run() {
results.push('adoption anchor and event evidence survive IndexedDB reopening and compose mixed geometry/content choices exactly without extra wire writes');
results.push('adoption anchor and event evidence survive IndexedDB reopening and compose mixed geometry/content choices exactly without extra wire writes');
awaitadoptionConsumerCase(name,assert);
awaitadoptionConsumerCase(name,assert);
results.push('adoption evidence is consumed across repeated recovery/reopening, preserving old requests, latest reviewed field baselines and structural lifecycles');
results.push('adoption evidence is consumed across repeated recovery/reopening, preserving old requests, latest reviewed field baselines and structural lifecycles');
test('a stalled CDP call rejects all pending work and cannot hide behind another unbounded report read',async()=>{constf=fixture(),one=assert.rejects(f.call('Runtime.evaluate'),/timed out/),two=assert.rejects(f.call('Page.captureScreenshot'),/timed out/);[...f.timers.values()][0]();awaitPromise.all([one,two]);assert.equal(f.timers.size,0);f.event('message',{id:1,result:{late:true}});awaitassert.rejects(f.call('Runtime.evaluate'),/timed out/);});
test('a stalled CDP call rejects all pending work and cannot hide behind another unbounded report read',async()=>{constf=fixture(),one=assert.rejects(f.call('Runtime.evaluate'),/timed out/),two=assert.rejects(f.call('Page.captureScreenshot'),/timed out/);[...f.timers.values()][0]();awaitPromise.all([one,two]);assert.equal(f.timers.size,0);f.event('message',{id:1,result:{late:true}});awaitassert.rejects(f.call('Runtime.evaluate'),/timed out/);});
test('connection loss and fatal target events reject pending calls without accepting dialogs',async()=>{for(consteventof['close','error','Inspector.targetCrashed','Target.targetCrashed','Inspector.detached','Page.javascriptDialogOpening']){constf=fixture(),result=assert.rejects(f.call('Runtime.evaluate'),/CDP/);event.includes('.')?f.event('message',{method:event}):f.event(event);awaitresult;assert.equal(f.timers.size,0);assert.deepEqual(f.sent.map(row=>row.method),['Runtime.evaluate']);}});
test('connection loss and fatal target events reject pending calls without accepting dialogs',async()=>{for(consteventof['close','error','Inspector.targetCrashed','Target.targetCrashed','Inspector.detached','Page.javascriptDialogOpening']){constf=fixture(),result=assert.rejects(f.call('Runtime.evaluate'),/CDP/);event.includes('.')?f.event('message',{method:event}):f.event(event);awaitresult;assert.equal(f.timers.size,0);assert.deepEqual(f.sent.map(row=>row.method),['Runtime.evaluate']);}});
// 普通命令错误只拒绝它自己,另一命令仍可成功且两者都清除定时器。
test('one protocol command error does not corrupt another request or leak its timer',async()=>{constf=fixture(),one=assert.rejects(f.call('bad'),/invalid/),two=f.call('good');f.event('message',{id:1,error:{message:'invalid'}});f.event('message',{id:2,result:{ok:true}});awaitone;assert.deepEqual(awaittwo,{ok:true});assert.equal(f.timers.size,0);});
test('one protocol command error does not corrupt another request or leak its timer',async()=>{constf=fixture(),one=assert.rejects(f.call('bad'),/invalid/),two=f.call('good');f.event('message',{id:1,error:{message:'invalid'}});f.event('message',{id:2,result:{ok:true}});awaitone;assert.deepEqual(awaittwo,{ok:true});assert.equal(f.timers.size,0);});
test('pointer move/up/cancel only match their owner and old lost-capture cannot cancel a replacement target',()=>{constcapture={},other={},owner={pointerId:7,capture};for(consttypeof['pointermove','pointerup','pointercancel']){assert.equal(ownsPointerCapture(owner,{pointerId:8,type}),false);assert.equal(ownsPointerCapture(owner,{pointerId:7,type}),true);}assert.equal(ownsPointerCapture(owner,{pointerId:7,type:'lostpointercapture',target:other}),false);assert.equal(ownsPointerCapture(owner,{pointerId:7,type:'lostpointercapture',target:capture}),true);assert.equal(ownsPointerCapture(null,{pointerId:7}),false);constrecaptured={hasPointerCapture:()=>true};assert.equal(ownsPointerCapture({pointerId:7,capture:recaptured},{pointerId:7,type:'lostpointercapture',target:recaptured}),false);});
test('pointer move/up/cancel only match their owner and old lost-capture cannot cancel a replacement target',()=>{constcapture={},other={},owner={pointerId:7,capture};for(consttypeof['pointermove','pointerup','pointercancel']){assert.equal(ownsPointerCapture(owner,{pointerId:8,type}),false);assert.equal(ownsPointerCapture(owner,{pointerId:7,type}),true);}assert.equal(ownsPointerCapture(owner,{pointerId:7,type:'lostpointercapture',target:other}),false);assert.equal(ownsPointerCapture(owner,{pointerId:7,type:'lostpointercapture',target:capture}),true);assert.equal(ownsPointerCapture(null,{pointerId:7}),false);constrecaptured={hasPointerCapture:()=>true};assert.equal(ownsPointerCapture({pointerId:7,capture:recaptured},{pointerId:7,type:'lostpointercapture',target:recaptured}),false);});
test('failed capture and detached release stay transport failures, while a held capture releases once',()=>{letheld=false,releases=0;consttarget={setPointerCapture(){held=true;},hasPointerCapture(){returnheld;},releasePointerCapture(){held=false;releases++;}};assert.equal(acquirePointerCapture(target,1),true);releasePointerCapture(target,1);releasePointerCapture(target,1);assert.equal(releases,1);assert.equal(acquirePointerCapture({...target,setPointerCapture(){throwError('detached');}},2),false);assert.doesNotThrow(()=>releasePointerCapture({...target,hasPointerCapture(){throwError('detached');}},2));});
test('failed capture and detached release stay transport failures, while a held capture releases once',()=>{letheld=false,releases=0;consttarget={setPointerCapture(){held=true;},hasPointerCapture(){returnheld;},releasePointerCapture(){held=false;releases++;}};assert.equal(acquirePointerCapture(target,1),true);releasePointerCapture(target,1);releasePointerCapture(target,1);assert.equal(releases,1);assert.equal(acquirePointerCapture({...target,setPointerCapture(){throwError('detached');}},2),false);assert.doesNotThrow(()=>releasePointerCapture({...target,hasPointerCapture(){throwError('detached');}},2));});
constGesture=memo(functionGesture({targetId='a',beginOnLayout=false}:{targetId?:string;beginOnLayout?:boolean}){constbinding=useGestureBindings({id:targetId,incarnation:1});gesture=binding;counts.gesture++;useLayoutEffect(()=>{if(beginOnLayout){assert(binding.begin().enabled,'replacement gesture did not begin');assert(binding.update({x:88}).enabled,'replacement gesture did not preview');}},[targetId,beginOnLayout]);returnnull;});
constGesture=memo(functionGesture({targetId='a',beginOnLayout=false}:{targetId?:string;beginOnLayout?:boolean}){constbinding=useGestureBindings({id:targetId,incarnation:1});gesture=binding;counts.gesture++;useLayoutEffect(()=>{if(beginOnLayout){assert(binding.begin().enabled,'replacement gesture did not begin');assert(binding.update({x:88}).enabled,'replacement gesture did not preview');}},[targetId,beginOnLayout]);returnnull;});
// 按逻辑操作 ID 观察操作及其请求;最初 missing 应没有记录,rename 后改绑真实 ID。
engine.editor.dispatch({type:'element.data',target:{id:'b',incarnation:1},patch:{title:'unrelated'}});awaitengine.settle();awaittick();assert(equal(baseline,counts),'unrelated element/ACK rerendered narrow hooks');assert(actions===originalActions,'action facade identity drifted');assert(titleSelections===initialTitleSelections,'unrelated edit/ACK evaluated the element selector');
engine.editor.dispatch({type:'element.data',target:{id:'b',incarnation:1},patch:{title:'unrelated'}});awaitengine.settle();awaittick();assert(equal(baseline,counts),'unrelated element/ACK rerendered narrow hooks');assert(actions===originalActions,'action facade identity drifted');assert(titleSelections===initialTitleSelections,'unrelated edit/ACK evaluated the element selector');
// 改 a 的 text 会执行其标题选择器,但标题没变时不得重渲染 ElementTitle。
consttitleRenders=counts.titleOnly;engine.editor.dispatch({type:'element.data',target:{id:'a',incarnation:1},patch:{text:'other field'}});awaitengine.settle();awaittick();assert(titleSelections>initialTitleSelections&&counts.titleOnly===titleRenders&&titleOnly==='a','field selector did not isolate title from another field on the same target');reports.push('element ID routing skips unrelated selector evaluation; field selectors observe their target without rerendering for other fields');
consttitleRenders=counts.titleOnly;engine.editor.dispatch({type:'element.data',target:{id:'a',incarnation:1},patch:{text:'other field'}});awaitengine.settle();awaittick();assert(titleSelections>initialTitleSelections&&counts.titleOnly===titleRenders&&titleOnly==='a','field selector did not isolate title from another field on the same target');reports.push('element ID routing skips unrelated selector evaluation; field selectors observe their target without rerendering for other fields');
constrenamed=actions.rename('renamed',{label:'Hook rename',groupId:'hook-rename'});awaitengine.settle();constrenamedHistory=engine.getSnapshot().history.entries.get(engine.getSnapshot().history.undo.at(-1)!);assert(renamedHistory?.label==='Hook rename'&&renamedHistory.groupId==='hook-rename','action facade dropped command history options');awaituntil(()=>title.title==='renamed'&&titleOnly==='renamed');assert(element.status==='present'&&element.element.data.title==='renamed','element did not observe canonical rename');awaitrender(<><Title/><Element/><ElementTitle/><Actions/><Mode/><Gesture/><Operationid={renamed.operationIds[0]}/><Status/></>);assert(operation?.operation.phase==='resolved'&&operation.request?.phase==='resolved','operation hook did not include its request');reports.push('selectors suppress unrelated element and ACK renders; bound actions share validated history/ledger and operation/request observation');
constrenamed=actions.rename('renamed',{label:'Hook rename',groupId:'hook-rename'});awaitengine.settle();constrenamedHistory=engine.getSnapshot().history.entries.get(engine.getSnapshot().history.undo.at(-1)!);assert(renamedHistory?.label==='Hook rename'&&renamedHistory.groupId==='hook-rename','action facade dropped command history options');awaituntil(()=>title.title==='renamed'&&titleOnly==='renamed');assert(element.status==='present'&&element.element.data.title==='renamed','element did not observe canonical rename');awaitrender(<><Title/><Element/><ElementTitle/><Actions/><Mode/><Gesture/><Operationid={renamed.operationIds[0]}/><Status/></>);assert(operation?.operation.phase==='resolved'&&operation.request?.phase==='resolved','operation hook did not include its request');reports.push('selectors suppress unrelated element and ACK renders; bound actions share validated history/ledger and operation/request observation');
consthistory=engine.getSnapshot().history.undo.length,revision=engine.getSnapshot().revision;assert(gesture.begin().enabled&&gesture.update({x:42}).enabled,'gesture did not acquire/update');awaituntil(()=>gesture.active&&gesture.geometry.x===42);assert(engine.getSnapshot().document.elements.get('a')!.structure==='node'&&(engine.getSnapshot().document.elements.get('a')asany).geometry.x===0,'preview mutated committed geometry');assert(engine.getSnapshot().revision===revision,'preview created an edit');gesture.cancel();awaituntil(()=>!gesture.active);assert(engine.getSnapshot().history.undo.length===history,'canceled preview changed history');assert(mode.setMode('editing').enabled,'view mode rejected');awaituntil(()=>mode.mode==='editing');constmodeRenders=counts.mode;engine.sessions.mode('w','a','resize');awaittick();assert(counts.mode===modeRenders,'another view changed this view mode');
consthistory=engine.getSnapshot().history.undo.length,revision=engine.getSnapshot().revision;assert(gesture.begin().enabled&&gesture.update({x:42}).enabled,'gesture did not acquire/update');awaituntil(()=>gesture.active&&gesture.geometry.x===42);assert(engine.getSnapshot().document.elements.get('a')!.structure==='node'&&(engine.getSnapshot().document.elements.get('a')asany).geometry.x===0,'preview mutated committed geometry');assert(engine.getSnapshot().revision===revision,'preview created an edit');gesture.cancel();awaituntil(()=>!gesture.active);assert(engine.getSnapshot().history.undo.length===history,'canceled preview changed history');assert(mode.setMode('editing').enabled,'view mode rejected');awaituntil(()=>mode.mode==='editing');constmodeRenders=counts.mode;engine.sessions.mode('w','a','resize');awaittick();assert(counts.mode===modeRenders,'another view changed this view mode');
// 第二次手势提交只新增一个历史单元,租约释放后旧 commit 不再可写。
assert(gesture.begin().enabled,'second gesture did not acquire');gesture.update({x:55});assert(gesture.commit().status==='applied','gesture did not commit');awaitengine.settle();assert(engine.getSnapshot().history.undo.length===history+1,'gesture did not create exactly one history entry');assert(gesture.commit().status==='rejected','released gesture lease remained writable');reports.push('gesture preview/cancel/commit owns a token and one history unit; modes stay scoped to their view');
assert(gesture.begin().enabled,'second gesture did not acquire');gesture.update({x:55});assert(gesture.commit().status==='applied','gesture did not commit');awaitengine.settle();assert(engine.getSnapshot().history.undo.length===history+1,'gesture did not create exactly one history entry');assert(gesture.commit().status==='rejected','released gesture lease remained writable');reports.push('gesture preview/cancel/commit owns a token and one history unit; modes stay scoped to their view');
constpreviousGesture=gesture,beforeOwnerSwap=engine.getSnapshot();awaitrender(<><Title/><Element/><ElementTitle/><Actions/><Mode/><GesturetargetId="b"beginOnLayout/><Operationid={renamed.operationIds[0]}/><Status/></>);constreplacementToken=engine.sessions.getSnapshot().gesture?.token;assert(!!replacementToken&&engine.sessions.getSnapshot().gesture?.targets[0].id==='b','old target cleanup canceled the new layout gesture');assert(previousGesture.commit().status==='rejected','old controls committed the replacement gesture');previousGesture.cancel();assert(engine.sessions.getSnapshot().gesture?.token===replacementToken,'old controls canceled the replacement gesture');assert(engine.getSnapshot()===beforeOwnerSwap,'owner switch or obsolete callback published document/history work');gesture.cancel();awaitrender(<><Title/><Element/><ElementTitle/><Actions/><Mode/><Gesture/><Operationid={renamed.operationIds[0]}/><Status/></>);reports.push('gesture controller replacement owns an independent lease; old cleanup, cancel and commit cannot touch a new target gesture');
constpreviousGesture=gesture,beforeOwnerSwap=engine.getSnapshot();awaitrender(<><Title/><Element/><ElementTitle/><Actions/><Mode/><GesturetargetId="b"beginOnLayout/><Operationid={renamed.operationIds[0]}/><Status/></>);constreplacementToken=engine.sessions.getSnapshot().gesture?.token;assert(!!replacementToken&&engine.sessions.getSnapshot().gesture?.targets[0].id==='b','old target cleanup canceled the new layout gesture');assert(previousGesture.commit().status==='rejected','old controls committed the replacement gesture');previousGesture.cancel();assert(engine.sessions.getSnapshot().gesture?.token===replacementToken,'old controls canceled the replacement gesture');assert(engine.getSnapshot()===beforeOwnerSwap,'owner switch or obsolete callback published document/history work');gesture.cancel();awaitrender(<><Title/><Element/><ElementTitle/><Actions/><Mode/><Gesture/><Operationid={renamed.operationIds[0]}/><Status/></>);reports.push('gesture controller replacement owns an independent lease; old cleanup, cancel and commit cannot touch a new target gesture');
engine.editor.updateRuntime(previous=>({...previous}));awaitengine.persist();awaittick();awaituntil(()=>status.status==='synced'&&!status.dirty);assert(!status.dirty,'durable-only metadata completion was not observable');f.fail(true);actions.rename('unsaved');awaituntil(()=>status.status==='blocked'&&status.error==='quota');assert(status.dirty,'failed persistence appeared durable');f.fail(false);awaitengine.retry();awaitengine.settle();awaituntil(()=>status.status==='synced'&&!status.dirty);reports.push('sync status observes durable-only completion and quota failure/retry without claiming in-memory edits are synced');
engine.editor.updateRuntime(previous=>({...previous}));awaitengine.persist();awaittick();awaituntil(()=>status.status==='synced'&&!status.dirty);assert(!status.dirty,'durable-only metadata completion was not observable');f.fail(true);actions.rename('unsaved');awaituntil(()=>status.status==='blocked'&&status.error==='quota');assert(status.dirty,'failed persistence appeared durable');f.fail(false);awaitengine.retry();awaitengine.settle();awaituntil(()=>status.status==='synced'&&!status.dirty);reports.push('sync status observes durable-only completion and quota failure/retry without claiming in-memory edits are synced');
awaitrender(<><Assetid={resource}slot="a"/><Assetid={resource}slot="b"/><Status/></>);awaituntil(()=>assetA?.status==='ready'&&assetB?.status==='ready');assert(assetA.url===assetB.url&&f.reads===reads+1,'two resource hooks did not share one display read/URL');consturl=assetA.url;assert(await(awaitfetch(url)).text()==='image bytes','asset hook did not expose original bytes');awaitrender(<><Assetid={resource}slot="a"/><Status/></>);assert(await(awaitfetch(url)).text()==='image bytes','unmounting one reader revoked the other');awaitrender(<Status/>);letrevoked=false;try{awaitfetch(url);}catch{revoked=true;}assert(revoked,'last resource reader did not release its URL');reports.push('resource hooks share a URL, retain remaining readers and release the last handle on unmount');
awaitrender(<><Assetid={resource}slot="a"/><Assetid={resource}slot="b"/><Status/></>);awaituntil(()=>assetA?.status==='ready'&&assetB?.status==='ready');assert(assetA.url===assetB.url&&f.reads===reads+1,'two resource hooks did not share one display read/URL');consturl=assetA.url;assert(await(awaitfetch(url)).text()==='image bytes','asset hook did not expose original bytes');awaitrender(<><Assetid={resource}slot="a"/><Status/></>);assert(await(awaitfetch(url)).text()==='image bytes','unmounting one reader revoked the other');awaitrender(<Status/>);letrevoked=false;try{awaitfetch(url);}catch{revoked=true;}assert(revoked,'last resource reader did not release its URL');reports.push('resource hooks share a URL, retain remaining readers and release the last handle on unmount');
// 读取尚未返回时卸载 hook,随后才放行;迟到资源结果应释放显示 pin,不能泄漏借用。
letrelease!:()=>void;f.gate(newPromise<void>(resolve=>release=resolve));constbefore=f.reads;awaitrender(<><Assetid={resource}slot="a"/><Status/></>);awaituntil(()=>f.reads>before);awaitrender(<Status/>);release();f.gate();awaittick();assert(!engine.getResourcePins().pins.get(resource)?.has('display'),'late resource load leaked a display pin');
letrelease!:()=>void;f.gate(newPromise<void>(resolve=>release=resolve));constbefore=f.reads;awaitrender(<><Assetid={resource}slot="a"/><Status/></>);awaituntil(()=>f.reads>before);awaitrender(<Status/>);release();f.gate();awaittick();assert(!engine.getResourcePins().pins.get(resource)?.has('display'),'late resource load leaked a display pin');
// 删除并撤销恢复元素后 incarnation 已变化;旧 facade 必须拒绝写入,不能自动改绑同 ID 的新对象。
awaitrender(<><Element/><Actions/><Gesture/><Status/></>);conststaleActions=actions;engine.editor.dispatch({type:'element.remove',target:{id:'a',incarnation:1}});awaitengine.settle();engine.editor.history.undo();awaitengine.settle();awaituntil(()=>element.status==='stale');constsnapshot=engine.getSnapshot();assert(staleActions.rename('stale').status==='rejected'&&engine.getSnapshot()===snapshot,'old incarnation facade edited a restored element');reports.push('late resource loads release their pins and stale action targets cannot retarget restored incarnations');
awaitrender(<><Element/><Actions/><Gesture/><Status/></>);conststaleActions=actions;engine.editor.dispatch({type:'element.remove',target:{id:'a',incarnation:1}});awaitengine.settle();engine.editor.history.undo();awaitengine.settle();awaituntil(()=>element.status==='stale');constsnapshot=engine.getSnapshot();assert(staleActions.rename('stale').status==='rejected'&&engine.getSnapshot()===snapshot,'old incarnation facade edited a restored element');reports.push('late resource loads release their pins and stale action targets cannot retarget restored incarnations');
awaitrender(<><Assetid={resource}slot="a"/><Status/></>);awaituntil(()=>assetA.status==='ready');awaitengine.close();awaituntil(()=>status.status==='closed'&&assetA.status==='error');reports.push('engine close is observable and releases mounted resource handles without owning a second project');
awaitrender(<><Assetid={resource}slot="a"/><Status/></>);awaituntil(()=>assetA.status==='ready');awaitengine.close();awaituntil(()=>status.status==='closed'&&assetA.status==='error');reports.push('engine close is observable and releases mounted resource handles without owning a second project');
constoldSnapshot=engine.getSnapshot();awaitrender(<><Assetid={otherResource}slot="a"/><Actions/><Status/></>,other.engine);awaituntil(()=>assetA.status==='ready');assert(await(awaitfetch(assetA.url)).text()==='other identity bytes','Provider change reused another engine resource handle');assert(actions.rename('second owner').status==='applied','new owner action did not dispatch');awaitother.engine.settle();assert(other.engine.getSnapshot().document.elements.get('a')!.data.title==='second owner'&&engine.getSnapshot()===oldSnapshot,'Provider change routed commands to the old owner');reports.push('Provider owner changes replace resource handles and action targets without writing to the previous engine');
constoldSnapshot=engine.getSnapshot();awaitrender(<><Assetid={otherResource}slot="a"/><Actions/><Status/></>,other.engine);awaituntil(()=>assetA.status==='ready');assert(await(awaitfetch(assetA.url)).text()==='other identity bytes','Provider change reused another engine resource handle');assert(actions.rename('second owner').status==='applied','new owner action did not dispatch');awaitother.engine.settle();assert(other.engine.getSnapshot().document.elements.get('a')!.data.title==='second owner'&&engine.getSnapshot()===oldSnapshot,'Provider change routed commands to the old owner');reports.push('Provider owner changes replace resource handles and action targets without writing to the previous engine');
}finally{awaitother.engine.close();}
}finally{awaitother.engine.close();}
// 部分来源完成前 b 和未知 ID 都是 unloaded;loading.complete 后才能分别判断 present 与 absent。
awaitrender(<><Lookup/><Status/></>,partial.engine);assert(b.status==='unloaded'&&missing.status==='unloaded','partial lookup guessed absence');constcompleted=awaitpartial.engine.loading.complete(partial.full,newMap([['canvas',1]]),{sourceToken:'hook-partial'});assert(completed.status==='applied','hook partial fixture did not complete');awaitpartial.engine.settle();awaituntil(()=>b.status==='present'&&missing.status==='absent');reports.push('element hooks preserve unloaded versus absent until source completion and then expose the newly loaded element');
awaitrender(<><Lookup/><Status/></>,partial.engine);assert(b.status==='unloaded'&&missing.status==='unloaded','partial lookup guessed absence');constcompleted=awaitpartial.engine.loading.complete(partial.full,newMap([['canvas',1]]),{sourceToken:'hook-partial'});assert(completed.status==='applied','hook partial fixture did not complete');awaitpartial.engine.settle();awaituntil(()=>b.status==='present'&&missing.status==='absent');reports.push('element hooks preserve unloaded versus absent until source completion and then expose the newly loaded element');
awaitwait(`!!document.querySelector('.react-flow__node[data-id="perf-frame"]')&&document.querySelectorAll('.react-flow__node').length>=2`,'initial large canvas did not render');
awaitwait(`!!document.querySelector('.react-flow__node[data-id="perf-frame"]')&&document.querySelectorAll('.react-flow__node').length>=2`,'initial large canvas did not render');
awaitwait(`document.activeElement===document.querySelector('.react-flow__node[data-id="perf-0"] textarea')&&!document.activeElement.readOnly`,'native text editing did not start');
awaitwait(`document.activeElement===document.querySelector('.react-flow__node[data-id="perf-0"] textarea')&&!document.activeElement.readOnly`,'native text editing did not start');
assert.equal(awaitevaluate('window.__performanceEditor.isConnected&&document.activeElement===window.__performanceEditor&&window.__performanceEditor.value.includes("Native editing remains responsive")'),true,'native input lost its DOM, focus or value');
assert.equal(awaitevaluate('window.__performanceEditor.isConnected&&document.activeElement===window.__performanceEditor&&window.__performanceEditor.value.includes("Native editing remains responsive")'),true,'native input lost its DOM, focus or value');
@@ -34,6 +48,7 @@ export async function measureRenderer({call,evaluate,documents,calls=[],root,par
assert.equal(awaitevaluate('(()=>{const r=window.__performanceEditor.getBoundingClientRect();return r.right<0||r.bottom<0||r.left>innerWidth||r.top>innerHeight;})()'),true,'IME probe did not move its owner offscreen');
assert.equal(awaitevaluate('(()=>{const r=window.__performanceEditor.getBoundingClientRect();return r.right<0||r.bottom<0||r.left>innerWidth||r.top>innerHeight;})()'),true,'IME probe did not move its owner offscreen');
@@ -41,6 +56,7 @@ export async function measureRenderer({call,evaluate,documents,calls=[],root,par
awaitwait(`[...document.querySelectorAll('[role=status]')].some(item=>item.textContent.trim()==='已同步')`,'native input did not synchronize before copy measurement');
awaitwait(`[...document.querySelectorAll('[role=status]')].some(item=>item.textContent.trim()==='已同步')`,'native input did not synchronize before copy measurement');
awaitwait(`!!document.querySelector('[aria-label="AI 创作"]')&&!document.querySelector('[aria-label="AI 创作"]').disabled`,'AI panel action did not become available');
awaitwait(`!!document.querySelector('[aria-label="AI 创作"]')&&!document.querySelector('[aria-label="AI 创作"]').disabled`,'AI panel action did not become available');
assert.equal(awaitevaluate('window.__performancePanel.isConnected&&document.activeElement===window.__performancePanel&&window.__performancePanel.value==="Stable host panel draft"'),true,'independent panel lost its focus or draft during pan');
assert.equal(awaitevaluate('window.__performancePanel.isConnected&&document.activeElement===window.__performancePanel&&window.__performancePanel.value==="Stable host panel draft"'),true,'independent panel lost its focus or draft during pan');
awaitwait(`[...document.querySelectorAll('[role=status]')].some(item=>item.textContent.trim()==='已同步')`,'AI settings did not save before copy');
awaitwait(`[...document.querySelectorAll('[role=status]')].some(item=>item.textContent.trim()==='已同步')`,'AI settings did not save before copy');
// blur 后从 fixture 服务文档核对配置已经保存,再恢复视口并关闭面板。
assert.equal(documents.get(root).elements.get('perf-0').data.ai?.prompt,'Stable host panel draft','host-panel blur lost saved settings');
assert.equal(documents.get(root).elements.get('perf-0').data.ai?.prompt,'Stable host panel draft','host-panel blur lost saved settings');
awaitevaluate(`(()=>{document.querySelector('[aria-label="关闭 AI 工具"]').click();const now=new DOMMatrix(getComputedStyle(document.querySelector('.react-flow__viewport')).transform),before=window.__editViewport;document.querySelector('.react-flow__pane').dispatchEvent(new WheelEvent('wheel',{deltaX:(now.e-before.e)/.5,deltaY:(now.f-before.f)/.5,bubbles:true,cancelable:true}));delete window.__performancePanel;delete window.__editViewport;})()`);
awaitevaluate(`(()=>{document.querySelector('[aria-label="关闭 AI 工具"]').click();const now=new DOMMatrix(getComputedStyle(document.querySelector('.react-flow__viewport')).transform),before=window.__editViewport;document.querySelector('.react-flow__pane').dispatchEvent(new WheelEvent('wheel',{deltaX:(now.e-before.e)/.5,deltaY:(now.f-before.f)/.5,bubbles:true,cancelable:true}));delete window.__performancePanel;delete window.__editViewport;})()`);
assert.equal(calls.filter(call=>call.method==='POST'&&call.path.endsWith('/ai/runs')).length,runStartsBefore,'editing settings started a generation task');
assert.equal(calls.filter(call=>call.method==='POST'&&call.path.endsWith('/ai/runs')).length,runStartsBefore,'editing settings started a generation task');
awaitwait(`!!document.querySelector('[aria-label="深复制选中内容"]')&&!document.querySelector('[aria-label="深复制选中内容"]').disabled`,'partition selection did not enable deep copy');
awaitwait(`!!document.querySelector('[aria-label="深复制选中内容"]')&&!document.querySelector('[aria-label="深复制选中内容"]').disabled`,'partition selection did not enable deep copy');
assert.equal(awaitevaluate('window.__performancePortal.isConnected&&document.activeElement===window.__performancePortal&&window.__performancePortal.value==="Retained portal title"'),true,'portal title draft was lost during offscreen pan');
assert.equal(awaitevaluate('window.__performancePortal.isConnected&&document.activeElement===window.__performancePortal&&window.__performancePortal.value==="Retained portal title"'),true,'portal title draft was lost during offscreen pan');
awaitwait(`document.querySelector('[aria-label="分区名称"]')?.value==='Retained portal title'&&[...document.querySelectorAll('[role=status]')].some(item=>item.textContent.trim()==='已同步')`,'portal title did not save and remount');
awaitwait(`document.querySelector('[aria-label="分区名称"]')?.value==='Retained portal title'&&[...document.querySelectorAll('[role=status]')].some(item=>item.textContent.trim()==='已同步')`,'portal title did not save and remount');
if(concurrentInput){duringCopyInput=awaiteditDuringCopy({call,evaluate,wait,count});assert.equal(duringCopyInput.value,sourceText+(count>=10000?'复制期间输入 A B C M T':'复制期间输入 A B C'),'concurrent typing lost pre-existing source text');console.log('PERF concurrent input '+JSON.stringify(duringCopyInput));}
if(concurrentInput){duringCopyInput=awaiteditDuringCopy({call,evaluate,wait,count});assert.equal(duringCopyInput.value,sourceText+(count>=10000?'复制期间输入 A B C M T':'复制期间输入 A B C'),'concurrent typing lost pre-existing source text');console.log('PERF concurrent input '+JSON.stringify(duringCopyInput));}
awaitwait(`document.querySelectorAll('.react-flow__node').length>0&&document.querySelector('[aria-label="画布导入"]')?.textContent.includes('已加入画布')&&[...document.querySelectorAll('[role=status]')].some(item=>item.textContent.trim()==='已同步')`,'large copy did not finish rendering and synchronization');
awaitwait(`document.querySelectorAll('.react-flow__node').length>0&&document.querySelector('[aria-label="画布导入"]')?.textContent.includes('已加入画布')&&[...document.querySelectorAll('[role=status]')].some(item=>item.textContent.trim()==='已同步')`,'large copy did not finish rendering and synchronization');
constinternalsBatches=awaitevaluate('window.__fixtureInternalsBatches??[]');assert.ok(internalsBatches.some(size=>size>1),'mounted nodes did not share a renderer measurement batch');
constinternalsBatches=awaitevaluate('window.__fixtureInternalsBatches??[]');assert.ok(internalsBatches.some(size=>size>1),'mounted nodes did not share a renderer measurement batch');
// 停止页面采样并验证服务元素数、复制 canvas 和上级连线。firstPresentedFrame 是检测新 DOM 后的下一次 rAF 近似值,不是 compositor 上屏证明。
assert.equal(document.elements.size,2*count+5);assert.ok(copiedCanvas);assert.ok([...document.elements.values()].some(element=>element.structure==='connection'&&element.source.elementId==='perf-source'&&element.target.elementId===copiedCanvas.parent.entryElementId));assert.ok(document.elements.has('perf-incoming'));assert.ok(measurements.firstPresentedFrameMs!==null&&measurements.localCompleteFrameMs!==null);if(count>=100)assert.ok(measurements.nodeCount<target,'offscreen DOM was not culled');assert.ok(awaitevaluate(`document.querySelector('[aria-label="画布导入"]').textContent.includes('${count+3} 项')`),'copy progress did not include all source records');
assert.equal(document.elements.size,2*count+5);assert.ok(copiedCanvas);assert.ok([...document.elements.values()].some(element=>element.structure==='connection'&&element.source.elementId==='perf-source'&&element.target.elementId===copiedCanvas.parent.entryElementId));assert.ok(document.elements.has('perf-incoming'));assert.ok(measurements.firstPresentedFrameMs!==null&&measurements.localCompleteFrameMs!==null);if(count>=100)assert.ok(measurements.nodeCount<target,'offscreen DOM was not culled');assert.ok(awaitevaluate(`document.querySelector('[aria-label="画布导入"]').textContent.includes('${count+3} 项')`),'copy progress did not include all source records');
if(concurrentInput){
if(concurrentInput){
@@ -105,17 +133,26 @@ export async function measureRenderer({call,evaluate,documents,calls=[],root,par
assert.equal(copiedFirst?.data.text,sourceText,'copy snapshot absorbed a later original edit');
assert.equal(copiedFirst?.data.text,sourceText,'copy snapshot absorbed a later original edit');
assert.equal(document.elements.get('perf-0').data.text,duringCopyInput.value,'ACK overwrote concurrent original text');
assert.equal(document.elements.get('perf-0').data.text,duringCopyInput.value,'ACK overwrote concurrent original text');
assert.equal(runStartsDuringScenario,0,'deep copy or settings unexpectedly started generation');
assert.equal(runStartsDuringScenario,0,'deep copy or settings unexpectedly started generation');
// 离屏源仍需保留两条可见分区连线,随后把最后一个副本节点平移到屏幕内,检查实际挂载。
constvisibleEdges=awaitevaluate('document.querySelectorAll(".react-flow__edge").length');assert.equal(visibleEdges,2,'frame connections did not render for the offscreen source');
constvisibleEdges=awaitevaluate('document.querySelectorAll(".react-flow__edge").length');assert.equal(visibleEdges,2,'frame connections did not render for the offscreen source');
// Completion is a model/progress check. Verify an offscreen copied card actually mounts after panning.
awaitwait(`!!document.querySelector('.react-flow__node[data-id="${copiedLast.id}"] textarea')`,'offscreen copied card did not mount after viewport pan');
awaitwait(`!!document.querySelector('.react-flow__node[data-id="${copiedLast.id}"] textarea')`,'offscreen copied card did not mount after viewport pan');
report={inputCheck,duringCopyInput,runStartsDuringScenario,baseline,heapSamples,browserLogs:browserLogs(),scenario:concurrentInput?'actual GraphPage deep copy with concurrent native input':'actual GraphPage deep copy of expanded partition',mode:`Next ${buildMode}${bundler}; isolated fixture HTTP; real Chrome/IndexedDB/Web Locks`,count,initialReadyMs,buildMs,internals:{batches:internalsBatches.length,maxBatch:Math.max(0,...internalsBatches),totalRequests:internalsBatches.reduce((sum,size)=>sum+size,0)},measurements,viewportCheck,browser:awaitcall('Browser.getVersion'),serverElements:document.elements.size,serverCanvases:document.canvases.size,wallMs:Date.now()-started};
report={inputCheck,duringCopyInput,runStartsDuringScenario,baseline,heapSamples,browserLogs:browserLogs(),scenario:concurrentInput?'actual GraphPage deep copy with concurrent native input':'actual GraphPage deep copy of expanded partition',mode:`Next ${buildMode}${bundler}; isolated fixture HTTP; real Chrome/IndexedDB/Web Locks`,count,initialReadyMs,buildMs,internals:{batches:internalsBatches.length,maxBatch:Math.max(0,...internalsBatches),totalRequests:internalsBatches.reduce((sum,size)=>sum+size,0)},measurements,viewportCheck,browser:awaitcall('Browser.getVersion'),serverElements:document.elements.size,serverCanvases:document.canvases.size,wallMs:Date.now()-started};
}catch(error){letmeasurements;try{measurements=awaitevaluate('window.__canvasPerformance?.stop()');}catch{}report={duringCopyInput,baseline,heapSamples,browserLogs:browserLogs(),scenario:concurrentInput?'actual GraphPage deep copy with concurrent native input':'actual GraphPage deep copy of expanded partition',count,buildMode,buildMs,error:error.message,measurements,serverElements:documents.get(root)?.elements.size,serverCanvases:documents.get(root)?.canvases.size,wallMs:Date.now()-started};awaitwriteFile(reportPath,JSON.stringify(report,null,2));throwerror;}
}catch(error){letmeasurements;try{measurements=awaitevaluate('window.__canvasPerformance?.stop()');}catch{}report={duringCopyInput,baseline,heapSamples,browserLogs:browserLogs(),scenario:concurrentInput?'actual GraphPage deep copy with concurrent native input':'actual GraphPage deep copy of expanded partition',count,buildMode,buildMs,error:error.message,measurements,serverElements:documents.get(root)?.elements.size,serverCanvases:documents.get(root)?.canvases.size,wallMs:Date.now()-started};awaitwriteFile(reportPath,JSON.stringify(report,null,2));throwerror;}
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.