Files
sausage/host/js/extensions-test.ts
T

75 lines
5.5 KiB
TypeScript

// File edits replace complete sets. Running calls and host-side recovery retain valid code.
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 } 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,loads:[]};
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;
return JSON.stringify(entries.filter((entry:any)=>entry.kind==='pi.tool-result').at(-1));
};
const source=(version:string,paired=false)=>`import {defineExtension,defineTool} from '@earendil-works/pi-durable';
import {obj} from '@earendil-works/pi-schema';
${paired?"import {value} from './value.ts';":""}
export default host=>{globalThis.shared.loads.push('${version}');
${paired?`if(value!=='${version}')throw new Error('Mixed file set');`:''}
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}'}]};}})]});};`;
try {
if(globalThis.extensionPhase!=='edits') {
const broken=globalThis.extensionPhase==='broken';
assert(!!loader.state().error===broken,`Startup status: ${JSON.stringify(loader.state())}`);
if(broken)assert(loader.state().current!==loader.state().good,'Broken current hash must differ from last good');
else assert(loader.state().current===loader.state().good,'Offline edit must become current');
assert((await turn()).includes(`result ${globalThis.extensionExpected}`),'Startup must use the expected file copy');
assert(!!JSON.parse(await fs.read('/proc/extensions')).error===broken,'Startup error must appear in proc');
} else {
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 until(()=>globalThis.shared.handle?.value()==='A');
const a=loader.state().good;
assert(a?.length===64,'Version must be a SHA-256 content hash');
assert((await turn()).includes('result A'),'Existing conversation must see the file edit');
const held=globalThis.shared.handle, retainedMethod=held.value;
await fs.write('/src/sausage/extensions/demo/server.ts','syntax !!! error');
await until(()=>loader.state().current!==a && !!loader.state().error);
assert(loader.state().good===a,'Broken edit must keep A');
assert(JSON.parse(await fs.read('/proc/extensions')).error,'Status must report the broken edit');
assert(held.value()==='A','A service remains');
assert((await turn()).includes('result A'),'A tool remains after the broken edit');
globalThis.shared.wait=true;
const running=turn();await until(()=>globalThis.shared.started);
// The server and its new dependency must install once, as one valid set.
await Promise.all([
fs.write('/src/sausage/extensions/demo/server.ts',source('C',true)),
fs.write('/src/sausage/extensions/demo/value.ts',"export const value='C';"),
fs.write('/src/sausage/extensions/demo/other.ts','export const unused=true;'),
]);
await until(()=>held.value()==='C');
assert(JSON.stringify(globalThis.shared.loads)==='["A","C"]','A burst must load exactly one complete set');
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');
const c=loader.state().good;
await loader.reload();
assert(loader.state().good===c && globalThis.shared.loads.length===2,'Unchanged files must not reload');
await fs.write('/src/sausage/offline-source.txt',source('D'));
}
} finally {globalThis.shared.gate.resolve();await loader.close();await harness.close(context);}