test/billion-messages.test.js
Rama main · réplica de lectura del repositorio original.
// 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/);
});
});