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); }); });