// tools/context-verity.test.mjs import { describe, it, expect } from 'vitest'; import { parseRef, CITATION_KINDS, resolveCitation } from './context-verity.mjs'; describe('parseRef', () => { it('парсит file:line', () => { expect(parseRef('tools/router-engine.mjs:106')).toEqual({ file: 'tools/router-engine.mjs', line: 106 }); }); it('голый файл без строки → line:null', () => { expect(parseRef('tools/x.mjs')).toEqual({ file: 'tools/x.mjs', line: null }); }); it('не-строка → null', () => { expect(parseRef(42)).toBe(null); }); it('CITATION_KINDS заморожен и содержит оба вида', () => { expect(CITATION_KINDS).toEqual(['EXTRACTED', 'INFERRED']); expect(Object.isFrozen(CITATION_KINDS)).toBe(true); }); }); describe('resolveCitation', () => { const fakeRead = (file) => file === 'tools/x.mjs' ? 'export function buildRouterPrompt() {}' : null; it('anchor найден в файле → resolved', () => { const r = resolveCitation({ ref: 'tools/x.mjs:1', anchor: 'buildRouterPrompt' }, fakeRead); expect(r.resolved).toBe(true); }); it('anchor НЕ найден → не resolved', () => { const r = resolveCitation({ ref: 'tools/x.mjs:1', anchor: 'runJudge' }, fakeRead); expect(r.resolved).toBe(false); }); it('файл не прочитан → не resolved', () => { const r = resolveCitation({ ref: 'tools/missing.mjs:1', anchor: 'xxxx' }, fakeRead); expect(r.resolved).toBe(false); }); it('нет anchor (SE-1: резолв по символу обязателен) → не resolved', () => { const r = resolveCitation({ ref: 'tools/x.mjs:1', anchor: '' }, fakeRead); expect(r.resolved).toBe(false); }); it('readFileImpl бросил → не resolved (не крашит)', () => { const r = resolveCitation({ ref: 'tools/x.mjs:1', anchor: 'xxxx' }, () => { throw new Error('io'); }); expect(r.resolved).toBe(false); }); }); import { verifyArtifact } from './context-verity.mjs'; describe('verifyArtifact — резолв EXTRACTED', () => { const read = (file) => file === 'a.mjs' ? 'function good(){}' : null; it('все EXTRACTED резолвятся → ok, нет flagged', () => { const art = [{ id: '1', claim: 'есть good', ref: 'a.mjs:1', kind: 'EXTRACTED', anchor: 'good' }]; const r = verifyArtifact(art, read); expect(r.ok).toBe(true); expect(r.flagged).toEqual([]); expect(r.extractedCount).toBe(1); }); it('неразрешённая EXTRACTED → flagged + не ok', () => { const art = [{ id: '2', claim: 'нет bad', ref: 'a.mjs:1', kind: 'EXTRACTED', anchor: 'absent' }]; const r = verifyArtifact(art, read); expect(r.ok).toBe(false); expect(r.flagged.map((f) => f.id)).toContain('2'); }); it('INFERRED не требует резолва', () => { const art = [ { id: '1', claim: 'есть good', ref: 'a.mjs:1', kind: 'EXTRACTED', anchor: 'good' }, { id: '3', claim: 'предположение', ref: 'a.mjs:1', kind: 'INFERRED', derivation_ref: 'd1' }, ]; const r = verifyArtifact(art, read); expect(r.ok).toBe(true); }); it('битая запись → flagged, не крашит', () => { const r = verifyArtifact([null, 'bad', { kind: 'EXTRACTED', anchor: 'good', ref: 'a.mjs:1', id: '1' }], read); expect(r.flagged.length).toBeGreaterThanOrEqual(2); }); it('INFERRED с пустым derivation_ref → flagged + не ok (VF-1)', () => { const art = [ { id: '1', claim: 'есть good', ref: 'a.mjs:1', kind: 'EXTRACTED', anchor: 'good' }, { id: '4', claim: 'предположение', ref: 'a.mjs:1', kind: 'INFERRED', derivation_ref: '' }, ]; const r = verifyArtifact(art, read); expect(r.ok).toBe(false); expect(r.flagged.map((f) => f.id)).toContain('4'); }); it('INFERRED без поля derivation_ref → flagged + не ok (VF-1)', () => { const art = [ { id: '1', claim: 'есть good', ref: 'a.mjs:1', kind: 'EXTRACTED', anchor: 'good' }, { id: '5', claim: 'предположение', ref: 'a.mjs:1', kind: 'INFERRED' }, ]; const r = verifyArtifact(art, read); expect(r.ok).toBe(false); expect(r.flagged.map((f) => f.id)).toContain('5'); }); it('неизвестный kind → flagged + не ok (SE-A1: kind вне CITATION_KINDS не проходит молча)', () => { const art = [ { id: '1', claim: 'есть good', ref: 'a.mjs:1', kind: 'EXTRACTED', anchor: 'good' }, { id: '7', claim: 'фантазия', ref: 'a.mjs:1', kind: 'FOO' }, ]; const r = verifyArtifact(art, read); expect(r.ok).toBe(false); expect(r.flagged.map((f) => f.id)).toContain('7'); }); it("kind:'extracted' (не тот регистр) → flagged, резолв не обходится (SE-A1)", () => { const art = [ { id: '1', claim: 'есть good', ref: 'a.mjs:1', kind: 'EXTRACTED', anchor: 'good' }, { id: '8', claim: 'фантазия', ref: 'a.mjs:1', kind: 'extracted', anchor: 'absent' }, ]; const r = verifyArtifact(art, read); expect(r.ok).toBe(false); expect(r.flagged.map((f) => f.id)).toContain('8'); }); }); // [НЕ-TDD: характеризация инварианта Task 3 (SE9) — тесты закрепляют уже реализованное // поведение, PASS сразу — норма, не нарушение TDD.] describe('secure-default downgrade + VA-9', () => { const read = (file) => file === 'a.mjs' ? 'function good(){}' : null; it('неразрешённая EXTRACTED понижается до INFERRED в entries (S2)', () => { const art = [{ id: '2', claim: 'нет bad', ref: 'a.mjs:1', kind: 'EXTRACTED', anchor: 'absent' }]; const r = verifyArtifact(art, read); expect(r.entries[0].kind).toBe('INFERRED'); expect(r.entries[0].downgraded_from).toBe('EXTRACTED'); expect(r.downgraded.map((d) => d.id)).toContain('2'); }); it('пустой артефакт (0 EXTRACTED) НЕ проверен (VA-9)', () => { expect(verifyArtifact([], read).ok).toBe(false); expect(verifyArtifact([{ id: '3', kind: 'INFERRED', ref: 'a.mjs:1', claim: 'x', derivation_ref: 'd' }], read).ok).toBe(false); }); it('не-массив → не проверен, не крашит', () => { expect(verifyArtifact(null, read).ok).toBe(false); expect(verifyArtifact('bad', read).ok).toBe(false); }); it('EXTRACTED без anchor → downgraded (SE6: anchor условно-обязателен для EXTRACTED)', () => { const art = [{ id: '6', claim: 'без якоря', ref: 'a.mjs:1', kind: 'EXTRACTED' }]; const r = verifyArtifact(art, read); expect(r.ok).toBe(false); expect(r.downgraded.map((d) => d.id)).toContain('6'); expect(r.entries[0].kind).toBe('INFERRED'); expect(r.entries[0].downgraded_from).toBe('EXTRACTED'); }); }); import { artifactHasUnresolvedExtracted } from './context-verity.mjs'; describe('artifactHasUnresolvedExtracted (✅O2 freeze-side guard)', () => { const read = (file) => file === 'a.mjs' ? 'function good(){}' : null; it('все EXTRACTED резолвятся → false (печать разрешена)', () => { const art = [{ id: '1', claim: 'good', ref: 'a.mjs:1', kind: 'EXTRACTED', anchor: 'good' }]; expect(artifactHasUnresolvedExtracted(art, read)).toBe(false); }); it('есть неразрешённая EXTRACTED → true (печать ОТКАЗана)', () => { const art = [{ id: '2', claim: 'bad', ref: 'a.mjs:1', kind: 'EXTRACTED', anchor: 'absent' }]; expect(artifactHasUnresolvedExtracted(art, read)).toBe(true); }); }); import { MIN_ANCHOR_LENGTH } from './context-verity.mjs'; describe('resolveCitation — порог длины anchor (SE-A2)', () => { const readY = (file) => file === 'tools/y.mjs' ? 'export const good = 1;' : null; it('MIN_ANCHOR_LENGTH экспортирован, >= 4', () => { expect(typeof MIN_ANCHOR_LENGTH).toBe('number'); expect(MIN_ANCHOR_LENGTH).toBeGreaterThanOrEqual(4); }); it("короткий anchor 'e' присутствует в файле, но короче порога → не resolved (SE-A2)", () => { const r = resolveCitation({ ref: 'tools/y.mjs:1', anchor: 'e' }, readY); expect(r.resolved).toBe(false); }); it('anchor длиной = порог и присутствует → resolved', () => { const r = resolveCitation({ ref: 'tools/y.mjs:1', anchor: 'good' }, readY); expect(r.resolved).toBe(true); }); it('короткий anchor в EXTRACTED → downgraded (verifyArtifact, SE-A2)', () => { const art = [{ id: 's', claim: 'короткий', ref: 'tools/y.mjs:1', kind: 'EXTRACTED', anchor: 'e' }]; const r = verifyArtifact(art, readY); expect(r.ok).toBe(false); expect(r.downgraded.map((d) => d.id)).toContain('s'); }); });