397777089e
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
183 lines
8.7 KiB
JavaScript
183 lines
8.7 KiB
JavaScript
// 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');
|
||
});
|
||
});
|