Luigit
repositories / will

will

owned by admin

src/agent/engine.test.ts

Raw
import assert from 'node:assert/strict'
import { mkdir, mkdtemp, writeFile } from 'node:fs/promises'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { test } from 'node:test'
import { createEngine, type SessionPort } from './engine.ts'
import { buildSystemPrompt, renderFacts, renderWakePrompt } from './prompt.ts'
import { WakeQueue } from './queue-store.ts'
import { AgentState } from './state.ts'

function fakeSession(): SessionPort & { prompts: string[]; failCount: number; hang: boolean } {
  const s = {
    prompts: [] as string[],
    failCount: 0,
    hang: false,
    async prompt(text: string) {
      s.prompts.push(text)
      if (s.failCount > 0) {
        s.failCount--
        throw new Error('model unavailable')
      }
      if (s.hang) await new Promise(() => {})
    },
    async abort() {},
  }
  return s as unknown as SessionPort & { prompts: string[]; failCount: number; hang: boolean }
}

async function setup() {
  const dir = await mkdtemp(join(tmpdir(), 'eng-'))
  const state = await AgentState.load(join(dir, 'state.json'))
  const queue = await WakeQueue.open(join(dir, 'wakeq.jsonl'))
  const audits: { kind: string; payload: unknown }[] = []
  return { dir, state, queue, audits }
}

const budget = { dailyCapCalls: 100, recoveryReserveFraction: 0.1 }

test('chat turn completes, budget spent, wake removed', async () => {
  const { state, queue, audits } = await setup()
  const session = fakeSession()
  const engine = createEngine({
    queue,
    state,
    session,
    budgetConfig: budget,
    buildFacts: async () => ({ timeUtc: '2026-09-12T04:00:00Z', revision: 'r1' }),
    audit: async (kind, payload) => {
      audits.push({ kind, payload })
    },
  })
  await queue.enqueue('chat', { text: 'hello' })
  const outcome = await engine.tick()
  assert.equal(outcome.kind, 'completed')
  assert.equal(queue.peek().length, 0)
  assert.equal(state.data.budget.used, 1)
  assert.ok(session.prompts[0]?.includes('hello'))
  assert.ok(session.prompts[0]?.includes('deployment_revision: r1'))
  assert.deepEqual(
    audits.map((a) => a.kind),
    ['wake_start', 'wake_end'],
  )
})

test('boot context appears only on the first turn', async () => {
  const { state, queue } = await setup()
  const session = fakeSession()
  const engine = createEngine({
    queue,
    state,
    session,
    budgetConfig: budget,
    buildFacts: async () => ({}),
    audit: async () => {},
  })
  await queue.enqueue('chat', {})
  await engine.tick()
  await queue.enqueue('chat', {})
  await engine.tick()
  assert.match(session.prompts[0] ?? '', /<boot_context\b/)
  assert.doesNotMatch(session.prompts[1] ?? '', /<boot_context\b/)
})

test('model failure retries with backoff, then defers after three attempts', async () => {
  const { state, queue, audits } = await setup()
  const session = fakeSession()
  let nowMs = Date.parse('2026-09-12T04:00:00Z')
  const engine = createEngine({
    queue,
    state,
    session,
    budgetConfig: budget,
    buildFacts: async () => ({}),
    audit: async (kind, payload) => {
      audits.push({ kind, payload })
    },
    clock: {
      now: () => new Date(nowMs),
    },
    backoffMs: () => 1000,
  })
  await queue.enqueue('chat', { text: 'x' })
  session.failCount = 3
  const r1 = await engine.tick()
  assert.equal(r1.kind, 'failed')
  assert.equal(queue.peek()[0]?.attempts, 1)
  nowMs += 2000
  const r2 = await engine.tick()
  assert.equal(r2.kind, 'failed')
  assert.equal(queue.peek()[0]?.attempts, 2)
  nowMs += 2000
  const r3 = await engine.tick()
  assert.equal(r3.kind, 'failed')
  assert.equal(queue.peek()[0]?.attempts, 0, 'deferred resets attempts for the later retry')
  assert.ok(queue.peek()[0]?.deferredUntil)
})

