diff --git a/tools/shadow-replay.mjs b/tools/shadow-replay.mjs index 68a42fda..3a48aa35 100644 --- a/tools/shadow-replay.mjs +++ b/tools/shadow-replay.mjs @@ -10,6 +10,8 @@ */ import { floorDecide } from './floor-decide.mjs'; import { snapshotNeeded } from './snapshot-decide.mjs'; +import { decide as supremeDecide } from './enforce-supreme-gate.mjs'; +import { freezePlan } from './plan-lock.mjs'; // ── Фикстуры (данные) ────────────────────────────────────────────────────── @@ -76,3 +78,16 @@ export function decideM6(toolUse) { const need = snapshotNeeded(toolUse && toolUse.name, (toolUse && toolUse.input) || {}); return { blocked: !!need, reason: need ? 'снимок нужен (разрушительный Bash)' : 'снимок не нужен' }; } + +export const REPLAY_KEY = 'shadow-replay-test-key'; + +/** Заморозить синтетический план тест-ключом (artifactId=null → без проверки артефакта). */ +export function freezeReplayPlan(steps) { + return freezePlan({ steps, artifactId: null, key: REPLAY_KEY, nowMs: 0 }); +} + +/** М2: план-матч ядро через decide(). frozenArtifact=null (план без artifact_id). */ +export function decideM2(toolUse, frozenPlan, stepPtr) { + const r = supremeDecide({ toolUse, frozenPlan, frozenArtifact: null, stepPtr, key: REPLAY_KEY }); + return { blocked: r.decision === 'block', reason: r.reason }; +} diff --git a/tools/shadow-replay.test.mjs b/tools/shadow-replay.test.mjs index fa496b9a..54435e2b 100644 --- a/tools/shadow-replay.test.mjs +++ b/tools/shadow-replay.test.mjs @@ -1,7 +1,7 @@ import { describe, it, expect } from 'vitest'; import { BENIGN_FIXTURE, RISKY_FIXTURE, M2_PLAN_STEPS, M2_EVENTS, M4_FIXTURE, - classifyOutcome, decideM5, decideM6, + classifyOutcome, decideM5, decideM6, decideM2, freezeReplayPlan, } from './shadow-replay.mjs'; describe('shadow-replay fixtures', () => { @@ -70,3 +70,19 @@ describe('М6 снимок адаптер', () => { expect(decideM6({ name: 'Bash', input: { command: 'git log' } }).blocked).toBe(false); }); }); + +describe('М2 стена адаптер', () => { + const plan = freezeReplayPlan(M2_PLAN_STEPS); + it('in-plan правка (шаг 1) → allow', () => { + expect(decideM2({ name: 'Edit', input: { file_path: 'app/Foo.php' } }, plan, 0).blocked).toBe(false); + }); + it('observe (Read) → allow', () => { + expect(decideM2({ name: 'Read', input: { file_path: 'app/Foo.php' } }, plan, 0).blocked).toBe(false); + }); + it('out-of-plan мутация → block', () => { + expect(decideM2({ name: 'Edit', input: { file_path: 'app/Other.php' } }, plan, 0).blocked).toBe(true); + }); + it('seed skill → allow', () => { + expect(decideM2({ name: 'Skill', input: { skill: 'superpowers:writing-plans' } }, plan, 0).blocked).toBe(false); + }); +});