b47a71c66b
Опечатанный ревью-план (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>
54 lines
3.9 KiB
JavaScript
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);
|
|
});
|
|
});
|