101 lines
8.3 KiB
TypeScript
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()}
|
|
}
|
|
}
|