/** * C4 regression: PermissionEngine action alignment with contracts §13. * Bug: local PermissionAction had prompt/read_only/sandbox/audit_log * which didn't match contracts allow/announce_then_run/ask_user/deny/block/refuse. * Fix: aligned all action values, fixed decision.redacted → decision.reason. */ import { describe, it, expect } from 'bun:test' import { readFileSync } from 'fs' import { join } from 'path' describe('C4: PermissionEngine action alignment', () => { const perm_src = readFileSync( join(import.meta.dir, '..', '..', 'src', 'security', 'PermissionEngine.ts'), 'utf-8' ) const registry_src = readFileSync( join(import.meta.dir, '..', '..', 'src', 'tools', 'ToolRegistry.ts'), 'utf-8' ) it('PermissionAction includes allow, announce_then_run, ask_user, deny, block, refuse', () => { const type_match = perm_src.match(/export type PermissionAction\s*=\s*\n([\s\S]*?)(?=\n\n|\nexport)/) expect(type_match).not.toBeNull() const type_block = type_match![1] expect(type_block).toContain("'allow'") expect(type_block).toContain("'announce_then_run'") expect(type_block).toContain("'ask_user'") expect(type_block).toContain("'deny'") expect(type_block).toContain("'block'") expect(type_block).toContain("'refuse'") }) it('PermissionAction does not include legacy prompt/read_only/sandbox/audit_log', () => { const type_match = perm_src.match(/export type PermissionAction\s*=\s*\n([\s\S]*?)(?=\n\n|\nexport)/) expect(type_match).not.toBeNull() const type_block = type_match![1] expect(type_block).not.toContain("'prompt'") expect(type_block).not.toContain("'read_only'") expect(type_block).not.toContain("'sandbox'") expect(type_block).not.toContain("'audit_log'") }) it('PermissionDecision has grant_scope field', () => { const iface_match = perm_src.match(/export interface PermissionDecision\s*\{([\s\S]*?)\}/) expect(iface_match).not.toBeNull() const iface_body = iface_match![1] expect(iface_body).toContain('grant_scope') }) it('finalize_decision uses decision.reason not decision.redacted', () => { // The finalize_decision method should reference decision.reason, not decision.redacted const finalize_match = perm_src.match(/private finalize_decision[\s\S]*?^ \}/m) expect(finalize_match).not.toBeNull() const finalize_body = finalize_match![0] expect(finalize_body).toContain('decision.reason') expect(finalize_body).not.toContain('decision.redacted') }) it('ToolRegistry execute_branch handles all 6 new actions', () => { const branch_match = registry_src.match(/private async execute_branch[\s\S]*?^ \}/m) expect(branch_match).not.toBeNull() const branch_body = branch_match![0] expect(branch_body).toContain("case 'allow'") expect(branch_body).toContain("case 'announce_then_run'") expect(branch_body).toContain("case 'ask_user'") expect(branch_body).toContain("case 'deny'") expect(branch_body).toContain("case 'block'") expect(branch_body).toContain("case 'refuse'") }) it('ToolRegistry execute_branch does not have legacy read_only/sandbox/audit_log', () => { const branch_match = registry_src.match(/private async execute_branch[\s\S]*?^ \}/m) expect(branch_match).not.toBeNull() const branch_body = branch_match![0] expect(branch_body).not.toContain("case 'read_only'") expect(branch_body).not.toContain("case 'sandbox'") expect(branch_body).not.toContain("case 'audit_log'") expect(branch_body).not.toContain("case 'prompt'") }) })