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

108 lines
6.3 KiB
JavaScript

import { describe, it, expect } from 'vitest';
import {
SPEC_VERITY_SLOTS, SPEC_DISCREPANCY_KINDS, validateSpecVerityTrace, groundSpecVerityTrace, buildSpecVerityPrompt, runSpecVerityCheck,
} from './spec-verity.mjs';
const OK = {
promised: [{ item: 'вход роутера первым', source: 'prompt' }, { item: 'inline без субагентов', source: 0 }],
discrepancies: [{ kind: 'fabricated', detail: 'спека добавила кэш, которого не просили', source: 'plan' }],
};
describe('SPEC_VERITY_SLOTS / SPEC_DISCREPANCY_KINDS', () => {
it('слоты promised + discrepancies', () => {
expect(SPEC_VERITY_SLOTS).toEqual(['promised', 'discrepancies']);
});
it('виды расхождения: missing/distorted/fabricated', () => {
expect(SPEC_DISCREPANCY_KINDS).toEqual(['missing', 'distorted', 'fabricated']);
});
});
describe('validateSpecVerityTrace (форма; пустой слот = флаг; source prompt|plan|число)', () => {
it('корректная трасса → ok', () => {
expect(validateSpecVerityTrace(OK)).toEqual({ ok: true, missingSlots: [], badItems: [] });
});
it('пустой promised → невалидно', () => {
const r = validateSpecVerityTrace({ ...OK, promised: [] });
expect(r.ok).toBe(false); expect(r.missingSlots).toContain('promised');
});
it('discrepancies не массив → невалидно', () => {
expect(validateSpecVerityTrace({ promised: OK.promised }).ok).toBe(false);
});
it('пустой discrepancies → валидно (спека верна)', () => {
expect(validateSpecVerityTrace({ promised: OK.promised, discrepancies: [] }).ok).toBe(true);
});
it('source plan допустим', () => {
expect(validateSpecVerityTrace({ promised: [{ item: 'x', source: 'plan' }], discrepancies: [] }).ok).toBe(true);
});
it('свободный kind → badItems', () => {
const r = validateSpecVerityTrace({ ...OK, discrepancies: [{ kind: 'whatever', detail: 'd', source: 'plan' }] });
expect(r.ok).toBe(false); expect(r.badItems).toContain('discrepancies[0]');
});
it('не объект → все слоты missing', () => {
expect(validateSpecVerityTrace(null).missingSlots).toEqual([...SPEC_VERITY_SLOTS]);
});
});
describe('groundSpecVerityTrace (source: prompt | plan | индекс ответа в диапазоне)', () => {
const trace = { promised: [{ item: 'a', source: 'prompt' }, { item: 'b', source: 'plan' }, { item: 'c', source: 1 }], discrepancies: [] };
it('все источники валидны → grounded', () => {
expect(groundSpecVerityTrace(trace, { answersCount: 2 })).toEqual({ grounded: true, dangling: [] });
});
it('индекс вне диапазона → dangling', () => {
expect(groundSpecVerityTrace(trace, { answersCount: 1 }).grounded).toBe(false);
});
it('plan валиден при нулевой истории', () => {
expect(groundSpecVerityTrace({ promised: [{ item: 'a', source: 'plan' }], discrepancies: [] }, { answersCount: 0 }).grounded).toBe(true);
});
});
describe('buildSpecVerityPrompt (чистая {system,user}; promised из якорей ДО спеки; ловит fabricated)', () => {
const base = { rawPrompt: 'вход роутера', answers: ['inline'], approvedPlan: 'план A', spec: 'спека текст' };
it('детерминирована', () => {
expect(buildSpecVerityPrompt(base).system).toBe(buildSpecVerityPrompt(base).system);
});
it('system: сперва promised из якорей, потом сверка спеки; описан fabricated', () => {
const { system } = buildSpecVerityPrompt(base);
expect(system.indexOf('promised')).toBeLessThan(system.indexOf('discrepancies'));
expect(system).toMatch(/fabricated/);
expect(system).toMatch(/написал сво|не обещано/i);
});
it('user: якоря (ответы + одобренный план) ФИЗИЧЕСКИ перед спекой', () => {
const { user } = buildSpecVerityPrompt(base);
expect(user.indexOf('ОДОБРЕННЫЙ ПЛАН')).toBeLessThan(user.indexOf('ГОТОВАЯ СПЕКА'));
expect(user).toMatch(/Ответ 0: inline/);
expect(user).toMatch(/план A/);
expect(user).toMatch(/спека текст/);
});
});
describe('runSpecVerityCheck (оркестратор; llmCall мокается; faithful кодом)', () => {
const base = { rawPrompt: 'вход роутера', answers: ['inline'], approvedPlan: 'план A', spec: 'спека' };
const okTrace = { promised: [{ item: 'вход роутера', source: 'prompt' }], discrepancies: [] };
it('нет расхождений → ok + faithful=true', async () => {
const r = await runSpecVerityCheck({ ...base, llmCall: async () => okTrace });
expect(r.ok).toBe(true); expect(r.faithful).toBe(true);
});
it('fabricated → ok + faithful=false', async () => {
const trace = { promised: okTrace.promised, discrepancies: [{ kind: 'fabricated', detail: 'лишний кэш', source: 'plan' }] };
const r = await runSpecVerityCheck({ ...base, llmCall: async () => trace });
expect(r.ok).toBe(true); expect(r.faithful).toBe(false); expect(r.discrepancies[0].kind).toBe('fabricated');
});
it('пустой promised → ok=false', async () => {
const r = await runSpecVerityCheck({ ...base, llmCall: async () => ({ promised: [], discrepancies: [] }) });
expect(r.ok).toBe(false);
});
it('висящая ссылка (номер вне истории) → ok=false', async () => {
const r = await runSpecVerityCheck({ ...base, llmCall: async () => ({ promised: [{ item: 'a', source: 5 }], discrepancies: [] }) });
expect(r.ok).toBe(false); expect(r.reason).toMatch(/висящ|dangling|ссыл/i);
});
it('строка JSON-fence → парсится', async () => {
const r = await runSpecVerityCheck({ ...base, llmCall: async () => '```json\n{"promised":[{"item":"a","source":"plan"}],"discrepancies":[]}\n```' });
expect(r.ok).toBe(true); expect(r.faithful).toBe(true);
});
it('исключение → ok=false', async () => {
const r = await runSpecVerityCheck({ ...base, llmCall: async () => { throw new Error('net'); } });
expect(r.ok).toBe(false); expect(r.reason).toMatch(/сбой|модел/i);
});
});