052703f4d0
Δ5: подлинность green = подпись ПОДПИСАНТА, не совпадение id (id = целостность).
greenSignaturesValid реконструирует подписанную тройку {criterion_id,
code_fingerprint, occurrence} из green-run и проверяет verifyGreen (floor-signer).
Синергия с 5.3: подмена отпечатка для прохода свежести ломает подпись здесь
(отпечаток входит в подписанную тройку). Чистая, fail-CLOSE (нет ключа/sig →
unsigned). Красные прогоны подписи не требуют (их ловит criteriaGreenMatched).
По авторитетному Δ6 — ОТДЕЛЬНЫЙ шаг 4 лесенки критерий-гейта.
+5 тестов. judge-gate-floor 37/37 (аддитивно; import floor-signer, цикла нет).
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
247 lines
14 KiB
JavaScript
247 lines
14 KiB
JavaScript
// 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);
|
|
});
|
|
});
|