Files
sausage/host/js/plan-todos-test.ts
T

152 lines
13 KiB
TypeScript

/** The six Ultimate plan/todos cases, plus real-loader turns and branch reset. */
import { BACKGROUND_CONTEXT as context } from '@earendil-works/chord/context';
import { createModels } from '@earendil-works/pi-ai/models';
import { fauxProvider, fauxAssistantMessage, fauxToolCall } from '@earendil-works/pi-ai/providers/faux';
import { Harness, createRegistry, defineExtension } from '@earendil-works/pi-durable';
import { openStorage } from './sqlite';
import { loadExtensions, fs, Metadata } from './extensions';
const assert = (value:any, message:string) => { if (!value) throw new Error(message); };
const equal = (actual:any, expected:any) => assert(JSON.stringify(actual) === JSON.stringify(expected), `Mismatch: ${JSON.stringify(actual)} != ${JSON.stringify(expected)}`);
const rejects = async (call:()=>Promise<any>, text:string) => { try { await call(); } catch(error) { assert(String(error).includes(text), String(error)); return; } throw new Error(`Expected ${text}`); };
const models = createModels(), faux = fauxProvider({tokensPerSecond:100000}); models.setProvider(faux.provider);
const registry = createRegistry(), storage = await openStorage(globalThis.storagePath);
const harness = await Harness.open(storage,{models,registry,settings:{get extensions(){return registry.snapshot().installed();}}},context);
const root = await harness.root(context,{agent:{model:{provider:'faux',modelId:'faux-1'}}});
const loader = await loadExtensions(harness,registry,storage,models,context);
assert(!loader.state().error, loader.state().error);
const base = `file:///run/sausage/extensions/${loader.state().good}/extensions`;
const planModule = await import(/* @vite-ignore */ `${base}/plan/server.ts`);
const todosModule = await import(/* @vite-ignore */ `${base}/todos/server.ts`);
const {Plan,TaskIndex,Branch,Started} = planModule;
const {Todos,Standing,add,mark,noteFor,DONE_LIMIT} = todosModule;
const call = async (args:any, id=root.id, name='plan') => {
const conversation = await harness.conversation(id,context);
await conversation.configure({model:{provider:'faux',modelId:'faux-1'},modelTools:{add:[todosModule.default().tools[0]]}},context);
faux.setResponses([fauxAssistantMessage([fauxToolCall(name,args)],{stopReason:'toolUse'}),fauxAssistantMessage('Complete')]);
await (await conversation.submit({type:'input',content:'Update ledger'},context)).wait(context);
await harness.waitForIdle(context);
return (await conversation.context(context)).entries.filter((entry:any)=>entry.kind==='pi.tool-result').at(-1).model[0];
};
const text = (result:any)=>result.content.map((block:any)=>block.text).join('');
harness.resume();
try {
if (globalThis.planTodosRestart) {
const saved = JSON.parse(await fs.read('/saved'));
equal(await harness.snapshot(Plan,root.id,context),saved.plan);
equal(await harness.snapshot(TaskIndex,root.id,context),saved.index);
equal(await harness.snapshot(Todos,root.id,context),saved.todos);
assert(!(await call({action:'show'})).isError,'Plan survives restart');
} else {
// Plan case 1: descendants, verdicts, memo replay, transactional receipts, nested namespaces.
const extension = planModule.default({metadata:Metadata});
const tool = extension.tools[0], replayResults:any[] = [], nestedResults:any[] = [];
let childId:number, grandchildId:number;
registry.install(defineExtension({...extension,tools:[{...tool,execute:async(args:any,api:any,ctx:any)=>{
if(args.title==='Nested') {
const results=[];
for(const namespace of ['first','second']) {
const nestedApi={...api,memo:(name:string,value:any,memoContext?:any)=>memoContext===undefined?api.memo(`${namespace}/${name}`,ctx):api.memo(`${namespace}/${name}`,value,memoContext)};
const first=await tool.execute(args,nestedApi,ctx);
const replayApi={...nestedApi,memo:(name:string,...rest:any[])=>name==='result'&&rest.length===1?Promise.resolve(undefined):nestedApi.memo(name,...rest)};
nestedResults.push([first,await tool.execute(args,replayApi,ctx)]);results.push(first);
}
return results[1];
}
const result=await tool.execute(args,api,ctx);
if(api.conversationId===childId && grandchildId===undefined) grandchildId=await api.commit(async(tx:any)=>(await tx.createConversation({ownership:{kind:'task',taskId:api.taskId}})).id,ctx);
if(args.action==='create') childId=await api.commit(async(tx:any)=>(await tx.createConversation({ownership:{kind:'task',taskId:api.taskId}})).id,ctx);
if(args.action==='add') {
const replayApi={...api,memo:(name:string,...rest:any[])=>name==='result'&&rest.length===1?Promise.resolve(undefined):api.memo(name,...rest)};
replayResults.push([result,await tool.execute(args,api,ctx),await tool.execute(args,replayApi,ctx)]);
}
return result;
}}]}));
await call({action:'create',title:'Ship ledger',body:'## Request\nDurable plans'});
await call({action:'add',epic:'E1',title:'Implement',role:'worker',description:'Build it'});
await call({action:'add',epic:'E1',title:'Review',role:'reviewer',description:'Check it',deps:['E1.1']});
await call({action:'update',key:'E1.1',status:'in_review',child:42,verdict:'Checks passed'});
await call({action:'note',key:'E1.1',text:'Checks passed'});
const saved=await harness.snapshot(Plan,root.id,context), task=saved.epics.E1.tasks['E1.1'];
equal(Object.keys(saved.epics.E1.tasks),['E1.1','E1.2']);
assert(saved.next===2 && task.status==='in_review' && task.child===42 && task.verdict==='Checks passed','Saved verdict');
assert(task.notes[0].text==='Checks passed' && typeof task.notes[0].at==='string','Saved note');
equal(saved.epics.E1.tasks['E1.2'].deps,['E1.1']);
assert(saved.epics.E1.tasks['E1.2'].role==='reviewer','Reviewer role');
assert(replayResults.length===2,'Two replayed additions');
for(const [result,memo,receipt] of replayResults){equal(memo,result);equal(receipt,result);}
for(const key of ['E1','E1.1']) equal(JSON.parse(text(await call({action:'show',key},childId!))),key==='E1'?saved.epics.E1:task);
for(const args of [{action:'create',title:'Forbidden',body:'No'},{action:'add',epic:'E1',title:'Forbidden',role:'worker',description:'No'},{action:'update',key:'E1.1',status:'done'},{action:'note',key:'E1.1',text:'No'}]) {
const denied=await call(args,childId!);assert(denied.isError && text(denied).includes(`Only conversation ${root.id} edits this plan`),'Descendant write refusal');
}
equal(JSON.parse(text(await call({action:'show',key:'E1.1'},grandchildId!))),task);
assert((await call({action:'note',key:'E1.1',text:'No'},grandchildId!)).isError,'Grandchild write refusal');
equal(await harness.snapshot(Plan,root.id,context),saved);
for(const args of [{action:'update',key:'E1.1',body:'No'},{action:'update',key:'E1',status:'todo'},{action:'show',key:'E99'}]) assert((await call(args)).isError,'Invalid plan edit');
equal(await harness.snapshot(Plan,root.id,context),saved);
await call({action:'update',key:'E1',status:'done',verdict:'Approved'});await call({action:'show'});
await call({action:'add',epic:'E1',title:'Nested',role:'worker',description:'Separate calls'});
assert(nestedResults.length===2 && JSON.stringify(nestedResults[0][0])!==JSON.stringify(nestedResults[1][0]),'Separate nested calls');
for(const [first,replay] of nestedResults)equal(first,replay);
equal(Object.keys((await harness.snapshot(Plan,root.id,context)).epics.E1.tasks),['E1.1','E1.2','E1.3','E1.4']);
registry.install(extension);
// Plan case 2: the child index tracks title changes and link replacement.
await harness.commit(async(tx:any)=>Object.assign(await tx.doc(Plan,root.id),Plan.definition.initial()),context);
const [first,second]=await harness.commit(async(tx:any)=>[(await tx.createConversation({ownership:{kind:'ownerless'}})).id,(await tx.createConversation({ownership:{kind:'ownerless'}})).id],context);
await call({action:'create',title:'Index',body:'Body'});
await call({action:'add',epic:'E1',title:'Build it',role:'worker',description:'D'});
await call({action:'update',key:'E1.1',child:first});
const index=async()=>(await harness.snapshot(TaskIndex,root.id,context)).children;
const title=async(id:number)=>(await harness.snapshot(Metadata,id,context)).title;
equal(await index(),{[first]:{coordinator:root.id,epic:'E1',key:'E1.1',title:'Build it',status:'backlog'}});equal(await title(first),'Build it');
await call({action:'update',key:'E1.1',status:'in_progress',title:'Build it well'});
assert((await index())[first].status==='in_progress','Index status');equal(await title(first),'Build it well');
await harness.commit(async(tx:any)=>{(await tx.doc(Metadata,first)).title='Renamed by hand';},context);
await call({action:'note',key:'E1.1',text:'Progress'});equal(await title(first),'Renamed by hand');
await call({action:'update',key:'E1.1',child:second});equal(Object.keys(await index()),[String(second)]);equal(await title(second),'Build it well');
// A new lineage branch clears the inherited plan in its first input commit.
const at=await root.commit((tx:any)=>tx.appendEntry(root.id,{kind:'branch.origin',data:{}}),context);
const branch=await root.commit(async(tx:any)=>{const record=await tx.forkConversation(root.id,at.id,{ownership:{kind:'ownerless'}});(await tx.doc(Branch,record.id)).branch={from:root.id,at:at.id,intent:'Fresh'};return record;},context);
assert(Object.keys((await harness.snapshot(Plan,branch.id,context)).epics).length===1,'Fork inherited plan');
equal(JSON.parse(text(await call({action:'show'},branch.id))),Plan.definition.initial());
assert((await harness.snapshot(Started,branch.id,context)).started,'Branch started');
// Todos cases 1-4 retain the original small transaction fixture.
const ULTIMATE=5;
const fakeTx=()=>{const docs=new Map();return {doc:async({definition:def}:any,id:number)=>{const key=`${def.kind}:${id}`;if(!docs.has(key))docs.set(key,def.kind==='ultimate.standing-agents'?{ultimate:ULTIMATE}:def.initial());return docs.get(key);}};};
const list=async(tx:any)=>(await tx.doc(Todos,1)).items;
{
const tx=fakeTx();equal((await add(tx,' Deploy fleet ','ultimate')).text,'Deploy fleet');
assert((await add(tx,'Rotate the key','user')).id===2,'Sequential todo ids');
await rejects(()=>add(tx,' ','user'),'nonempty');await rejects(()=>add(tx,'x','lander'),'owner');equal((await list(tx)).map((todo:any)=>todo.id),[1,2]);
}
{
const tx=fakeTx();for(let i=0;i<DONE_LIMIT+2;i++)await add(tx,`t${i}`,'ultimate');
assert(typeof(await mark(tx,1,true)).done==='number','Tick');assert((await mark(tx,1,false)).done===undefined,'Reopen');
for(const todo of (await list(tx)).slice(0,-1))todo.done=todo.id;
await mark(tx,DONE_LIMIT+2,true);equal((await list(tx)).map((todo:any)=>todo.id),Array.from({length:DONE_LIMIT},(_,i)=>i+3));await rejects(()=>mark(tx,999,true),'No to-do 999');
}
{
const tx=fakeTx(), content=async()=>(await noteFor(tx,ULTIMATE,0))?.entries[0].model[0].content;
assert(await content()===undefined,'Empty note');await add(tx,'Mine','ultimate');await add(tx,'Theirs','user');
equal(await content(),"Open to-dos (todo tool: done when finished):\nYours:\n- 1: Mine\nThe user's:\n- 2: Theirs");assert(await content()===undefined,'Unchanged note');
await mark(tx,1,true);await mark(tx,2,true);equal(await content(),'No to-dos are open.');assert(await content()===undefined,'Unchanged empty note');
}
{
const tx=fakeTx(), extension=todosModule.default(), tool=extension.tools[0];
const run=(conversationId:number,args:any)=>tool.execute(args,{conversationId,commit:(fn:any)=>fn(tx)},context);
await rejects(()=>run(9,{action:'list'}),'Only @ultimate');equal(await run(ULTIMATE,{action:'add',text:'Ship it',owner:'user'}),{output:[{type:'text',text:'Added 1 for user: Ship it'}]});
equal(await run(ULTIMATE,{action:'list'}),{output:[{type:'text',text:"The user's:\n- 1: Ship it"}]});
const before=(conversationId:number)=>extension.hooks[0].handlers.beforeInput({}, {tx,conversationId},context);
assert(await before(9)===undefined,'Nonstanding note');assert(await before(ULTIMATE)!==undefined,'Standing note');
}
// The real world also calls todo through the loaded factory and places its note.
await harness.commit(async(tx:any)=>{(await tx.doc(Standing,root.id)).ultimate=root.id;},context);
assert(text(await call({action:'add',text:'Real world',owner:'ultimate'},root.id,'todo')).includes('Added 1'),'Loaded todo tool');
assert(text(await call({action:'list'},root.id,'todo')).includes('Real world'),'Loaded todo list');
assert((await root.context(context)).entries.some((entry:any)=>entry.kind==='todos.note'),'Real beforeInput note');
await fs.write('/saved',JSON.stringify({plan:await harness.snapshot(Plan,root.id,context),index:await harness.snapshot(TaskIndex,root.id,context),todos:await harness.snapshot(Todos,root.id,context)}));
}
} finally {await loader.close();await harness.close(context);}