import { test } from 'node:test'; import assert from 'node:assert/strict'; import { createDocument, applyChanges, createRegistry, builtinComponents } from '../../src/lib/canvas-core/index.ts'; import { openProjectEngine } from '../../src/lib/canvas-runtime/engine.ts'; import { decodeRecovery } from '../../src/lib/canvas-runtime/recovery.ts'; import { createFoundryPersistence, createFoundryRuns, mergeFoundryRunContext } from '../../src/lib/canvas-service/index.ts'; import { createNetworkGate } from '../../src/lib/canvas-runtime/network.ts'; const id=n=>`10000000-0000-4000-8000-${String(n).padStart(12,'0')}`,root=id(1),child=id(2),workspace=id(3),account=id(4),target={id:'a',incarnation:1}; const registry=createRegistry(builtinComponents()),node=(name,owner=root)=>({id:name,incarnation:1,structure:'node',ownerCanvasId:owner,componentId:'text',dataVersion:1,data:{title:name,text:'original'},geometry:{x:0,y:0,width:260,height:200,rotation:0}}); const original=createDocument(root,[{id:root,parent:null},{id:child,parent:{canvasId:root,entryElementId:'f'}}],[node('a'),{...node('f'),componentId:'frame'}, {...node('future',child),componentId:'plugin.future',dataVersion:9,data:{opaque:{keep:true}}}]); const runInput={target,boardId:root,modelId:'text-model',prompt:'Generate',parameters:{},propagate:false}; const defer=()=>{let resolve;const promise=new Promise(yes=>resolve=yes);return{promise,resolve};}; const tick=async()=>{for(let i=0;i<100;i++)await Promise.resolve();}; function clockFixture(){let now=1000,next=0;const timers=new Map();return{now:()=>now,setTimer(fn,delay){const id=++next;timers.set(id,{fn,due:now+delay});return id;},clearTimer(id){timers.delete(id);},async advance(ms){now+=ms;for(let count=0;count<100;count++){const ready=[...timers].find(([,value])=>value.due<=now);if(!ready)break;timers.delete(ready[0]);ready[1].fn();await tick();}await tick();},timers};} function storageFixture(){let image,owner;const storage={broken:false,saved:[],async claim(scope){assert.equal(owner,undefined);owner={scope,active:true,fence:1,async release(){owner.active=false;owner=undefined;}};return owner;},async recover(){return image&&structuredClone(image);},async commit(leader,batch){assert.equal(leader,owner);if(storage.broken)throw new Error('quota');image=structuredClone(batch.image);storage.saved.push(image);return{durableRootRevision:image.rootRevision};},async readFile(){},close(){},image:()=>structuredClone(image)};return storage;} function fixture(){ let document=original,versions=new Map([[root,1],[child,7]]),next=100,runCounter=500,postGate,docGate; const storage=storageFixture(),clock=clockFixture(),calls=[],receipts=new Map(),runs=new Map(),errors=[]; const chunk=board=>({documentFormatVersion:1,projectId:root,complete:false,removedCanvasIds:[],canvases:[document.canvases.get(board)],elements:[...document.elements.values()].filter(element=>element.ownerCanvasId===board),tombstones:Object.fromEntries(document.tombstones),versions:{[board]:versions.get(board)}}); const partial=ids=>({documentFormatVersion:1,projectId:root,complete:false,removedCanvasIds:[],canvases:ids.map(id=>document.canvases.get(id)),elements:[...document.elements.values()].filter(element=>ids.includes(element.ownerCanvasId)),tombstones:Object.fromEntries(document.tombstones),versions:Object.fromEntries(ids.map(id=>[id,versions.get(id)]))}); const context=()=>({...partial([...document.canvases.keys()]),complete:true}); const transport=async request=>{ calls.push(request); if(request.method==='GET'&&request.path.endsWith('/model'))return{status:200,body:{model:context()}}; if(request.path.endsWith('/model/operations')){ if(docGate)await docGate.promise; const input=JSON.parse(request.body);let body=receipts.get(request.requestId); if(!body){ if(Object.entries(input.expected_versions).some(([board,version])=>versions.get(board)!==version))body={protocol_version:2,request_id:request.requestId,status:'not-applied',terminal:true,error:'version_conflict'}; else{for(const operation of input.operations)document=applyChanges(document,operation.changes);for(const board of Object.keys(input.expected_versions))versions.set(board,versions.get(board)+1);body={protocol_version:2,request_id:request.requestId,status:'applied',operation_ids:input.operations.map(operation=>operation.id),model:partial(Object.keys(input.expected_versions))};} receipts.set(request.requestId,structuredClone(body)); } return{status:body.status==='applied'?200:412,body}; } if(request.method==='POST'&&/\/ai\/runs\/[^/]+\/(cancel|apply)$/.test(request.path)){ const action=request.path.endsWith('/cancel')?'run.cancel':'run.apply',runId=request.path.split('/').at(-2),run=runs.get(runId),input=JSON.parse(request.body);let body=receipts.get(request.requestId); if(!body){ if(action==='run.cancel'){ run.status='cancelled';run.events.push({seq:run.events.length+1,run_id:runId,type:'run.completed',status:'cancelled'}); body={protocol_version:2,request_id:request.requestId,operation_id:input.operation_id,action,status:'applied',run_id:runId,run_status:'cancelled'}; }else{ const owner=document.elements.get(input.node_id).ownerCanvasId;assert.equal(input.target_canvas_id,owner);assert.equal(input.target_incarnation,1);assert.equal(request.expectedVersion,versions.get(owner));const event=run.events.find(event=>event.type==='step.output'&&event.output.output_id===input.output_id),element=document.elements.get(input.node_id);assert.ok(event); document=applyChanges(document,[{kind:'field',target,field:'data.text',before:element.data.text,after:event.output.text,beforePresent:true,afterPresent:true}]);versions.set(owner,versions.get(owner)+1); body={protocol_version:2,request_id:request.requestId,operation_id:input.operation_id,action,status:'applied',run_id:runId,step_id:'step-a',board_id:owner,origin_board_id:root,canonical_root:root,node_id:input.node_id,output_id:input.output_id,target_incarnation:1,document_format_version:1,canvas_version:versions.get(owner),model:owner===root?chunk(root):context()}; run.events.push({...structuredClone(body),seq:run.events.length+1,type:'step.applied'}); } receipts.set(request.requestId,structuredClone(body)); } if(action==='run.cancel'&&f.loseCancel||action==='run.apply'&&f.loseApply)return{status:503,body:{error:'response_lost'}}; return{status:200,body}; } if(request.method==='POST'&&request.path.endsWith('/ai/runs')){ const input=JSON.parse(request.body);let body=receipts.get(request.requestId); if(!body){ if(f.rejectAdmission){body={protocol_version:2,request_id:request.requestId,operation_id:input.operation_id,action:'run.start',status:'not-applied',terminal:true,error:'version_conflict'};receipts.set(request.requestId,body);if(postGate)await postGate.promise;return f.loseAdmission?{status:503,body:{error:'response_lost'}}:{status:412,body};} assert.equal(request.expectedVersion,versions.get(root));assert.deepEqual(input.expected_versions,Object.fromEntries(versions)); const element=document.elements.get(input.node_id),before=element.data.ai; document=applyChanges(document,[{kind:'field',target,field:'data.ai',before:before??null,beforePresent:before!==undefined,after:{enabled:true,modelId:input.model_id,prompt:input.prompt,parameters:input.parameters,propagate:input.propagate},afterPresent:true}]);versions.set(root,versions.get(root)+1); const runId=id(++runCounter),plan={steps:[{step_id:'step-a',board_id:root,node_id:'a',target_incarnation:1,canonical_root:root,kind:'text',model_id:'text-model',status:'queued',dependencies:[],static_inputs:[]}],edges:[],frozen_versions:Object.fromEntries(versions)}; body={protocol_version:2,request_id:request.requestId,operation_id:input.operation_id,admission:'accepted',status:'queued',run_id:runId,plan,canvas_version:versions.get(root),target_incarnation:1,document_format_version:1,model:chunk(root)}; receipts.set(request.requestId,structuredClone(body));runs.set(runId,{status:'queued',plan,events:[{seq:1,run_id:runId,type:'run.queued',status:'queued'}],outputs:[]}); } if(postGate)await postGate.promise; if(f.loseAdmission)return{status:503,body:{error:'response_lost'}}; return{status:202,body}; } if(request.path.includes('/ai/operations/')){const receipt=receipts.get(request.path.split('/').at(-1));return receipt?{status:200,body:receipt}:{status:404,body:{protocol_version:2,request_id:request.path.split('/').at(-1),status:'unknown'}};} if(request.path.includes('/ai/runs/')){ const url=new URL(request.path,'https://fixture.test'),runId=url.pathname.split('/').at(-1),run=runs.get(runId),after=Number(url.searchParams.get('after_seq')); const events=run.events.filter(event=>event.seq>after).slice(0,f.pageSize??100); const response={status:200,body:{run_id:runId,status:run.status,plan:run.plan,events,next_seq:events.at(-1)?.seq??after,has_more:after+events.lengthid(++next),persistence:createFoundryPersistence({workspaceId:workspace,rootId:root,registry,transport}),runs:(()=>{const port=createFoundryRuns({workspaceId:workspace,rootId:root,registry,transport});return{...port,accept(...args){if(f.failAdmissionCodec)throw Error('codec_temporarily_unavailable');return port.accept(...args);},acceptAction(...args){if(f.failActionCodec)throw Error('codec_temporarily_unavailable');return port.acceptAction(...args);}};})(),onError:error=>errors.push(error)});}, move(owner){const element=document.elements.get('a');document=applyChanges(document,[{kind:'field',target,field:'ownerCanvasId',before:element.ownerCanvasId,after:owner}]);versions.set(element.ownerCanvasId,versions.get(element.ownerCanvasId)+1);versions.set(owner,versions.get(owner)+1);}, finish(text='generated',conflict=false){ const [runId,run]=[...runs].at(-1),element=document.elements.get('a'),owner=element.ownerCanvasId; if(!conflict){document=applyChanges(document,[{kind:'field',target,field:'data.text',before:element.data.text,after:text,beforePresent:true,afterPresent:true}]);versions.set(owner,versions.get(owner)+1);} run.status=conflict?'conflict':'succeeded';run.outputs=[{step_id:'step-a',output:{output_id:id(999),kind:'text'}}]; run.events.push({seq:2,run_id:runId,type:'step.output',step_id:'step-a',board_id:owner,origin_board_id:root,canonical_root:root,target_incarnation:1,node_id:'a',...(conflict&&owner!==root?{document_format_version:1,canvas_version:versions.get(owner),model:structuredClone(context())}:{}),output:{output_id:id(999),kind:'text',text}}, {seq:3,run_id:runId,type:conflict?'step.conflict':'step.applied',step_id:'step-a',board_id:owner,origin_board_id:root,canonical_root:root,node_id:'a',canvas_version:versions.get(owner),target_incarnation:1,document_format_version:1,model:structuredClone(owner===root?chunk(root):context())}, {seq:4,run_id:runId,type:'run.completed',status:run.status}); },server:()=>({document,versions}), };return f; } test('AI admission orders prior and later edits, persists its job, and does not hold the writer for execution',async()=>{ const f=fixture(),engine=await f.open(),gate=f.holdAdmission();const detach=engine.sessions.attach('view'); engine.editor.dispatch({type:'element.data',target,patch:{title:'before click'}}); const start=engine.startRun(runInput);assert.equal(start.status,'applied'); const later=engine.editor.dispatch({type:'element.geometry',target,patch:{x:200}}); assert.equal(engine.getSnapshot().document.elements.get('a').geometry.x,200); assert.ok(engine.getSnapshot().runtime.ledger.operations.get(later.operationIds[0]).intent.dependencies.includes(start.operationIds[0])); await tick();assert.equal(f.calls.filter(call=>call.method==='POST').length,2); const admission=f.calls.find(call=>call.path.endsWith('/ai/runs')); assert.equal(JSON.parse(admission.body).expected_versions[root],2);assert.equal(f.server().document.elements.get('a').data.title,'before click'); assert.equal(f.server().document.elements.get('a').geometry.x,0);detach();gate.resolve();await engine.settle(); const run=[...engine.getSnapshot().runtime.runs.values()][0];assert.ok(run);assert.equal(run.cursor,0); assert.equal(engine.getSnapshot().runtime.ledger.operations.get(start.operationIds[0]).phase,'resolved'); assert.equal(f.server().document.elements.get('a').geometry.x,200);assert.equal(run.status,'queued'); assert.equal(engine.startRun(runInput).code,'node_busy'); f.finish();f.pageSize=2;await f.clock.advance(0);await engine.settle(); assert.equal(engine.getSnapshot().runtime.runs.get(run.id).cursor,2);assert.equal(engine.getSnapshot().runtime.runs.get(run.id).hasMore,true);assert.equal(engine.startRun(runInput).code,'node_busy'); await f.clock.advance(1);await engine.settle(); const finished=engine.getSnapshot();assert.equal(finished.runtime.runs.get(run.id).status,'succeeded');assert.equal(finished.runtime.runs.get(run.id).cursor,4); assert.equal(finished.document.elements.get('a').data.text,'generated');assert.equal(finished.document.elements.get('a').geometry.x,200);assert.equal(finished.document.elements.get('future').data.opaque.keep,true);assert.equal(finished.history.undo.length,2); assert.equal(finished.operations.size,2);assert.equal(finished.runtime.ledger.operations.size,3); const restored=decodeRecovery(f.storage.image(),registry);assert.equal(restored.runtime.runs.get(run.id).cursor,4);assert.equal(restored.runtime.runs.get(run.id).outputEvents[0].output.text,'generated'); await engine.close(); }); test('lost AI admission response recovers its original request and run without a second invocation',async()=>{ const f=fixture();f.loseAdmission=true;const engine=await f.open();engine.startRun(runInput);await engine.settle(); assert.equal(engine.getSnapshot().runtime.runs.size,0);const frozen=[...engine.getSnapshot().runtime.ledger.requests.values()][0];await engine.close(); f.loseAdmission=false;const recovered=await f.open();await f.clock.advance(5000);await recovered.settle(); assert.equal(f.calls.filter(call=>call.method==='POST'&&call.path.endsWith('/ai/runs')).length,1); assert.ok(f.calls.some(call=>call.path.endsWith('/ai/operations/'+frozen.id))); assert.equal(recovered.getSnapshot().runtime.runs.size,1);assert.equal([...recovered.getSnapshot().runtime.ledger.requests.values()][0].body,frozen.body); f.finish();await f.clock.advance(1000);await recovered.settle();assert.equal(recovered.getSnapshot().document.elements.get('a').data.text,'generated');await recovered.close(); }); test('run event waits for outstanding receipts and retains a content conflict, output and page across recovery',async()=>{ const f=fixture(),engine=await f.open();engine.startRun(runInput);await engine.settle(); const gate=f.holdDocument();engine.editor.dispatch({type:'element.data',target,patch:{text:'local edit'}});await tick();f.finish(); await f.clock.advance(0);await tick();const run=[...engine.getSnapshot().runtime.runs.values()][0]; assert.equal(run.cursor,0);assert.equal(run.error,'run_waiting_for_receipts');assert.ok(run.pendingPage);assert.equal(engine.getSnapshot().document.elements.get('a').data.text,'local edit'); gate.resolve();await engine.settle();await f.clock.advance(1000);await engine.settle(); const conflict=engine.getSnapshot().runtime.runs.get(run.id);assert.equal(conflict.cursor,0);assert.equal(conflict.blocked,true);assert.ok(conflict.pendingPage);assert.equal(conflict.outputs.length,1);assert.equal(conflict.outputEvents[0].output.text,'generated'); assert.equal(engine.getSnapshot().document.elements.get('a').data.text,'local edit');await engine.close(); const recovered=await f.open();assert.equal(recovered.getSnapshot().runtime.runs.get(run.id).pendingPage.next,4);assert.equal(recovered.getSnapshot().document.elements.get('a').data.text,'local edit');await recovered.close(); }); test('older run event cannot rewind a later canonical document receipt',async()=>{ const f=fixture(),engine=await f.open();engine.startRun(runInput);await engine.settle();f.finish(); engine.editor.dispatch({type:'element.geometry',target,patch:{x:500}});await engine.settle(); // The server rejects its stale version first. Adopt an externally refreshed full base only after receipt resolution. const server=f.server();const refreshed=engine.refresh(server.document,server.versions);assert.equal(refreshed.status,'applied'); await f.clock.advance(0);await engine.settle(); assert.equal(engine.getSnapshot().document.elements.get('a').geometry.x,500);assert.equal(engine.getSnapshot().document.elements.get('a').data.text,'generated'); assert.equal([...engine.getSnapshot().runtime.runs.values()][0].cursor,4);await engine.close(); }); test('run durability failure prevents admission and close fences late poll responses',async()=>{ const f=fixture(),engine=await f.open();await engine.persist();f.storage.broken=true;engine.startRun(runInput);await tick();assert.equal(f.calls.length,0); f.storage.broken=false;await engine.retry();await f.clock.advance(0);await f.clock.advance(1000);await engine.settle();assert.equal(f.runs.size,1); f.pollGate=defer();await f.clock.advance(0);await tick();const revision=engine.getSnapshot().rootRevision; const closing=engine.close();f.finish();f.pollGate.resolve();await closing;await tick(); assert.equal(engine.getSnapshot().rootRevision,revision);assert.equal(engine.getSnapshot().document.elements.get('a').data.text,'original'); }); test('transport gate bounds combined run/read/write capacity and drops aborted waiters',async()=>{ const gate=createNetworkGate(2),release=defer(),controller=new AbortController();let active=0,max=0; const execute=signal=>gate.run(signal,async()=>{max=Math.max(max,++active);await release.promise;active--;}); const first=execute(new AbortController().signal),second=execute(new AbortController().signal),cancelled=execute(controller.signal);controller.abort();await assert.rejects(cancelled); const third=execute(new AbortController().signal);await tick();assert.equal(active,2);release.resolve();await Promise.all([first,second,third]);assert.equal(max,2);assert.equal(active,0); }); test('malformed admission identity keeps its receipt and claims until the original query repairs it',async()=>{ const f=fixture(),real=f.transport; const runs=createFoundryRuns({workspaceId:workspace,rootId:root,registry,transport:async request=>{const response=await real(request);return request.method==='POST'?{...response,body:{...response.body,target_incarnation:99}}:response;}}); const engine=await openProjectEngine({scope:{accountId:account,workspaceId:workspace,projectId:root},registry,document:original,versions:new Map([[root,1],[child,7]]),storage:f.storage,clock:f.clock,createId:(()=>{let next=900;return()=>id(++next)})(),persistence:createFoundryPersistence({workspaceId:workspace,rootId:root,registry,transport:real}),runs}); engine.startRun(runInput);await engine.settle();const request=[...engine.getSnapshot().runtime.ledger.requests.values()][0]; assert.equal(request.outcome,'applied');assert.equal(request.phase,'waiting');assert.equal(engine.getSnapshot().runtime.runs.size,0);assert.ok(engine.getSnapshot().runtime.receipts.has(request.id));assert.equal(engine.getSnapshot().document.elements.get('a').data.ai,undefined); await engine.retry();await f.clock.advance(0);await engine.settle();assert.equal(engine.getSnapshot().runtime.runs.size,1);assert.equal(f.calls.filter(call=>call.method==='POST').length,1);await engine.close(); }); test('foreign event identity and corrupted persisted run bindings cannot overwrite the working document',async()=>{ const f=fixture(),engine=await f.open();engine.startRun(runInput);await engine.settle();f.finish(); const remote=[...f.runs.values()][0];remote.events[2].target_incarnation=99; await f.clock.advance(0);await engine.settle();const run=[...engine.getSnapshot().runtime.runs.values()][0]; assert.equal(run.blocked,true);assert.equal(run.cursor,0);assert.equal(engine.getSnapshot().document.elements.get('a').data.text,'original'); const corrupted=f.storage.image();corrupted.payload.runtime.runs[0][1].target.incarnation=99; assert.throws(()=>decodeRecovery(corrupted,registry),/corrupt_recovery_image/); const wrongOwner=f.storage.image();wrongOwner.payload.runtime.ledger.requests[0][1].ownerCanvasId=child; assert.throws(()=>decodeRecovery(wrongOwner,registry),/corrupt_recovery_image/);await engine.close(); }); test('even a higher-version malformed event cannot restore an old incarnation',async()=>{ const f=fixture(),engine=await f.open();engine.startRun(runInput);await engine.settle();f.finish(); const server=f.server(),records=[...server.document.elements.values()].map(element=>element.id==='a'?{...element,incarnation:2,data:{...element.data,text:'restored content'}}:element),restored=createDocument(root,[...server.document.canvases.values()],records,new Map([['a',1]])),nextVersion=server.versions.get(root)+1; assert.equal(engine.refresh(restored,new Map([[root,nextVersion],[child,7]])).status,'applied'); const event=[...f.runs.values()][0].events[2];event.canvas_version=nextVersion+1;event.model.versions[root]=nextVersion+1; await f.clock.advance(0);await engine.settle();assert.equal(engine.getSnapshot().document.elements.get('a').incarnation,2);assert.equal(engine.getSnapshot().document.elements.get('a').data.text,'restored content');assert.equal([...engine.getSnapshot().runtime.runs.values()][0].blocked,true);await engine.close(); }); test('queryable zero-effect admission rejection releases ordering without discarding later editing',async()=>{ const f=fixture();f.rejectAdmission=true;f.loseAdmission=true;const engine=await f.open();const started=engine.startRun(runInput);engine.editor.dispatch({type:'element.geometry',target,patch:{x:700}});await engine.settle(); assert.equal(f.server().document.elements.get('a').geometry.x,0);await engine.close();f.loseAdmission=false; const recovered=await f.open();await f.clock.advance(5000);await recovered.settle(); const record=recovered.getSnapshot().runtime.ledger.operations.get(started.operationIds[0]);assert.equal(record.phase,'resolved');assert.equal(record.resolution,'not-applied');assert.equal(record.blockers[0],'version_conflict');assert.equal(recovered.getSnapshot().runtime.runs.size,0); assert.equal(f.server().document.elements.get('a').geometry.x,700);assert.equal(f.calls.filter(call=>call.method==='POST'&&call.path.endsWith('/ai/runs')).length,1);assert.equal(recovered.getSnapshot().history.undo.length,1);const decoded=decodeRecovery(f.storage.image(),registry);assert.equal(decoded.runtime.ledger.operations.get(started.operationIds[0]).resolution,'not-applied');const corrupt=f.storage.image();corrupt.payload.runtime.receipts[0][1].terminal=false;assert.throws(()=>decodeRecovery(corrupt,registry),/corrupt_recovery_image/);await recovered.close(); }); test('durable cancellation survives response loss and does not create content history',async()=>{ const f=fixture(),engine=await f.open();engine.startRun(runInput);await engine.settle();const run=[...engine.getSnapshot().runtime.runs.values()][0];f.loseCancel=true; const cancelled=engine.cancelRun(run.id);assert.equal(cancelled.status,'applied');await engine.settle();assert.equal(f.runs.get(run.id).status,'cancelled');assert.equal(engine.getSnapshot().history.undo.length,0); const request=[...engine.getSnapshot().runtime.ledger.requests.values()].find(request=>request.kind==='run.cancel');assert.equal(request.canvasIds.length,0);await engine.close();f.loseCancel=false; const recovered=await f.open();await f.clock.advance(5000);await recovered.settle();assert.equal(recovered.getSnapshot().runtime.ledger.operations.get(cancelled.operationIds[0]).phase,'resolved');assert.equal(recovered.getSnapshot().runtime.runs.get(run.id).status,'cancelled');assert.equal(f.calls.filter(call=>call.method==='POST'&&call.path.endsWith('/cancel')).length,1);assert.ok(f.calls.some(call=>call.path.endsWith('/ai/operations/'+request.id)));await recovered.close(); }); test('explicit output application is immediate and undoable while unknown; recovery and redo never regenerate',async()=>{ const f=fixture(),engine=await f.open();engine.startRun(runInput);await engine.settle();f.finish('chosen output',true);await f.clock.advance(0);await engine.settle();const run=[...engine.getSnapshot().runtime.runs.values()][0];assert.equal(run.status,'conflict'); f.loseApply=true;const applied=engine.applyRunOutput(run.id,target,id(999));assert.equal(applied.status,'applied');assert.equal(engine.getSnapshot().document.elements.get('a').data.text,'chosen output');assert.equal(engine.getSnapshot().history.undo.length,1);assert.equal(engine.getSnapshot().operations.get(applied.operationIds[0]).operationType,'run.apply');await engine.settle(); assert.equal(engine.editor.history.undo().status,'applied');assert.equal(engine.getSnapshot().document.elements.get('a').data.text,'original');await engine.settle();assert.equal(f.server().document.elements.get('a').data.text,'chosen output');await engine.close(); f.loseApply=false;const recovered=await f.open();await f.clock.advance(5000);await recovered.settle();assert.equal(recovered.getSnapshot().document.elements.get('a').data.text,'original');assert.equal(f.server().document.elements.get('a').data.text,'original');assert.equal(recovered.getSnapshot().history.redo.length,1);assert.ok(recovered.getSnapshot().runtime.runs.get(run.id).appliedOutputIds.includes(id(999))); assert.equal(recovered.editor.history.redo().status,'applied');await recovered.settle();assert.equal(f.server().document.elements.get('a').data.text,'chosen output');assert.equal(f.calls.filter(call=>call.method==='POST'&&call.path.endsWith('/apply')).length,1);assert.equal(f.calls.filter(call=>call.method==='POST'&&call.path.endsWith('/ai/runs')).length,1);await recovered.close(); }); test('a poll captured before cancellation cannot regress the acknowledged control state',async()=>{ const f=fixture(),engine=await f.open();engine.startRun(runInput);await engine.settle();const run=[...engine.getSnapshot().runtime.runs.values()][0];f.pollGate=defer();await f.clock.advance(0);await tick(); engine.cancelRun(run.id);await tick();assert.equal(engine.getSnapshot().runtime.runs.get(run.id).status,'cancelled');assert.equal(engine.getSnapshot().runtime.runs.get(run.id).controlEpoch,1); f.pollGate.resolve();await engine.settle();assert.equal(engine.getSnapshot().runtime.runs.get(run.id).status,'cancelled');assert.equal(engine.getSnapshot().runtime.runs.get(run.id).cursor,0); await f.clock.advance(1);await engine.settle();assert.equal(engine.getSnapshot().runtime.runs.get(run.id).status,'cancelled');assert.equal(engine.getSnapshot().runtime.runs.get(run.id).cursor,2);await engine.close(); }); test('AI action rejection proof must match request, operation, action and run; bare errors remain unknown',async()=>{ const requestId=id(8800),operationId=id(8801),runId=id(8802),signal=new AbortController().signal; for(const kind of ['run.start','run.cancel','run.apply']){ const expectedVersions=kind==='run.cancel'?{}:{[root]:1},payload={protocol_version:2,request_id:requestId,operation_id:operationId,...(kind==='run.start'?{target_incarnation:1,expected_versions:expectedVersions,node_id:'a',model_id:'text',prompt:'Generate',parameters:{},propagate:false}:kind==='run.apply'?{target_canvas_id:root,target_incarnation:1,node_id:'a',output_id:id(8803)}:{})}; const request={id:requestId,kind,protocolVersion:2,ownerCanvasId:root,...(kind!=='run.start'?{runId}:{}),operationIds:[operationId],canvasIds:Object.keys(expectedVersions),expectedVersions,body:JSON.stringify(payload)}; const proof={protocol_version:2,request_id:requestId,operation_id:operationId,action:kind,...(kind!=='run.start'?{run_id:runId}:{}),status:'not-applied',terminal:true,error:'rejected'}; let response={status:200,body:proof};const port=createFoundryRuns({workspaceId:workspace,rootId:root,registry,transport:async()=>response});assert.equal((await port.query(request,signal)).outcome,'not-applied'); for(const patch of [{request_id:id(8890)},{operation_id:id(8891)},{action:'different'},{terminal:false},...(kind!=='run.start'?[{run_id:id(8892)}]:[])]){response={status:200,body:{...proof,...patch}};assert.equal((await port.query(request,signal)).outcome,'unresolved');} response={status:403,body:{error:'forbidden'}};const denied=await port.query(request,signal);assert.equal(denied.outcome,'unresolved');assert.equal(denied.blocked,true); response={status:404,body:{protocol_version:2,request_id:requestId,status:'unknown'}};const unknown=await port.query(request,signal);assert.equal(unknown.outcome,'unresolved');assert.equal(unknown.blocked,undefined); } }); test('a completed run follows an external owner-only move and publishes one global element without duplicate history',async()=>{ const f=fixture(),engine=await f.open();engine.startRun(runInput);await engine.settle();f.move(child);f.finish('moved output');await f.clock.advance(0);await engine.settle(); const snapshot=engine.getSnapshot(),run=[...snapshot.runtime.runs.values()][0];assert.equal(snapshot.document.elements.get('a').ownerCanvasId,child);assert.equal(snapshot.document.elements.get('a').data.text,'moved output');assert.equal([...snapshot.document.elements.values()].filter(element=>element.id==='a').length,1);assert.equal(snapshot.document.elements.get('future').data.opaque.keep,true);assert.equal(snapshot.history.undo.length,0);assert.equal(run.boardId,root);assert.equal(run.plan.steps[0].board_id,root);assert.equal(run.cursor,4); assert.equal(decodeRecovery(f.storage.image(),registry).document.elements.get('a').ownerCanvasId,child);await engine.close(); }); test('a relocated context waits for receipts in every affected canvas and preserves an unrelated pending title',async()=>{ const f=fixture(),engine=await f.open();engine.startRun(runInput);await engine.settle();const gate=f.holdDocument();engine.editor.dispatch({type:'element.data',target:{id:'f',incarnation:1},patch:{title:'local title'}});await tick();f.move(child);f.finish();await f.clock.advance(0);await tick(); assert.equal([...engine.getSnapshot().runtime.runs.values()][0].error,'run_waiting_for_receipts');assert.equal(engine.getSnapshot().document.elements.get('a').ownerCanvasId,root);gate.resolve();await engine.settle();await f.clock.advance(1000);await engine.settle(); assert.equal(engine.getSnapshot().document.elements.get('a').ownerCanvasId,child);assert.equal(engine.getSnapshot().document.elements.get('a').data.text,'generated');assert.equal(engine.getSnapshot().document.elements.get('f').data.title,'local title');await engine.close(); }); test('a pending geometry edit cannot silently reuse the coordinates of an old owner',async()=>{ const f=fixture(),engine=await f.open();engine.startRun(runInput);await engine.settle();const gate=f.holdDocument();engine.editor.dispatch({type:'element.geometry',target,patch:{x:200}});await tick();f.move(child);f.finish();await f.clock.advance(0);gate.resolve();await engine.settle();await f.clock.advance(1000);await engine.settle(); const run=[...engine.getSnapshot().runtime.runs.values()][0];assert.equal(run.error,'ownership_conflict');assert.equal(run.blocked,true);assert.ok(run.pendingPage);assert.equal(engine.getSnapshot().document.elements.get('a').ownerCanvasId,root);assert.equal(engine.getSnapshot().document.elements.get('a').geometry.x,200);await engine.close(); }); test('a moved content conflict adopts its owner context and explicit application/undo uses that current canvas',async()=>{ const f=fixture(),engine=await f.open();engine.startRun(runInput);await engine.settle();f.move(child);f.finish('chosen moved output',true);await f.clock.advance(0);await engine.settle();const run=[...engine.getSnapshot().runtime.runs.values()][0];assert.equal(run.status,'conflict');assert.equal(engine.getSnapshot().document.elements.get('a').ownerCanvasId,child); assert.equal(engine.applyRunOutput(run.id,target,id(999)).status,'applied');await engine.settle();assert.equal(engine.getSnapshot().document.elements.get('a').data.text,'chosen moved output');const apply=f.calls.find(call=>call.path.endsWith('/apply'));assert.equal(JSON.parse(apply.body).target_canvas_id,child);assert.ok(apply.path.includes('/materials/'+root+'/')); assert.equal(engine.editor.history.undo().status,'applied');await engine.settle();assert.equal(f.server().document.elements.get('a').ownerCanvasId,child);assert.equal(f.server().document.elements.get('a').data.text,'original');await engine.close(); }); test('complete run context retains later unrelated canvas ACKs and rejects changed data at the same version',()=>{ const third=id(22),canvases=[...original.canvases.values(),{id:third,parent:{canvasId:root,entryElementId:'g'}}],frame={...node('g'),componentId:'frame'},moved={...node('a',child),data:{title:'a',text:'before'}},sibling={...node('sibling',third),data:{title:'newer title',text:''}},elements=[moved,original.elements.get('f'),original.elements.get('future'),frame,sibling],document=createDocument(root,canvases,elements),snapshot={document,runtime:{acknowledged:document,versions:new Map([[root,3],[child,8],[third,10]])}}; const model={documentFormatVersion:1,projectId:root,complete:true,removedCanvasIds:[],canvases,elements:elements.map(element=>element.id==='a'?{...element,data:{...element.data,text:'generated'}}:element.id==='sibling'?{...element,data:{title:'old title',text:''}}:element),tombstones:{},versions:{[root]:3,[child]:9,[third]:9}}; const proposal=mergeFoundryRunContext(model,snapshot,registry,root);assert.equal(proposal.document.elements.get('a').data.text,'generated');assert.equal(proposal.document.elements.get('sibling').data.title,'newer title');assert.equal(proposal.versions.get(third),10); const invalid=structuredClone(model);invalid.elements.find(element=>element.id==='f').data.title='same-version change';assert.throws(()=>mergeFoundryRunContext(invalid,snapshot,registry,root),/same_version_context_conflict/); }); test('a saved successful AI admission recovers its RunRecord and later local configuration without another invocation',async()=>{ const f=fixture();f.failAdmissionCodec=true;const engine=await f.open();engine.startRun(runInput);await engine.settle(); const request=[...engine.getSnapshot().runtime.ledger.requests.values()][0];assert.equal(request.outcome,'applied');assert.equal(request.phase,'waiting');assert.equal(engine.getSnapshot().runtime.runs.size,0); const config={enabled:true,modelId:'text-model',prompt:'Local next prompt',parameters:{},propagate:false};const edit=engine.editor.dispatch({type:'element.data',target,patch:{ai:config}});await engine.settle();f.finish('already generated'); const session=await engine.prepareRecovery();assert.deepEqual(session.acknowledgedRequestIds,[request.id]);assert.deepEqual(session.pendingOperationIds,edit.operationIds);assert.ok(session.conflict); const plan=session.plan(new Map([[edit.operationIds[0],'keep-local']]));f.failAdmissionCodec=false;await session.resolve(plan);await engine.settle();await f.clock.advance(0);await engine.settle(); const current=engine.getSnapshot();assert.equal(current.runtime.runs.size,1);assert.equal([...current.runtime.runs.values()][0].cursor,4);assert.equal([...current.runtime.runs.values()][0].status,'succeeded');assert.equal(current.document.elements.get('a').data.text,'already generated');assert.deepEqual(current.document.elements.get('a').data.ai,config); assert.equal(f.calls.filter(call=>call.method==='POST'&&call.path.endsWith('/ai/runs')).length,1);assert.equal(current.runtime.ledger.requests.get(request.id).body,request.body);assert.equal(current.runtime.ledger.requests.get(request.id).phase,'resolved'); await engine.close();const restored=await f.open();assert.equal(restored.getSnapshot().runtime.runs.size,1);await restored.close(); }); test('saved successful cancel and apply receipts recover controls without sending the action again',async()=>{ for(const action of ['cancel','apply']){ const f=fixture(),engine=await f.open();engine.startRun(runInput);await engine.settle();const runId=[...engine.getSnapshot().runtime.runs.keys()][0]; if(action==='apply'){f.finish('retained output',true);await f.clock.advance(0);await engine.settle();} f.failActionCodec=true;if(action==='cancel')engine.cancelRun(runId);else engine.applyRunOutput(runId,target,id(999));await engine.settle(); const request=[...engine.getSnapshot().runtime.ledger.requests.values()].find(request=>request.kind==='run.'+action);assert.equal(request.outcome,'applied');assert.equal(request.phase,'waiting'); let edit;if(action==='apply'){edit=engine.editor.dispatch({type:'element.data',target,patch:{text:'later local'}});await engine.settle();f.move(child);} const session=await engine.prepareRecovery();assert.deepEqual(session.acknowledgedRequestIds,[request.id]); f.failActionCodec=false;if(edit){assert.ok(session.conflict);await session.resolve(session.plan(new Map([[edit.operationIds[0],'keep-local']])));}else await session.apply(); await engine.settle();await f.clock.advance(0);await engine.settle();const run=engine.getSnapshot().runtime.runs.get(runId); assert.equal(f.calls.filter(call=>call.method==='POST'&&call.path.endsWith('/'+action)).length,1);assert.equal(engine.getSnapshot().runtime.ledger.requests.get(request.id).body,request.body);assert.equal(run.controlEpoch,1); if(action==='cancel')assert.equal(run.status,'cancelled');else{assert.ok(run.appliedOutputIds.includes(id(999)));assert.equal(engine.getSnapshot().document.elements.get('a').data.text,'later local');assert.equal(engine.getSnapshot().document.elements.get('a').ownerCanvasId,child);const documentCall=f.calls.filter(call=>call.method==='POST'&&call.path.endsWith('/model/operations')).at(-1);assert.deepEqual(Object.keys(JSON.parse(documentCall.body).expected_versions),[child]);} await engine.close(); } }); test('historical AI success proofs bind immutable request, operation, run, target, version and frozen plan',async()=>{ for(const action of ['start','cancel','apply']){ const f=fixture(),engine=await f.open();engine.startRun(runInput);await engine.settle();const runId=[...engine.getSnapshot().runtime.runs.keys()][0]; if(action==='cancel'){engine.cancelRun(runId);await engine.settle();} if(action==='apply'){f.finish('retained',true);await f.clock.advance(0);await engine.settle();engine.applyRunOutput(runId,target,id(999));await engine.settle();} const request=[...engine.getSnapshot().runtime.ledger.requests.values()].find(request=>request.kind==='run.'+action),receipt=f.receipts.get(request.id),port=createFoundryRuns({workspaceId:workspace,rootId:root,registry,transport:f.transport}); const proof=port.verifyApplied(request,receipt);proof.model.assertCanonical(f.server()); if(action==='start'){const subset=structuredClone(receipt);delete subset.plan.frozen_versions[child];assert.ok(port.verifyApplied(request,subset).admission);const foreign=structuredClone(subset);foreign.plan.frozen_versions[id(9900)]=1;assert.throws(()=>port.verifyApplied(request,foreign),/run_identity_mismatch/);} const mutations=[body=>body.request_id=id(9991),body=>body.operation_id=id(9992),body=>body.action='different',body=>body.run_id=action==='start'?'invalid-run-id':id(9993)]; if(action==='start')mutations.push(body=>body.target_incarnation=2,body=>body.canvas_version++,body=>body.plan.frozen_versions[root]++); if(action==='apply')mutations.push(body=>body.target_incarnation=2,body=>body.canvas_version++,body=>body.output_id=id(9994),body=>body.board_id=child); for(const mutate of mutations){const invalid=structuredClone(receipt);mutate(invalid);assert.throws(()=>port.verifyApplied(request,invalid),/run_identity_mismatch|receipt/);} await engine.close(); } });