Files
brain/tools/escape-grant.test.mjs
T
Дмитрий bbc053e0a6 feat: D1 — благословлённый ops-runbook (деплой выполняет агент под ревью)
Деплой, помеченный **Kind:** deploy и опечатанный (наставник+судья GO,
judge_mode=live-block), агент выполняет по белому списку шагов под ОДНИМ
согласием владельца `FLOOR-ESCAPE: ops-runbook:<plan-hash>` — без аварийного
выхода на каждую команду. «Ядерный» набор (rm -rf/force-push/migrate:fresh/
db:wipe) остаётся на per-command escape.

- plan-lock: freezePlan принимает kind (в подписанную базу + хеш, как delivery);
  не-'normal' добавляет поле, обычные планы байт-идентичны старым печатям.
- plan-skills: parsePlanKind (**Kind:** deploy|normal, default normal).
- seal-orchestration: sealablePlan/sealPlan прокидывают kind в печать.
- escape-grant: loadOpsRunbookGrants (окно = существование плана, БЕЗ 5-мин
  фильтра) + opsRunbookGrantOpen (точный матч на plan_id).
- floor-decide: floorDecide получает инъектируемый blessedOps(cmd); content-block
  команда из набора пропускается, ЯДЕРНЫЙ набор (bashIsFloor) исключён из послабления.
- blessed-ops (новый модуль-мост): buildBlessedOps + loadBlessedOpsForSession —
  знает план+пол, чтобы СОХРАНИТЬ Δ9 (enforce-floor не зависит от модуля печати плана).
  Предикат пускает команду только дословно из Bash-листов опечатанного deploy-плана.
- enforce-floor: gated — blessed-ops грузит план/гранты ТОЛЬКО при открытом
  ops-runbook-гранте; без согласия владельца пол плана не касается (Δ9 цел).

План: docs/superpowers/plans/2026-06-18-blessed-ops-runbook-plan.md
Спека: docs/superpowers/specs/2026-06-18-blessed-ops-runbook-design.md §3.1-3.7.
+33 теста, свод 4299 passed / 2 skipped.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-18 13:19:22 +03:00

