Files
Endoleap/tests/headless/foundry.integration.mjs

110 lines
25 KiB
JavaScript

/** Invoked by Foundry's isolated PostgreSQL/HTTP integration test on its CI runner.
* No mocked model writes: only the recovery store and lost-response fault are local.
*/
import assert from 'node:assert/strict';
import {randomUUID} from 'node:crypto';
import {createDocument,createRegistry,builtinComponents} from '../../src/lib/canvas-core/index.ts';
import {openProjectEngine} from '../../src/lib/canvas-runtime/engine.ts';
import {createModelPreview} from '../../src/lib/canvas-runtime/model-preview.ts';
import {createFoundryTransport,createFoundryPersistence,loadFoundryModel} from '../../src/lib/canvas-service/foundry.ts';
import {createFoundryAssets} from '../../src/lib/canvas-service/assets.ts';
import {createFoundryRuns} from '../../src/lib/canvas-service/runs.ts';
import {createCanvasMigration} from '../../src/lib/canvas-migration.ts';
import {verifyFoundryAI} from './foundry-ai.integration.mjs';
import {decodeModelStream} from '../../src/lib/canvas-service/model-stream.ts';
const {FOUNDRY_HTTP_URL:url,FOUNDRY_TEST_PROJECT:project,FOUNDRY_TEST_ACCOUNT:accountId,FOUNDRY_TEST_WORKSPACE:workspaceId,FOUNDRY_TEST_ROOT:rootId,FOUNDRY_TEST_TOKEN:token}=process.env;
assert.ok(url&&project&&accountId&&workspaceId&&rootId&&token,'requires isolated Foundry HTTP fixture');
const registry=createRegistry(builtinComponents()),basePath=`${url}/projects/${project}/`,calls=[];
let loseNextAck=false,loseNextAssetAck=false,loseNextAIAdmission=false,loseNextAIAction=false;
const authenticatedFetch=async(resource,init={})=>{assert.equal(new URL(resource).origin,new URL(url).origin);const headers=new Headers(init.headers);headers.set('Authorization',`Bearer ${token}`);headers.set('X-Expected-Account',accountId);return fetch(resource,{...init,headers});};
const rawTransport=createFoundryTransport({basePath,accountId,isCurrentIdentity:()=>true,fetch:authenticatedFetch});
const transport=async input=>{const call={method:input.method,path:input.path,requestId:input.requestId,body:input.body};calls.push(call);const result=await rawTransport(input);if(input.path.includes('/canvas/ai/')||input.path.endsWith('/canvas/model/operations'))call.response={httpStatus:result.status,...Object.fromEntries(['protocol_version','request_id','operation_id','action','admission','status','error'].filter(key=>result.body?.[key]!==undefined).map(key=>[key,result.body[key]]))};if(loseNextAIAdmission&&input.method==='POST'&&input.path.endsWith('/ai/runs')&&result.status===202){loseNextAIAdmission=false;throw Error('injected response loss after AI admission');}if(loseNextAIAction&&input.method==='POST'&&/\/(cancel|apply)$/.test(input.path)&&result.status===200){loseNextAIAction=false;throw Error('injected response loss after AI action');}if(loseNextAssetAck&&input.method==='POST'&&input.path.endsWith('/assets')&&result.status>=200&&result.status<300){loseNextAssetAck=false;throw Error('injected upload response loss');}if(loseNextAck&&input.method==='POST'&&result.status===200){loseNextAck=false;throw Error('injected response loss after server commit');}return result;};
const path=`workspaces/${workspaceId}/materials/${rootId}/canvas`;
const migration=await loadFoundryModel(transport,workspaceId,rootId,registry);assert.equal(migration.status,'migration-required');
const migrationRecords=new Map(),migrationStorage={getItem:key=>migrationRecords.get(key)??null,setItem:(key,value)=>migrationRecords.set(key,value),removeItem:key=>migrationRecords.delete(key)};
const migrationOptions={scope:{accountId,workspaceId},registry,transport,storage:migrationStorage,isCurrentIdentity:()=>true,createId:randomUUID,lock:async work=>work()};
loseNextAck=true;const admission=createCanvasMigration(migrationOptions);assert.equal((await admission.start(rootId,async()=>{})).status,'pending');assert.equal(loseNextAck,false);
const frozenMigration=admission.read(),migrationId=frozenMigration.id,migrationBody=frozenMigration.body,migrationPosts=calls.filter(call=>call.method==='POST'&&call.path.endsWith('/model/migrate')).length;
assert.equal((await createCanvasMigration({...migrationOptions,createId:()=>assert.fail('recovery allocated a new migration identity')}).resume()).status,'applied');
assert.equal(calls.filter(call=>call.method==='POST'&&call.path.endsWith('/model/migrate')).length,migrationPosts);assert.equal(migrationRecords.size,0);
const migrated=await transport({method:'GET',path:path+'/model/operations/'+migrationId});assert.equal(migrated.status,200);assert.equal(migrated.body.status,'applied');
const retried=await transport({method:'POST',path:path+'/model/migrate',body:migrationBody,requestId:migrationId});assert.deepEqual(retried.body.identity_map,migrated.body.identity_map);
let initial=await loadFoundryModel(transport,workspaceId,rootId,registry);assert.equal(initial.status,'ready');
const sourceFrame=[...initial.document.elements.values()].find(element=>element.componentId==='frame'),sourceMember=[...initial.document.elements.values()].find(element=>element.structure==='node'&&element.ownerCanvasId!==rootId);assert.ok(sourceFrame&&sourceMember);
const scope={accountId,workspaceId,projectId:rootId},files=new Map(),errors=[];let image=null,owner,now=0;
const sharedRecords=new Map(),depotPort={async read(_leader,authority,id){return sharedRecords.get(JSON.stringify([authority,id]));},async collect(){throw Error('remote consumer does not collect');}};
const share=batch=>{for(const write of batch.depot?.writes??[]){const key=JSON.stringify([write.ref.authority,write.ref.asset.id]);sharedRecords.set(key,{ref:write.ref,bytes:write.bytes??sharedRecords.get(key)?.bytes});}};
const storage={assetDepot:depotPort,async claim(){assert.equal(owner,undefined);const held={scope,fence:1,active:true,async release(){held.active=false;owner=undefined;}};owner=held;return held;},async recover(){return structuredClone(image);},async commit(held,batch){assert.strictEqual(held,owner);const next=structuredClone(batch.image);for(const file of batch.files??[])files.set(file.id,file.bytes);for(const id of batch.deleteFiles??[])files.delete(id);image=next;share(batch);return{durableRootRevision:next.rootRevision};},async readFile(_held,id){return files.get(id);},close(){}};
let nextTimer=0;const timers=new Map();
const tick=()=>{const ready=[...timers].filter(([,timer])=>timer.due<=now).sort((a,b)=>a[1].due-b[1].due||a[0]-b[0]);let fired=0;for(const[id,timer]of ready)if(timers.delete(id)){fired++;timer.callback();}return fired;};
const persistence=createFoundryPersistence({registry,workspaceId,rootId,transport}),assets=createFoundryAssets({workspaceId,project,origin:'https://api.example.com',transport,displayURL:id=>`${basePath}workspaces/${workspaceId}/assets/${encodeURIComponent(id)}?variant=preview`}),clock={now:()=>now,setTimer(callback,delay){const id=++nextTimer;timers.set(id,{callback,due:now+delay});return id;},clearTimer(id){timers.delete(id);}};
const open=(extra={})=>openProjectEngine({scope,registry,storage,persistence,assets,document:initial.document,versions:initial.versions,clock,createId:randomUUID,onError:error=>errors.push(error),...extra});
const node=(id,ownerCanvasId=rootId)=>({id,incarnation:1,ownerCanvasId,componentId:'text',dataVersion:1,structure:'node',data:{title:id,text:''},geometry:{x:0,y:0,width:100,height:80,rotation:0}});
let engine=await open();
try{
loseNextAck=true;assert.equal(engine.editor.dispatch({type:'element.data',target:{id:sourceMember.id,incarnation:1},patch:{text:'committed before lost ACK'}}).status,'applied');await engine.settle();const request=[...engine.getSnapshot().runtime.ledger.requests.values()].at(-1);assert.equal(request.phase,'waiting');const sent=calls.filter(call=>call.method==='POST'&&call.path.endsWith('/model/operations')).length;
await engine.close();now=10_000;engine=await open();await engine.settle();const restored=engine.getSnapshot().runtime.ledger.requests.get(request.id);assert.equal(restored.body,request.body);assert.equal(restored.phase,'resolved');assert.ok(calls.some(call=>call.method==='GET'&&call.path.endsWith('/'+request.id)));assert.equal(calls.filter(call=>call.method==='POST'&&call.path.endsWith('/model/operations')).length,sent);
let live=await loadFoundryModel(transport,workspaceId,rootId,registry);assert.equal(live.document.elements.get(sourceMember.id).data.text,'committed before lost ACK');
const anchor=node(randomUUID()),frame={...node(randomUUID()),componentId:'frame',geometry:{x:100,y:50,width:500,height:400,rotation:30},data:{title:'Nested source',text:'',presentation:{contentScale:2}}},childId=randomUUID(),member=node(randomUUID(),childId),nested={...node(randomUUID(),childId),componentId:'frame'},grandchildId=randomUUID(),leaf=node(randomUUID(),grandchildId),edge={id:randomUUID(),incarnation:1,ownerCanvasId:rootId,componentId:'connection',dataVersion:1,structure:'connection',data:{},source:{elementId:anchor.id},target:{elementId:frame.id},path:null};
assert.equal(engine.editor.transaction(tx=>{tx.dispatch({type:'element.create',element:anchor});tx.dispatch({type:'element.create',element:frame});tx.dispatch({type:'canvas.create',canvas:{id:childId,parent:{canvasId:rootId,entryElementId:frame.id}}});tx.dispatch({type:'element.create',element:member});tx.dispatch({type:'element.create',element:nested});tx.dispatch({type:'canvas.create',canvas:{id:grandchildId,parent:{canvasId:childId,entryElementId:nested.id}}});tx.dispatch({type:'element.create',element:leaf});tx.dispatch({type:'element.create',element:edge});}).status,'applied');await engine.settle();
const creation=[...engine.getSnapshot().runtime.ledger.requests.values()].at(-1);assert.equal(creation.phase,'resolved','nested partition creation was not confirmed: '+JSON.stringify({phase:creation.phase,outcome:creation.outcome,error:creation.error,response:calls.findLast(call=>call.requestId===creation.id)?.response}));
live=await loadFoundryModel(transport,workspaceId,rootId,registry);assert.equal(live.document.elements.get(member.id).ownerCanvasId,childId);assert.ok(live.document.canvases.has(childId));
const copiedId=await engine.imports.duplicate([{id:frame.id,incarnation:1},{id:member.id,incarnation:1}],{viewCanvasId:rootId});await engine.settle();const copied=engine.imports.getJobs().get(copiedId);assert.equal(copied.status,'completed');const copiedFrame=copied.stableIds['element:'+frame.id],copiedMember=copied.stableIds['element:'+member.id];assert.ok(copiedFrame&&copiedMember);
live=await loadFoundryModel(transport,workspaceId,rootId,registry);assert.ok(live.document.elements.has(copiedMember));assert.ok([...live.document.elements.values()].some(element=>element.structure==='connection'&&element.source.elementId===anchor.id&&element.target.elementId===copiedFrame));assert.equal(engine.editor.history.undo().status,'applied');await engine.settle();assert.equal(engine.editor.history.redo().status,'applied');await engine.settle();assert.equal(engine.getSnapshot().document.elements.get(copiedFrame).incarnation,2);
// Dissolve is an owning-tree edit, not recursive deletion. Lose the successful
// real DB response, reopen/query, then restore and repeat the same history unit.
loseNextAck=true;assert.equal(engine.editor.dispatch({type:'canvas.dissolve',canvasId:childId,entry:{id:frame.id,incarnation:1}}).status,'applied');await engine.settle();
const dissolveRequest=[...engine.getSnapshot().runtime.ledger.requests.values()].at(-1),dissolveWrites=calls.filter(call=>call.method==='POST'&&call.path.endsWith('/model/operations')).length;assert.equal(dissolveRequest.phase,'waiting');assert.equal(loseNextAck,false,'dissolve did not receive a successful real service response');
await engine.close();now+=10_000;engine=await open();await engine.settle();assert.equal(engine.getSnapshot().runtime.ledger.requests.get(dissolveRequest.id).phase,'resolved');assert.equal(engine.getSnapshot().runtime.ledger.requests.get(dissolveRequest.id).body,dissolveRequest.body);assert.equal(calls.filter(call=>call.method==='POST'&&call.path.endsWith('/model/operations')).length,dissolveWrites);
live=await loadFoundryModel(transport,workspaceId,rootId,registry);assert.equal(live.document.canvases.has(childId),false);assert.equal(live.document.elements.get(member.id).ownerCanvasId,rootId);assert.equal(live.document.elements.get(nested.id).ownerCanvasId,rootId);assert.equal(live.document.canvases.get(grandchildId).parent.canvasId,rootId);assert.equal(live.document.elements.get(leaf.id).ownerCanvasId,grandchildId);assert.deepEqual(live.document.elements.get(leaf.id).geometry,leaf.geometry);assert.equal(live.document.elements.get(frame.id).incarnation,1);assert.deepEqual(live.document.elements.get(edge.id).target,{elementId:frame.id});
assert.equal(engine.editor.history.undo().status,'applied');await engine.settle();assert.equal(engine.getSnapshot().document.elements.get(member.id).ownerCanvasId,childId);assert.equal(engine.editor.history.redo().status,'applied');await engine.settle();assert.equal(engine.getSnapshot().document.canvases.has(childId),false);assert.equal(engine.editor.history.undo().status,'applied');await engine.settle();
live=await loadFoundryModel(transport,workspaceId,rootId,registry);assert.equal(live.document.elements.get(member.id).ownerCanvasId,childId);assert.equal(live.document.canvases.get(childId).parent.entryElementId,frame.id);assert.equal(live.document.canvases.get(grandchildId).parent.canvasId,childId);assert.equal(live.document.elements.get(leaf.id).ownerCanvasId,grandchildId);
const importId=engine.imports.startNdjson(new Blob([JSON.stringify({kind:'element',key:'native',record:node('native','$target')})+'\n'+JSON.stringify({kind:'unsupported'})+'\n']),rootId);await engine.settle();const imported=engine.imports.getJobs().get(importId);assert.equal(imported.status,'partial');assert.equal(imported.committedSourceKeys.length,1);
// Put a deterministic sorted tail in a different owner. Every original ID is
// lower than these records, so the first real 64-record frame certifies the
// root/new child while the original child is provably still incomplete.
const namespace=[...engine.getSnapshot().document.elements.keys(),...engine.getSnapshot().document.canvases.keys(),...engine.getSnapshot().document.tombstones.keys()];
const maximum=namespace.reduce((max,id)=>{const value=BigInt('0x'+id.replaceAll('-',''));return value>max?value:max;},0n);assert.ok(maximum+70n<(1n<<128n));
const tail=Array.from({length:70},(_,i)=>{const hex=(maximum+BigInt(i+1)).toString(16).padStart(32,'0');return node(`${hex.slice(0,8)}-${hex.slice(8,12)}-${hex.slice(12,16)}-${hex.slice(16,20)}-${hex.slice(20)}`,sourceMember.ownerCanvasId);});
assert.equal(engine.editor.transaction(tx=>{for(const element of tail)tx.dispatch({type:'element.create',element});}).status,'applied');await engine.settle();
// Consume the real negotiated stream through the production decoder, pausing
// after the first nonempty frame. HTTP/DB ACKs may advance before its old tail.
await engine.close();image=null;initial=await loadFoundryModel(transport,workspaceId,rootId,registry);
const streamed=await transport({method:'GET',path:path+'/model',responseFormat:'model-stream-v1'});assert.ok(streamed.stream);const controller=new AbortController(),preview=createModelPreview(registry);let capture,sourceToken,sourceVersions;
const stream=decodeModelStream(streamed.stream,rootId,registry,controller.signal);
while(true){const next=await stream.next();assert.equal(next.done,false);const chunk=next.value;
const value=preview.update(chunk.value);if(chunk.value.status==='receiving'){capture=value;sourceToken=chunk.snapshotToken;sourceVersions=chunk.versions;if(chunk.value.receivedElements>0)break;}
}
assert.ok(capture.sourceCanvases&&capture.elementOwners);assert.ok(capture.document.elements.size<initial.document.elements.size);assert.equal(capture.document.elements.has(tail.at(-1).id),false);
const loading={version:6,sourceToken,sourceCanvases:capture.sourceCanvases,elementOwners:capture.elementOwners,canvasElementCounts:capture.canvasElementCounts,declaredCanvasIds:capture.sourceCanvases.map(canvas=>canvas.id),receivedElements:capture.receivedElements,totalElements:capture.totalElements};
engine=await open({document:capture.document,versions:sourceVersions,loading});assert.equal(engine.editor.getQueries().membersResult(sourceMember.ownerCanvasId).complete,false);assert.equal(engine.imports.canStart(childId).enabled,true);const partialId=engine.imports.startNdjson(new Blob([JSON.stringify({kind:'element',key:'partial-native',record:node('partial-native','$target')})+'\n']),childId);await engine.settle();assert.equal(engine.imports.getJobs().get(partialId).status,'completed');assert.equal(engine.loading.getState().version,6);
const partialCopyId=await engine.imports.duplicate([{id:frame.id,incarnation:1}],{viewCanvasId:rootId});await engine.settle();const partialCopy=engine.imports.getJobs().get(partialCopyId);assert.equal(partialCopy.status,'completed');assert.equal(engine.getSnapshot().document.elements.has(tail.at(-1).id),false);const partialFrame=partialCopy.stableIds['element:'+frame.id];assert.ok(partialFrame);
// The upload commits on the real server, then its response is deliberately
// lost. Reopening must query that upload before either image document sends.
loseNextAssetAck=true;const imageId=randomUUID();let imageResource;
const imageResult=engine.stageFiles([{name:'live.png',bytes:new Blob([Buffer.from('iVBORw0KGgoAAAANSUhEUgAAAAMAAAACCAIAAAASFvFNAAAAEElEQVR4nGP4z8AAQQxwFgBB0gX7h/C5SAAAAABJRU5ErkJggg==','base64')],{type:'image/png'})}],([ref],tx)=>{imageResource=ref;tx.dispatch({type:'element.create',element:{...node(imageId,childId),componentId:'image',data:{title:'Live scoped image',text:'',assetId:ref.id,src:ref.src}}});});assert.equal(imageResult.status,'applied');assert.equal(engine.getSnapshot().document.elements.has(imageId),true);await engine.settle();const upload=[...engine.getSnapshot().runtime.ledger.requests.values()].find(request=>request.kind==='asset.upload');assert.ok(upload);assert.equal(loseNextAssetAck,false,'successful upload response was not intercepted');assert.equal(upload.phase,'waiting');const uploadCount=calls.filter(call=>call.method==='POST'&&call.path.endsWith('/assets')).length;
const imageCopyId=await engine.imports.duplicate([{id:imageId,incarnation:1}],{viewCanvasId:childId});await engine.settle();const imageCopy=engine.imports.getJobs().get(imageCopyId);assert.equal(imageCopy.status,'completed');const copiedImage=imageCopy.stableIds['element:'+imageId];assert.equal(engine.getSnapshot().document.elements.get(copiedImage).data.assetId,imageResource.id);assert.equal(engine.getSnapshot().document.elements.has(tail.at(-1).id),false);
await engine.close();now+=10_000;engine=await open({document:capture.document,versions:sourceVersions,loading});await engine.settle();assert.equal(engine.getSnapshot().runtime.ledger.requests.get(upload.id).phase,'resolved');assert.equal(engine.getSnapshot().runtime.ledger.requests.get(upload.id).body,upload.body);assert.equal(calls.filter(call=>call.method==='POST'&&call.path.endsWith('/assets')).length,uploadCount);assert.ok(calls.some(call=>call.method==='GET'&&call.path.endsWith('/assets/operations/'+upload.id)));
live=await loadFoundryModel(transport,workspaceId,rootId,registry);assert.equal(live.document.elements.get(imageId).data.assetId,imageResource.id);assert.equal(live.document.elements.get(copiedImage).data.assetId,imageResource.id);
let terminal;for await(const chunk of stream){preview.update(chunk.value);if(chunk.value.status==='ready')terminal=chunk.value;}assert.ok(terminal,'real source never reached terminal and EOF');
assert.equal((await engine.loading.complete(terminal.document,sourceVersions,{sourceToken})).status,'applied');await engine.settle();live=await loadFoundryModel(transport,workspaceId,rootId,registry);assert.ok(live.document.elements.has(engine.imports.getJobs().get(partialId).stableIds['element:partial-native']));assert.ok(live.document.elements.has(partialFrame));assert.ok(live.document.elements.has(tail.at(-1).id));assert.ok([...live.document.elements.values()].some(element=>element.structure==='connection'&&element.source.elementId===anchor.id&&element.target.elementId===partialFrame));
const authorized=await assets.authority.resolve(imageResource.id,new AbortController().signal),originalBytes=await assets.authority.read(imageResource.id,new AbortController().signal),record=engine.getSnapshot().runtime.resources.get(imageResource.id);
assert.equal(authorized.digest,record.digest);assert.deepEqual(authorized.asset,record.stored);assert.equal(originalBytes.size,record.byteLength);assert.deepEqual(Buffer.from(await originalBytes.arrayBuffer()),Buffer.from(await files.get(imageResource.id).arrayBuffer()));assert.equal(calls.filter(call=>call.method==='POST'&&call.path.endsWith('/assets')).length,uploadCount);
// A second canonical root in the same workspace adopts the first root's
// authorized identity. Shared bytes remain local; the real server validates
// the new document reference independently, with no second upload.
await engine.close();
const targetCreated=await transport({method:'POST',path:`workspaces/${workspaceId}/materials`,body:JSON.stringify({kind:'board',title:'Cross-project depot target',content:{}})});assert.equal(targetCreated.status,201);const targetRoot=targetCreated.body.id;
const targetMigration=await loadFoundryModel(transport,workspaceId,targetRoot,registry);assert.equal(targetMigration.status,'migration-required');const targetMigrationId=randomUUID();assert.equal((await transport({method:'POST',path:`workspaces/${workspaceId}/materials/${targetRoot}/canvas/model/migrate`,requestId:targetMigrationId,body:JSON.stringify({protocol_version:2,request_id:targetMigrationId,expected_versions:Object.fromEntries(targetMigration.sourceVersions)})})).status,200);
const targetInitial=await loadFoundryModel(transport,workspaceId,targetRoot,registry);assert.equal(targetInitial.status,'ready');let targetImage=null,targetOwner,adoptionOnly=true;const targetFiles=new Map();
const targetStorage={...storage,async claim(targetScope){assert.equal(targetOwner,undefined);const held={scope:targetScope,fence:1,active:true,async release(){held.active=false;targetOwner=undefined;}};targetOwner=held;return held;},async recover(){return structuredClone(targetImage);},async commit(held,batch){assert.strictEqual(held,targetOwner);if(adoptionOnly)assert.equal((batch.files??[]).length,0,'adoption duplicated project-local bytes');for(const file of batch.files??[]){assert.equal(file.bytes.type.startsWith('image/'),false,'managed image copy duplicated bitmap bytes');targetFiles.set(file.id,file.bytes);}for(const id of batch.deleteFiles??[])targetFiles.delete(id);targetImage=structuredClone(batch.image);share(batch);return{durableRootRevision:targetImage.rootRevision};},async readFile(_held,id){return targetFiles.get(id);}};
const targetOptions={scope:{accountId,workspaceId,projectId:targetRoot},registry,storage:targetStorage,assets,persistence:createFoundryPersistence({registry,workspaceId,rootId:targetRoot,transport}),document:targetInitial.document,versions:targetInitial.versions,clock,createId:randomUUID,onError:error=>errors.push(error)};
engine=await openProjectEngine(targetOptions);const adoptedId=randomUUID();assert.equal((await engine.adoptResource(imageResource.id,(handle,tx)=>tx.dispatch({type:'element.create',element:{...node(adoptedId,targetRoot),componentId:'image',data:{title:'Adopted existing image',text:'',assetId:handle.id,src:handle.src}}}))).status,'applied');await engine.settle();assert.equal(targetImage.formatVersion,13);assert.equal((await loadFoundryModel(transport,workspaceId,targetRoot,registry)).document.elements.get(adoptedId).data.assetId,imageResource.id);assert.equal(calls.filter(call=>call.method==='POST'&&call.path.endsWith('/assets')).length,uploadCount);
await engine.close();engine=await openProjectEngine(targetOptions);const display=await engine.displayResource(imageResource.id);assert.ok(display.url.startsWith('blob:'));assert.deepEqual(Buffer.from(await(await fetch(display.url)).arrayBuffer()),Buffer.from(await originalBytes.arrayBuffer()));display.release();assert.equal(engine.editor.history.undo().status,'applied');await engine.settle();assert.equal(engine.editor.history.redo().status,'applied');await engine.settle();assert.equal((await loadFoundryModel(transport,workspaceId,targetRoot,registry)).document.elements.get(adoptedId).incarnation,2);assert.equal(calls.filter(call=>call.method==='POST'&&call.path.endsWith('/assets')).length,uploadCount);
let aiReport={};if(process.env.FOUNDRY_TEST_AI_WORKER==='1'){
await engine.close();adoptionOnly=false;aiReport=await verifyFoundryAI({open:()=>openProjectEngine({...targetOptions,runs:createFoundryRuns({workspaceId,rootId:targetRoot,registry,transport})}),transport,rootId:targetRoot,imageId:adoptedId,assets,calls,tick,readDisplay:async url=>{const response=await authenticatedFetch(url);assert.equal(response.status,200);return Buffer.from(await response.arrayBuffer());},advance:()=>{now+=10_000;},loseAdmission:()=>{loseNextAIAdmission=true;},loseAction:()=>{loseNextAIAction=true;}});
}
assert.ok(errors.every(error=>String(error).includes('injected response loss')),'unexpected engine error: '+errors.map(String).join(','));
console.log(JSON.stringify({status:'ok',...aiReport,migrationCoordinator:true,migrationLostAckRecovered:true,migrationRetry:true,realHttp:true,lostAckRecovered:true,atomicPartition:true,deepCopyUpperEdge:true,dissolveCanvas:true,dissolveLostAckRecovered:true,dissolveUndoRedo:true,nativePartialReport:true,loadingImportOldCapture:true,unloadedTailDuringCopy:true,scopedImageLostAck:true,copiedPendingImageSharesUpload:true,authorizedAssetMetadata:true,originalAssetBytes:true,crossProjectAdoption:true,adoptionReopenedBytes:true,requests:calls.length}));
}finally{await engine.close();}