/** Remote authenticated HTTP/DB integration. Only the model output is a fixture. */ import assert from 'node:assert/strict'; import {randomUUID,createHash} from 'node:crypto'; export async function verifyFoundryAI({open,transport,rootId,imageId,assets,calls,tick,readDisplay,advance,loseAdmission,loseAction}){ const path=`workspaces/${process.env.FOUNDRY_TEST_WORKSPACE}/materials/${rootId}/canvas/ai`; const control=async action=>{const response=await transport({method:'POST',path:path+'/fixture/'+action,signal:AbortSignal.timeout(15_000)});assert.equal(response.status,200,action+': '+JSON.stringify(response.body));return response.body;}; const posts=suffix=>calls.filter(call=>call.method==='POST'&&call.path.endsWith(suffix)); let engine=await open();const signal=new AbortController().signal; const reopen=async()=>{await engine.close();advance();engine=await open();await engine.settle();}; const poll=async()=>{await engine.retry();for(let turns=0;turns<100;turns++){if(!tick())return;await engine.settle();}assert.fail('AI timer drain exceeded its bound');}; try{ const catalog=await engine.models(rootId,signal);assert.ok(catalog.models.some(model=>model.id==='fixture-text'));assert.ok(catalog.models.some(model=>model.id==='fixture-image')); const target={id:randomUUID(),incarnation:1}; assert.equal(engine.editor.dispatch({type:'element.create',element:{id:target.id,incarnation:1,ownerCanvasId:rootId,componentId:'text',dataVersion:1,structure:'node',data:{title:'Real AI adapter',text:'before generation',extension:{keep:true}},geometry:{x:0,y:0,width:100,height:80,rotation:0}}}).status,'applied');await engine.settle(); const input={target,boardId:rootId,modelId:'fixture-text',prompt:'Fixture generation',parameters:{},propagate:false}; const begin=async value=>{const result=engine.startRun(value);assert.equal(result.status,'applied',JSON.stringify(result));await engine.settle();return result;}; const byOperation=id=>[...engine.getSnapshot().runtime.runs.values()].find(run=>run.operationId===id); const initialStarts=posts('/ai/runs').length,initialUploads=posts('/assets').length; // A committed admission whose response was lost must be queried after reopen. loseAdmission();const first=await begin(input),admission=[...engine.getSnapshot().runtime.ledger.requests.values()].find(request=>request.kind==='run.start'&&request.operationIds.includes(first.operationIds[0])); assert.equal(admission.phase,'waiting');assert.equal(posts('/ai/runs').at(-1).response?.httpStatus,202,'must lose a committed admission, not a rejection');const admissionCount=posts('/ai/runs').length; await reopen();assert.equal(posts('/ai/runs').length,admissionCount);assert.equal(engine.getSnapshot().runtime.ledger.requests.get(admission.id).body,admission.body); assert.ok(calls.some(call=>call.method==='GET'&&call.path.endsWith('/ai/operations/'+admission.id))); const recovered=engine.getSnapshot().runtime.ledger.requests.get(admission.id); assert.equal(recovered.phase,'resolved','AI admission recovery: '+JSON.stringify({phase:recovered.phase,error:recovered.error,requestId:admission.id,operationId:first.operationIds[0],responses:calls.filter(call=>call.path.endsWith('/ai/runs')||call.path.endsWith('/ai/operations/'+admission.id)).map(call=>({method:call.method,response:call.response}))})); const firstRun=byOperation(first.operationIds[0]);assert.ok(firstRun,'resolved admission must publish a RunRecord');assert.equal(firstRun.status,'queued'); assert.equal(engine.editor.dispatch({type:'element.geometry',target,patch:{x:77}}).status,'applied');await engine.settle(); await control('execute');await poll(); const generatedText=engine.getSnapshot().runtime.runs.get(firstRun.id); assert.equal(generatedText.status,'succeeded',JSON.stringify({status:generatedText.status,error:generatedText.error,blocked:generatedText.blocked,cursor:generatedText.cursor})); assert.equal(engine.getSnapshot().document.elements.get(target.id).data.text,'headless fixture output');assert.equal(engine.getSnapshot().document.elements.get(target.id).geometry.x,77); assert.deepEqual(engine.getSnapshot().document.elements.get(target.id).data.extension,{keep:true}); // Queued cancellation is durable and does not execute the generation transport. const second=await begin({...input,prompt:'Fixture cancellation'}),secondRun=byOperation(second.operationIds[0]),beforeCancel=(await control('stats')).executions; loseAction();assert.equal(engine.cancelRun(secondRun.id).status,'applied');await engine.settle(); const cancellation=[...engine.getSnapshot().runtime.ledger.requests.values()].find(request=>request.kind==='run.cancel'&&request.runId===secondRun.id);assert.equal(cancellation.phase,'waiting');const cancellationCount=posts('/cancel').length; await reopen();assert.equal(posts('/cancel').length,cancellationCount);assert.equal(engine.getSnapshot().runtime.ledger.requests.get(cancellation.id).body,cancellation.body);assert.equal(engine.getSnapshot().runtime.runs.get(secondRun.id).status,'cancelled'); await poll();assert.equal(engine.getSnapshot().runtime.runs.get(secondRun.id).hasMore,false,'drain the cancelled run event cursor before another admission'); assert.equal((await control('stats')).executions,beforeCancel); // Edit while the provider is held, then retain and explicitly apply its output. const third=await begin({...input,prompt:'Fixture conflict'}),thirdRun=byOperation(third.operationIds[0]);await control('arm'); const executing=control('execute').then(value=>({value}),error=>({error})); try{ await control('wait'); assert.equal(engine.editor.dispatch({type:'element.data',target,patch:{text:'user edit while executing'}}).status,'applied');await engine.settle(); }finally{await control('release');} const completed=await executing;if(completed.error)throw completed.error;await poll(); const conflict=engine.getSnapshot().runtime.runs.get(thirdRun.id);assert.equal(conflict.status,'conflict');assert.equal(engine.getSnapshot().document.elements.get(target.id).data.text,'user edit while executing'); const outputId=conflict.outputEvents[0]?.output.output_id;assert.equal(typeof outputId,'string'); loseAction();assert.equal(engine.applyRunOutput(conflict.id,target,outputId).status,'applied');await engine.settle(); const application=[...engine.getSnapshot().runtime.ledger.requests.values()].find(request=>request.kind==='run.apply'&&request.runId===conflict.id);assert.equal(application.phase,'waiting');const applyCount=posts('/apply').length; assert.equal(engine.editor.history.undo().status,'applied');await engine.settle();await reopen(); assert.equal(engine.getSnapshot().document.elements.get(target.id).data.text,'user edit while executing');assert.equal(posts('/apply').length,applyCount);assert.equal(engine.getSnapshot().runtime.ledger.requests.get(application.id).body,application.body); assert.ok(engine.getSnapshot().runtime.runs.get(conflict.id).appliedOutputIds.includes(outputId)); assert.equal(engine.editor.history.redo().status,'applied');await engine.settle();assert.equal(engine.getSnapshot().document.elements.get(target.id).data.text,'headless fixture output'); assert.equal(posts('/apply').length,applyCount); // Managed image results are readable, and copying shares bytes without a run. const image=engine.getSnapshot().document.elements.get(imageId),oldAsset=image.data.assetId; const fourth=await begin({target:{id:image.id,incarnation:image.incarnation},boardId:rootId,modelId:'fixture-image',prompt:'Fixture image',parameters:{},propagate:false}); const fourthRun=byOperation(fourth.operationIds[0]);await control('execute');await poll(); assert.equal(engine.getSnapshot().runtime.runs.get(fourthRun.id).status,'succeeded');const generated=engine.getSnapshot().document.elements.get(imageId);assert.notEqual(generated.data.assetId,oldAsset); const metadata=await assets.authority.resolve(generated.data.assetId,signal),original=await assets.authority.read(generated.data.assetId,signal),display=await engine.displayResource(generated.data.assetId); try{const bytes=Buffer.from(await original.arrayBuffer());assert.deepEqual([...bytes.subarray(0,8)],[137,80,78,71,13,10,26,10]);assert.equal(createHash('sha256').update(bytes).digest('hex'),metadata.digest);assert.equal(metadata.asset.width,3);assert.equal(metadata.asset.height,2);assert.ok((await readDisplay(display.url)).length>0);}finally{display.release();} const runCount=engine.getSnapshot().runtime.runs.size,copyId=await engine.imports.duplicate([{id:generated.id,incarnation:generated.incarnation}],{viewCanvasId:rootId});await engine.settle(); const copy=engine.imports.getJobs().get(copyId);assert.equal(copy.status,'completed');assert.equal(engine.getSnapshot().document.elements.get(copy.stableIds['element:'+generated.id]).data.assetId,generated.data.assetId); assert.equal(engine.getSnapshot().runtime.runs.size,runCount);assert.equal(posts('/ai/runs').length,initialStarts+4);assert.equal(posts('/assets').length,initialUploads);assert.equal((await control('stats')).executions,3); return {runtimeAI:true,aiLostAdmission:true,aiCancelReconciled:true,aiConflictApplyUndo:true,aiManagedImage:true,aiCopyNoRun:true,aiExecutionCount:3}; }finally{await engine.close();} }