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

183 lines
8.7 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// 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');
});
});