Files
brain/tools/router-verity.test.mjs
T

154 lines
9.1 KiB
JavaScript

import { describe, it, expect } from 'vitest';
import {
VERITY_SLOTS, DISCREPANCY_KINDS, validateVerityTrace, groundVerityTrace, buildVerityPrompt, runVerityCheck,
} from './router-verity.mjs';
const OK_TRACE = {
owner_asks: [
{ ask: 'начать с входа роутера', source: 'prompt' },
{ ask: 'без субагентов, inline', source: 0 },
],
discrepancies: [
{ kind: 'missing', detail: 'inline-режим в плане не отражён', source: 0 },
],
};
describe('VERITY_SLOTS / DISCREPANCY_KINDS — закрытые наборы (§2.4 / §6.2)', () => {
it('обязательные слоты анти-внушения', () => {
expect(VERITY_SLOTS).toContain('owner_asks');
expect(VERITY_SLOTS).toContain('discrepancies');
});
it('виды расхождения — закрытый список', () => {
expect(DISCREPANCY_KINDS).toEqual(['missing', 'distorted']);
});
});
describe('validateVerityTrace (механический валидатор формы; пустой слот = флаг)', () => {
it('полная корректная трасса → ok', () => {
expect(validateVerityTrace(OK_TRACE)).toEqual({ ok: true, missingSlots: [], badItems: [] });
});
it('пустой owner_asks → невалидно (анти-внушение: независимый вывод обязателен)', () => {
const r = validateVerityTrace({ ...OK_TRACE, owner_asks: [] });
expect(r.ok).toBe(false); expect(r.missingSlots).toContain('owner_asks');
});
it('discrepancies отсутствует (не массив) → невалидно', () => {
const r = validateVerityTrace({ owner_asks: OK_TRACE.owner_asks });
expect(r.ok).toBe(false); expect(r.missingSlots).toContain('discrepancies');
});
it('пустой discrepancies (массив) → ВАЛИДНО (план верен — расхождений нет)', () => {
expect(validateVerityTrace({ owner_asks: OK_TRACE.owner_asks, discrepancies: [] }).ok).toBe(true);
});
it('пункт owner_asks без source → badItems', () => {
const r = validateVerityTrace({ ...OK_TRACE, owner_asks: [{ ask: 'x' }] });
expect(r.ok).toBe(false); expect(r.badItems).toContain('owner_asks[0]');
});
it('пункт discrepancies со свободным kind → badItems (только закрытый список)', () => {
const r = validateVerityTrace({ ...OK_TRACE, discrepancies: [{ kind: 'maybe', detail: 'd', source: 0 }] });
expect(r.ok).toBe(false); expect(r.badItems).toContain('discrepancies[0]');
});
it('не объект → невалидно, все слоты missing', () => {
expect(validateVerityTrace(null).ok).toBe(false);
expect(validateVerityTrace('x').missingSlots).toEqual([...VERITY_SLOTS]);
});
});
describe('groundVerityTrace (§2.2 — source резолвится: prompt | индекс ответа в диапазоне)', () => {
const trace = {
owner_asks: [{ ask: 'a', source: 'prompt' }, { ask: 'b', source: 1 }],
discrepancies: [{ kind: 'missing', detail: 'd', source: 0 }],
};
it('все источники в диапазоне → grounded', () => {
expect(groundVerityTrace(trace, { answersCount: 2 })).toEqual({ grounded: true, dangling: [] });
});
it('ссылка на несуществующий ответ → dangling (роутер сослался мимо истории)', () => {
const r = groundVerityTrace(trace, { answersCount: 1 }); // индекс 1 вне [0,1)
expect(r.grounded).toBe(false);
expect(r.dangling.some((d) => d.includes('owner_asks[1]'))).toBe(true);
});
it('источник из discrepancies вне диапазона → dangling', () => {
const t = { owner_asks: [{ ask: 'a', source: 'prompt' }], discrepancies: [{ kind: 'missing', detail: 'd', source: 5 }] };
expect(groundVerityTrace(t, { answersCount: 2 }).grounded).toBe(false);
});
it('prompt всегда валиден даже при нулевой истории', () => {
const t = { owner_asks: [{ ask: 'a', source: 'prompt' }], discrepancies: [] };
expect(groundVerityTrace(t, { answersCount: 0 }).grounded).toBe(true);
});
});
describe('buildVerityPrompt (чистая {system,user}; анти-внушение §2.4 зашит порядком+физикой)', () => {
const base = { rawPrompt: 'начни с входа роутера', answers: ['inline без субагентов', 'граф решений отдельно'], plan: 'план: правлю router-engine' };
it('детерминирована: тот же вход → тот же текст', () => {
const a = buildVerityPrompt(base);
const b = buildVerityPrompt(base);
expect(a.system).toBe(b.system);
expect(a.user).toBe(b.user);
});
it('system: инструкция порядка — сперва owner_asks из сырых слов, ПОТОМ discrepancies', () => {
const { system } = buildVerityPrompt(base);
const iAsks = system.indexOf('owner_asks');
const iDisc = system.indexOf('discrepancies');
expect(iAsks).toBeGreaterThanOrEqual(0);
expect(iAsks).toBeLessThan(iDisc); // порядок инструкций
expect(system).toMatch(/анти-внушени|не поддакивай/i);
});
it('user: сырые слова владельца ФИЗИЧЕСКИ перед планом', () => {
const { user } = buildVerityPrompt(base);
const iRaw = user.indexOf('СЫРОЙ ПРОМПТ');
const iPlan = user.indexOf('ПЛАН КОНТРОЛЛЕРА');
expect(iRaw).toBeGreaterThanOrEqual(0);
expect(iRaw).toBeLessThan(iPlan);
});
it('user: ВСЯ история ответов нумеруется по порядку (0..N-1)', () => {
const { user } = buildVerityPrompt(base);
expect(user).toMatch(/Ответ 0: inline без субагентов/);
expect(user).toMatch(/Ответ 1: граф решений отдельно/);
});
it('пустая история → «(ответов пока нет)», план может отсутствовать', () => {
const { user } = buildVerityPrompt({ rawPrompt: 'старт', answers: [], plan: null });
expect(user).toMatch(/ответов пока нет/);
expect(user).toMatch(/плана пока нет/);
});
it('закрытый список kind в инструкции вывода', () => {
expect(buildVerityPrompt(base).system).toMatch(/missing.*distorted|distorted.*missing/);
});
});
describe('runVerityCheck (оркестратор; llmCall мокается; faithful — кодом из discrepancies)', () => {
const okTrace = {
owner_asks: [{ ask: 'вход роутера первым', source: 'prompt' }],
discrepancies: [],
};
const base = { rawPrompt: 'вход роутера первым', answers: [], plan: 'план' };
it('валидная заземлённая трасса без расхождений → ok + faithful=true', async () => {
const r = await runVerityCheck({ ...base, llmCall: async () => okTrace });
expect(r.ok).toBe(true); expect(r.faithful).toBe(true); expect(r.discrepancies).toEqual([]);
});
it('есть расхождение → ok + faithful=false (код считает, не роутер)', async () => {
const trace = { owner_asks: okTrace.owner_asks, discrepancies: [{ kind: 'missing', detail: 'inline не отражён', source: 0 }] };
const r = await runVerityCheck({ rawPrompt: 'x', answers: ['inline'], plan: 'p', llmCall: async () => trace });
expect(r.ok).toBe(true); expect(r.faithful).toBe(false); expect(r.discrepancies.length).toBe(1);
});
it('пустой owner_asks (поддакнул не выводя) → ok=false, не результат', async () => {
const r = await runVerityCheck({ ...base, llmCall: async () => ({ owner_asks: [], discrepancies: [] }) });
expect(r.ok).toBe(false); expect(r.reason).toMatch(/слот|owner_asks|невалид/i);
});
it('ссылка на несуществующий ответ → ok=false (заземление)', async () => {
const trace = { owner_asks: [{ ask: 'a', source: 3 }], discrepancies: [] };
const r = await runVerityCheck({ rawPrompt: 'x', answers: ['one'], plan: 'p', llmCall: async () => trace });
expect(r.ok).toBe(false); expect(r.reason).toMatch(/висящ|источник|dangling/i);
});
it('llmCall вернул строку с JSON-fence → парсится (DRY parseRouterResponse)', async () => {
const r = await runVerityCheck({ ...base, llmCall: async () => '```json\n{"owner_asks":[{"ask":"a","source":"prompt"}],"discrepancies":[]}\n```' });
expect(r.ok).toBe(true); expect(r.faithful).toBe(true);
});
it('llmCall вернул мусор → ok=false (не падает)', async () => {
const r = await runVerityCheck({ ...base, llmCall: async () => null });
expect(r.ok).toBe(false);
});
it('llmCall кинул исключение → ok=false, reason про сбой', async () => {
const r = await runVerityCheck({ ...base, llmCall: async () => { throw new Error('network'); } });
expect(r.ok).toBe(false); expect(r.reason).toMatch(/сбой|модел/i);
});
});