test/billion-codex.test.js
Rama main · réplica de lectura del repositorio original.
// Billion on either CLI (server/billion.js): the command for each, Codex's
// AGENTS.md, which CLI wins, and the order a switch goes in.
import { describe, it, expect, vi } from 'vitest';
import { mkdtempSync, writeFileSync } from 'fs';
import { join } from 'path';
import { tmpdir } from 'os';
import { parseCommand } from '../lib/helpers.js';
import { billionCommand, agentsMdText, billionAgent, saveBillionAgent, billionAgentWarning, switchBillion, noAgentCommand, NO_CODEX_NOTICE } from '../server/billion.js';
const dir = '/home/me/.agent-007/billion';
const promptOf = (cmd) => parseCommand(cmd).args.at(-1);
const ID = '019a0000-0000-7000-8000-000000000002';
describe('billionCommand on Codex', () => {
it('starts codex without approvals or sandbox, with the first-run prompt', () => {
const cmd = billionCommand({ agent: 'codex', created: true, dir, projectsHint: null });
expect(parseCommand(cmd).file).toBe('codex');
expect(parseCommand(cmd).args[0]).toBe('--dangerously-bypass-approvals-and-sandbox');
expect(promptOf(cmd)).toMatch(/first run/);
});
it('resumes its session by id after a restart, and only a real id', () => {
expect(parseCommand(billionCommand({ agent: 'codex', created: false, codexSessionId: ID, dir })).args.slice(0, 2)).toEqual(['resume', ID]);
expect(billionCommand({ agent: 'codex', created: false, codexSessionId: '--last', dir })).not.toMatch(/resume/);
expect(billionCommand({ agent: 'codex', created: false, codexSessionId: null, dir })).not.toMatch(/resume/);
});
it('after a switch starts fresh and reads STATE.md and HANDOVER.md first', () => {
for (const agent of ['codex', 'claude']) {
const cmd = billionCommand({ agent, created: false, handover: true, hasConversation: true, codexSessionId: ID, dir });
expect(cmd).not.toMatch(/resume|--continue/);
expect(promptOf(cmd)).toMatch(/Read STATE\.md and then HANDOVER\.md/);
}
});
it('names its CLI and the model lists on every start, on both CLIs, quoted intact', () => {
const models = { claude: ['fable', 'opus'], codex: ['gpt-5.5', 'gpt-5.5-mini'] };
const starts = [{ created: true }, { created: false, codexSessionId: ID, hasConversation: true }, { created: false, handover: true }];
for (const agent of ['claude', 'codex']) {
for (const start of starts) {
const prompt = promptOf(billionCommand({ agent, dir, ...start, models, toolsFile: '/cfg/billion-tools.json' }));
expect(prompt).toContain(`You run on ${agent === 'codex' ? 'Codex' : 'Claude Code'}; a card you post without \`agent\` goes to the same CLI.`);
expect(prompt).toContain('Models available now: claude: fable, opus; codex: gpt-5.5, gpt-5.5-mini.');
expect(prompt).toMatch(/Name both `agent` and `model` on every card/);
const late = promptOf(billionCommand({ agent, dir, ...start, models: null, toolsFile: '/cfg/billion-tools.json' }));
expect(late).toContain('The model list is not ready yet; read it in /cfg/billion-tools.json');
expect(late).not.toContain('Models available now');
}
}
});
it('without codex, prints how to install it', () => {
expect(noAgentCommand('codex')).toContain(JSON.stringify(NO_CODEX_NOTICE).slice(1, 40));
});
});
describe('AGENTS.md', () => {
const charter = "# Billion's charter\n\nThe rules.\n";
it('is CLAUDE.md with the charter in place of its import, so the owner\'s rules come after it', () => {
const text = agentsMdText(charter, "# Billion\n\n@CHARTER.md\n\n## Owner's rules\n\n- Repos in ~/P\n");
expect(text).toMatch(/^<!-- Written by Agent 007/);
expect(text).not.toMatch(/^@CHARTER\.md/m);
expect(text.indexOf('The rules.')).toBeLessThan(text.indexOf('- Repos in ~/P'));
});
it('still puts the charter first, and says who wins, when the import is gone', () => {
const text = agentsMdText(charter, '- Never merge on Fridays\n');
expect(text.indexOf('The rules.')).toBeLessThan(text.indexOf('Fridays'));
expect(text).toMatch(/take precedence over the charter above/);
});
it('copies a charter that holds $& patterns as it is', () => {
expect(agentsMdText('cost $& and $1\n', '@CHARTER.md\n')).toContain('cost $& and $1');
});
});
describe('which CLI Billion runs on', () => {
const file = () => join(mkdtempSync(join(tmpdir(), 'a007-bagent-')), 'billion-agent.json');
it('is BILLION_AGENT, claude by default and for anything else', () => {
expect(billionAgent({}, file())).toBe('claude');
expect(billionAgent({ BILLION_AGENT: ' Codex ' }, file())).toBe('codex');
expect(billionAgent({ BILLION_AGENT: 'gemini' }, file())).toBe('claude');
expect(billionAgentWarning({ BILLION_AGENT: 'gemini' })).toMatch(/not claude or codex/);
expect(billionAgentWarning({ BILLION_AGENT: 'codex' })).toBeNull();
});
it('a saved switch wins until BILLION_AGENT changes', () => {
const f = file();
saveBillionAgent('codex', {}, f);
expect(billionAgent({}, f)).toBe('codex');
expect(billionAgent({ BILLION_AGENT: 'claude' }, f)).toBe('claude');
saveBillionAgent('claude', { BILLION_AGENT: 'codex' }, f);
expect(billionAgent({ BILLION_AGENT: 'codex' }, f)).toBe('claude');
writeFileSync(f, 'not json');
expect(billionAgent({ BILLION_AGENT: 'codex' }, f)).toBe('codex');
});
});
describe('switchBillion', () => {
const steps = () => {
const order = [];
return {
order,
writeHandover: vi.fn((d, o) => order.push(['handover', o.from, o.to])),
saveAgent: vi.fn((a) => order.push(['save', a])),
stop: vi.fn(async () => { order.push(['stop']); return { queue: ['mail'], ahead: 0 }; }),
start: vi.fn((o) => { order.push(['start', o]); return { session: { id: 'new' } }; }),
};
};
it('hands over, saves, stops, then starts the other CLI with the mail', async () => {
const s = steps();
const result = await switchBillion({ current: { agent: 'claude', exited: false }, dir, ...s });
expect(result).toEqual({ session: { id: 'new' } });
expect(s.order).toEqual([
['handover', 'claude', 'codex'], ['save', 'codex'], ['stop'],
['start', { handover: true, carried: { queue: ['mail'], ahead: 0 } }],
]);
});
it('starts a stopped Billion on the new CLI without stopping anything', async () => {
const s = steps();
await switchBillion({ to: 'claude', current: null, currentAgent: 'codex', dir, ...s });
expect(s.stop).not.toHaveBeenCalled();
expect(s.order[0]).toEqual(['handover', 'codex', 'claude']);
});
it('keeps the current CLI when the conversation handover cannot be written', async () => {
const s = steps();
s.writeHandover.mockImplementation(() => { throw new Error('disk full'); });
vi.spyOn(console, 'error').mockImplementation(() => {});
expect((await switchBillion({ current: { agent: 'codex' }, dir, ...s })).error).toMatch(/handover/);
expect(s.saveAgent).not.toHaveBeenCalled();
expect(s.stop).not.toHaveBeenCalled();
expect(s.start).not.toHaveBeenCalled();
});
it('refuses the CLI it already runs on, and one it does not know', async () => {
const s = steps();
expect((await switchBillion({ to: 'codex', current: { agent: 'codex', exited: false }, dir, ...s })).error).toMatch(/already runs on Codex/);
expect((await switchBillion({ to: 'gpt', current: null, currentAgent: 'claude', dir, ...s })).error).toMatch(/not gpt/);
expect(s.order).toEqual([]);
});
});