Ctrl K
ComunidadGuías…

test/billion-wake.test.js

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

Archivo original ↗Preguntar a KiBot
// The server's operating loop for Billion (server/billion-wake.js).
import { describe, it, expect, vi } from 'vitest';
import { wakeTick, setNextWake, nextWakeAt, WAKE_PROMPT, WAKE_QUIET_MIN, WAKE_BUSY_MIN, OWNER_QUIET_MS } from '../server/billion-wake.js';
import { sendText, dropMessages } from '../server/messages.js';

const MIN = 60_000;
const T0 = 1_000_000_000;
let ids = 0;
// A Billion resting at its prompt, inbox open, started at T0.
const billion = (over = {}) => ({
  id: `wake-${++ids}`, isBillion: true, exited: false, messagesHeld: false, createdAt: T0,
  state: 'WAITING', stateChangedAt: T0, lastOutputAt: T0, isTUI: true,
  lastStrippedLine: '>', recentStrippedLines: ['>'], ringBuffer: { getAll: () => [] },
  pty: { write: vi.fn() }, ...over,
});
const tick = (s, at, busy = false) => { const send = vi.fn(() => true); return { woke: wakeTick(s, { now: at, busy, send }), send }; };

describe('wakeTick', () => {
  it('wakes every 30 minutes when quiet, typing the cycle prompt', () => {
    const s = billion();
    expect(tick(s, T0 + (WAKE_QUIET_MIN - 1) * MIN).woke).toBe(false);
    const { woke, send } = tick(s, T0 + WAKE_QUIET_MIN * MIN);
    expect(woke).toBe(true);
    expect(send).toHaveBeenCalledWith(s, WAKE_PROMPT, T0 + WAKE_QUIET_MIN * MIN);
    // Measured from that wake now.
    expect(nextWakeAt(s, false)).toBe(T0 + 2 * WAKE_QUIET_MIN * MIN);
  });

  it('wakes every 3 minutes while its cards are in progress or in Review', () => {
    expect(tick(billion(), T0 + WAKE_BUSY_MIN * MIN, true).woke).toBe(true);
  });

  it('never mid-turn, at a dialog, or with its inbox closed', () => {
    const due = T0 + WAKE_QUIET_MIN * MIN;
    expect(tick(billion({ state: 'WORKING' }), due).woke).toBe(false);
    expect(tick(billion({ state: 'MESSAGE' }), due).woke).toBe(false);
    expect(tick(billion({ messagesHeld: true }), due).woke).toBe(false);
    expect(tick(billion({ exited: true }), due).woke).toBe(false);
  });

  it('not within two minutes of the owner typing there', () => {
    const due = T0 + WAKE_QUIET_MIN * MIN;
    const s = billion({ lastUserInputAt: due - OWNER_QUIET_MS + 1000 });
    expect(tick(s, due).woke).toBe(false);
    expect(tick(s, due + 1000).woke).toBe(true);
  });

  it('lets waiting mail go first', () => {
    const s = billion({ messagesHeld: true });
    sendText(s, 'a notice');
    s.messagesHeld = false;
    expect(tick(s, T0 + WAKE_QUIET_MIN * MIN).woke).toBe(false);
    dropMessages(s.id);
  });
});

describe('set_next_wake', () => {
  it('sets the next wake only, then the pace comes back', () => {
    const s = billion();
    expect(setNextWake(s, 10, T0)).toEqual({ at: T0 + 10 * MIN });
    expect(tick(s, T0 + 9 * MIN, true).woke).toBe(false);   // later than the busy pace
    expect(tick(s, T0 + 10 * MIN, true).woke).toBe(true);
    expect(s.wakeAt).toBeNull();
    expect(nextWakeAt(s, true)).toBe(T0 + (10 + WAKE_BUSY_MIN) * MIN);
  });

  it('takes 3 to 60 whole minutes', () => {
    const s = billion();
    for (const bad of [2, 61, 4.5, '10', undefined]) expect(setNextWake(s, bad).error).toMatch(/3 to 60/);
    expect(setNextWake(s, 3).at).toBeGreaterThan(0);
    expect(setNextWake(s, 60).at).toBeGreaterThan(0);
  });
});

describe('the set_next_wake tool', async () => {
  const { handleMcpMessage, toolsFor } = await import('../server/mcp.js');
  const call = (ctx, minutes) => handleMcpMessage({ jsonrpc: '2.0', id: 1, method: 'tools/call', params: { name: 'set_next_wake', arguments: { minutes } } }, ctx);

  it('is Billion\'s alone', () => {
    expect(toolsFor({ isBillion: true }).some(t => t.name === 'set_next_wake')).toBe(true);
    expect(toolsFor({}).some(t => t.name === 'set_next_wake')).toBe(false);
    expect(call({ session: {} }, 10).error.message).toMatch(/Unknown tool/);
  });

  it('sets the wake and says when, or why not', () => {
    const s = billion();
    const ctx = { session: s, setNextWake: (m) => setNextWake(s, m) };
    expect(call(ctx, 15).result.isError).toBe(false);
    expect(s.wakeAt).toBeGreaterThan(Date.now());
    expect(call(ctx, 90).result).toMatchObject({ isError: true, content: [{ text: expect.stringMatching(/3 to 60/) }] });
  });
});

describe('billionBusy', async () => {
  const { billionBusy } = await import('../server/billion-wake.js');
  const NOW = T0 + 60 * MIN;
  const since = T0;
  const card = (over) => ({ postedByBillion: true, state: 'in-progress', agentSessionId: 'w', ...over });
  const worker = (over) => ({ exited: false, state: 'WORKING', lastOutputAt: NOW, ...over });
  const busy = (jobs, session) => billionBusy(jobs, () => session, since, NOW);

  it('counts a Billion card whose worker is running', () => {
    expect(busy([card()], worker())).toBe(true);
    expect(busy([card({ postedByBillion: false })], worker())).toBe(false);
  });

  it('not a stalled, needs-you or gone worker: a stalled In-progress card keeps the quiet pace', () => {
    expect(busy([card()], worker({ state: 'WAITING', lastOutputAt: NOW - 60 * MIN }))).toBe(false);
    expect(busy([card()], worker({ state: 'MESSAGE' }))).toBe(false);
    expect(busy([card()], null)).toBe(false);
    const s = billion();
    const stalled = billionBusy([card()], () => worker({ state: 'WAITING', lastOutputAt: NOW - 60 * MIN }), since, NOW);
    expect(tick(s, T0 + WAKE_BUSY_MIN * MIN, stalled).woke).toBe(false);
    expect(tick(s, T0 + WAKE_QUIET_MIN * MIN, stalled).woke).toBe(true);
  });

  it('counts a card that reached Review or finished CI since the last wake, not one sitting there', () => {
    const iso = (t) => new Date(t).toISOString();
    expect(busy([card({ state: 'review', reviewAt: iso(since + MIN) })])).toBe(true);
    expect(busy([card({ state: 'review', reviewAt: iso(since - MIN), ciNotifiedAt: iso(since + MIN) })])).toBe(true);
    expect(busy([card({ state: 'review', reviewAt: iso(since - MIN) })])).toBe(false);
    expect(busy([card({ state: 'done', reviewAt: iso(since + MIN) })])).toBe(false);
  });
});