Ctrl K
ComunidadGuías…

__tests__/operator-tools.test.js

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

Archivo original ↗Preguntar a KiBot
/**
 * The operator's hands, against a fake context: what they refuse by construction (a flow without a
 * verify step, a watcher without a role step or a budget, auto-send), and that the reads and waits are
 * wired to the right internals and marked repeatable.
 */
import { describe, it, expect } from 'vitest';
import fs from 'fs';
import { Registry } from '../src/operator/registry.js';
import { registerOperatorTools, flowProblems, sanitizeFlow, isWatcher, summarizeRun } from '../src/operator/tools.js';

const watcher = { name: 'Reddit · post watcher', active: true, autoApprove: true,
  nodes: [{ id: 'trigger', type: 'trigger', trigger: { type: 'schedule', every: 'minute', n: 15 } }, { id: 'n0', type: 'agent', role: 'reddit-watch', profile: 'reddit', goal: 'gather', maxSteps: 30 }], edges: [{ from: 'trigger', to: 'n0' }] };
const oneOff = { name: 'LinkedIn · scout', nodes: [{ id: 't', type: 'trigger' }, { id: 'a', type: 'agent', role: 'x', goal: 'read', maxSteps: 20 }, { id: 'v', type: 'verify', url: 'https://www.linkedin.com/feed/', text: 'Feed' }], edges: [] };

function fakeCtx() {
  const saved = [];
  return {
    saved,
    ops: { readGuide: (s) => 'GUIDE ' + (s || 'all'), readMemory: () => 'notes', appendMemory: () => 42, ringLines: ({ grep }) => ['09:00:00 line ' + (grep || '')] },
    workflows: { all: () => [{ id: 'w1', name: 'W', active: true, nodes: watcher.nodes }, { id: 'f1', name: 'F', nodes: oneOff.nodes }], read: (id) => (id === 'f1' ? { id: 'f1', ...oneOff } : null), runsFor: () => [{ id: 'r1', workflow_id: 'w1', status: 'done', steps: [{ node_id: 'v', type: 'verify', status: 'done', output: { found: true } }] }], readRun: (id) => (id === 'r1' ? { id: 'r1', status: 'done', steps: [] } : null) },
    feed: { getConfig: () => ({ mode: 'posts', meName: 'Wesley', postUrls: ['https://www.facebook.com/groups/g/posts/1/'] }), setConfig: (id, c) => ({ id, ...c }), list: () => [{ key: 'k', title: 't', kind: 'reply', handled: false, fields: { status: 'waiting on you', author: 'Joe' }, draft: 'hi' }], counts: () => ({ total: 1, unhandled: 1 }), stateOf: () => 'drafted' },
    health: (id) => ({ id, running: false, stale: false }),
    runningWatchers: new Set(['facebook-post-watcher']),
    postIdOf: (u) => (String(u).match(/posts\/(\d+)|post_id=(\d+)/) || []).slice(1).find(Boolean) || null,
    runWatcher: (id) => ({ runId: id + '-1', status: 'running' }),
    setActive: (id, a) => ({ id, active: a }),
    probe: (id, url, expand) => ({ url, expand, count: 3 }),
    jobsSummary: () => ({ jobs: [], history: [] }), jobDetail: (id) => ({ id, steps: [] }), sessions: () => [], look: (p) => ({ profile: p, url: 'https://x', controls: [] }),
    agentTools: () => [{ name: 'look' }], listRoles: () => [{ name: 'a', site: 'facebook' }, { name: 'b', site: 'reddit' }], getRole: (n) => (n === 'a' ? { name: 'a', prompt: 'p' } : null), saveRole: (name, role) => ({ id: name || 'new', ...role }),
    saveFlow: (f) => { saved.push(f); return { id: f.id || 'new', ...f }; }, runFlow: (id, input) => ({ runId: id + '-r', input }), platforms: () => [],
  };
}

