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

101 lines
8.3 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,conversation=root)=>{
faux.setResponses([fauxAssistantMessage([fauxToolCall('automation',args)],{stopReason:'toolUse'}),fauxAssistantMessage('done')])
await (await conversation.submit({type:'input',content:'Configure automation'},context)).wait(context)
const result=(await conversation.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://host.sausage/whoami\nprintf '\\n'\ncurl 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)
const {Started}=await import(/* @vite-ignore */ `${globalThis.automationBase}/messages/addresses.ts`)
const entry=await harness.commit((tx:any)=>tx.appendEntry(subscriber.id,{kind:'automation.origin',data:{}}),context)
const creatorRecord=await harness.commit(async(tx:any)=>{
const fork=await tx.forkConversation(subscriber.id,entry.id,{ownership:{kind:'ownerless'}})
;(await tx.doc(Started,fork.id)).by=root.id
return fork
},context)
const creator=await harness.conversation(creatorRecord.id,context)
await call({action:'create',name:'creator grant',ask:'Fetch and fire',subscribers:[subscriber.id]},creator)
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===creator.id,'The creator owns execution')
const log=await fs.read('/src/sausage/automations/A1/log')
assert(log.includes('accepted'),'The creator chain reaches the starter grant')
assert(JSON.stringify(JSON.parse(log.split('\n')[0]).conversations)===JSON.stringify([String(creator.id),String(subscriber.id),String(root.id)]),'The host appends the starter after fork ancestry')
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===creator.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()}
}
}