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] ?? '', / { 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, //) 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({}), '\n\n') assert.match(renderFacts({ worktreeHead: 'abc', worktreeDirty: true }), /abc \(dirty\)/) })