Ctrl K
ComunidadGuías…

__tests__/tools.test.js

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

Archivo original ↗Preguntar a KiBot
/**
 * tools/ — the tools extracted out of run().
 *
 * WHY THESE TESTS ARE SHORT, and the whole point of the split: every one of these behaviours was
 * already covered, but only through the entire agent loop — a fake page, a scripted model, a job
 * store, fifty lines of setup to prove that "save this sentence" saves a sentence. A tool that is
 * its own module is testable as one, which is what makes the next tool cheap to add.
 */
import { describe, it, expect, vi } from 'vitest';
import * as registry from '../src/tools/index.js';
import profiles from '../src/tools/profiles.js';

function ctx(over = {}) {
  const steps = [], said = [];
  return {
    steps, said,
    observe: (t) => said.push(t),
    step: (kind, text) => steps.push({ kind, text }),
    switchedSession: vi.fn(),
    describeProfiles: () => 'fb — facebook (in use)',
    page: () => ({ url: () => 'https://example.test/here' }),
    session: () => ({ id: 's1', profile: 'fb' }),
    setSession: vi.fn(),
    switchProfile: null,
    ...over,
  };
}

describe('the registry', () => {
  it('claims exactly the tools that have been moved, and nothing else', () => {
    // The loop falls through to its remaining switch for anything absent, so a name claimed here
    // and not implemented would silently do nothing at all.
    for (const name of ['list_profiles', 'use_profile', 'remember_about_me', 'save_my_writing',
                        'describe_my_voice', 'waiting_on', 'whose_is_this', 'record_reply',
                        'conversation', 'save_gig', 'save_reach', 'save_opportunity',
                        'diagnostics', 'note', 'finish',
                        'scroll', 'back', 'look', 'read', 'open']) {
      expect(registry.has(name), name).toBe(true);
      expect(typeof registry.REGISTRY[name], name).toBe('function');
    }
    /* Still in the switch. These are the ones the GUARDS act on — a click can be refused, a type
       can publish, an act waits for a person — so they move last, where a mistake is hardest to see
       and the policy that protects a real account is nearest. */
    for (const notYet of ['click', 'type', 'act', 'sweep', 'google', 'dig', 'save_lead']) {
      expect(registry.has(notYet), notYet).toBe(false);
    }
  });

  it('never claims a name it cannot run', () => {
    for (const [name, fn] of Object.entries(registry.REGISTRY)) {
      expect(typeof fn, name).toBe('function');
    }
  });
});

describe('use_profile — a switch is a different browser, not a setting', () => {
  it('hands the new session back, so the loop stops driving the old one', async () => {
    /*
     * The bug this guards: every helper reads the CURRENT page, and a switch that updated only some
     * of them left the agent working the browser it had left while the person watched the new one
     * do nothing.
     */
    const next = { id: 's2', profile: 'li' };
    const c = ctx({ switchProfile: vi.fn(async () => next) });
    await profiles.use_profile(c, { profile: 'li' });
    expect(c.setSession).toHaveBeenCalledWith(next);
    expect(c.switchedSession).toHaveBeenCalledWith({ sessionId: 's2', profile: 'li' });
    expect(c.said.join(' ')).toMatch(/Switched to "li"/);
  });

  it('says so plainly when switching is not available in this run', async () => {
    const c = ctx({ switchProfile: null });
    await profiles.use_profile(c, { profile: 'li' });
    expect(c.said[0]).toMatch(/not available in this run/);
    expect(c.setSession).not.toHaveBeenCalled();
  });

  it('list_profiles reads, and does not open anything', async () => {
    const c = ctx();
    await profiles.list_profiles(c);
    expect(c.said[0]).toMatch(/fb — facebook/);
    expect(c.setSession).not.toHaveBeenCalled();
  });
});

describe('current_url', () => {
  it('reads a youtube channel id out of the path so a link can be built, not guessed', async () => {
    const c = ctx({ page: () => ({ url: () => 'https://studio.youtube.com/channel/UCabc123DEF/editing/branding', title: async () => 'YouTube Studio' }) });
    await registry.run('current_url', c);
    expect(c.said.join(' ')).toMatch(/UCabc123DEF/);
    expect(c.said.join(' ')).toMatch(/studio\.youtube\.com\/channel\/UCabc123DEF/);
  });
  it('still reports the plain URL when there is no id in the path', async () => {
    const c = ctx({ page: () => ({ url: () => 'https://example.test/here', title: async () => 'X' }) });
    await registry.run('current_url', c);
    expect(c.said.join(' ')).toMatch(/example\.test\/here/);
  });
});