Files
brain/tools/m5-floor-invariants.test.mjs
T

139 lines
8.2 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.
import { describe, it, expect } from 'vitest';
import { readFileSync, readdirSync } from 'node:fs';
import { fileURLToPath } from 'node:url';
import { dirname, join } from 'node:path';
// Сквозные CI-инварианты Машины 5 (N5/Δ9). Файл создаётся в Пакете 1 (Δ9-б seed),
// наполняется далее в Пакете 6 (N5 строгая проверка пола, Δ9-а floor-суперсет,
// escape≠protection). Все потребители разрушительности — единый источник
// classify-destructive.mjs (classifyDestructive).
const TOOLS_DIR = dirname(fileURLToPath(import.meta.url));
// Конструкт-маркер прежних дублей: оба удалённых классификатора объявлялись как
// `const DESTRUCTIVE_RE = /.../`. Токен-grep (migrate:fresh/format) НЕ годится —
// migrate:fresh легитимно есть в whitelist router-gate/supreme-gate (Пакет 2 переносит),
// а `format` — обычное слово. Таргетим само объявление символа.
const DESTRUCTIVE_DECL_RE = /DESTRUCTIVE_RE\s*=/;
describe('Δ9(б) — единственный источник классификатора разрушительности (Step 1.8)', () => {
it('positive control: детектор ловит объявление, но не комментарий (не вакуумный)', () => {
expect(DESTRUCTIVE_DECL_RE.test('const DESTRUCTIVE_RE = /rm/;')).toBe(true);
expect(DESTRUCTIVE_DECL_RE.test('// локальный DESTRUCTIVE_RE удалён')).toBe(false);
});
it('ни один tools/*.mjs (кроме classify-destructive.mjs) не объявляет DESTRUCTIVE_RE', () => {
const files = readdirSync(TOOLS_DIR).filter(
(f) => f.endsWith('.mjs') && !f.endsWith('.test.mjs') && f !== 'classify-destructive.mjs',
);
const offenders = [];
for (const f of files) {
const src = readFileSync(join(TOOLS_DIR, f), 'utf8');
if (DESTRUCTIVE_DECL_RE.test(src)) offenders.push(f);
}
expect(offenders).toEqual([]);
});
});
import { finalGate, runGateLadder } from './judge-orchestrator.mjs';
import { classifyDestructive } from './classify-destructive.mjs';
// 6.1 (N5) — строгая проверка пола. Запирает фиксы аудита M1-M4 (finalGate !== false,
// runGateLadder ok !== true + try/catch) «по всем полам разом»: пол судится строго — любой
// недоказанный / упавший / не-true результат → блок. Поведенческие инварианты (краснеют при
// реверте фикса) + точечный структурный grep по judge-orchestrator (центр «по всем полам разом»).
describe('N5 (6.1) — строгая проверка пола: недоказанный/упавший пол → блок', () => {
it('finalGate: снять вето пола может ТОЛЬКО явный floorBlocked===false', () => {
expect(finalGate({ judgeDecision: 'GO', floorBlocked: false })).toBe('allow');
for (const fb of [undefined, null, 0, '', 'false', true]) {
expect(finalGate({ judgeDecision: 'GO', floorBlocked: fb })).toBe('block');
}
});
it('runGateLadder: шаг с ok !== true (truthy-строка) НЕ проходит', () => {
const r = runGateLadder([{ name: 'truthy', run: () => ({ ok: 'yes' }) }]);
expect(r.passed).toBe(false);
expect(r.stoppedAt).toBe('truthy');
});
it('runGateLadder: шаг вернул undefined → fail-closed (не проходит)', () => {
expect(runGateLadder([{ name: 'nil', run: () => undefined }]).passed).toBe(false);
});
it('runGateLadder: шаг бросил исключение → fail-closed (не проходит)', () => {
const r = runGateLadder([{ name: 'boom', run: () => { throw new Error('io'); } }]);
expect(r.passed).toBe(false);
expect(r.stoppedAt).toBe('boom');
});
// Структурный страж: токенайзерные .ok в floor-decide — НЕ floor-вердикты, потому scope
// grep'а = только judge-orchestrator (там runGateLadder/finalGate — «по всем полам разом»).
const ORCH = readFileSync(join(TOOLS_DIR, 'judge-orchestrator.mjs'), 'utf8');
const TRUTHY_OK_RE = /\.ok\s*\)|\.ok\s*\?|\.ok\s*&&/;
it('positive control: детектор ловит truthy-потребление .ok (не вакуумный)', () => {
expect(TRUTHY_OK_RE.test('if (result.ok) accept();')).toBe(true);
expect(TRUTHY_OK_RE.test('if (result.ok === true) accept();')).toBe(false);
});
it('judge-orchestrator не потребляет вердикт пола через truthy .ok (только строгое сравнение)', () => {
expect(TRUTHY_OK_RE.test(ORCH)).toBe(false);
});
});
// 6.2 (Δ9-а) — floor-набор не усыхает. Снимок команд, которые ОБЯЗАНЫ оставаться floor:true:
// удаление строки из FLOOR_RE classify-destructive без ADR флипнёт одну → CI краснеет.
describe('Δ9(а) (6.2) — анти-усыхание floor-набора', () => {
const FLOOR_SNAPSHOT = [
'git push --force',
'git push --force-with-lease origin main',
'git push -f',
'git reset --hard HEAD~3',
'php artisan migrate:fresh',
'php artisan migrate:reset',
'php artisan migrate:refresh',
'php artisan db:wipe',
'rm -rf build',
'rm -fr node_modules',
'rm --recursive --force build',
];
for (const cmd of FLOOR_SNAPSHOT) {
it(`остаётся floor:true: ${cmd}`, () => {
expect(classifyDestructive(cmd).floor).toBe(true);
});
}
it('positive control: не-разрушительная команда floor:false (снимок не «всё подряд»)', () => {
expect(classifyDestructive('git status').floor).toBe(false);
});
});
// 6.4 (escape≠protection) — защитный пол enforce'ится fail-close хуками БЕЗ override-вокабуляра.
// Правка, переносящая защиту пола под override (как у дисциплинарных хуков), падает здесь.
describe('escape≠protection (6.4) — floor-хуки без override-вокабуляра', () => {
const PROTECTIVE = ['enforce-floor.mjs', 'floor-decide.mjs'];
const OVERRIDE_RE = /override/i;
it('positive control: детектор ловит слово override (не вакуумный)', () => {
expect(OVERRIDE_RE.test('if (hasOverride) allow();')).toBe(true);
expect(OVERRIDE_RE.test('floor blocks always')).toBe(false);
});
for (const f of PROTECTIVE) {
it(`${f} не ссылается на override-вокабуляр`, () => {
const src = readFileSync(join(TOOLS_DIR, f), 'utf8');
expect(OVERRIDE_RE.test(src)).toBe(false);
});
}
});
// 7.6 (Блок 4.6) — разделение дисциплина/защита: мутация P18 + неподделываемый по-критерийный
// GREEN (criterion-green, Пакет 5) — это ЗАЩИТА (без override-вокабуляра). tdd-gate остаётся
// ДИСЦИПЛИНОЙ (fail-open + override допустимы) в ОТДЕЛЬНОМ хуке. Структурный инвариант: модули
// защиты-пола GREEN не несут override; перенос мутации/GREEN-пола под override упадёт здесь.
describe('7.6 — мутация/GREEN-пол это защита без override (отдельно от tdd-gate дисциплины)', () => {
const GREEN_PROTECTION = ['criterion-green.mjs', 'floor-signer.mjs'];
const OVERRIDE_RE2 = /override/i;
it('positive control: детектор override не вакуумен', () => {
expect(OVERRIDE_RE2.test('allowOverride()')).toBe(true);
expect(OVERRIDE_RE2.test('signed green')).toBe(false);
});
for (const f of GREEN_PROTECTION) {
it(`${f} (защита GREEN) не ссылается на override-вокабуляр`, () => {
const src = readFileSync(join(TOOLS_DIR, f), 'utf8');
expect(OVERRIDE_RE2.test(src)).toBe(false);
});
}
});