Ctrl K
ComunidadGuías…

__tests__/operator-harness.test.js

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

Archivo original ↗Preguntar a KiBot
/**
 * The operator harness, driven by a scripted model: the task list is state, the identical-call
 * breaker refuses a repeat and ends a run that keeps repeating, a run ends only through finish (prose
 * alone is nudged, then finished as blocked), the budget grows with done tasks and ends the run when
 * spent, the owner's `say` lands as the next turn, and the journal is written. No network, no disk
 * beyond a temp dir.
 */
import { describe, it, expect } from 'vitest';
import fs from 'node:fs';
import os from 'node:os';
import path from 'node:path';
import { OperatorRun } from '../src/operator/harness.js';
import { Registry } from '../src/operator/registry.js';

const tmp = () => fs.mkdtempSync(path.join(os.tmpdir(), 'op-'));
const scripted = (turns) => { let i = 0; return async ({ messages }) => { const t = turns[Math.min(i, turns.length - 1)]; i++; return typeof t === 'function' ? t(messages) : t; }; };
const call = (name, args) => ({ content: '', toolCalls: [{ name, args }] });
const say = (content) => ({ content, toolCalls: [] });

function baseRegistry() {
  const reg = new Registry();
  let reads = 0;
  reg.register('read_thing', 'read', { type: 'object', properties: { id: { type: 'string' } } }, async ({ id }) => ({ id, reads: ++reads, value: 'v' + id }));
  reg.register('wait_thing', 'wait', { type: 'object', properties: {} }, async () => ({ waited: true }), { repeatable: true });
  reg.register('fail_thing', 'fail', { type: 'object', properties: {} }, async () => ({ error: 'nope' }));
  return reg;
}

