Files
brain/tools/commit-grant.test.mjs
T
Дмитрий b47a71c66b feat: D2 — канал коммита под ревью (агент коммитит под commit:<hash>)
Опечатанный ревью-план (GO наставника+судьи, judge_mode=live-block) + одно
согласие владельца `FLOOR-ESCAPE: commit:<plan-hash>` → агент делает git
add/commit/push без терминала владельца. Гейт ПРИСУТСТВИЯ (router-gate
git-approval) отходит; гейты КАЧЕСТВА (criterion-gate/verify-gate) НЕ тронуты —
код-коммит всё равно требует по-критерийный GREEN и свежую расписку. Согласия
деплоя (ops-runbook:) и коммита (commit:) — раздельные кнопки.

- escape-grant: обобщён plan-scoped загрузчик (loadPlanScopedGrants/
  planScopedGrantOpen, окно = существование плана); D1 ops-runbook стал тонкой
  обёрткой; добавлены commit: COMMIT_GRANT_PREFIX/loadCommitGrants/commitGrantOpen.
- commit-grant (новый мост план↔router-gate): commitGrantOpenForSession —
  открыт ли commit:<hash> на валидный sealed live-block план сессии.
- shell-content-rules classifyGitCommand: conditional-git пускается при
  ctx.commitGrantOpen; GIT_HARD (force-push/--no-verify/-c) блокирует ПЕРВЫМ
  (качество/безопасность не ослаблены).
- enforce-router-gate: main кладёт ctx.commitGrantOpen (gated через мост).

План: docs/superpowers/plans/2026-06-18-agent-commit-channel-plan.md
Спека: docs/superpowers/specs/2026-06-18-agent-commit-channel-design.md §3.1-3.2.

ОТЛОЖЕНО (требует решения владельца, в хвосте плана):
- §3.3 docs/ops без criterion/verify: .md уже пропускается; расширение на
  не-.md ops-артефакты конфликтует с CLAUDE.md §13 v2.40 — нужен явный список.
- §3.4 десинк push-последним-шагом: рискованная правка снятия печати стены.

+22 теста, свод 4319 passed / 2 skipped.

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

54 lines
3.9 KiB
JavaScript

import { describe, it, expect } from 'vitest';
import { commitGrantOpenForSession } from './commit-grant.mjs';
import { freezePlan } from './plan-lock.mjs';
import { classifyGitCommand } from './shell-content-rules.mjs';
describe('D2 критерий §5 — канал коммита под ревью (сквозной)', () => {
const K = 'k-d2e2e';
const plan = freezePlan({ steps: [{ op: 'Write', object: 'tools/x.mjs' }], judgeMode: 'live-block', key: K, nowMs: 1 });
it('опечатанный план + commit:<hash> → git commit allow', () => {
const open = commitGrantOpenForSession('S', { loadGrantsImpl: () => [{ action: `commit:${plan.plan_id}`, ts: 1 }], loadPlanImpl: () => plan, keyImpl: () => K, verifyImpl: () => true });
expect(open).toBe(true);
expect(classifyGitCommand('git commit -m x', { commitGrantOpen: open }).result).toBe('allow');
});
it('грант на ЧУЖОЙ хеш → git commit block (default-deny держит)', () => {
const open = commitGrantOpenForSession('S', { loadGrantsImpl: () => [{ action: 'commit:OTHER', ts: 1 }], loadPlanImpl: () => plan, keyImpl: () => K, verifyImpl: () => true });
expect(open).toBe(false);
expect(classifyGitCommand('git commit -m x', { commitGrantOpen: open }).result).toBe('block');
});
it('force-push блокируется даже под открытым commit-грантом (качество держит)', () => {
const open = commitGrantOpenForSession('S', { loadGrantsImpl: () => [{ action: `commit:${plan.plan_id}`, ts: 1 }], loadPlanImpl: () => plan, keyImpl: () => K, verifyImpl: () => true });
expect(classifyGitCommand('git push --force', { commitGrantOpen: open }).result).toBe('block');
});
});
describe('commitGrantOpenForSession (D2 — gated: нет гранта → план не грузим)', () => {
const K = 'k-cg';
it('нет commit-грантов → false, план НЕ грузится', () => {
let planLoaded = false;
const r = commitGrantOpenForSession('S', { loadGrantsImpl: () => [], loadPlanImpl: () => { planLoaded = true; return null; }, keyImpl: () => K, verifyImpl: () => true });
expect(r).toBe(false);
expect(planLoaded).toBe(false);
});
it('грант на хеш + sealed live-block план → true', () => {
const plan = freezePlan({ steps: [{ op: 'Write', object: 'a.mjs' }], judgeMode: 'live-block', key: K, nowMs: 1 });
const r = commitGrantOpenForSession('S', { loadGrantsImpl: () => [{ action: `commit:${plan.plan_id}`, ts: 1 }], loadPlanImpl: () => plan, keyImpl: () => K, verifyImpl: () => true });
expect(r).toBe(true);
});
it('грант на ЧУЖОЙ хеш → false', () => {
const plan = freezePlan({ steps: [{ op: 'Write', object: 'a.mjs' }], judgeMode: 'live-block', key: K, nowMs: 1 });
expect(commitGrantOpenForSession('S', { loadGrantsImpl: () => [{ action: 'commit:OTHER', ts: 1 }], loadPlanImpl: () => plan, keyImpl: () => K, verifyImpl: () => true })).toBe(false);
});
it('judge_mode не live-block → false', () => {
const plan = freezePlan({ steps: [{ op: 'Write', object: 'a.mjs' }], judgeMode: 'shadow', key: K, nowMs: 1 });
expect(commitGrantOpenForSession('S', { loadGrantsImpl: () => [{ action: `commit:${plan.plan_id}`, ts: 1 }], loadPlanImpl: () => plan, keyImpl: () => K, verifyImpl: () => true })).toBe(false);
});
it('печать невалидна (verifyImpl→false) → false', () => {
const plan = freezePlan({ steps: [{ op: 'Write', object: 'a.mjs' }], judgeMode: 'live-block', key: K, nowMs: 1 });
expect(commitGrantOpenForSession('S', { loadGrantsImpl: () => [{ action: `commit:${plan.plan_id}`, ts: 1 }], loadPlanImpl: () => plan, keyImpl: () => K, verifyImpl: () => false })).toBe(false);
});
it('нет плана → false', () => {
expect(commitGrantOpenForSession('S', { loadGrantsImpl: () => [{ action: 'commit:x', ts: 1 }], loadPlanImpl: () => null, keyImpl: () => K, verifyImpl: () => true })).toBe(false);
});
});