Files
brain/tools/judge-gate-floor.test.mjs
T

278 lines
16 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/judge-gate-floor.test.mjs
import { describe, it, expect } from 'vitest';
import {
existenceCheck, specToPlanCoverage, k5CriterionCheck, criteriaGreenMatched, skillTakenByJournal,
} from './judge-gate-floor.mjs';
describe('existenceCheck (Гейт-1 шаг 1, $0): вопросы отвечены + навыки реально вызваны', () => {
it('ok когда все вопросы отвечены и все обещанные навыки в журнале', () => {
const r = existenceCheck({
questions: [{ id: 'q1', answered: true }, { id: 'q2', answered: true }],
skillsPromised: ['brainstorming', 'frontend-design'],
skillsInvoked: ['brainstorming', 'frontend-design'],
});
expect(r.ok).toBe(true);
expect(r.unanswered).toEqual([]);
expect(r.missingSkills).toEqual([]);
});
it('повисший вопрос → не ok', () => {
const r = existenceCheck({ questions: [{ id: 'q1', answered: false }], skillsPromised: [], skillsInvoked: [] });
expect(r.ok).toBe(false);
expect(r.unanswered).toEqual(['q1']);
});
it('обещанный навык не вызван по журналу → не ok (фейк-покрытие)', () => {
const r = existenceCheck({ questions: [], skillsPromised: ['frontend-design'], skillsInvoked: ['brainstorming'] });
expect(r.ok).toBe(false);
expect(r.missingSkills).toEqual(['frontend-design']);
});
});
describe('specToPlanCoverage (Гейт-2 пол): каждый § спеки покрыт шагом', () => {
it('все секции покрыты → ok', () => {
const r = specToPlanCoverage({ specSections: ['§1', '§2'], planSteps: [{ ref: '§1' }, { ref: '§2' }, { ref: '§1' }] });
expect(r.ok).toBe(true);
});
it('секция без шага → uncovered', () => {
const r = specToPlanCoverage({ specSections: ['§1', '§2', '§3'], planSteps: [{ ref: '§1' }] });
expect(r.ok).toBe(false);
expect(r.uncovered.sort()).toEqual(['§2', '§3']);
});
});
// 5.2 (C3/F18): значимость шага — МЕХАНИЧЕСКАЯ (из класса действия в judge-gate-floor),
// НЕ из контроллер-флагов trivial/significant. Флаги больше не разжалуют значимый шаг;
// разжаловать может только реально-читающий op. Разрушительный object перевешивает op.
import { isSignificantStep } from './judge-gate-floor.mjs';
describe('k5CriterionCheck (Гейт-2, K5): значимый шаг → конкретный критерий; «проверено» не факт', () => {
it('значимый шаг с конкретным критерием → ok', () => {
const r = k5CriterionCheck({ planSteps: [{ n: 1, criterion: 'тест RlsSmokeTest проходит' }] });
expect(r.ok).toBe(true);
});
it('значимый шаг без критерия → missing', () => {
const r = k5CriterionCheck({ planSteps: [{ n: 1 }] });
expect(r.ok).toBe(false);
expect(r.missingCriterion).toEqual([1]);
});
it('критерий = голое слово «проверено» → за факт НЕ берём → missing', () => {
const r = k5CriterionCheck({ planSteps: [{ n: 1, criterion: 'проверено' }, { n: 2, criterion: 'готово' }] });
expect(r.ok).toBe(false);
expect(r.missingCriterion).toEqual([1, 2]);
});
it('чистое чтение (op:Read) без критерия → не требуется (механически не значим)', () => {
const r = k5CriterionCheck({ planSteps: [{ n: 1, op: 'Read', object: 'tools/x.mjs' }] });
expect(r.ok).toBe(true);
});
it('сомнение в значимости → шаг считается значимым (нельзя «разжаловать» чтобы проскочить)', () => {
const r = k5CriterionCheck({ planSteps: [{ n: 5 }] }); // significant не указан → значим
expect(r.ok).toBe(false);
expect(r.missingCriterion).toEqual([5]);
});
it('C3/F18: контроллер-флаг trivial:true НЕ разжалует мутирующий шаг (Write без критерия → missing)', () => {
const r = k5CriterionCheck({ planSteps: [{ n: 7, op: 'Write', object: 'tools/x.mjs', trivial: true }] });
expect(r.ok).toBe(false);
expect(r.missingCriterion).toEqual([7]);
});
it('C3/F18: контроллер-флаг significant:false НЕ разжалует мутирующий шаг (Bash без критерия → missing)', () => {
const r = k5CriterionCheck({ planSteps: [{ n: 8, op: 'Bash', object: 'git commit', significant: false }] });
expect(r.ok).toBe(false);
expect(r.missingCriterion).toEqual([8]);
});
it('обход через op:Read при разрушительном object → всё равно значим (missing)', () => {
const r = k5CriterionCheck({ planSteps: [{ n: 9, op: 'Read', object: 'rm -rf build' }] });
expect(r.ok).toBe(false);
expect(r.missingCriterion).toEqual([9]);
});
});
describe('isSignificantStep (5.2): механическая значимость по классу действия', () => {
it('мутирующие op значимы (Write/Edit/MultiEdit/NotebookEdit/Bash)', () => {
for (const op of ['Write', 'Edit', 'MultiEdit', 'NotebookEdit', 'Bash']) {
expect(isSignificantStep({ op, object: 'x' })).toBe(true);
}
});
it('чистое чтение не значимо (Read/Grep/Glob/LS/NotebookRead)', () => {
for (const op of ['Read', 'Grep', 'Glob', 'LS', 'NotebookRead']) {
expect(isSignificantStep({ op, object: 'x' })).toBe(false);
}
});
it('разрушительный object перевешивает readonly-op (анти-обход)', () => {
expect(isSignificantStep({ op: 'Read', object: 'git push --force' })).toBe(true);
});
it('неизвестный/пустой op → значим (сомнение → значим, fail-CLOSE)', () => {
expect(isSignificantStep({ n: 1 })).toBe(true);
expect(isSignificantStep(null)).toBe(true);
});
});
describe('criteriaGreenMatched (Гейт-3, $0): критерий ↔ настоящий зелёный прогон', () => {
it('каждый критерий сопоставлен с зелёным прогоном → ok', () => {
const r = criteriaGreenMatched({ criteria: [{ id: 'c1' }, { id: 'c2' }], greenRuns: [{ criterion_id: 'c1', green: true }, { criterion_id: 'c2', green: true }] });
expect(r.ok).toBe(true);
});
it('критерий без зелёного прогона → unproven', () => {
const r = criteriaGreenMatched({ criteria: [{ id: 'c1' }, { id: 'c2' }], greenRuns: [{ criterion_id: 'c1', green: true }] });
expect(r.ok).toBe(false);
expect(r.unproven).toEqual(['c2']);
});
it('прогон есть, но КРАСНЫЙ (green:false) → критерий не доказан', () => {
const r = criteriaGreenMatched({ criteria: [{ id: 'c1' }], greenRuns: [{ criterion_id: 'c1', green: false }] });
expect(r.ok).toBe(false);
expect(r.unproven).toEqual(['c1']);
});
});
describe('skillTakenByJournal (A2, K2): навык взят по журналу, не по тексту', () => {
it('требуемые навыки есть в журнале → ok', () => {
expect(skillTakenByJournal({ requiredSkills: ['Pest'], journalSkillCalls: ['Pest', 'Read'] }).ok).toBe(true);
});
it('требуемый навык отсутствует в журнале → missing', () => {
const r = skillTakenByJournal({ requiredSkills: ['Pest'], journalSkillCalls: ['Read'] });
expect(r.ok).toBe(false);
expect(r.missing).toEqual(['Pest']);
});
});
import { gate1CoverageGate, criteriaFromSealedPlan } from './judge-gate-floor.mjs';
describe('gate1CoverageGate (#3 — машина охвата на Гейте-1: дыра/цикл/сирота/просьбы)', () => {
it('охват готов (ready) → ок', () => {
expect(gate1CoverageGate({ readiness: { ready: true, items: [] } }).ok).toBe(true);
});
it('есть дыра/цикл/сирота → блок + перечень провалов', () => {
const readiness = { ready: false, items: [
{ label: 'Все нужды покрыты (нет дыр)', ok: false, pointer: '§A findHoles', detail: [{ need: 'x' }] },
{ label: 'Нет циклов', ok: true },
] };
const r = gate1CoverageGate({ readiness });
expect(r.ok).toBe(false);
expect(r.failures).toHaveLength(1);
expect(r.failures[0].label).toMatch(/нужды/);
});
it('машина охвата не отработала (нет результата) → fail-closed', () => {
expect(gate1CoverageGate({ readiness: null }).ok).toBe(false);
});
});
describe('criteriaFromSealedPlan (#5, F3/F9 — критерии из печати, не дозаполнены на демо)', () => {
it('все критерии из запечатанного набора → ок', () => {
expect(criteriaFromSealedPlan({ criteria: [{ id: 'c1' }, { id: 'c2' }], sealedCriterionIds: ['c1', 'c2'] }).ok).toBe(true);
});
it('критерий не из печати (дозаполнен на демо) → флаг', () => {
const r = criteriaFromSealedPlan({ criteria: [{ id: 'c1' }, { id: 'c9' }], sealedCriterionIds: ['c1'] });
expect(r.ok).toBe(false);
expect(r.unsealed).toEqual(['c9']);
});
});
// 5.3 (Δ2): свежесть отпечатка в judge-gate-floor — green засчитывается только при совпадении
// code_fingerprint зелёного прогона с ТЕКУЩИМ (изменённые файлы шага + тесты). Правка файла
// после прогона меняет текущий отпечаток → green аннулируется. Отдельный шаг лесенки (Δ6).
import { fingerprintFresh } from './judge-gate-floor.mjs';
describe('fingerprintFresh (5.3, Δ2): green свеж только при совпадении code_fingerprint', () => {
it('отпечаток зелёного совпадает с текущим → свеж (ok)', () => {
const r = fingerprintFresh({
greenRuns: [{ criterion_id: 'c1', green: true, code_fingerprint: 'aa' }],
currentFingerprints: { c1: 'aa' },
});
expect(r.ok).toBe(true);
expect(r.stale).toEqual([]);
});
it('файл изменён после прогона (отпечаток разошёлся) → stale, не ok', () => {
const r = fingerprintFresh({
greenRuns: [{ criterion_id: 'c1', green: true, code_fingerprint: 'aa' }],
currentFingerprints: { c1: 'bb' },
});
expect(r.ok).toBe(false);
expect(r.stale).toEqual(['c1']);
});
it('нет текущего отпечатка для критерия → stale (fail-CLOSE)', () => {
const r = fingerprintFresh({
greenRuns: [{ criterion_id: 'c1', green: true, code_fingerprint: 'aa' }],
currentFingerprints: {},
});
expect(r.ok).toBe(false);
expect(r.stale).toEqual(['c1']);
});
it('красный прогон (green:false) не проверяется на свежесть (его забота — criteriaGreenMatched)', () => {
const r = fingerprintFresh({
greenRuns: [{ criterion_id: 'c1', green: false, code_fingerprint: 'aa' }],
currentFingerprints: { c1: 'bb' },
});
expect(r.ok).toBe(true);
expect(r.stale).toEqual([]);
});
it('нет зелёных прогонов → ok (присутствие зелёного — отдельный шаг)', () => {
expect(fingerprintFresh({ greenRuns: [], currentFingerprints: {} }).ok).toBe(true);
});
});
// 5.4 (Δ5): подлинность green — подпись подписанта (floor-signer), НЕ совпадение id.
// judge-gate-floor реконструирует подписанную тройку {criterion_id, code_fingerprint,
// occurrence} из green-run и проверяет verifyGreen. Синергия с 5.3: подмена отпечатка для
// прохода шага свежести ломает подпись здесь. Отдельный шаг лесенки (Δ6 шаг 4), fail-CLOSE.
import { greenSignaturesValid } from './judge-gate-floor.mjs';
import { signGreen } from './floor-signer.mjs';
const SK = 'signer-test-key';
describe('greenSignaturesValid (5.4, Δ5): green засчитан только при валидной подписи подписанта', () => {
it('подпись подписанта валидна → ok', () => {
const rec = signGreen({ criterion_id: 'c1', code_fingerprint: 'aa', occurrence: 1 }, SK);
const run = { ...rec, green: true };
expect(greenSignaturesValid({ greenRuns: [run], key: SK }).ok).toBe(true);
});
it('green:true без подписи → unsigned (незаверенный нельзя выдать за зелёный)', () => {
const run = { criterion_id: 'c1', green: true, code_fingerprint: 'aa', occurrence: 1 };
const r = greenSignaturesValid({ greenRuns: [run], key: SK });
expect(r.ok).toBe(false);
expect(r.unsigned).toEqual(['c1']);
});
it('подмена отпечатка после подписи → подпись не сходится → unsigned', () => {
const rec = signGreen({ criterion_id: 'c1', code_fingerprint: 'aa', occurrence: 1 }, SK);
const run = { ...rec, green: true, code_fingerprint: 'bb' }; // подменили отпечаток после подписи
expect(greenSignaturesValid({ greenRuns: [run], key: SK }).ok).toBe(false);
});
it('красный прогон (green:false) не требует подписи (его ловит criteriaGreenMatched)', () => {
const run = { criterion_id: 'c1', green: false, code_fingerprint: 'aa', occurrence: 1 };
expect(greenSignaturesValid({ greenRuns: [run], key: SK }).ok).toBe(true);
});
it('нет ключа → подпись непроверяема → unsigned (fail-CLOSE)', () => {
const rec = signGreen({ criterion_id: 'c1', code_fingerprint: 'aa', occurrence: 1 }, SK);
const run = { ...rec, green: true };
expect(greenSignaturesValid({ greenRuns: [run], key: undefined }).ok).toBe(false);
});
});
import { extractSkillCalls as extractSkillCalls4b } from './enforce-skill-journaler.mjs';
// М7 Фаза 4b §5 coverage-map: decomposition (скрытое дробление) ловит Гейт-1 existenceCheck
// через ЖУРНАЛ (обещанные навыки ← extractSkillCalls), не текст. Proof покрытия ДО retire
// no-op enforce-decomposition-detector (Ф8); live-wiring «обещанные ← журнал» в judge-gate — Ф7.
describe('existenceCheck как decomposition look-ahead через журнал (М7 Фаза 4b §4.2)', () => {
const journal = [
{ payload: { op: 'Skill', object: 'superpowers:test-driven-development' } },
{ payload: { op: 'Edit', object: 'foo.mjs' } },
];
it('promised planning skill НЕ вызван (по журналу) → existenceCheck ловит (missingSkills непуст)', () => {
const invoked = extractSkillCalls4b(journal);
const r = existenceCheck({
questions: [{ id: 'q1', answered: true }],
skillsPromised: ['superpowers:writing-plans'],
skillsInvoked: invoked,
});
expect(r.ok).toBe(false);
expect(r.missingSkills).toContain('superpowers:writing-plans');
});
it('promised skill вызван (в журнале) → existenceCheck ok', () => {
const invoked = extractSkillCalls4b(journal);
const r = existenceCheck({
questions: [{ id: 'q1', answered: true }],
skillsPromised: ['superpowers:test-driven-development'],
skillsInvoked: invoked,
});
expect(r.ok).toBe(true);
});
});