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