Exercise runtime AI actions through the authenticated Foundry integration client
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
/** 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,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();await engine.settle();};
|
||||
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');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');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 firstRun=byOperation(first.operationIds[0]);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();
|
||||
assert.equal(engine.getSnapshot().runtime.runs.get(firstRun.id).status,'succeeded');
|
||||
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');
|
||||
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),display=await engine.displayResource(generated.data.assetId);
|
||||
try{const bytes=Buffer.from(await(await fetch(display.url)).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);}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();}
|
||||
}
|
||||
@@ -8,13 +8,15 @@ 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;
|
||||
let loseNextAck=false,loseNextAssetAck=false,loseNextAIAdmission=false,loseNextAIAction=false;
|
||||
const rawTransport=createFoundryTransport({basePath,accountId,isCurrentIdentity:()=>true,fetch:async(resource,init)=>{const headers=new Headers(init.headers);headers.set('Authorization',`Bearer ${token}`);return fetch(resource,{...init,headers});}});
|
||||
const transport=async input=>{calls.push({method:input.method,path:input.path,requestId:input.requestId,body:input.body});const result=await rawTransport(input);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 transport=async input=>{calls.push({method:input.method,path:input.path,requestId:input.requestId,body:input.body});const result=await rawTransport(input);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)});
|
||||
@@ -81,6 +83,9 @@ try{
|
||||
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,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',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}));
|
||||
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();}
|
||||
|
||||
Reference in New Issue
Block a user