describe('OperatorRun', () => {
  it('runs tools, keeps the task list as state, and ends through finish with a report', async () => {
    const reg = baseRegistry();
    const chat = scripted([
      call('save_task_list', { tasks: [{ title: 'read' }, { title: 'prove' }] }),
      call('read_thing', { id: 'a' }),
      call('update_task', { index: 0, done: true, note: 'read a' }),
      call('finish', { status: 'done', summary: 'a is v-a', report: { evidence: 'read_thing returned va' } }),
    ]);
    const run = new OperatorRun({ goal: 'read a', chat, registry: reg, systemPrompt: 'sys', persistDir: tmp() });
    const v = await run.run();
    expect(v.status).toBe('done'); expect(v.finalLine).toMatch(/^DONE: a is v-a/); expect(v.report.evidence).toMatch(/va/);
    expect(v.tasks[0].done).toBe(true); expect(v.tasks[0].note).toBe('read a'); expect(v.tasks[1].done).toBe(false);
    expect(v.events.some((e) => e.kind === 'tool' && e.name === 'read_thing')).toBe(true);
  });

  it('refuses an identical non-repeatable call with a nudge, lets a repeatable one through, and stops a run that keeps repeating', async () => {
    const reg = baseRegistry();
    const chat = scripted([call('read_thing', { id: 'x' }), call('read_thing', { id: 'x' }), call('wait_thing', {}), call('wait_thing', {}), call('read_thing', { id: 'x' }), call('read_thing', { id: 'x' }), call('read_thing', { id: 'x' }), call('read_thing', { id: 'x' })]);
    const run = new OperatorRun({ goal: 'g', chat, registry: reg, systemPrompt: 'sys', persistDir: tmp() });
    const v = await run.run();
    const toolEvents = v.events.filter((e) => e.kind === 'tool');
    expect(toolEvents.filter((e) => e.name === 'read_thing').length).toBe(1);     // executed once, then refused
    expect(toolEvents.filter((e) => e.name === 'wait_thing').length).toBe(2);     // repeatable: both ran
    expect(v.status).toBe('blocked'); expect(v.finalLine).toMatch(/stuck repeating read_thing/);
    const refused = run.messages.filter((m) => m.role === 'tool' && /already called read_thing/.test(m.content));
    expect(refused.length).toBeGreaterThan(0);
  });

  it('nudges prose without tools, then finishes as blocked; a DONE: line in prose is honoured', async () => {
    const reg = baseRegistry();
    const v1 = await new OperatorRun({ goal: 'g', chat: scripted([say('thinking…'), say('still thinking'), say('and more'), say('hm')]), registry: reg, systemPrompt: 'sys', persistDir: tmp() }).run();
    expect(v1.status).toBe('blocked'); expect(v1.finalLine).toMatch(/without finishing/);
    const v2 = await new OperatorRun({ goal: 'g', chat: scripted([say('DONE: it works, verified')]), registry: reg, systemPrompt: 'sys', persistDir: tmp() }).run();
    expect(v2.status).toBe('done'); expect(v2.finalLine).toBe('DONE: it works, verified');
  });

  it('ends when the budget is spent and grows the budget as tasks get done', async () => {
    const reg = baseRegistry();
    let n = 0;
    const chat = async () => ({ content: '', toolCalls: [{ name: 'read_thing', args: { id: String(n++) } }] });
    const v = await new OperatorRun({ goal: 'g', chat, registry: reg, systemPrompt: 'sys', persistDir: tmp(), startIterations: 5, maxIterations: 8 }).run();
    expect(v.status).toBe('blocked'); expect(v.iterations).toBe(5); expect(v.finalLine).toMatch(/budget spent/);
    const reg2 = baseRegistry(); let m = 0;
    const chat2 = async () => (m === 0 ? (m++, call('save_task_list', { tasks: [{ title: 't' }] })) : m === 1 ? (m++, call('update_task', { index: 0, done: true })) : ({ content: '', toolCalls: [{ name: 'read_thing', args: { id: String(m++) } }] }));
    const v2 = await new OperatorRun({ goal: 'g', chat: chat2, registry: reg2, systemPrompt: 'sys', persistDir: tmp(), startIterations: 3, maxIterations: 10 }).run();
    expect(v2.iterations).toBe(10);   // 3 + 40 earned, capped at max 10
  });

  it('ends a run whose tools keep failing, and delivers the owner\'s say as the next user turn', async () => {
    const reg = baseRegistry(); let k = 0;
    const chat = async () => ({ content: '', toolCalls: [{ name: 'fail_thing', args: { n: k++ } }] });
    const v = await new OperatorRun({ goal: 'g', chat, registry: reg, systemPrompt: 'sys', persistDir: tmp() }).run();
    expect(v.status).toBe('blocked'); expect(v.finalLine).toMatch(/eight tool calls in a row failed/);
    const seen = [];
    const run = new OperatorRun({ goal: 'g', chat: scripted([(msgs) => { seen.push(msgs.map((x) => x.content)); run.say('look at Joe'); return call('read_thing', { id: '1' }); }, (msgs) => { seen.push(msgs.map((x) => x.content)); return call('finish', { status: 'done', summary: 'ok' }); }]), registry: reg, systemPrompt: 'sys', persistDir: tmp() });
    await run.run();
    expect(seen[1].some((c) => /The owner says: look at Joe/.test(c))).toBe(true);
  });

  it('journals the run to disk with the transcript for resume', async () => {
    const dir = tmp(); const reg = baseRegistry();
    const run = new OperatorRun({ goal: 'g', chat: scripted([call('finish', { status: 'done', summary: 'ok' })]), registry: reg, systemPrompt: 'sys', persistDir: dir });
    await run.run();
    const rec = JSON.parse(fs.readFileSync(path.join(dir, run.id + '.json'), 'utf8'));
    expect(rec.status).toBe('done'); expect(rec.messages.length).toBeGreaterThan(2); expect(rec.events.some((e) => e.kind === 'end')).toBe(true);
  });
});