test('turn timeout enqueues exactly one recovery wake and completes the original', async () => {
  const { state, queue, audits } = await setup()
  const session = fakeSession()
  const engine = createEngine({
    queue,
    state,
    session,
    budgetConfig: budget,
    buildFacts: async () => ({}),
    audit: async (kind, payload) => {
      audits.push({ kind, payload })
    },
    turnDeadlineMs: 50,
  })
  await queue.enqueue('chat', { text: 'slow' })
  session.hang = true
  const outcome = await engine.tick()
  assert.equal(outcome.kind, 'timeout')
  const pending = queue.peek()
  assert.equal(pending.length, 1)
  assert.equal(pending[0]?.kind, 'recovery')
  assert.deepEqual(
    (pending[0]?.payload as { interruptedWakeId?: string }).interruptedWakeId,
    outcome.wake?.id,
  )
  assert.ok(audits.some((a) => a.kind === 'wake_timeout'))

  session.hang = false
  assert.equal((await engine.tick()).kind, 'completed')
  const recoveryPrompt = session.prompts[1] ?? ''
  assert.match(recoveryPrompt, /turn watchdog fired/)
  assert.doesNotMatch(recoveryPrompt, /inspect|continue|replay/i)
})

test('budget cap blocks ordinary wakes but reserve admits recovery wakes', async () => {
  const { state, queue } = await setup()
  const session = fakeSession()
  const engine = createEngine({
    queue,
    state,
    session,
    budgetConfig: { dailyCapCalls: 2, recoveryReserveFraction: 0.5 },
    buildFacts: async () => ({}),
    audit: async () => {},
  })
  await state.update((d) => {
    d.budget = { day: new Date().toISOString().slice(0, 10), used: 2, usedRecovery: 0 }
  })
  await queue.enqueue('chat', {})
  const blocked = await engine.tick()
  assert.equal(blocked.kind, 'budget-blocked')
  assert.equal(queue.peek().length, 1, 'wake stays queued')

  await queue.enqueue('deploy_failure', { revision: 'r9' })
  const recovery = await engine.tick()
  assert.equal(recovery.kind, 'completed')
})

test('storage stop pauses ordinary wakes, recovery continues', async () => {
  const { state, queue } = await setup()
  const session = fakeSession()
  const engine = createEngine({
    queue,
    state,
    session,
    budgetConfig: budget,
    buildFacts: async () => ({}),
    audit: async () => {},
  })
  engine.setStoragePaused(true)
  await queue.enqueue('chat', {})
  assert.equal((await engine.tick()).kind, 'storage-blocked')
  await queue.enqueue('ci_failure', {})
  assert.equal((await engine.tick()).kind, 'completed')
})

test('system prompt is exactly the lexically ordered personality', async () => {
  const dir = await mkdtemp(join(tmpdir(), 'persona-'))
  await mkdir(dir, { recursive: true })
  await writeFile(join(dir, '10-identity.md'), '# identity\nI am will.', 'utf8')
  await writeFile(join(dir, '20-body.md'), '# body\nI live in a pod.', 'utf8')
  const prompt = await buildSystemPrompt(dir)
  assert.equal(prompt, '# identity\nI am will.\n\n# body\nI live in a pod.')
  assert.doesNotMatch(prompt, /available_tools|operational_rules/)
})

test('wake prompts render per-kind templates with facts', async () => {
  const facts = { timeUtc: '2026-09-12T04:00:00Z', revision: 'r1', budgetUsed: 3, budgetCap: 100 }
  const chat = renderWakePrompt(
    {
      kind: 'chat',
      payload: {
        messageId: 5,
        senderName: 'friend',
        text: 'look',
        triggerReason: 'mention',
        media: [{ digest: 'a'.repeat(64), type: 'image/png' }],
      },
    },
    facts,
  )
  assert.match(chat, /from: friend/)
  assert.match(chat, /image\/png/)
  assert.match(chat, /model_budget_used: 3\/100/)

  const hb = renderWakePrompt({ kind: 'heartbeat', payload: {} }, facts)
  assert.match(hb, /<heartbeat_wake\/>/)
  assert.doesNotMatch(hb, /Review|Act|quiet/)

  const df = renderWakePrompt(
    { kind: 'deploy_failure', payload: { revision: 'r9', kind: 'probation-failed' } },
    facts,
  )
  assert.match(df, /r9/)
  assert.doesNotMatch(df, /diagnose|push|fix commit/i)
})

test('facts block omits missing values instead of rendering undefined', () => {
  assert.equal(renderFacts({}), '<wake_facts>\n\n</wake_facts>')
  assert.match(renderFacts({ worktreeHead: 'abc', worktreeDirty: true }), /abc \(dirty\)/)
})