// 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(/^