79 lines
5.8 KiB
TypeScript
79 lines
5.8 KiB
TypeScript
// A world git repo exercises commit reloads, old calls, registry defaults, and recovery.
|
|
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 } from '@earendil-works/pi-durable';
|
|
import { openStorage } from './sqlite';
|
|
import { loadExtensions, fs, git, exec } from './extensions';
|
|
const assert = (value:any,message:string)=>{if(!value)throw new Error(message);};
|
|
const until = async (test:()=>Promise<boolean>|boolean)=>{const deadline=Date.now()+15000;while(!await test()){if(Date.now()>deadline)throw new Error('Reload deadline');await new Promise(r=>setTimeout(r,0));}};
|
|
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'}}});
|
|
globalThis.shared={gate:Promise.withResolvers(),started:false,wait:false};
|
|
const loader=await loadExtensions(harness,registry,storage,models,context);
|
|
harness.resume();
|
|
const turn = async()=> {
|
|
faux.setResponses([fauxAssistantMessage([fauxToolCall('demo',{})],{stopReason:'toolUse'}),fauxAssistantMessage('done')]);
|
|
const submission=await root.submit({type:'input',content:'call demo'},context);
|
|
await submission.wait(context);
|
|
const entries=(await root.context(context)).entries;
|
|
const result=entries.filter((entry:any)=>entry.kind==='pi.tool-result').at(-1);
|
|
return JSON.stringify(result);
|
|
};
|
|
try {
|
|
if(globalThis.extensionRestart) {
|
|
assert(loader.state().error,'Broken tip must report its error');
|
|
assert(await fs.read('/src/sausage/.git/sausage-last-good')===globalThis.extensionGood,'Last good must survive');
|
|
assert((await turn()).includes('result C'),'Restart must use C');
|
|
} else {
|
|
const env = await exec(['env'],{cwd:'/',env:[['EXTENSION_OPTION','yes']],conversations:['1'],timeout:3});
|
|
assert(env.code===0 && env.stdout.includes('EXTENSION_OPTION=yes'),'Exec environment');
|
|
await fs.write('/src/sausage/temporary','one');await fs.append('/src/sausage/temporary','two');
|
|
await fs.rename('/src/sausage/temporary','/src/sausage/renamed');
|
|
assert(await fs.read('/src/sausage/renamed')==='onetwo','File mutation');
|
|
assert((await fs.lstat('/src/sausage/renamed')).kind==='file','lstat');await fs.remove('/src/sausage/renamed');
|
|
const source=(version:string)=>`import {defineExtension,defineTool} from '@earendil-works/pi-durable';
|
|
import {obj} from '@earendil-works/pi-schema';
|
|
export default host=>{if(defineTool!==globalThis.__sausage.modules['@earendil-works/pi-durable'].defineTool)throw new Error('Duplicate durable instance');host.implement({name:'demo.service'},{value:()=> '${version}'});
|
|
globalThis.shared.handle=host.resource('handle',()=>host.need({name:'demo.service'}));
|
|
return defineExtension({name:'demo',tools:[defineTool({name:'demo',description:'Demo',parameters:obj({}),execute:async()=>{if(globalThis.shared.wait){globalThis.shared.started=true;await globalThis.shared.gate.promise;}return {output:[{type:'text',text:'result ${version}'}]};}})]});};`;
|
|
await fs.mkdir('/src/sausage/extensions');await fs.mkdir('/src/sausage/extensions/demo');
|
|
await fs.write('/src/sausage/extensions/demo/server.ts',source('A'));
|
|
await git('/src/sausage','add','.');await git('/src/sausage','commit','-m','A');
|
|
const a=await git('/src/sausage','rev-parse','HEAD');
|
|
await until(()=>loader.state().good===a);
|
|
assert((await turn()).includes('result A'),'Existing conversation must see A');
|
|
const held=globalThis.shared.handle;
|
|
assert(held.value()==='A','A service');
|
|
const retainedMethod=held.value;
|
|
await fs.write('/src/sausage/extensions/demo/server.ts','syntax !!! error');
|
|
await git('/src/sausage','add','.');await git('/src/sausage','commit','-m','B');
|
|
const b=await git('/src/sausage','rev-parse','HEAD');
|
|
await until(()=>loader.state().current===b && !!loader.state().error);
|
|
assert(loader.state().good===a,'Broken B must keep A');
|
|
assert(JSON.parse(await fs.read('/proc/extensions')).error,'Status must report B');
|
|
assert(held.value()==='A','A service remains');
|
|
assert((await turn()).includes('result A'),'A tool remains after broken B');
|
|
globalThis.shared.wait=true;
|
|
const running=turn();await until(()=>globalThis.shared.started);
|
|
await fs.write('/src/sausage/extensions/demo/server.ts',source('C'));
|
|
await git('/src/sausage','add','.');await git('/src/sausage','commit','-m','C');
|
|
const c=await git('/src/sausage','rev-parse','HEAD');
|
|
await until(()=>loader.state().good===c);
|
|
assert(held.value()==='C','A need handle must resolve C');
|
|
assert(retainedMethod()==='C','A retained method must resolve C');
|
|
globalThis.shared.wait=false;globalThis.shared.gate.resolve();
|
|
assert((await running).includes('result A'),'Old call must finish on A');
|
|
assert((await turn()).includes('result C'),'Next call must use C');
|
|
await git('/src/sausage','update-ref','refs/heads/main',b);
|
|
await until(()=>loader.state().current===b && !!loader.state().error);
|
|
assert((await exec(['extensions-revert'],{cwd:'/'})).code===0,'Revert command');
|
|
await until(()=>loader.state().current===c && !loader.state().error);
|
|
await git('/src/sausage','update-ref','refs/heads/main',b);
|
|
await until(()=>loader.state().current===b && !!loader.state().error);
|
|
await fs.write('/src/sausage/good.txt',c);
|
|
}
|
|
} finally {globalThis.shared.gate.resolve();await loader.close();await harness.close(context);}
|