// tools/enforce-reconcile.test.mjs // 8.2 (Δ3, Машина 5 Пакет 8) — PostToolUse-реконсилер: двунаправленная сверка журнала // НАМЕРЕНИЙ (пред-запись supreme-gate, 8.1) с фактом исполнения. // reconcileAction — исполненное действие имеет совпадающую пред-запись? нет → action-without-record. // findOrphanIntents — пред-записи без соответствующего исполнения → record-without-action. // WARN-уровень (не блок): расхождение — сигнал владельцу, не остановка (PreToolUse-пол уже отработал). import { describe, it, expect } from 'vitest'; import { reconcileAction, findOrphanIntents } from './enforce-reconcile.mjs'; describe('reconcileAction (8.2): действие без журнальной пред-записи', () => { const journal = [ { op: 'Write', object: 'tools/foo.mjs', step: 1 }, { op: 'Bash', object: 'npx vitest run foo', step: 2 }, ]; it('действие с совпадающей пред-записью → matched', () => { expect(reconcileAction({ action: { op: 'Write', object: 'tools/foo.mjs' }, journalEntries: journal }).matched).toBe(true); }); it('действие БЕЗ пред-записи → action-without-record (возможен обход стены)', () => { const r = reconcileAction({ action: { op: 'Write', object: 'tools/evil.mjs' }, journalEntries: journal }); expect(r.matched).toBe(false); expect(r.flag).toBe('action-without-record'); }); it('пустой журнал → action-without-record', () => { expect(reconcileAction({ action: { op: 'Bash', object: 'x' }, journalEntries: [] }).matched).toBe(false); }); }); describe('findOrphanIntents (8.2): запись без действия', () => { const journal = [ { op: 'Write', object: 'tools/foo.mjs', step: 1 }, { op: 'Bash', object: 'npx vitest run foo', step: 2 }, ]; it('все намерения исполнены → ok, без сирот', () => { const executed = [{ op: 'Write', object: 'tools/foo.mjs' }, { op: 'Bash', object: 'npx vitest run foo' }]; const r = findOrphanIntents({ journalEntries: journal, executedActions: executed }); expect(r.ok).toBe(true); expect(r.orphans).toEqual([]); }); it('намерение без исполнения → record-without-action (сирота)', () => { const executed = [{ op: 'Write', object: 'tools/foo.mjs' }]; const r = findOrphanIntents({ journalEntries: journal, executedActions: executed }); expect(r.ok).toBe(false); expect(r.flag).toBe('record-without-action'); expect(r.orphans).toHaveLength(1); expect(r.orphans[0].object).toBe('npx vitest run foo'); }); }); // ── R-30/reconcile reader: pure reconcileEvent (harness-событие → WARN или null) ── import { reconcileEvent } from './enforce-reconcile.mjs'; describe('reconcileEvent (reconcile reader, pure)', () => { it('действие совпало с пред-записью → null (нет WARN)', () => { const event = { tool_name: 'Write', tool_input: { file_path: '/x/y.txt' } }; const journalEntries = [{ op: 'Write', object: '/x/y.txt' }]; expect(reconcileEvent({ event, journalEntries })).toBe(null); }); it('действие без пред-записи → WARN-строка', () => { const event = { tool_name: 'Write', tool_input: { file_path: '/x/y.txt' } }; const w = reconcileEvent({ event, journalEntries: [] }); expect(typeof w).toBe('string'); expect(w).toContain('без журнальной пред-записи'); }); it('нет имени инструмента → null (не кричим)', () => { expect(reconcileEvent({ event: {}, journalEntries: [] })).toBe(null); expect(reconcileEvent({ event: null, journalEntries: [] })).toBe(null); }); it('Bash action матчится по команде', () => { const event = { tool_name: 'Bash', tool_input: { command: 'git status' } }; const journalEntries = [{ op: 'Bash', object: 'git status' }]; expect(reconcileEvent({ event, journalEntries })).toBe(null); }); });