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