Ctrl K
ComunidadGuías…

__tests__/local-prompt.test.js

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

Archivo original ↗Preguntar a KiBot
/*
 * THE PROMPT, AND THE ONE BUG IN IT THAT COSTS A WHOLE TRAINING ROUND.
 *
 * Everything here guards a failure that produces a working-looking model and a bad one:
 *
 *   - the catalogue is missing, so the model must memorise the tool list and every new tool needs a
 *     training round before it can ever be chosen;
 *   - the catalogue describes a tool that is not the tool that runs, which is how `download_file`
 *     sat in the live catalogue for months pointing at dead code;
 *   - the numbered list is absent, so `click [13]` is a target derived from nothing and the model
 *     learns to guess indices — the exact mistake the fine-tune exists to remove;
 *   - a STALE numbered list is included, which is worse than none: it teaches the model to act on
 *     numbers that have already expired.
 */
import { describe, it, expect } from 'vitest';
import { systemFor, userFor, catalogue, signatureOf, purposeOf } from '../src/localPrompt.js';

const fn = (name, description, properties = {}, required = []) => ({
  type: 'function',
  function: { name, description, parameters: { type: 'object', properties, required } },
});

const TOOLS = [
  fn('look', 'Every clickable thing, numbered. The numbers are only valid until the page changes.'),
  fn('click', 'Click a numbered element on the page.', { index: { type: 'integer' }, why: { type: 'string' } }, ['index']),
  fn('type', 'Type into a numbered field.', { index: { type: 'integer' }, text: { type: 'string' } }, ['index', 'text']),
];

describe('the catalogue', () => {
  it('lists every tool with its arguments, so an unseen tool can still be chosen', () => {
    const c = catalogue(TOOLS);
    expect(c).toContain('look()');
    expect(c).toContain('click(index, why?)');
    /* Required arguments bare, optional marked — the shape of the answer reads left to right. */
    expect(c).toContain('type(index, text)');
  });

  it('keeps the LAST declaration when a name is declared twice', () => {
    /* The live agent merges handlers with Object.assign, so the later declaration is the one that
       runs. A catalogue that documents the earlier one describes behaviour that does not exist —
       which is precisely how one tool in this codebase was unreachable for months while being
       advertised to the model on every single call. */
    const dupes = [...TOOLS, fn('click', 'Actually follows a link by url.', { url: { type: 'string' } }, ['url'])];
    const c = catalogue(dupes);
    expect(c).toContain('click(url)');
    expect(c).not.toContain('click(index, why?)');
    expect(c.match(/^click\(/gm)).toHaveLength(1);
  });

  it('clips a paragraph to one clause instead of paying for it on every example', () => {
    /* The live descriptions are paragraphs, and prompt length sets training time directly. */
    const long = fn('dig', 'Read the page properly. '.repeat(40));
    expect(purposeOf(long.function.description).length).toBeLessThanOrEqual(90);
  });

  it('leaves a short description whole instead of eating its last word', () => {
    /* "Go to a web address." has no whitespace after the full stop, so a sentence search finds
       nothing; trimming a partial word off a string that was never truncated then produced
       "Go to a web" — every brief tool in the catalogue lost its object. */
    expect(purposeOf('Go to a web address.')).toBe('Go to a web address');
    expect(purposeOf('Go back to the previous page.')).toBe('Go back to the previous page');
  });

  it('cuts a long description at its first sentence, not mid-word', () => {
    const p = purposeOf('The CONTROLS on this page — buttons, links and fields — as a numbered list. More text here.');
    expect(p).toBe('The CONTROLS on this page');
  });

  it('survives a tool with no description and one with no arguments', () => {
    expect(catalogue([fn('back', '')])).toBe('back()');
    expect(signatureOf({ name: 'look' })).toBe('look()');
  });
});

describe('the system message', () => {
  it('carries the catalogue and the answer shape', () => {
    const s = systemFor({ role: 'research.web', site: 'google', tools: TOOLS });
    expect(s).toContain('research.web');
    expect(s).toContain('google');
    expect(s).toContain('click(index, why?)');
    expect(s).toContain('{"tool":"<name>","args":{...}}');
  });

  it('puts the role playbook in the prompt, not in the adapter', () => {
    /* Playbooks are edited by hand between rounds. In the prompt a correction lands this afternoon;
       baked into an adapter it waits for a night of training on somebody's laptop. */
    const s = systemFor({ role: 'sales', tools: TOOLS, playbook: 'Never quote a price.' });
    expect(s).toContain('Never quote a price.');
  });

  it('is identical for identical inputs, because train and serve both call it', () => {
    const a = systemFor({ role: 'r', site: 's', tools: TOOLS });
    const b = systemFor({ role: 'r', site: 's', tools: TOOLS });
    expect(a).toBe(b);
  });
});

describe('the user message', () => {
  const marks = '[1] Sign in\n[2] Dodaj ofertę\n[3] Close';

  it('INCLUDES THE NUMBERED LIST, which is the only thing that makes an index learnable', () => {
    const u = userFor({ goal: 'Submit the offer', observed: [{ kind: 'look', text: 'useme.com — 3 things to click', marks }] });
    expect(u).toContain('[2] Dodaj ofertę');
  });

  it('includes only the MOST RECENT list, never an expired one', () => {
    /* Two lists in one prompt is worse than none. The older numbers no longer refer to anything on
       screen, and including them teaches exactly the stale-index click this is meant to prevent. */
    const u = userFor({
      goal: 'g',
      observed: [
        { kind: 'look', text: 'page one — 2 things', marks: '[1] Old thing\n[2] Older thing' },
        { kind: 'open', text: 'https://example.com/two' },
        { kind: 'look', text: 'page two — 3 things', marks },
      ],
    });
    expect(u).toContain('[2] Dodaj ofertę');
    expect(u).not.toContain('Old thing');
    /* The earlier look still appears as a line — it happened — just without its expired numbers. */
    expect(u).toContain('page one — 2 things');
  });

  it('says so plainly when nothing has been seen', () => {
    expect(userFor({ goal: 'Start' })).toContain('- nothing yet');
  });

  it('keeps the goal first, because it is the thing that does not change', () => {
    expect(userFor({ goal: 'Find three suppliers', observed: [] }).startsWith('GOAL: Find three suppliers')).toBe(true);
  });
});

describe('the student sees the page it decides on', () => {
  it('shows the page on the most recent observation that has one, and only that one', () => {
    /* The teacher was handed six thousand characters of the page; the student was told a page
       had been read. Two rounds scored zero on open, run_script and dig for exactly that. */
    const u = userFor({ goal: 'find the suppliers', observed: [
      { kind: 'read', text: 'read the page (900 characters)', content: 'OLD PAGE: nothing here' },
      { kind: 'scroll', text: 'scrolled down' },
      { kind: 'read', text: 'read the page (1200 characters)', content: 'NEW PAGE: Suppliers - Acme, Globex' },
    ] });
    expect(u).toMatch(/NEW PAGE: Suppliers/);
    expect(u).not.toMatch(/OLD PAGE/);
  });

  it('lets the numbered list win where a look came after the read', () => {
    /* A look step records only marks, and they are the page for a click. */
    const u = userFor({ goal: 'g', observed: [
      { kind: 'read', text: 'read the page (1200 characters)', content: 'PAGE TEXT' },
      { kind: 'look', text: 'Acme - 4 things to click', marks: '[1] Suppliers [2] Contact' },
    ] });
    expect(u).toMatch(/\[1\] Suppliers/);
    expect(u).not.toMatch(/PAGE TEXT/);
  });
});