/** 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 {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/'))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 migrationId=randomUUID(),migrationBody=JSON.stringify({protocol_version:2,request_id:migrationId,expected_versions:Object.fromEntries(migration.sourceVersions)}); const migrated=await transport({method:'POST',path:path+'/model/migrate',body:migrationBody,requestId: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'},childId=randomUUID(),member=node(randomUUID(),childId),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:edge});}).status,'applied');await engine.settle(); 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); 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.sizecanvas.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; 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);targetImage=structuredClone(batch.image);share(batch);assert.equal((batch.files??[]).length,0,'adoption duplicated project-local bytes');return{durableRootRevision:targetImage.rootRevision};},async readFile(){return;}}; 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();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,migrationRetry:true,realHttp:true,lostAckRecovered:true,atomicPartition:true,deepCopyUpperEdge: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();}