Files
Endoleap/tests/headless/service.test.mjs

162 lines
18 KiB
JavaScript

import { test } from 'node:test';
import assert from 'node:assert/strict';
import { readFile } from 'node:fs/promises';
import { createDocument, createRegistry, builtinComponents, applyChanges } from '../../src/lib/canvas-core/index.ts';
import { createLedger } from '../../src/lib/canvas-runtime/ledger.ts';
import { createFoundryPersistence, createFoundryTransport, loadFoundryModel } from '../../src/lib/canvas-service/foundry.ts';
const root='10000000-0000-4000-8000-000000000001',child='10000000-0000-4000-8000-000000000002',workspace='10000000-0000-4000-8000-000000000003',account='10000000-0000-4000-8000-000000000004';
const operationId='90000000-0000-4000-8000-000000000001',requestId='90000000-0000-4000-8000-000000000002';
const registry=createRegistry(builtinComponents());
const node=(id,owner=root)=>({id,incarnation:1,ownerCanvasId:owner,componentId:'text',dataVersion:1,structure:'node',data:{title:id,text:'preserved'},geometry:{x:0,y:0,width:260,height:200,rotation:0}});
const a=node('a'),f={...node('f'),componentId:'frame'},future={...node('future',child),componentId:'plugin.future',dataVersion:9,data:{custom:{keep:[1,'x']}},extension:{retained:true}};
const canvases=[{id:root,parent:null},{id:child,parent:{canvasId:root,entryElementId:'f'}}];
const document=createDocument(root,canvases,[a,f,future]);
const snapshot={revision:1,rootRevision:1,document,history:{undo:[],redo:[],entries:new Map()},operations:new Map(),runtime:{acknowledged:document,versions:new Map([[root,3],[child,7]]),ledger:createLedger().getSnapshot(),receipts:new Map(),conflicts:new Map()}};
const intent={id:operationId,operationType:'document.change',operationSchemaVersion:1,sequence:1,origin:'user',canvasIds:[root],dependencies:[],changes:[{kind:'field',target:{id:'a',incarnation:1},field:'data.title',before:'a',after:'renamed',beforePresent:true,afterPresent:true}]};
const chunk=(complete=true)=>({documentFormatVersion:1,projectId:root,canvases:complete?canvases:[canvases[0]],elements:complete?[a,f,future]:[{...a,data:{...a.data,title:'renamed'}},f],tombstones:{},versions:complete?{[root]:3,[child]:7}:{[root]:4},complete,removedCanvasIds:[]});
function setup(transport=async()=>({status:200,body:{protocol_version:2,request_id:requestId,status:'applied',operation_ids:[operationId],model:chunk(false)}})){
const port=createFoundryPersistence({workspaceId:workspace,rootId:root,registry,transport});
const compiled=port.compile(intent,requestId,snapshot),ledger=createLedger();ledger.add(intent,{a:'exists'});
const request=ledger.freeze({id:requestId,operationIds:[operationId],...compiled});return{port,request,compiled};
}
test('initial model read is explicit about migration/coverage and owns opaque records',async()=>{
const calls=[];
const required=await loadFoundryModel(async request=>{calls.push(request);return{status:409,body:{error:'canonical_migration_required',document_format_version:1,root_id:root,source_versions:{[root]:1}}}},workspace,root,registry);
assert.equal(required.status,'migration-required');assert.equal(required.sourceVersions.get(root),1);assert.equal(calls[0].method,'GET');
const source=chunk(),loaded=await loadFoundryModel(async()=>({status:200,body:{model:source}}),workspace,child,registry);
assert.equal(loaded.document.projectId,root);assert.deepEqual(loaded.document.elements.get('future'),future);
assert.equal(Object.isFrozen(source.elements[0]),false);source.elements[0]={...a,data:{title:'caller mutation',text:''}};
assert.equal(loaded.document.elements.get('a').data.title,'a');
await assert.rejects(()=>loadFoundryModel(async()=>({status:200,body:{model:chunk(false)}}),workspace,root,registry),/incomplete_initial_model/);
});
test('compiler freezes protocol 2 conditional changes and transports the exact original bytes',async()=>{
const calls=[],{port,request,compiled}=setup(async input=>{calls.push(input);return{status:200,body:{protocol_version:2,request_id:requestId,status:'applied',operation_ids:[operationId],model:chunk(false)}}});
assert.equal(compiled.protocolVersion,2);assert.equal(request.protocolVersion,2);
const body=JSON.parse(request.body);assert.deepEqual(body.operations,[{id:operationId,changes:intent.changes}]);assert.equal(Object.hasOwn(body,'document'),false);
assert.deepEqual(body,JSON.parse(await readFile(new URL('./fixtures/foundry-model-operation-v2.json',import.meta.url),'utf8')));
const originalBytes=' \n'+request.body+' ',frozen={...request,body:originalBytes};
assert.equal((await port.submit(frozen,new AbortController().signal)).outcome,'applied');
assert.equal(calls[0].body,originalBytes);assert.equal(calls[0].requestId,requestId);
await port.query(frozen,new AbortController().signal);assert.equal(calls[1].method,'GET');assert.equal(calls[1].body,undefined);assert.ok(calls[1].path.endsWith('/'+requestId));
});
test('only explicit identity-matched terminal proof is zero-write; bare 4xx stays unresolved and blocked',async()=>{
for(const [response,outcome,blocked]of[
[{status:409,body:{protocol_version:2,request_id:requestId,status:'not-applied',terminal:true,error:'version_conflict'}},'not-applied',false],
[{status:409,body:{error:'idempotency_conflict'}},'unresolved',true],
[{status:403,body:{error:'forbidden'}},'unresolved',true],
[{status:404,body:{status:'unknown',request_id:requestId}},'unresolved',false],
]){
const{port,request}=setup(async()=>response);const status=await port.query(request,new AbortController().signal);
assert.equal(status.outcome,outcome);assert.equal(!!status.blocked,blocked);
}
});
test('partial canonical receipts replace participants and retain unrelated future components and versions',async()=>{
const{port,request}=setup();const status=await port.submit(request,new AbortController().signal),proposal=port.canonical(request,status,snapshot);
assert.equal(proposal.document.elements.get('a').data.title,'renamed');assert.deepEqual(proposal.document.elements.get('future'),future);assert.equal(proposal.versions.get(child),7);assert.equal(proposal.versions.get(root),4);
const broken=structuredClone(status);broken.canonical.model.versions={[root]:4,[child]:8};
assert.throws(()=>port.canonical(request,broken,snapshot),/invalid_canonical_response|receipt_coverage_mismatch/);
const wrong=structuredClone(status);wrong.canonical.model.elements.push({...future,ownerCanvasId:root});
assert.throws(()=>port.canonical(request,wrong,snapshot),/canonical_scope_mismatch/);
assert.equal(snapshot.runtime.acknowledged.elements.get('a').data.title,'a');
});
test('missing logical coverage and altered frozen metadata do not masquerade as successful requests',async()=>{
const calls=[],{port,request}=setup(async input=>{calls.push(input);return{status:200,body:{protocol_version:2,request_id:requestId,status:'applied',operation_ids:[],model:chunk(false)}}});
const incomplete=await port.submit(request,new AbortController().signal);assert.equal(incomplete.outcome,'unresolved');assert.equal(incomplete.blocked,true);assert.equal(incomplete.error,'receipt_coverage_mismatch');
const corrupt=await port.submit({...request,expectedVersions:{[root]:99}},new AbortController().signal);
assert.equal(corrupt.blocked,true);assert.equal(calls.length,1);assert.equal(corrupt.error,'frozen_request_contract_mismatch');
});
test('new canvases carry an explicit absent version without confusing format/protocol/document clocks',()=>{
const canvas='10000000-0000-4000-8000-000000000005',{port}=setup();
const create={...intent,canvasIds:[root,canvas],changes:[{kind:'canvas',id:canvas,before:null,after:{id:canvas,parent:{canvasId:root,entryElementId:'f'}}}]};
const compiled=port.compile(create,requestId,snapshot);assert.equal(compiled.expectedVersions[canvas],0);assert.equal(compiled.expectedVersions[root],3);assert.equal(compiled.protocolVersion,2);
const ledger=createLedger();ledger.add(create,{});assert.equal(ledger.freeze({id:requestId,operationIds:[operationId],...compiled}).expectedVersions[canvas],0);
});
test('browser transport fences identity changes and never rebuilds a frozen body',async()=>{
let current=true;const calls=[];
const transport=createFoundryTransport({accountId:account,isCurrentIdentity:()=>current,fetch:async(url,init)=>{calls.push([url,init]);return new Response('{"status":"unknown"}',{status:404,headers:{'Content-Type':'application/json'}})}});
await transport({path:'fixture',method:'POST',requestId,body:' {"literal":"bytes"} '});
assert.equal(calls[0][1].headers['X-Expected-Account'],account);assert.equal(calls[0][1].body,' {"literal":"bytes"} ');assert.equal(calls[0][1].headers['Idempotency-Key'],requestId);
current=false;await assert.rejects(()=>transport({path:'fixture',method:'GET'}),/identity_changed/);assert.equal(calls.length,1);
const late=createFoundryTransport({accountId:account,isCurrentIdentity:()=>current,fetch:async()=>{current=false;return new Response('{}')}});current=true;
await assert.rejects(()=>late({path:'fixture',method:'GET'}),/identity_changed/);
});
test('shared service transport binds the immutable AI admission version header',async()=>{
const calls=[];
const transport=createFoundryTransport({accountId:account,isCurrentIdentity:()=>true,fetch:async(url,input)=>{calls.push({url,input});return new Response('{}',{status:200,headers:{'Content-Type':'application/json'}})}});
await transport({method:'POST',path:'fixture',body:' {"frozen": true} ',requestId,expectedVersion:7});
assert.equal(calls[0].input.headers['If-Match'],'"7"');assert.equal(calls[0].input.headers['Idempotency-Key'],requestId);assert.equal(calls[0].input.body,' {"frozen": true} ');
for(const value of [0,-1,1.5,NaN,Number.MAX_SAFE_INTEGER+1])await assert.rejects(()=>transport({method:'POST',path:'fixture',expectedVersion:value}),/invalid_server_version/);
assert.equal(calls.length,1);
});
test('recovery reader requires a complete canonical model for its bound project and forwards cancellation',async()=>{
const controller=new AbortController(),calls=[];
const{port}=setup(async request=>{calls.push(request);return{status:200,body:{model:chunk()}};});
const loaded=await port.read(controller.signal);assert.equal(loaded.document.projectId,root);assert.equal(loaded.versions.get(child),7);
assert.equal(calls[0].method,'GET');assert.equal(calls[0].signal,controller.signal);assert.equal(calls[0].body,undefined);assert.equal(calls[0].requestId,undefined);
for(const[response,error]of[
[{status:200,body:{model:chunk(false)}},/incomplete_initial_model/],
[{status:409,body:{error:'canonical_migration_required',document_format_version:1,root_id:root,source_versions:{[root]:1}}},/canonical_migration_required/],
[{status:200,body:{model:{documentFormatVersion:1,projectId:child,canvases:[{id:child,parent:null}],elements:[],tombstones:{},versions:{[child]:1},complete:true,removedCanvasIds:[]}}},/project_mismatch/],
[{status:403,body:{error:'forbidden'}},/forbidden/],
]){const{port:invalid}=setup(async()=>response);await assert.rejects(invalid.read(controller.signal),error);}
});
test('historical applied proof validates frozen identity and coverage, retains newer reads and rejects same-version divergence',async()=>{
const{port,request}=setup(),status=await port.submit(request,new AbortController().signal),proof=port.verifyApplied(request,status.canonical),proposal=port.canonical(request,status,snapshot);
proof.assertCanonical(proposal);assert.equal(proof.versions.get(root),4);assert.equal(proof.versions.set,undefined);
const changed=createDocument(root,canvases,[{...a,data:{...a.data,title:'later remote'}},f,future]);
proof.assertCanonical({document:changed,versions:new Map([[root,5],[child,7]])});
assert.throws(()=>proof.assertCanonical({document:changed,versions:new Map([[root,4],[child,7]])}),/canonical_version_mismatch/);
assert.throws(()=>proof.assertCanonical({document,versions:new Map([[root,3],[child,7]])}),/recovery_read_behind_receipt/);
for(const mutate of[
body=>body.request_id=account,body=>body.operation_ids=[account],body=>body.protocol_version=99,body=>body.status='accepted',body=>body.model.complete=true,body=>body.model.versions[root]=5,
]){const body=structuredClone(status.canonical);mutate(body);assert.throws(()=>port.verifyApplied(request,body),/receipt|canonical/);}
const tombstone=structuredClone(status.canonical);tombstone.model.tombstones={deleted:2};const lifecycle=port.verifyApplied(request,tombstone);
assert.throws(()=>lifecycle.assertCanonical({document:changed,versions:new Map([[root,5],[child,7]])}),/stale_canonical_lifecycle/);
});
test('ordered batch freezes every logical identity, conditional change and participant version, requiring complete receipt coverage', async () => {
const secondId='90000000-0000-4000-8000-000000000003';
const second={...intent,id:secondId,sequence:2,dependencies:[intent.id],changes:[{...intent.changes[0],before:'renamed',after:'final'}]};
const calls=[],port=createFoundryPersistence({workspaceId:workspace,rootId:root,registry,transport:async input=>{calls.push(input);return{status:200,body:{protocol_version:2,request_id:requestId,status:'applied',operation_ids:[intent.id,second.id],model:chunk(false)}}}});
const compiled=port.compileBatch([intent,second],requestId,snapshot),ledger=createLedger();ledger.add(intent,{a:'exists'});ledger.add(second,{a:'exists'});
const request=ledger.freeze({...compiled,id:requestId,operationIds:[intent.id,second.id]});
assert.deepEqual(JSON.parse(request.body).operations,[intent,second].map(({id,changes})=>({id,changes})));
assert.deepEqual(request.expectedVersions,{[root]:3});assert.equal((await port.submit(request,new AbortController().signal)).outcome,'applied');assert.equal(calls[0].body,request.body);
const invalid=createFoundryPersistence({workspaceId:workspace,rootId:root,registry,transport:async()=>({status:200,body:{protocol_version:2,request_id:requestId,status:'applied',operation_ids:[second.id],model:chunk(false)}})});
assert.equal((await invalid.query(request,new AbortController().signal)).error,'receipt_coverage_mismatch');
assert.throws(()=>port.compileBatch([second,intent],requestId,snapshot),/invalid_operation_order/);
assert.throws(()=>port.compileBatch([intent,intent],requestId,snapshot),/operation_interpreter_unavailable/);
});
test('Foundry compiles the proven projection without changing logical intents or receipt coverage',async()=>{
const middle='temporary '.repeat(100),secondId='90000000-0000-4000-8000-000000000003';
const first={...intent,changes:[{...intent.changes[0],after:middle}]},second={...intent,id:secondId,sequence:2,dependencies:[first.id],changes:[{...intent.changes[0],before:middle,after:'final'}]},ledger=createLedger();
ledger.add(first,{a:'exists'});ledger.add(second,{a:'exists'});
const port=createFoundryPersistence({workspaceId:workspace,rootId:root,registry,transport:async()=>({status:200,body:{protocol_version:2,request_id:requestId,status:'applied',operation_ids:[first.id,second.id],model:chunk(false)}})});
const compiled=port.compileBatch([first,second],requestId,{...snapshot,runtime:{...snapshot.runtime,ledger:ledger.getSnapshot()}}),request=ledger.freeze({...compiled,id:requestId,operationIds:[first.id,second.id]}),wire=JSON.parse(request.body).operations;
assert.ok(JSON.stringify(wire).length<JSON.stringify([first,second].map(({id,changes})=>({id,changes}))).length);
assert.equal(applyChanges(document,wire.flatMap(op=>op.changes)).elements.get('a').data.title,'final');
assert.equal(ledger.getSnapshot().operations.get(first.id).intent.changes[0].after,middle);
assert.deepEqual(wire.map(op=>op.id),request.operationIds);assert.equal((await port.submit(request,new AbortController().signal)).outcome,'applied');
});
test('Foundry receipt summaries are opt-in, retain execution metadata and cannot masquerade as raw recovery proofs',()=>{
const{request,port:rawPort}=setup();assert.equal(rawPort.compactResolvedReceipt,undefined);const port=createFoundryPersistence({workspaceId:workspace,rootId:root,registry,transport:async()=>{},resolvedReceiptPolicy:'summary'}),receipt={protocol_version:2,request_id:requestId,status:'applied',operation_ids:[operationId],model:chunk(false)},applied=port.canonical(request,{outcome:'applied',canonical:receipt},snapshot),resolved={...request,phase:'resolved',outcome:'applied'},summary=port.compactResolvedReceipt(resolved,receipt,applied);assert.strictEqual(port.compactResolvedReceipt(request,receipt,applied),receipt);assert.deepEqual(summary,{localReceiptFormat:'foundry.reconciled-document',localReceiptVersion:1,protocol_version:2,request_id:requestId,status:'applied',operation_ids:[operationId],projectId:root,versions:{[root]:4}});assert.equal(Object.hasOwn(summary,'model'),false);assert.equal(receipt.model.elements.length,2);assert.throws(()=>port.verifyApplied(request,summary),/receipt_already_reconciled/);assert.throws(()=>port.canonical(request,{outcome:'applied',canonical:summary},snapshot),/receipt_already_reconciled/);assert.throws(()=>port.verifyApplied(request,{...summary,localReceiptVersion:2}),/receipt_summary_interpreter_unavailable/);assert.deepEqual([...port.verifyApplied(request,receipt).versions],[[root,4]]);
});
test('summary policy preserves opaque, future and unfamiliar receipt data unchanged',()=>{
const{request}=setup(),port=createFoundryPersistence({workspaceId:workspace,rootId:root,registry,transport:async()=>{},resolvedReceiptPolicy:'summary'}),raw={protocol_version:2,request_id:requestId,status:'applied',operation_ids:[operationId],model:chunk(false)},applied=port.canonical(request,{outcome:'applied',canonical:raw},snapshot),cases=[{...raw,extension:{keep:true}},{...raw,protocol_version:3},{...raw,model:{...raw.model,documentFormatVersion:2}},{...raw,model:{...raw.model,futureField:'keep'}},{...raw,model:{...raw.model,elements:[...raw.model.elements,{...future,id:'opaque',ownerCanvasId:root}]}},{...raw,model:{...raw.model,elements:[{...a,extension:{keep:true}},f]}}];for(const value of cases)assert.strictEqual(port.compactResolvedReceipt({...request,phase:'resolved',outcome:'applied'},value,applied),value);assert.strictEqual(port.compactResolvedReceipt({...request,kind:'run.apply'},raw,applied),raw);assert.throws(()=>createFoundryPersistence({workspaceId:workspace,rootId:root,registry,transport:async()=>{},resolvedReceiptPolicy:'invalid'}),/invalid_receipt_policy/);
});