Files
brain/tools/judge-orchestrator.test.mjs
T

289 lines
14 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-orchestrator.test.mjs
import { describe, it, expect } from 'vitest';
import {
gateFires, runGateLadder, finalGate, appealStep, judgeHealth, gate3Terminal, logVerdict,
} from './judge-orchestrator.mjs';
describe('gateFires (A0: ворота срабатывают СТРУКТУРНО от состояния, не по желанию)', () => {
it('готово и не запечатано → ворота срабатывают', () => {
expect(gateFires({ ready: true, alreadySealed: false })).toBe(true);
});
it('не готово → не срабатывают', () => {
expect(gateFires({ ready: false, alreadySealed: false })).toBe(false);
});
it('уже запечатано → не пере-срабатывают', () => {
expect(gateFires({ ready: true, alreadySealed: true })).toBe(false);
});
});
describe('runGateLadder (лесенка: провал любого шага → стоп раньше)', () => {
it('все шаги ок → passed', () => {
const r = runGateLadder([
{ name: 'существование', run: () => ({ ok: true }) },
{ name: 'верность', run: () => ({ ok: true }) },
{ name: 'судья', run: () => ({ ok: true, decision: 'GO' }) },
]);
expect(r.passed).toBe(true);
});
it('первый провал → стоп раньше, последующие НЕ запускаются', () => {
let ran3 = false;
const r = runGateLadder([
{ name: 'существование', run: () => ({ ok: false, detail: 'вопрос повис' }) },
{ name: 'верность', run: () => ({ ok: true }) },
{ name: 'судья', run: () => { ran3 = true; return { ok: true }; } },
]);
expect(r.passed).toBe(false);
expect(r.stoppedAt).toBe('существование');
expect(ran3).toBe(false);
});
// fix: tools/judge-orchestrator.mjs (аудит M1-M4, свежий объектив) — упавший шаг пола.
// Раньше step.run() без try/catch пробрасывал исключение вместо «пол не пройден». Теперь
// бросок шага пола = лесенка НЕ пройдена (fail-closed → блок), последующие не запускаются.
it('шаг пола кинул исключение → лесенка не пройдена (fail-closed), следующий не запускается', () => {
let ran2 = false;
const r = runGateLadder([
{ name: 'существование', run: () => { throw new Error('I/O пол упал'); } },
{ name: 'верность', run: () => { ran2 = true; return { ok: true }; } },
]);
expect(r.passed).toBe(false);
expect(r.stoppedAt).toBe('существование');
expect(ran2).toBe(false);
});
});
describe('finalGate (J9: «да» судьи НЕ отпирает пол)', () => {
it('пол блокирует (необратимое) → блок даже при GO судьи', () => {
expect(finalGate({ judgeDecision: 'GO', floorBlocked: true })).toBe('block');
});
it('пол чист + судья GO → allow', () => {
expect(finalGate({ judgeDecision: 'GO', floorBlocked: false })).toBe('allow');
});
it('пол чист + судья NO-GO → block', () => {
expect(finalGate({ judgeDecision: 'NO-GO', floorBlocked: false })).toBe('block');
});
// fix: tools/judge-orchestrator.mjs (K, аудит M1-M4) — снять вето пола может только явный false
it('floorBlocked undefined/null (проверка пола кинула/не дала ответ) → block при GO судьи', () => {
expect(finalGate({ judgeDecision: 'GO', floorBlocked: undefined })).toBe('block');
expect(finalGate({ judgeDecision: 'GO', floorBlocked: null })).toBe('block');
});
});
describe('appealStep (F8: 3 круга на одно несогласие → владелец)', () => {
it('круги 1..3 не эскалируют', () => {
expect(appealStep({ round: 0 })).toEqual({ round: 1, toOwner: false });
expect(appealStep({ round: 1 })).toEqual({ round: 2, toOwner: false });
expect(appealStep({ round: 2 })).toEqual({ round: 3, toOwner: false });
});
it('4-й круг → к владельцу', () => {
expect(appealStep({ round: 3 })).toEqual({ round: 4, toOwner: true });
});
});
describe('judgeHealth (J5: упал/деградировал → громкий крик + деградация до пола)', () => {
it('доступен и не деградировал → на посту', () => {
expect(judgeHealth({ available: true, degraded: false })).toEqual({ onPost: true, cry: false, mode: 'judge' });
});
it('недоступен → пост пустой, крик, режим пола', () => {
expect(judgeHealth({ available: false, degraded: false })).toEqual({ onPost: false, cry: true, mode: 'floor-only' });
});
it('деградировал → крик + режим пола', () => {
expect(judgeHealth({ available: true, degraded: true }).mode).toBe('floor-only');
});
});
describe('gate3Terminal (F10: Гейт-3 ничего не печатает вниз)', () => {
it('GO → принято, печати-вниз НЕТ', () => {
expect(gate3Terminal({ decision: 'GO' })).toEqual({ accepted: true, sealDown: null });
});
it('NO-GO → не принято', () => {
expect(gate3Terminal({ decision: 'NO-GO' })).toEqual({ accepted: false, sealDown: null });
});
});
describe('logVerdict (J8: каждый вердикт append-only в журнал)', () => {
it('пишет запись вердикта через инъектированный журнал', () => {
const written = [];
const entry = logVerdict({ verdict: { decision: 'GO', functionName: 'gate1' }, nowMs: 5, journal: (e) => written.push(e) });
expect(written).toHaveLength(1);
expect(entry.kind).toBe('verdict');
expect(entry.decision).toBe('GO');
});
});
import { a2CaseSelect, runGateFunction } from './judge-orchestrator.mjs';
import { classifyDestructive } from './classify-destructive.mjs';
describe('a2CaseSelect (#4 — A2 различает расхождение vs разрушительную команду)', () => {
it('разрушительные команды → destructive', () => {
expect(a2CaseSelect({ op: 'Bash', object: 'rm -rf build' })).toBe('destructive');
expect(a2CaseSelect({ op: 'Bash', object: 'psql -c "DROP TABLE deals"' })).toBe('destructive');
expect(a2CaseSelect({ op: 'Bash', object: 'git push --force origin main' })).toBe('destructive');
expect(a2CaseSelect({ op: 'Bash', object: 'php artisan migrate' })).toBe('destructive');
});
it('обычное действие → divergence', () => {
expect(a2CaseSelect({ op: 'Edit', object: 'tools/foo.mjs' })).toBe('divergence');
expect(a2CaseSelect({ op: 'Bash', object: 'git status' })).toBe('divergence');
});
});
describe('a2CaseSelect — parity/consolidation на classifyDestructive (Step 1.6)', () => {
// a2CaseSelect делегирует classifyDestructive.suspicious. former-destructive стабильны;
// new: format/db:wipe старый локальный DESTRUCTIVE_RE терял → теперь destructive.
const CASES = [
'rm -rf build',
'psql -c "DROP TABLE deals"',
'git push --force origin main',
'php artisan migrate',
'php artisan db:wipe',
'format D:',
'git status',
'tools/foo.mjs',
];
for (const cmd of CASES) {
it(`a2CaseSelect согласован с classifyDestructive.suspicious для ${cmd}`, () => {
const expected = classifyDestructive(cmd).suspicious ? 'destructive' : 'divergence';
expect(a2CaseSelect({ op: 'Bash', object: cmd })).toBe(expected);
});
}
});
describe('runGateFunction (#4 — упаковка: пол ($0) → судья)', () => {
it('весь пол ок + судья GO → GO на стадии judge', () => {
const r = runGateFunction({
floorSteps: [{ name: 'существование', run: () => ({ ok: true }) }],
runEngine: () => ({ decision: 'GO' }),
});
expect(r.decision).toBe('GO');
expect(r.stage).toBe('judge');
});
it('провал пола → NO-GO на стадии floor, судья не зовётся', () => {
let engineRan = false;
const r = runGateFunction({
floorSteps: [{ name: 'существование', run: () => ({ ok: false, detail: 'вопрос повис' }) }],
runEngine: () => { engineRan = true; return { decision: 'GO' }; },
});
expect(r.decision).toBe('NO-GO');
expect(r.stage).toBe('floor');
expect(r.stoppedAt).toBe('существование');
expect(engineRan).toBe(false);
});
});
// 5.5 (Δ6): критерий-гейт = лесенка из РОВНО 4 шагов поверх существующей runGateLadder (НЕ
// плодим criterionFullyProven). Структурный тест «ровно эти 4 шага» — лекарство «забыл шаг».
// И-семантика и короткое замыкание на первом провале наследуются от runGateLadder.
import { criterionGateSteps, runCriterionGate } from './judge-orchestrator.mjs';
import { signGreen } from './floor-signer.mjs';
const GK = 'gate-signer-key';
function validScenario() {
const rec = signGreen({ criterion_id: 'c1', code_fingerprint: 'fp1', occurrence: 1 }, GK);
return {
criteria: [{ id: 'c1' }],
greenRuns: [{ ...rec, green: true }],
sealedCriterionIds: ['c1'],
currentFingerprints: { c1: 'fp1' },
signerKey: GK,
};
}
describe('criterionGateSteps (5.5, Δ6): структура лесенки — РОВНО 4 шага', () => {
it('ровно 4 шага в каноническом порядке (забыть шаг невозможно)', () => {
const steps = criterionGateSteps(validScenario());
expect(steps.map((s) => s.name)).toEqual([
'criteria-from-sealed-plan',
'criteria-green-matched',
'fingerprint-fresh',
'green-signatures-valid',
]);
});
it('каждый шаг — {name, run()} (run возвращает {ok:boolean})', () => {
const steps = criterionGateSteps(validScenario());
for (const s of steps) {
expect(typeof s.run).toBe('function');
expect(typeof s.run().ok).toBe('boolean');
}
});
});
describe('runCriterionGate (5.5, Δ6): И-семантика 4 шагов через runGateLadder', () => {
it('все 4 шага ок → passed', () => {
expect(runCriterionGate(validScenario()).passed).toBe(true);
});
it('критерий не из печати → стоп на шаге 1', () => {
const r = runCriterionGate({ ...validScenario(), sealedCriterionIds: [] });
expect(r.passed).toBe(false);
expect(r.stoppedAt).toBe('criteria-from-sealed-plan');
});
it('нет зелёного прогона → стоп на шаге 2', () => {
const r = runCriterionGate({ ...validScenario(), greenRuns: [] });
expect(r.passed).toBe(false);
expect(r.stoppedAt).toBe('criteria-green-matched');
});
it('отпечаток разошёлся (правка после прогона) → стоп на шаге 3', () => {
const r = runCriterionGate({ ...validScenario(), currentFingerprints: { c1: 'fp-OTHER' } });
expect(r.passed).toBe(false);
expect(r.stoppedAt).toBe('fingerprint-fresh');
});
it('подпись подписанта не сходится (чужой ключ) → стоп на шаге 4', () => {
const r = runCriterionGate({ ...validScenario(), signerKey: 'wrong-key' });
expect(r.passed).toBe(false);
expect(r.stoppedAt).toBe('green-signatures-valid');
});
});
import {
planGateSteps,
runPlanGate,
runCriterionGate as runCriterionGate4c,
} from './judge-orchestrator.mjs';
import { signGreen as signGreen4c } from './floor-signer.mjs';
describe('Гейт-2 planGateSteps/runPlanGate (М7 Фаза 4c §4.2): план-требование sealed, не text-mention', () => {
it('planGateSteps возвращает РОВНО 2 именованных шага в порядке coverage → criterion', () => {
const steps = planGateSteps({ specSections: ['§1'], planSteps: [] });
expect(steps.map((s) => s.name)).toEqual(['spec-to-plan-coverage', 'k5-criterion-check']);
});
it('§ спеки НЕ покрыт шагом плана → NO-GO на spec-to-plan-coverage', () => {
const r = runPlanGate({ specSections: ['§1', '§2'], planSteps: [{ ref: '§1', op: 'Read', criterion: 'смотрю файл целиком' }] });
expect(r.passed).toBe(false);
expect(r.stoppedAt).toBe('spec-to-plan-coverage');
});
it('значимый шаг без конкретного критерия → NO-GO на k5-criterion-check', () => {
const r = runPlanGate({ specSections: ['§1'], planSteps: [{ n: 1, ref: '§1', op: 'Edit', criterion: 'готово' }] });
expect(r.passed).toBe(false);
expect(r.stoppedAt).toBe('k5-criterion-check');
});
it('спека покрыта + значимый шаг несёт конкретный критерий → passed', () => {
const r = runPlanGate({ specSections: ['§1'], planSteps: [{ n: 1, ref: '§1', op: 'Edit', criterion: 'тест enforce-x возвращает block=true' }] });
expect(r.passed).toBe(true);
});
});
describe('Гейт-3 runCriterionGate как verify-before-push absorption (М7 Фаза 4c §4.2)', () => {
const SK4c = 'phase4c-signer-key';
it('подписанный зелёный по критерию из печати → passed (расписка М5, не само-sentinel)', () => {
const rec = signGreen4c({ criterion_id: 'c1', code_fingerprint: 'aa', occurrence: 1 }, SK4c);
const green = { ...rec, green: true };
const r = runCriterionGate4c({
criteria: [{ id: 'c1' }],
greenRuns: [green],
sealedCriterionIds: ['c1'],
currentFingerprints: { c1: 'aa' },
signerKey: SK4c,
});
expect(r.passed).toBe(true);
});
it('зелёный без подписи (само-написанный sentinel) → NO-GO', () => {
const green = { criterion_id: 'c1', green: true, code_fingerprint: 'aa', occurrence: 1 };
const r = runCriterionGate4c({
criteria: [{ id: 'c1' }],
greenRuns: [green],
sealedCriterionIds: ['c1'],
currentFingerprints: { c1: 'aa' },
signerKey: SK4c,
});
expect(r.passed).toBe(false);
});
});