Ctrl K
ComunidadGuías…

test/billion-handover.test.js

Rama main · réplica de lectura del repositorio original.

Archivo original ↗Preguntar a KiBot
// HANDOVER.md (server/billion-handover.js): the end of the old CLI's
// conversation as plain text, read from sample transcripts, never real ones.
import { describe, it, expect } from 'vitest';
import { mkdtempSync, mkdirSync, readFileSync, writeFileSync, realpathSync } from 'fs';
import { join } from 'path';
import { tmpdir } from 'os';
import { transcriptMessages, handoverText, writeHandover, HANDOVER_CHARS } from '../server/billion-handover.js';

const fixture = (name) => readFileSync(join(import.meta.dirname, 'fixtures', 'billion-transcripts', name), 'utf8');
const tmp = (p) => realpathSync(mkdtempSync(join(tmpdir(), p)));

describe('transcriptMessages', () => {
  it('skips malformed blocks without losing valid conversation text', () => {
    const line = JSON.stringify({ type: 'assistant', message: { content: [null, 42, {}, { type: 'text', text: 'Resume safely' }] } });
    expect(transcriptMessages('claude', `invalid json\n${line}`)).toEqual([{ role: 'assistant', text: 'Resume safely' }]);
  });

  it('bounds large tool histories and preserves the newest result and both ends of long content', () => {
    const lines = Array.from({ length: 80 }, (_, i) => JSON.stringify({ type: 'response_item', payload: {
      type: 'function_call_output', call_id: `call-${i}`, output: `START-${i} ` + 'x'.repeat(20000) + ` END-${i}`,
    } })).join('\n');
    const out = transcriptMessages('codex', lines);
    expect(out.length).toBeLessThan(80);
    expect(out.reduce((sum, m) => sum + m.text.length, 0)).toBeLessThanOrEqual(HANDOVER_CHARS);
    expect(out.at(-1).text).toContain('START-79');
    expect(out.at(-1).text).toContain('END-79');
    expect(out.at(-1).text).toContain('middle omitted');
  });

  it('exports custom Codex calls and results but excludes private reasoning', () => {
    const lines = [
      { type: 'custom_tool_call', call_id: 'c1', name: 'apply_patch', input: 'patch content' },
      { type: 'custom_tool_call_output', call_id: 'c1', output: { success: true } },
      { type: 'reasoning', content: 'private' },
    ].map(payload => JSON.stringify({ type: 'response_item', payload })).join('\n');
    expect(transcriptMessages('codex', lines)).toEqual([
      { role: 'tool', text: 'Tool call c1: apply_patch\npatch content' },
      { role: 'tool', text: 'Tool result c1:\n{"success":true}' },
    ]);
  });

  it('keeps what the owner and Billion said in a Claude Code transcript, nothing else', () => {
    expect(transcriptMessages('claude', fixture('claude.jsonl'))).toEqual([
      { role: 'user', text: 'This is your first run. Introduce yourself.' },
      { role: 'assistant', text: "Hi, I'm Billion." },
      { role: 'tool', text: 'Tool call t1: Bash\n{"command":"git log"}' },
      { role: 'tool', text: 'Tool result t1:\nabc123 first run' },
      { role: 'assistant', text: "What's the mission?" },
      { role: 'user', text: 'Ship the Windows build.' },
      { role: 'assistant', text: 'Got it: the Windows build.' },
    ]);
  });

  it('keeps what was typed and answered in a Codex rollout, not its instructions', () => {
    expect(transcriptMessages('codex', fixture('codex.jsonl'))).toEqual([
      { role: 'user', text: 'Run one operating cycle as defined in CHARTER.md.' },
      { role: 'tool', text: 'Tool call : shell\n{}' },
      { role: 'assistant', text: 'Card 12 is in Review; merging #140.' },
    ]);
  });

  it('keeps only the last messages, each cut to a readable length', () => {
    const lines = Array.from({ length: 30 }, (_, i) => JSON.stringify({
      type: i % 2 ? 'assistant' : 'user', message: { content: i === 29 ? 'x'.repeat(20000) : `m${i}` },
    })).join('\n');
    const out = transcriptMessages('claude', lines, 20);
    expect(out).toHaveLength(20);
    expect(out[0].text).toBe('m10');
    expect(out[19].text.length).toBeLessThan(8200);
    expect(out[19].text).toContain('middle omitted');
  });
});

describe('writeHandover', () => {
  it('writes the last messages of the newest Claude Code conversation in the folder', () => {
    const dir = tmp('a007-ho-dir-');
    const claude = tmp('a007-ho-claude-');
    const project = join(claude, 'projects', dir.replace(/[^A-Za-z0-9]/g, '-'));
    mkdirSync(project, { recursive: true });
    writeFileSync(join(project, 'a.jsonl'), fixture('claude.jsonl'));
    const { path, messages } = writeHandover(dir, { from: 'claude', to: 'codex', homes: { claude, codex: tmp('a007-ho-codex-') }, now: new Date('2026-09-26T12:00:00Z') });
    expect(messages).toBe(7);
    const text = readFileSync(path, 'utf8');
    expect(text).toContain('when Billion moved from Claude Code to Codex');
    expect(text).toContain('Read `STATE.md` first');
    expect(text).toContain('`git log -10`');
    expect(text).toContain('> Ship the Windows build.');
    expect(text).toContain('### Billion');
    expect(text).toContain('Tool result t1');
    expect(text).not.toContain('> private');
  });

  it('finds the Codex rollout whose cwd is the folder', () => {
    const dir = tmp('a007-ho-dir-');
    const codex = tmp('a007-ho-codex-');
    const day = join(codex, 'sessions', '2026', '09', '26');
    mkdirSync(day, { recursive: true });
    writeFileSync(join(day, 'rollout-a.jsonl'), fixture('codex.jsonl').replaceAll('CWD', JSON.stringify(dir).slice(1, -1)));
    writeFileSync(join(day, 'rollout-b.jsonl'), fixture('codex.jsonl').replaceAll('CWD', '/elsewhere').replace('merging #140', 'someone else'));
    writeHandover(dir, { from: 'codex', to: 'claude', homes: { claude: tmp('a007-ho-claude-'), codex } });
    const text = readFileSync(join(dir, 'HANDOVER.md'), 'utf8');
    expect(text).toContain('from Codex to Claude Code');
    expect(text).toContain('merging #140');
  });

  it('says so when there is no conversation to hand over', () => {
    expect(handoverText({ from: 'codex', to: 'claude', file: null, messages: [] })).toMatch(/No Codex conversation was found/);
  });
});