describe('everything gb_walk sends, the server accepts', () => {
  /*
   * Shipped and broken in one go: `ask` was added to the gb_walk schema and used inside the
   * server's startWalk, but never added to startWalk's own parameter list. Every walk answered
   * "did not work: ask is not defined" and the assistant retried it. Nothing caught it, because
   * the tool and its handler live in different files and no test compared them.
   *
   * So: discover what the tool actually SENDS by calling it against a recording context, then
   * check the server's destructuring list names each one. Text, not types, because that is where
   * the drift is.
   */
  it('startWalk destructures every key gb_walk passes it', async () => {
    const ctx = fakeCtx();
    let sent = null;
    ctx.startWalk = (a) => { sent = a; return { jobId: 'j-1' }; };
    const reg = new Registry();
    registerOperatorTools(reg, ctx);
    await reg.execute('gb_walk', { goal: 'a plan', ask: 'what I said', profile: 'google' });
    expect(sent).toBeTruthy();
    expect(sent.ask).toBe('what I said');

    const src = fs.readFileSync(new URL('../src/server.js', import.meta.url), 'utf8');
    const m = /startWalk: async \(\{([^}]*)\}\)/.exec(src);
    expect(m).toBeTruthy();
    const accepted = m[1].split(',').map((s) => s.trim().split(/[:=]/)[0].trim()).filter(Boolean);
    for (const key of Object.keys(sent)) expect(accepted).toContain(key);
  });
});
describe('flowProblems — a watcher judged as a watcher, a flow as a flow', () => {
  it('accepts a good watcher and a good one-off flow', () => { expect(isWatcher(watcher)).toBe(true); expect(flowProblems(watcher)).toEqual([]); expect(flowProblems(oneOff)).toEqual([]); });
  it('refuses a watcher without a schedule, a role step, or a budget', () => {
    expect(flowProblems({ ...watcher, nodes: watcher.nodes.map((n) => (n.type === 'trigger' ? { ...n, trigger: { type: 'schedule' } } : n)) }).join(' ')).toMatch(/schedule trigger needs/);
    expect(flowProblems({ ...watcher, nodes: watcher.nodes.map((n) => (n.type === 'agent' ? { ...n, role: '' } : n)) }).join(' ')).toMatch(/agent step with a role/);
    expect(flowProblems({ ...watcher, nodes: watcher.nodes.map((n) => (n.type === 'agent' ? { ...n, maxSteps: 0 } : n)) }).join(' ')).toMatch(/needs a budget/);
  });
  it('refuses a one-off flow without a verify step, or with a verify that takes its url from another step', () => {
    expect(flowProblems({ ...oneOff, nodes: oneOff.nodes.filter((n) => n.type !== 'verify') }).join(' ')).toMatch(/verify step/);
    expect(flowProblems({ ...oneOff, nodes: oneOff.nodes.map((n) => (n.type === 'verify' ? { ...n, url: '{{scrape.url}}' } : n)) }).join(' ')).toMatch(/must not take its url/);
  });
  it('sanitize: auto-send always off; a watcher may be active, a one-off never', () => {
    expect(sanitizeFlow(watcher)).toMatchObject({ autoApprove: false, active: true });
    expect(sanitizeFlow({ ...oneOff, active: true, autoApprove: true })).toMatchObject({ autoApprove: false, active: false });
  });
});