176 lines
9.6 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 { canonicalAction, escapeGrantOpen, FLOOR_ESCAPE_WINDOW_MS } from './escape-grant.mjs';
import { loadOpsRunbookGrants, opsRunbookGrantOpen, OPS_RUNBOOK_PREFIX } from './escape-grant.mjs';
const ID = (s) => s; // normalizeImpl-заглушка для путей
describe('ops-runbook грант (D1 — окно = существование плана, не 5 мин)', () => {
const mkFs = (records) => ({
existsSync: () => true,
readFileSync: () => records.map((r) => JSON.stringify(r)).join('\n'),
});
it('OPS_RUNBOOK_PREFIX = "ops-runbook:"', () => {
expect(OPS_RUNBOOK_PREFIX).toBe('ops-runbook:');
});
it('opsRunbookGrantOpen: грант на ЭТОТ plan_id → true', () => {
expect(opsRunbookGrantOpen('HASH1', [{ action: 'ops-runbook:HASH1', ts: 1 }])).toBe(true);
});
it('opsRunbookGrantOpen: грант на ЧУЖОЙ хеш → false', () => {
expect(opsRunbookGrantOpen('HASH2', [{ action: 'ops-runbook:HASH1', ts: 1 }])).toBe(false);
});
it('opsRunbookGrantOpen: пустой/не-массив/пустой planId → false', () => {
expect(opsRunbookGrantOpen('H', [])).toBe(false);
expect(opsRunbookGrantOpen('H', null)).toBe(false);
expect(opsRunbookGrantOpen('', [{ action: 'ops-runbook:', ts: 1 }])).toBe(false);
});
it('loadOpsRunbookGrants: запись старше 5 мин НЕ отфильтрована (окно = план)', () => {
const old = 1000; const now = old + 10 * 60 * 1000;
const fs = mkFs([{ type: 'floor_escape', action: 'ops-runbook:HASH1', ts: old }]);
const grants = loadOpsRunbookGrants('S', now, { keyImpl: () => null, fsImpl: fs, runtimeDir: '/rt' });
expect(grants.some((g) => g.action === 'ops-runbook:HASH1')).toBe(true);
});
it('loadOpsRunbookGrants: обычные (не ops-runbook) floor_escape игнорирует', () => {
const fs = mkFs([{ type: 'floor_escape', action: 'bash:rm -rf x', ts: 1 }]);
expect(loadOpsRunbookGrants('S', 2, { keyImpl: () => null, fsImpl: fs, runtimeDir: '/rt' })).toEqual([]);
});
it('loadOpsRunbookGrants: future-ts (ts > now) отброшен (нижняя граница времени)', () => {
const fs = mkFs([{ type: 'floor_escape', action: 'ops-runbook:HASH1', ts: 5000 }]);
expect(loadOpsRunbookGrants('S', 1000, { keyImpl: () => null, fsImpl: fs, runtimeDir: '/rt' })).toEqual([]);
});
});
describe('escape-grant canonicalAction', () => {
it('Bash → bash:<normalized command>', () => {
expect(canonicalAction('Bash', { command: 'git push --force' }, { normalizeImpl: ID }))
.toBe('bash:git push --force');
});
it('Write → write:<normalized path>', () => {
expect(canonicalAction('Write', { file_path: '/a/.env' }, { normalizeImpl: ID }))
.toBe('write:/a/.env');
});
it('MCP → mcp:<tool>:<args>', () => {
expect(canonicalAction('mcp__x__send', { url: 'http://1.2.3.4' }, { normalizeImpl: ID }))
.toBe('mcp:mcp__x__send:{"url":"http://1.2.3.4"}');
});
});
describe('escape-grant escapeGrantOpen', () => {
const now = 1_000_000;
const fresh = (action) => ({ action, ts: now - 1000 });
it('точное совпадение свежего непогашенного → open', () => {
expect(escapeGrantOpen('bash:git push --force', [fresh('bash:git push --force')], [], now)).toBe(true);
});
it('несовпавшая строка → closed', () => {
expect(escapeGrantOpen('bash:git push --force', [fresh('bash:reset --hard')], [], now)).toBe(false);
});
it('погашенный (action в consumed) → closed (one-shot)', () => {
const g = fresh('bash:x'); expect(escapeGrantOpen('bash:x', [g], [{ action: 'bash:x', ts: g.ts }], now)).toBe(false);
});
it('устаревший (> окна) → closed', () => {
expect(escapeGrantOpen('bash:x', [{ action: 'bash:x', ts: now - FLOOR_ESCAPE_WINDOW_MS - 1 }], [], now)).toBe(false);
});
it('из будущего (ts > now) → closed', () => {
expect(escapeGrantOpen('bash:x', [{ action: 'bash:x', ts: now + 1000 }], [], now)).toBe(false);
});
it('пустой список → closed', () => {
expect(escapeGrantOpen('bash:x', [], [], now)).toBe(false);
});
});
// escape-grant.mjs — единый findOpenGrant (open ↔ consume один предикат свежести, M6 FIX-2)
import { findOpenGrant } from './escape-grant.mjs';
describe('escape-grant findOpenGrant (M6 FIX-2)', () => {
const now = 1_000_000;
it('при дублях возвращает СВЕЖИЙ непогашенный грант, не future-ts', () => {
const fresh = { action: 'bash:x', ts: now - 5 };
const future = { action: 'bash:x', ts: now + 5 };
expect(findOpenGrant('bash:x', [future, fresh], [], now)).toEqual(fresh);
});
it('нет открытого (только future-ts) → null', () => {
expect(findOpenGrant('bash:x', [{ action: 'bash:x', ts: now + 5 }], [], now)).toBe(null);
});
it('погашенный → null (one-shot)', () => {
const g = { action: 'bash:x', ts: now - 5 };
expect(findOpenGrant('bash:x', [g], [{ action: 'bash:x', ts: g.ts }], now)).toBe(null);
});
});
// M7 Task 1.2b (P-2, КРИТ): canonicalAction обязан иметь ветку PowerShell. Без неё все PS-команды
// схлопываются в 'write:' (input.command не в PATH_FIELDS) → один escape-грант разблокирует ЛЮБУЮ
// PS-команду в окне, а тест специфичности проходил бы зелёным ложно (a===b==='write:').
describe('escape-grant canonicalAction — PowerShell (P-2)', () => {
it('PowerShell специфичен (разные команды → разные ключи)', () => {
const a = canonicalAction('PowerShell', { command: 'Remove-Item -Recurse -Force C:\\x' });
const b = canonicalAction('PowerShell', { command: 'Invoke-WebRequest https://e.rf' });
expect(a).toMatch(/^powershell:/);
expect(a).not.toBe(b); // НЕ оба 'write:' (баг до 1.2b даёт a===b==='write:')
expect(a).not.toBe('write:');
});
it('PowerShell нормализует пробелы (тот же ключ при whitespace-дрейфе)', () => {
const a = canonicalAction('PowerShell', { command: 'Remove-Item -Recurse -Force C:\\x' });
const b = canonicalAction('PowerShell', { command: 'Remove-Item -Recurse -Force C:\\x' });
expect(a).toBe(b);
});
});
describe('canonicalAction тотальна (M7 Фаза 0, правило 7а, SE-I/L6)', () => {
for (const bad of [undefined, null, 123, true, 'x']) {
it(`не бросает на мусорном toolName=${String(bad)}`, () => {
expect(() => canonicalAction(bad, bad)).not.toThrow();
});
}
it('не бросает когда геттер input.command кидает (RED до фикса)', () => {
const evil = {};
Object.defineProperty(evil, 'command', { get() { throw new Error('boom'); } });
expect(() => canonicalAction('Bash', evil)).not.toThrow();
});
it('не бросает когда injected normalizeImpl кидает (Write)', () => {
const boom = () => { throw new Error('boom'); };
expect(() => canonicalAction('Write', { file_path: '/a' }, { normalizeImpl: boom })).not.toThrow();
});
it('регресс: валидные ключи не сломаны', () => {
expect(canonicalAction('Bash', { command: 'git status' })).toBe('bash:git status');
expect(canonicalAction('PowerShell', { command: 'Get-ChildItem' })).toBe('powershell:Get-ChildItem');
});
});
import { escapeAllowsEvent } from './escape-grant.mjs';
describe('escapeAllowsEvent — panic-предикат (M7 Фаза 2, правило 7б)', () => {
const now = 1_000_000;
const ev = (tool_name, tool_input) => ({ tool_name, tool_input });
it('матч-грант на действие события → true', () => {
const action = canonicalAction('Bash', { command: 'git push --force' });
const grants = [{ action, ts: now - 1000 }];
expect(escapeAllowsEvent(ev('Bash', { command: 'git push --force' }), grants, [], now)).toBe(true);
});
it('несовпавший грант → false', () => {
const grants = [{ action: 'bash:reset --hard', ts: now - 1000 }];
expect(escapeAllowsEvent(ev('Bash', { command: 'git push --force' }), grants, [], now)).toBe(false);
});
it('пустые гранты → false', () => {
expect(escapeAllowsEvent(ev('Bash', { command: 'x' }), [], [], now)).toBe(false);
});
it('малформ-event не бросает → false', () => {
expect(escapeAllowsEvent(undefined, [], [], now)).toBe(false);
expect(escapeAllowsEvent(null, null, null, now)).toBe(false);
});
});
// sub-plan E Task 1 (✅O13): escape привязан к КОНКРЕТНОМУ скилу
describe('canonicalAction — Skill (✅O13)', () => {
it('Skill → skill:<имя в нижнем регистре>, не write:cwd', () => {
expect(canonicalAction('Skill', { skill: 'Audit-Context-Building:Audit-Context-Building' }))
.toBe('skill:audit-context-building:audit-context-building');
});
it('разные скилы → разные каноны (не агностичны)', () => {
expect(canonicalAction('Skill', { skill: 'sharp-edges:sharp-edges' }))
.not.toBe(canonicalAction('Skill', { skill: 'variant-analysis:variants' }));
});
it('пустой skill → skill: (детерминирован, не cwd)', () => {
expect(canonicalAction('Skill', {})).toBe('skill:');
});
it('не-Skill инструменты не затронуты (Bash остаётся bash:)', () => {
expect(canonicalAction('Bash', { command: 'ls' })).toMatch(/^bash:/);
});
});