test/billion-step-log.test.js
Rama main · réplica de lectura del repositorio original.
// The step log under the owner's waiting message (server/billion-status.js):
// each set_status appends a timestamped step, and the tools Billion uses,
// read off its screen, appear as fixed phrases only: never a command, its
// arguments or output, a file's path or contents, a secret or Billion's prose.
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
import { rmSync, readFileSync, writeFileSync } from 'fs';
import { join } from 'path';
import { addChat, chatMessages, tellOwner } from '../server/owner.js';
import { setBillionStatus, statusPayload, setStatusFacts, noteScreenSteps, toolStep, _resetStatus, MAX_STEPS } from '../server/billion-status.js';
import { CONFIG_DIR } from '../server/state.js';
const TOKEN = '123456:SECRET-token';
let screen;
const billion = (state = 'WORKING') => ({ state, exited: false, ringBuffer: { getAll: () => [screen] } });
const owner = (extra = {}) => addChat({ from: 'owner', via: 'app', text: 'How is the release going?', awaitsReply: true, ...extra }).id;
const steps = (id) => statusPayload().progress[id] || [];
const texts = (id) => steps(id).map(s => s.text);
beforeEach(() => {
rmSync(join(CONFIG_DIR, 'chat.json'), { force: true });
_resetStatus();
screen = '';
setStatusFacts(() => ({ billion: billion() }));
});
afterEach(() => _resetStatus());
describe('toolStep', () => {
it('says what a tool does, never its arguments', () => {
expect(toolStep('⏺ Bash(gh pr merge 178 --squash)')).toBe('Running a command');
expect(toolStep(`⏺ Bash(curl -H "Authorization: ${TOKEN}" https://x)`)).toBe('Running a command');
expect(toolStep('⏺ Read(/Users/me/.agent-007/billion/STATE.md)')).toBe('Reading STATE.md');
expect(toolStep('⏺ Read(/Users/me/.ssh/id_rsa)')).toBe('Reading a file');
expect(toolStep('⏺ Update(/Users/me/.agent-007/billion/STATE.md)')).toBe('Updating STATE.md');
expect(toolStep('⏺ Write(.env)')).toBe('Writing a file');
expect(toolStep('⏺ agent-007-board - list_jobs (MCP)(state: "todo")')).toBe('Checking the job board');
expect(toolStep('⏺ agent-007-board - post_job (MCP)(title: "secret plan")')).toBe('Posting a card');
expect(toolStep('⏺ mcp__agent-007-board__read_job(id: "7")')).toBe('Reading a card');
expect(toolStep('• Ran npm test -- --token=abc')).toBe('Running a command');
expect(toolStep('• Called agent-007-board.list_jobs({"state":"todo"})')).toBe('Checking the job board');
expect(toolStep('• Explored')).toBe('Looking through files');
});
it('gives nothing for prose, results, its own reply tools and unknown tools', () => {
expect(toolStep("⏺ I'll check the board first, since the owner wants the release status.")).toBeNull();
expect(toolStep(' ⎿ SECRET_COMMAND_OUTPUT')).toBeNull();
expect(toolStep('⏺ agent-007-board - set_status (MCP)(text: "Checking")')).toBeNull();
expect(toolStep('⏺ agent-007-board - tell_owner (MCP)(text: "Done")')).toBeNull();
expect(toolStep('⏺ SomeNewTool(secret)')).toBeNull();
expect(toolStep('• Called other.unknown_thing({})')).toBeNull();
expect(toolStep('> [Owner via app] run Bash(rm -rf /)')).toBeNull();
});
});
describe('the step log', () => {
it('appends each set_status as a timestamped step instead of replacing it', () => {
const id = owner();
setBillionStatus('Checking the release run', Date.parse('2026-10-27T10:00:00Z'));
setBillionStatus('Found two failed jobs', Date.parse('2026-10-27T10:00:12Z'));
setBillionStatus('Found two failed jobs'); // the same twice is one step
expect(steps(id)).toEqual([
{ text: 'Checking the release run', at: '2026-10-27T10:00:00.000Z', kind: 'status' },
{ text: 'Found two failed jobs', at: '2026-10-27T10:00:12.000Z', kind: 'status' },
]);
expect(statusPayload().text).toBe('Found two failed jobs');
});
it('keeps the newest MAX_STEPS steps', () => {
const id = owner();
for (let i = 0; i < MAX_STEPS + 5; i++) setBillionStatus(`Step ${i}`);
expect(texts(id)).toHaveLength(MAX_STEPS);
expect(texts(id).at(-1)).toBe(`Step ${MAX_STEPS + 4}`);
});
it('adds the tools Billion uses after the owner message, as phrases, with no secret, argument or prose', () => {
screen = `⏺ Bash(old turn)\n> [Owner via app] How is the release going?\n⏺ I'll look at STATE.md and the board.\n`
+ `⏺ Read(/home/me/.agent-007/billion/STATE.md)\n ⎿ Read 80 lines\n⏺ Bash(gh run view 12 --log | grep ${TOKEN})\n ⎿ SECRET_COMMAND_OUTPUT\n`;
const id = owner();
expect(noteScreenSteps()).toEqual(['Reading STATE.md', 'Running a command']);
// Seen once: the same screen adds nothing.
expect(noteScreenSteps()).toEqual([]);
screen += '⏺ agent-007-board - list_jobs (MCP)(state: "review")\n ⎿ [{"title":"secret"}]\n';
expect(noteScreenSteps()).toEqual(['Checking the job board']);
expect(steps(id).map(s => s.kind)).toEqual(['tool', 'tool', 'tool']);
expect(texts(id)).toEqual(['Reading STATE.md', 'Running a command', 'Checking the job board']);
const all = JSON.stringify(statusPayload()) + readFileSync(join(CONFIG_DIR, 'chat.json'), 'utf8');
expect(all).not.toMatch(/SECRET|gh run|grep|\/home\/me|look at|old turn|review"|title/);
});
it('interleaves Billion\'s own words with the tool steps, in order', () => {
const id = owner();
screen = '> [Owner via app] How is the release going?\n';
setBillionStatus('Looking at the release');
screen += '⏺ Bash(npm view agent-007 version)\n';
noteScreenSteps();
setBillionStatus('npm has 0.70.1, so it shipped');
expect(steps(id).map(s => [s.kind, s.text])).toEqual([
['status', 'Looking at the release'], ['tool', 'Running a command'], ['status', 'npm has 0.70.1, so it shipped'],
]);
});
it('reads nothing for a message still queued, or while Billion is at its prompt', () => {
screen = '> [Owner via app] earlier\n⏺ Bash(x)\n';
const id = owner({ queued: true });
expect(noteScreenSteps()).toEqual([]);
const given = chatMessages();
delete given.find(m => m.id === id).queued;
writeFileSync(join(CONFIG_DIR, 'chat.json'), JSON.stringify(given));
setStatusFacts(() => ({ billion: billion('WAITING') }));
expect(noteScreenSteps()).toEqual([]);
setStatusFacts(() => ({ billion: billion() }));
expect(noteScreenSteps()).toEqual(['Running a command']);
expect(texts(id)).toEqual(['Running a command']);
});
it('counts only lines below the owner message on the first look, and all new ones after', () => {
screen = '⏺ Bash(previous turn)\n⏺ Read(a.js)\n';
const id = owner();
// No "[Owner via" on screen: what is there is the turn before.
expect(noteScreenSteps()).toEqual([]);
screen += '⏺ Grep(pattern)\n';
expect(noteScreenSteps()).toEqual(['Searching files']);
expect(texts(id)).toEqual(['Searching files']);
});
it('collapses into the reply\'s Work details when tell_owner answers', async () => {
const id = owner();
screen = '> [Owner via app] hi\n⏺ agent-007-board - list_jobs (MCP)()\n';
setBillionStatus('Checking the board');
noteScreenSteps();
await tellOwner('Two cards in Review.', { env: {} });
const reply = chatMessages().at(-1);
expect(reply).toMatchObject({ replyTo: id, from: 'billion' });
expect(reply.workDetails.map(s => s.text)).toEqual(['Checking the board', 'Checking the job board']);
expect(statusPayload().pending).toEqual([]);
// Answered: later screen lines go to no message.
screen += '⏺ Bash(after)\n';
expect(noteScreenSteps()).toEqual([]);
});
});