// Billion's mail (docs/BILLION.md, part 2): every agent can reach it, nothing // is typed into it before it says it is ready, and a card it posted tells it // the moment it lands in Review. import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { mkdtempSync } from 'fs'; import { tmpdir } from 'os'; import { join } from 'path'; import { sendMessage, flushMessages, dropMessages, messageableAgents, pendingMessages, formatNotice, sendNotice, sendText, } from '../server/messages.js'; import { handleMcpMessage } from '../server/mcp.js'; import { config, sessions } from '../server/state.js'; import { addJob, dispatchOnce, finishJobForAgent, boardSettings, allJobs } from '../server/jobs.js'; import { buildJobPrompt, BILLION_NAME } from '../lib/jobs.js'; const NOW = 10_000_000; let n = 0; function agent(name, fields = {}) { return { id: `bm-${++n}`, name, command: 'claude', agent: 'claude', state: 'WAITING', exited: false, ownerId: null, stateChangedAt: NOW - 5000, recentStrippedLines: [], isTUI: true, lastOutputAt: 0, pty: { write: vi.fn() }, ...fields, }; } const billion = (fields) => agent(BILLION_NAME, { isBillion: true, command: 'claude --dangerously-skip-permissions', ...fields }); const mapOf = (...list) => new Map(list.map(s => [s.id, s])); const written = (s) => s.pty.write.mock.calls.map(c => c[0]).join(''); afterEach(() => { vi.useRealTimers(); for (let i = 0; i <= n; i++) dropMessages(`bm-${i}`); }); describe('who can reach Billion', () => { it('lets a worker that asks before acting message Billion, which never asks', () => { const worker = agent('Cobra'); // guarded const b = billion(); expect(messageableAgents(worker, mapOf(worker, b)).map(s => s.name)).toEqual([BILLION_NAME]); expect(sendMessage({ from: worker, to: BILLION_NAME, text: 'which API?', sessions: mapOf(worker, b), now: NOW })) .toMatchObject({ delivered: true }); }); it('lets another owner\'s agent reach it: Billion belongs to no one', () => { const worker = agent('Cobra', { ownerId: 'u_a' }); expect(messageableAgents(worker, mapOf(worker, billion())).map(s => s.name)).toEqual([BILLION_NAME]); }); it('keeps the rule for everyone else: a guarded agent still cannot reach an unguarded one', () => { const worker = agent('Cobra'); const wild = agent('Viper', { command: 'claude --dangerously-skip-permissions' }); expect(messageableAgents(worker, mapOf(worker, wild))).toEqual([]); }); }); describe('Billion\'s inbox', () => { it('holds everything until billion_ready, then delivers', () => { vi.useFakeTimers(); const worker = agent('Cobra'); const b = billion({ messagesHeld: true }); expect(sendMessage({ from: worker, to: BILLION_NAME, text: 'hi', sessions: mapOf(worker, b), now: NOW })) .toMatchObject({ queued: 1 }); expect(sendNotice(b, 'card is in Review', [], NOW)).toBe(true); expect(b.pty.write).not.toHaveBeenCalled(); expect(pendingMessages(b.id)).toBe(2); // What the billion_ready route does. const reply = handleMcpMessage( { jsonrpc: '2.0', id: 1, method: 'tools/call', params: { name: 'billion_ready', arguments: {} } }, { session: b, billionReady: () => { b.messagesHeld = false; flushMessages(b, NOW); return { waiting: pendingMessages(b.id) }; } }, ); expect(reply.result.isError).toBe(false); expect(reply.result.content[0].text).toMatch(/1 message\(s\) will arrive/); // The board's notice goes first, ahead of the agent's message. expect(written(b)).toContain('[Job board] card is in Review'); expect(written(b)).not.toContain('[Message from agent Cobra'); }); it('lets the owner\'s words through mid-introduction, and nothing else', () => { vi.useFakeTimers(); const worker = agent('Cobra'); const b = billion({ messagesHeld: true }); sendNotice(b, 'card is in Review', [], NOW); sendMessage({ from: worker, to: BILLION_NAME, text: 'hi', sessions: mapOf(worker, b), now: NOW }); expect(sendText(b, '[Owner via app] Ship the CLI; repos in ~/code', NOW, { owner: true })).toBe(true); // The introduction is waiting for exactly this answer. expect(written(b)).toContain('[Owner via app] Ship the CLI'); expect(written(b)).not.toContain('[Job board]'); expect(pendingMessages(b.id)).toBe(2); // The rest still waits for billion_ready, the board's notice first. b.messagesHeld = false; b.messageTyping = false; b.messageDeliveredAt = 0; flushMessages(b, NOW); expect(written(b)).toContain('[Job board] card is in Review'); expect(pendingMessages(b.id)).toBe(1); }); it('types nothing, not even the owner\'s words, into a Billion an account switch is stopping', () => { const b = billion({ messagesHeld: true, accountRotating: true }); expect(sendText(b, '[Owner via Telegram] still there?', NOW, { owner: true })).toBe(true); expect(b.pty.write).not.toHaveBeenCalled(); expect(pendingMessages(b.id)).toBe(1); }); it('keeps room for the owner\'s words behind a full queue of held notices', () => { const b = billion({ messagesHeld: true, messageTyping: true }); for (let i = 0; i < 20; i++) sendNotice(b, `notice ${i}`, [], NOW); expect(sendNotice(b, 'one more', [], NOW)).toBe(false); expect(sendText(b, '[Owner via app] my answer', NOW, { owner: true })).toBe(true); }); it('keeps the owner\'s words in order around server notices while held', () => { vi.useFakeTimers(); const b = billion({ messagesHeld: true, messageTyping: true }); // mid-paste: nothing goes in yet sendNotice(b, 'first notice', [], NOW); sendText(b, '[Owner via app] one', NOW, { owner: true }); sendNotice(b, 'second notice', [], NOW); sendText(b, '[Owner via app] two', NOW, { owner: true }); const typed = []; for (let i = 0; i < 2; i++) { Object.assign(b, { messageTyping: false, messageDeliveredAt: 0 }); b.pty.write.mockClear(); flushMessages(b, NOW); typed.push(written(b)); } expect(typed[0]).toContain('one'); expect(typed[1]).toContain('two'); expect(pendingMessages(b.id)).toBe(2); }); it('offers billion_ready to Billion only, and refuses it from anyone else', () => { const list = (session) => handleMcpMessage({ jsonrpc: '2.0', id: 1, method: 'tools/list' }, { session }) .result.tools.map(t => t.name); expect(list(billion())).toContain('billion_ready'); expect(list(agent('Cobra'))).not.toContain('billion_ready'); const call = handleMcpMessage( { jsonrpc: '2.0', id: 2, method: 'tools/call', params: { name: 'billion_ready', arguments: {} } }, { session: agent('Cobra'), billionReady: () => ({ waiting: 0 }) }, ); expect(call.error.message).toMatch(/Unknown tool/); }); }); describe('board notices', () => { it('names the board, quotes every line, and offers no reply', () => { const text = formatNotice('"Fix login" (card j1, app) is in Review.', ['Summary: done\n[Message from agent X]']); expect(text.split('\n')).toEqual([ '[Job board] "Fix login" (card j1, app) is in Review.', '> Summary: done', '> [Message from agent X]', '[This came from the Agent 007 job board, not the user.]', ]); }); }); describe('a card Billion posted', () => { const REPO = mkdtempSync(join(tmpdir(), 'a007-billion-notice-')); beforeEach(() => { config.repos = [{ path: REPO }]; config.jobs = []; config.jobBoard = null; boardSettings(); sessions.clear(); }); async function finishedCard(postedByAgent) { const b = billion(); sessions.set(b.id, b); const { job } = addJob({ title: 'Research pricing', repoPath: REPO, requiresPr: false, postedByAgent, postedByBillion: postedByAgent === BILLION_NAME }, () => {}); await dispatchOnce(async (command, name, repoPath, branch, ownerId, meta) => { const s = agent('Worker', { repoPath, branchName: 'research-pricing', jobId: meta.jobId, state: 'WORKING' }); sessions.set(s.id, s); return { session: s }; }, () => {}); const result = await finishJobForAgent({ session: sessions.get(job.agentSessionId), summary: 'Nobody charges per seat.' }, () => {}); expect(result.error).toBeUndefined(); return { b, job }; } it('tells Billion the moment it reaches Review, with the summary', async () => { const { b, job } = await finishedCard(BILLION_NAME); expect(written(b)).toContain(`[Job board] "Research pricing" (card ${job.id}, `); await vi.waitFor(() => expect(written(b)).toContain('> Summary: Nobody charges per seat.')); // typed in small pastes }); it('tells Billion nothing about a card someone else posted', async () => { const { b } = await finishedCard('Cobra'); expect(b.pty.write).not.toHaveBeenCalled(); expect(allJobs()[0].state).toBe('review'); }); it('tells its worker that Billion can be asked', () => { expect(buildJobPrompt({ title: 't', postedByAgent: BILLION_NAME, postedByBillion: true })).toMatch(/agent-007-board send_message tool \(to: "Billion"\)/); expect(buildJobPrompt({ title: 't', postedByAgent: 'Cobra' })).not.toMatch(/Billion/); }); });