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

91 lines
7.5 KiB
TypeScript

/** Snapshot-loaded schedules, pause cases, and durable admission across SIGKILL. */
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'
import { cases,assert,until } from './automations-test-support'
import { clean } from './automations-pause-fixture'
import { emit,defineEvent } from './events'
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,now:()=>Date.now()+(globalThis.automationMode==='restart'?7200000:0),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)
globalThis.automationBase=`file:///run/sausage/extensions/${loader.state().good}/extensions`
const {Automations}=await import(/* @vite-ignore */ `${globalThis.automationBase}/automations/server.ts`)
const call=async(args:any)=>{
faux.setResponses([fauxAssistantMessage([fauxToolCall('automation',args)],{stopReason:'toolUse'}),fauxAssistantMessage('done')])
await (await root.submit({type:'input',content:'Configure automation'},context)).wait(context)
const result=(await root.context(context)).entries.filter((entry:any)=>entry.kind==='pi.tool-result').at(-1)
assert(result && !result.model?.[0]?.isError,JSON.stringify(result))
}
harness.resume()
if(globalThis.automationMode==='prepare') {
await call({action:'create',name:'restart once',schedule:`at ${new Date(Date.now()+3600000).toISOString()}`,subscribers:[root.id]})
await until(async()=>Object.values((await harness.snapshot(Automations,context)).automations).length===1)
const a=Object.values((await harness.snapshot(Automations,context)).automations)[0] as any
await until(async()=>(await harness.getTask(a.scheduler,context)).state.status==='sleeping')
await fs.write('/automation-ready','ready')
await new Promise(()=>{})
} else if(globalThis.automationMode==='restart') {
faux.setResponses([fauxAssistantMessage('woken once')])
await until(async()=>(await harness.snapshot(Automations,context)).automations.A1.fires===1)
await until(async()=>(await root.context(context)).entries.some((entry:any)=>entry.model?.some((m:any)=>typeof m.content==='string'&&m.content.includes('[automation A1 fired]'))))
await harness.waitForIdle(context)
const ledger=await harness.snapshot(Automations,context)
assert(ledger.automations.A1.fires===1&&!ledger.automations.A1.enabled,'Exactly one durable fire')
const entries=(await root.context(context)).entries.filter((entry:any)=>entry.model?.some((m:any)=>m.role==='user'&&typeof m.content==='string'&&m.content.includes('[automation A1 fired]')))
assert(entries.length===1,'Exactly one subscriber input')
await loader.close();await harness.close(context);await storage.close()
} else if(globalThis.automationMode==='triggers') {
await call({action:'create',name:'event',event:{name:'workspaces.landed',where:{title:'ship'}},subscribers:[root.id]})
await harness.commit((tx:any)=>emit(tx,defineEvent('workspaces.landed'),{sha:'abcdef',title:'ship'}),context)
await until(async()=>(await harness.snapshot(Automations,context)).automations.A1.fires===1)
await until(async()=>(await root.context(context)).entries.some((entry:any)=>entry.model?.some((m:any)=>typeof m.content==='string'&&m.content.includes('[automation A1 fired]'))))
await harness.waitForIdle(context)
await call({action:'create',name:'context',condition:{metric:'context',of:`#${root.id}`,above:75,cooldownMs:0},subscribers:[root.id]})
const {ContextUsageDoc}=await import(/* @vite-ignore */ `${globalThis.automationBase}/automations/triggers.ts`)
await harness.commit(async(tx:any)=>Object.assign(await tx.doc(ContextUsageDoc,root.id),{percent:80,tokens:800,window:1000}),context)
await until(async()=>(await harness.snapshot(Automations,context)).automations.A2.fires===1)
await until(async()=>(await root.context(context)).entries.some((entry:any)=>entry.model?.some((m:any)=>typeof m.content==='string'&&m.content.includes('[automation A2 fired]'))))
await harness.waitForIdle(context)
await loader.close();await harness.close(context);await storage.close()
} else if(globalThis.automationMode==='grant') {
for(const path of ['/src/sausage/automations','/src/sausage/automations/A1']) await fs.mkdir(path)
await fs.write('/src/sausage/automations/A1/script.sh',`curl http://127.0.0.1:${globalThis.automationPort}/token\nprintf '\\nFIRE delivered\\n'\nprintf 'FIRE ignored\\n' >&2\nprintf 'DONE\\n'`)
const subscriber=await harness.createConversation({ownership:{kind:'ownerless'}},context)
await subscriber.configure({model:{provider:'faux',modelId:'faux-1'}},context)
await call({action:'create',name:'creator grant',ask:'Fetch and fire',subscribers:[subscriber.id]})
await until(async()=>(await harness.snapshot(Automations,context))?.automations.A1?.script?.state==='done')
await until(async()=>(await subscriber.context(context)).entries.some((entry:any)=>entry.model?.some((m:any)=>typeof m.content==='string'&&m.content.includes('[automation A1 fired]'))))
await harness.waitForIdle(context)
const ledger=await harness.snapshot(Automations,context)
assert(ledger.automations.A1.fires===1,'Only stdout fires')
assert(ledger.automations.A1.executionConversation===root.id,'The creator owns execution')
assert((await fs.read('/src/sausage/automations/A1/log')).includes('accepted'),'The creator grant reaches curl')
const entries=(await root.context(context)).entries.filter((entry:any)=>entry.model?.some((m:any)=>m.role==='user'&&typeof m.content==='string'&&m.content.includes('[automation A1 fired]')))
assert(entries.length===0,'The creator is not a subscriber')
const delivered=(await subscriber.context(context)).entries.filter((entry:any)=>entry.model?.some((m:any)=>m.role==='user'&&typeof m.content==='string'&&m.content.includes('[automation A1 fired]')))
assert(delivered.length===1,'Exactly one script subscriber input')
const replacement=await harness.createConversation({ownership:{kind:'ownerless'}},context)
await call({action:'subscribe',id:'A1',to:`#${replacement.id}`})
await call({action:'unsubscribe',id:'A1',to:`#${subscriber.id}`})
const changed=(await harness.snapshot(Automations,context)).automations.A1
assert(changed.executionConversation===root.id,'Subscription changes retain the creator')
assert(JSON.stringify(changed.subscribers)===JSON.stringify([replacement.id]),'Subscription ids normalize')
await loader.close();await harness.close(context);await storage.close()
} else {
await loader.close();await harness.close(context);await storage.close()
const zones=Intl.supportedValuesOf('timeZone')
assert(zones.includes('Europe/Vienna') && zones.includes('America/New_York') && zones.includes('Asia/Tokyo'),'V8 provides IANA zone data')
console.log(`IANA zones: ${zones.length}`)
await import('./automations-schedule-cases')
await import('./automations-pause-cases')
for(const test of cases) {
try {await test.run();console.log(`PASS ${test.name}`)} finally {await clean()}
}
}