describe('registerOperatorTools — the hands on a fake context', () => {
  it('registers security recon and red team tools (gb_recon, gb_security_eval, gb_redteam_probe, gb_recon_probe, gb_page_script)', async () => {
    const reg = new Registry();
    const reconTargetMock = { score: 85, grade: 'B', findings: [], waf: { detected: false } };
    const ctx = {
      ...fakeCtx(),
      recon: {
        auditTarget: async (t) => ({ target: t, ...reconTargetMock }),
        auditActivePage: async () => ({ advisories: [], sensitiveStorageFound: 0 }),
        runRedteamAudit: async (t) => ({ target: t, endpoints: { findings: [] } }),
        testHttpSessionInvalidation: async () => ({ vulnerable: false }),
        executeSecurityScript: async (p, s) => ({ success: true })
      }
    };
    registerOperatorTools(reg, ctx);
    for (const t of ['gb_recon', 'gb_security_eval', 'gb_redteam_probe', 'gb_recon_probe', 'gb_page_script']) {
      expect(reg.has(t), t).toBe(true);
    }
    const res = JSON.parse(await reg.execute('gb_recon', { target: 'useme.com' }));
    expect(res.target).toBe('useme.com');
    expect(res.grade).toBe('B');
  });

  it('registers the full belt with repeatable reads/waits', () => {
    const reg = new Registry(); const n = registerOperatorTools(reg, fakeCtx());
    expect(n).toBeGreaterThan(25);
    for (const t of ['gb_guide', 'gb_memory_write', 'gb_logs', 'gb_runs_recent', 'gb_job', 'gb_look', 'gb_busy', 'gb_watchers', 'gb_watcher_feed', 'gb_watcher_config', 'gb_watcher_posts', 'gb_watcher_run', 'gb_watcher_wait', 'gb_watcher_probe', 'gb_role_get', 'gb_role_update', 'gb_flow_save', 'gb_flow_run', 'gb_flow_wait', 'gb_platforms']) expect(reg.has(t), t).toBe(true);
    expect(reg.get('gb_watcher_wait').repeatable).toBe(true); expect(reg.get('gb_logs').repeatable).toBe(true); expect(reg.get('gb_flow_save').repeatable).toBe(false);
  });
  it('gb_flow_save refuses a bad watcher and saves a good one with active kept and auto-send off', async () => {
    const ctx = fakeCtx(); const reg = new Registry(); registerOperatorTools(reg, ctx);
    const bad = JSON.parse(await reg.execute('gb_flow_save', { flow: { ...watcher, nodes: watcher.nodes.filter((n) => n.type !== 'agent') } }));
    expect(bad.saved).toBe(false); expect(ctx.saved.length).toBe(0);
    const good = JSON.parse(await reg.execute('gb_flow_save', { flow: watcher }));
    expect(good).toMatchObject({ saved: true, watcher: true, active: true }); expect(ctx.saved[0].autoApprove).toBe(false);
  });
  it('gb_watchers lists only scheduled flows with config and health; gb_watcher_posts adds by post id once', async () => {
    const ctx = fakeCtx(); const reg = new Registry(); registerOperatorTools(reg, ctx);
    const ws = JSON.parse(await reg.execute('gb_watchers', {}));
    expect(ws.length).toBe(1); expect(ws[0]).toMatchObject({ id: 'w1', config: { mode: 'posts', meName: 'Wesley' } });
    const p = JSON.parse(await reg.execute('gb_watcher_posts', { watcherId: 'w1', add: 'https://www.facebook.com/groups/g/posts/1/?x=1' }));
    expect(p.postUrls.length).toBe(1);                                   // same post id → not added twice
    const bad = JSON.parse(await reg.execute('gb_watcher_posts', { watcherId: 'w1', add: 'https://www.facebook.com/' }));
    expect(bad.error).toMatch(/not a post link/);
  });
  it('gb_busy, gb_guide, gb_logs and gb_watcher_feed read the right internals', async () => {
    const reg = new Registry(); registerOperatorTools(reg, fakeCtx());
    expect(JSON.parse(await reg.execute('gb_busy', {})).running).toEqual(['facebook-post-watcher']);
    expect(await reg.execute('gb_guide', { section: 'watchers' })).toBe('GUIDE watchers');
    expect(await reg.execute('gb_logs', { grep: 'ERROR' })).toMatch(/line ERROR/);
    const feed = JSON.parse(await reg.execute('gb_watcher_feed', { watcherId: 'w1', onlyWaiting: true }));
    expect(feed.items[0]).toMatchObject({ author: 'Joe', state: 'drafted', draft: 'hi' });
  });
  it('summarizeRun marks verified only when every verify step found its text', () => {
    expect(summarizeRun({ id: 'r', status: 'done', steps: [{ node_id: 'v', type: 'verify', status: 'done', output: { found: true } }] }).verified).toBe(true);
    expect(summarizeRun({ id: 'r', status: 'done', steps: [{ node_id: 'v', type: 'verify', status: 'done', output: { found: false } }] }).verified).toBe(false);
  });
});