Ctrl K
ComunidadGuías…

__tests__/flow-cards.test.js

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

Archivo original ↗Preguntar a KiBot
// Route cards for a flow's steps.
//
// The card mechanism was built for Herald and gated to it (`/^herald\./`). A flow STEP is the better
// candidate by a wide margin: it performs the same act on the same site on EVERY run, which is the
// repetition a card pays for — a scout that walks eight pages to read a listing walks them again
// tomorrow, and once the browser knows which request actually returned that listing the whole walk
// becomes one in-page fetch. Cheaper in tokens, faster, and immune to the layout moving.
//
// The intent is the STEP (`flow:<flow>:<node>`), never the role: several flows may share a role, and
// two steps of one flow must learn two different cards.
import { describe, it, expect } from 'vitest';
import { readFileSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import jobs from '../src/jobs.js';

const agentSrc = readFileSync(fileURLToPath(new URL('../src/agent.js', import.meta.url)), 'utf8');
const serverSrc = readFileSync(fileURLToPath(new URL('../src/server.js', import.meta.url)), 'utf8');

describe('a flow step learns a card of its own', () => {
  it('keys the intent on the STEP, so a shared role never collides with itself', () => {
    expect(agentSrc).toMatch(/const flowIntent = \(job\.workflowId && job\.nodeId\) \? `flow:\$\{job\.workflowId\}:\$\{job\.nodeId\}` : null;/);
    expect(agentSrc).toMatch(/const cardIntent = flowIntent \|\| roles\.canonical\(role\);/);
  });

  it('learns for a flow step AND still for a Herald walk', () => {
    expect(agentSrc).toMatch(/const learnsCards = !!flowIntent \|\| \/\^herald\\.\/\.test\(String\(role \|\| ''\)\);/);
  });

  it('arms the recorder and asks the store with that same intent — one name, both directions', () => {
    expect(agentSrc).toMatch(/session\.recorder\.arm\(\{ intent: cardIntent, verification: settings\.replayVerification \|\| null \}\)/);
    expect(agentSrc).toMatch(/const intent = cardIntent;/);
    // never the role name again in either place
    expect(agentSrc).not.toMatch(/recorder\.arm\(\{ intent: roles\.canonical\(role\) \}\)/);
  });

  it('the safety is untouched: an unproven card still walks the UI, a failure quarantines', () => {
    expect(agentSrc).toMatch(/An unproven card \(freshly recorded, never verified\) plans to 'ui' on/);
    expect(agentSrc).toMatch(/route card — \$\{plan\.reason\}/);
    expect(agentSrc).toMatch(/LEARN THE CARD — but only from a walk that actually SUCCEEDED/);
  });
});

describe('the step id reaches the job that does the work', () => {
  it('the flow driver passes it', () => {
    expect(serverSrc).toMatch(/runId: runId \|\| null, nodeId: node\.id \|\| null,/);
  });

  it('a job keeps it, and every view of that job hands it back', () => {
    const j = jobs.create({ owner: 'o', goal: 'read the listing', workflowId: 'useme-scout', runId: 'useme-scout-1', nodeId: 'extract' });
    expect(j.nodeId).toBe('extract');
    expect(jobs.view(j).nodeId).toBe('extract');
    expect(jobs.get(j.id).nodeId).toBe('extract');
  });

  it('a job that is not part of a flow simply has none, and learns nothing', () => {
    const j = jobs.create({ owner: 'o', goal: 'look around' });
    expect(j.nodeId).toBeNull();
    expect(j.workflowId).toBeNull();
  });
});