/**
 * @license
 * Copyright 2026 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */
import { describe, it, expect, vi, beforeEach, afterEach, } from 'vitest';
import { checkPolicy, updatePolicy, getPolicyDenialError } from './policy.js';
import { MessageBusType, } from '../confirmation-bus/types.js';
import { ApprovalMode, PolicyDecision } from '../policy/types.js';
import { escapeRegex } from '../policy/utils.js';
import { ToolConfirmationOutcome, } from '../tools/tools.js';
import { ROOT_SCHEDULER_ID, } from './types.js';
import { DiscoveredMCPTool } from '../tools/mcp-tool.js';
import { Scheduler } from './scheduler.js';
import { ToolErrorType } from '../tools/tool-error.js';
describe('policy.ts', () => {
    describe('checkPolicy', () => {
        it('should return the decision from the policy engine', async () => {
            const mockPolicyEngine = {
                check: vi.fn().mockResolvedValue({ decision: PolicyDecision.ALLOW }),
            };
            const mockConfig = {
                getPolicyEngine: vi.fn().mockReturnValue(mockPolicyEngine),
            };
            mockConfig.config =
                mockConfig;
            const toolCall = {
                request: { name: 'test-tool', args: {} },
                tool: { name: 'test-tool' },
            };
            const result = await checkPolicy(toolCall, mockConfig);
            expect(result.decision).toBe(PolicyDecision.ALLOW);
            expect(mockPolicyEngine.check).toHaveBeenCalledWith({ name: 'test-tool', args: {} }, undefined, undefined, undefined);
        });
        it('should pass serverName and toolAnnotations for MCP tools', async () => {
            const mockPolicyEngine = {
                check: vi.fn().mockResolvedValue({ decision: PolicyDecision.ALLOW }),
            };
            const mockConfig = {
                getPolicyEngine: vi.fn().mockReturnValue(mockPolicyEngine),
            };
            mockConfig.config =
                mockConfig;
            const mcpTool = Object.create(DiscoveredMCPTool.prototype);
            mcpTool.serverName = 'my-server';
            mcpTool._toolAnnotations = { readOnlyHint: true };
            const toolCall = {
                request: { name: 'mcp-tool', args: {} },
                tool: mcpTool,
            };
            await checkPolicy(toolCall, mockConfig);
            expect(mockPolicyEngine.check).toHaveBeenCalledWith({ name: 'mcp-tool', args: {} }, 'my-server', { readOnlyHint: true }, undefined);
        });
        it('should respect disableAlwaysAllow from config', async () => {
            const mockPolicyEngine = {
                check: vi.fn().mockResolvedValue({ decision: PolicyDecision.ALLOW }),
            };
            const mockConfig = {
                getPolicyEngine: vi.fn().mockReturnValue(mockPolicyEngine),
                getDisableAlwaysAllow: vi.fn().mockReturnValue(true),
            };
            mockConfig.config =
                mockConfig;
            const toolCall = {
                request: { name: 'test-tool', args: {} },
                tool: { name: 'test-tool' },
            };
            // Note: checkPolicy calls config.getPolicyEngine().check()
            // The PolicyEngine itself is already configured with disableAlwaysAllow
            // when created in Config. Here we are just verifying that checkPolicy
            // doesn't somehow bypass it.
            await checkPolicy(toolCall, mockConfig);
            expect(mockPolicyEngine.check).toHaveBeenCalled();
        });
        it('should throw if ASK_USER is returned in non-interactive mode', async () => {
            const mockPolicyEngine = {
                check: vi.fn().mockResolvedValue({ decision: PolicyDecision.ASK_USER }),
            };
            const mockConfig = {
                getPolicyEngine: vi.fn().mockReturnValue(mockPolicyEngine),
                isInteractive: vi.fn().mockReturnValue(false),
            };
            mockConfig.config =
                mockConfig;
            const toolCall = {
                request: { name: 'test-tool', args: {} },
                tool: { name: 'test-tool' },
            };
            await expect(checkPolicy(toolCall, mockConfig)).rejects.toThrow(/not supported in non-interactive mode/);
        });
        it('should return DENY without throwing', async () => {
            const mockPolicyEngine = {
                check: vi.fn().mockResolvedValue({ decision: PolicyDecision.DENY }),
            };
            const mockConfig = {
                getPolicyEngine: vi.fn().mockReturnValue(mockPolicyEngine),
            };
            mockConfig.config =
                mockConfig;
            const toolCall = {
                request: { name: 'test-tool', args: {} },
                tool: { name: 'test-tool' },
            };
            const result = await checkPolicy(toolCall, mockConfig);
            expect(result.decision).toBe(PolicyDecision.DENY);
        });
        it('should return ASK_USER without throwing in interactive mode', async () => {
            const mockPolicyEngine = {
                check: vi.fn().mockResolvedValue({ decision: PolicyDecision.ASK_USER }),
            };
            const mockConfig = {
                getPolicyEngine: vi.fn().mockReturnValue(mockPolicyEngine),
                isInteractive: vi.fn().mockReturnValue(true),
            };
            mockConfig.config =
                mockConfig;
            const toolCall = {
                request: { name: 'test-tool', args: {} },
                tool: { name: 'test-tool' },
            };
            const result = await checkPolicy(toolCall, mockConfig);
            expect(result.decision).toBe(PolicyDecision.ASK_USER);
        });
        it('should return ALLOW if decision is ASK_USER and request is client-initiated', async () => {
            const mockPolicyEngine = {
                check: vi.fn().mockResolvedValue({ decision: PolicyDecision.ASK_USER }),
            };
            const mockConfig = {
                getPolicyEngine: vi.fn().mockReturnValue(mockPolicyEngine),
                isInteractive: vi.fn().mockReturnValue(true),
            };
            const toolCall = {
                request: { name: 'test-tool', args: {}, isClientInitiated: true },
                tool: { name: 'test-tool' },
            };
            const result = await checkPolicy(toolCall, mockConfig);
            expect(result.decision).toBe(PolicyDecision.ALLOW);
        });
        it('should still return DENY if request is client-initiated but policy says DENY', async () => {
            const mockPolicyEngine = {
                check: vi.fn().mockResolvedValue({ decision: PolicyDecision.DENY }),
            };
            const mockConfig = {
                getPolicyEngine: vi.fn().mockReturnValue(mockPolicyEngine),
            };
            const toolCall = {
                request: { name: 'test-tool', args: {}, isClientInitiated: true },
                tool: { name: 'test-tool' },
            };
            const result = await checkPolicy(toolCall, mockConfig);
            expect(result.decision).toBe(PolicyDecision.DENY);
        });
    });
    describe('updatePolicy', () => {
        it('should set AUTO_EDIT mode for auto-edit transition tools', async () => {
            const mockConfig = {
                setApprovalMode: vi.fn(),
            };
            mockConfig.config =
                mockConfig;
            const mockMessageBus = {
                publish: vi.fn(),
            };
            mockConfig.messageBus =
                mockMessageBus;
            const tool = { name: 'replace' }; // 'replace' is in EDIT_TOOL_NAMES
            await updatePolicy(tool, ToolConfirmationOutcome.ProceedAlways, undefined, mockConfig, mockMessageBus);
            expect(mockConfig.setApprovalMode).toHaveBeenCalledWith(ApprovalMode.AUTO_EDIT);
            expect(mockMessageBus.publish).not.toHaveBeenCalled();
        });
        it('should handle standard policy updates (persist=false)', async () => {
            const mockConfig = {
                setApprovalMode: vi.fn(),
            };
            mockConfig.config =
                mockConfig;
            const mockMessageBus = {
                publish: vi.fn(),
            };
            mockConfig.messageBus =
                mockMessageBus;
            const tool = { name: 'test-tool' };
            await updatePolicy(tool, ToolConfirmationOutcome.ProceedAlways, undefined, mockConfig, mockMessageBus);
            expect(mockMessageBus.publish).toHaveBeenCalledWith(expect.objectContaining({
                type: MessageBusType.UPDATE_POLICY,
                toolName: 'test-tool',
                persist: false,
            }));
        });
        it('should handle standard policy updates with persistence', async () => {
            const mockConfig = {
                isTrustedFolder: vi.fn().mockReturnValue(false),
                getWorkspacePoliciesDir: vi.fn().mockReturnValue(undefined),
                setApprovalMode: vi.fn(),
            };
            mockConfig.config =
                mockConfig;
            const mockMessageBus = {
                publish: vi.fn(),
            };
            mockConfig.messageBus =
                mockMessageBus;
            const tool = { name: 'test-tool' };
            await updatePolicy(tool, ToolConfirmationOutcome.ProceedAlwaysAndSave, undefined, mockConfig, mockMessageBus);
            expect(mockMessageBus.publish).toHaveBeenCalledWith(expect.objectContaining({
                type: MessageBusType.UPDATE_POLICY,
                toolName: 'test-tool',
                persist: true,
            }));
        });
        it('should handle shell command prefixes', async () => {
            const mockConfig = {
                setApprovalMode: vi.fn(),
            };
            mockConfig.config =
                mockConfig;
            const mockMessageBus = {
                publish: vi.fn(),
            };
            mockConfig.messageBus =
                mockMessageBus;
            const tool = { name: 'run_shell_command' };
            const details = {
                type: 'exec',
                command: 'ls -la',
                rootCommand: 'ls',
                rootCommands: ['ls'],
                title: 'Shell',
                onConfirm: vi.fn(),
            };
            await updatePolicy(tool, ToolConfirmationOutcome.ProceedAlways, details, mockConfig, mockMessageBus);
            expect(mockMessageBus.publish).toHaveBeenCalledWith(expect.objectContaining({
                type: MessageBusType.UPDATE_POLICY,
                toolName: 'run_shell_command',
                commandPrefix: ['ls'],
            }));
        });
        it('should handle MCP policy updates (server scope)', async () => {
            const mockConfig = {
                setApprovalMode: vi.fn(),
            };
            mockConfig.config =
                mockConfig;
            const mockMessageBus = {
                publish: vi.fn(),
            };
            mockConfig.messageBus =
                mockMessageBus;
            const tool = { name: 'mcp-tool' };
            const details = {
                type: 'mcp',
                serverName: 'my-server',
                toolName: 'mcp-tool',
                toolDisplayName: 'My Tool',
                title: 'MCP',
                onConfirm: vi.fn(),
            };
            await updatePolicy(tool, ToolConfirmationOutcome.ProceedAlwaysServer, details, mockConfig, mockMessageBus);
            expect(mockMessageBus.publish).toHaveBeenCalledWith(expect.objectContaining({
                type: MessageBusType.UPDATE_POLICY,
                toolName: 'mcp_my-server_*',
                mcpName: 'my-server',
                persist: false,
            }));
        });
        it('should NOT publish update for ProceedOnce', async () => {
            const mockConfig = {
                setApprovalMode: vi.fn(),
            };
            mockConfig.config =
                mockConfig;
            const mockMessageBus = {
                publish: vi.fn(),
            };
            mockConfig.messageBus =
                mockMessageBus;
            const tool = { name: 'test-tool' };
            await updatePolicy(tool, ToolConfirmationOutcome.ProceedOnce, undefined, mockConfig, mockMessageBus);
            expect(mockMessageBus.publish).not.toHaveBeenCalled();
            expect(mockConfig.setApprovalMode).not.toHaveBeenCalled();
        });
        it('should NOT publish update for Cancel', async () => {
            const mockConfig = {
                setApprovalMode: vi.fn(),
            };
            mockConfig.config =
                mockConfig;
            const mockMessageBus = {
                publish: vi.fn(),
            };
            mockConfig.messageBus =
                mockMessageBus;
            const tool = { name: 'test-tool' };
            await updatePolicy(tool, ToolConfirmationOutcome.Cancel, undefined, mockConfig, mockMessageBus);
            expect(mockMessageBus.publish).not.toHaveBeenCalled();
        });
        it('should NOT publish update for ModifyWithEditor', async () => {
            const mockConfig = {
                setApprovalMode: vi.fn(),
            };
            mockConfig.config =
                mockConfig;
            const mockMessageBus = {
                publish: vi.fn(),
            };
            mockConfig.messageBus =
                mockMessageBus;
            const tool = { name: 'test-tool' };
            await updatePolicy(tool, ToolConfirmationOutcome.ModifyWithEditor, undefined, mockConfig, mockMessageBus);
            expect(mockMessageBus.publish).not.toHaveBeenCalled();
        });
        it('should handle MCP ProceedAlwaysTool (specific tool name)', async () => {
            const mockConfig = {
                setApprovalMode: vi.fn(),
            };
            mockConfig.config =
                mockConfig;
            const mockMessageBus = {
                publish: vi.fn(),
            };
            mockConfig.messageBus =
                mockMessageBus;
            const tool = { name: 'mcp-tool' };
            const details = {
                type: 'mcp',
                serverName: 'my-server',
                toolName: 'mcp-tool',
                toolDisplayName: 'My Tool',
                title: 'MCP',
                onConfirm: vi.fn(),
            };
            await updatePolicy(tool, ToolConfirmationOutcome.ProceedAlwaysTool, details, mockConfig, mockMessageBus);
            expect(mockMessageBus.publish).toHaveBeenCalledWith(expect.objectContaining({
                type: MessageBusType.UPDATE_POLICY,
                toolName: 'mcp-tool', // Specific name, not wildcard
                mcpName: 'my-server',
                persist: false,
            }));
        });
        it('should handle MCP ProceedAlways (persist: false)', async () => {
            const mockConfig = {
                setApprovalMode: vi.fn(),
            };
            mockConfig.config =
                mockConfig;
            const mockMessageBus = {
                publish: vi.fn(),
            };
            mockConfig.messageBus =
                mockMessageBus;
            const tool = { name: 'mcp-tool' };
            const details = {
                type: 'mcp',
                serverName: 'my-server',
                toolName: 'mcp-tool',
                toolDisplayName: 'My Tool',
                title: 'MCP',
                onConfirm: vi.fn(),
            };
            await updatePolicy(tool, ToolConfirmationOutcome.ProceedAlways, details, mockConfig, mockMessageBus);
            expect(mockMessageBus.publish).toHaveBeenCalledWith(expect.objectContaining({
                type: MessageBusType.UPDATE_POLICY,
                toolName: 'mcp-tool',
                mcpName: 'my-server',
                persist: false,
            }));
        });
        it('should handle MCP ProceedAlwaysAndSave (persist: true)', async () => {
            const mockConfig = {
                isTrustedFolder: vi.fn().mockReturnValue(false),
                getWorkspacePoliciesDir: vi.fn().mockReturnValue(undefined),
                setApprovalMode: vi.fn(),
            };
            mockConfig.config =
                mockConfig;
            const mockMessageBus = {
                publish: vi.fn(),
            };
            mockConfig.messageBus =
                mockMessageBus;
            const tool = { name: 'mcp-tool' };
            const details = {
                type: 'mcp',
                serverName: 'my-server',
                toolName: 'mcp-tool',
                toolDisplayName: 'My Tool',
                title: 'MCP',
                onConfirm: vi.fn(),
            };
            await updatePolicy(tool, ToolConfirmationOutcome.ProceedAlwaysAndSave, details, mockConfig, mockMessageBus);
            expect(mockMessageBus.publish).toHaveBeenCalledWith(expect.objectContaining({
                type: MessageBusType.UPDATE_POLICY,
                toolName: 'mcp-tool',
                mcpName: 'my-server',
                persist: true,
            }));
        });
        it('should determine persistScope: workspace in trusted folders', async () => {
            const mockConfig = {
                isTrustedFolder: vi.fn().mockReturnValue(true),
                getWorkspacePoliciesDir: vi
                    .fn()
                    .mockReturnValue('/mock/project/policies'),
                setApprovalMode: vi.fn(),
            };
            const mockMessageBus = {
                publish: vi.fn(),
            };
            const tool = { name: 'test-tool' };
            await updatePolicy(tool, ToolConfirmationOutcome.ProceedAlwaysAndSave, undefined, {
                config: mockConfig,
            }, mockMessageBus);
            expect(mockMessageBus.publish).toHaveBeenCalledWith(expect.objectContaining({
                persistScope: 'workspace',
            }));
        });
        it('should determine persistScope: user in untrusted folders', async () => {
            const mockConfig = {
                isTrustedFolder: vi.fn().mockReturnValue(false),
                getWorkspacePoliciesDir: vi
                    .fn()
                    .mockReturnValue('/mock/project/policies'),
                setApprovalMode: vi.fn(),
            };
            const mockMessageBus = {
                publish: vi.fn(),
            };
            const tool = { name: 'test-tool' };
            await updatePolicy(tool, ToolConfirmationOutcome.ProceedAlwaysAndSave, undefined, {
                config: mockConfig,
            }, mockMessageBus);
            expect(mockMessageBus.publish).toHaveBeenCalledWith(expect.objectContaining({
                persistScope: 'user',
            }));
        });
        it('should narrow edit tools with argsPattern', async () => {
            const mockConfig = {
                isTrustedFolder: vi.fn().mockReturnValue(false),
                getWorkspacePoliciesDir: vi.fn().mockReturnValue(undefined),
                getTargetDir: vi.fn().mockReturnValue('/mock/dir'),
                setApprovalMode: vi.fn(),
            };
            const mockMessageBus = {
                publish: vi.fn(),
            };
            const tool = { name: 'write_file' };
            const details = {
                type: 'edit',
                title: 'Edit',
                filePath: 'src/foo.ts',
                fileName: 'foo.ts',
                fileDiff: '--- foo.ts\n+++ foo.ts\n@@ -1 +1 @@\n-old\n+new',
                originalContent: 'old',
                newContent: 'new',
            };
            await updatePolicy(tool, ToolConfirmationOutcome.ProceedAlwaysAndSave, details, {
                config: mockConfig,
            }, mockMessageBus);
            expect(mockMessageBus.publish).toHaveBeenCalledWith(expect.objectContaining({
                toolName: 'write_file',
                argsPattern: '\\\\0' + escapeRegex('"file_path":"src/foo.ts"') + '\\\\0',
            }));
        });
        it('should work when context is created via Object.create (prototype chain)', async () => {
            const mockConfig = {
                setApprovalMode: vi.fn(),
            };
            const mockMessageBus = {
                publish: vi.fn(),
            };
            const baseContext = {
                config: mockConfig,
                messageBus: mockMessageBus,
            };
            const protoContext = Object.create(baseContext);
            expect(Object.keys(protoContext)).toHaveLength(0);
            expect(protoContext.config).toBe(mockConfig);
            expect(protoContext.messageBus).toBe(mockMessageBus);
            const tool = { name: 'test-tool' };
            await updatePolicy(tool, ToolConfirmationOutcome.ProceedAlways, undefined, protoContext, mockMessageBus);
            expect(mockMessageBus.publish).toHaveBeenCalledWith(expect.objectContaining({
                type: MessageBusType.UPDATE_POLICY,
                toolName: 'test-tool',
                persist: false,
            }));
        });
    });
    describe('getPolicyDenialError', () => {
        it('should return default denial message when no rule provided', () => {
            const mockConfig = {
                getApprovalMode: vi.fn().mockReturnValue(ApprovalMode.DEFAULT),
            };
            mockConfig.config = mockConfig;
            const { errorMessage, errorType } = getPolicyDenialError(mockConfig);
            expect(errorMessage).toBe('Tool execution denied by policy.');
            expect(errorType).toBe(ToolErrorType.POLICY_VIOLATION);
        });
        it('should return custom deny message if provided', () => {
            const mockConfig = {
                getApprovalMode: vi.fn().mockReturnValue(ApprovalMode.DEFAULT),
            };
            mockConfig.config = mockConfig;
            const rule = {
                toolName: '*',
                decision: PolicyDecision.DENY,
                denyMessage: 'Custom Deny',
            };
            const { errorMessage, errorType } = getPolicyDenialError(mockConfig, rule);
            expect(errorMessage).toBe('Tool execution denied by policy. Custom Deny');
            expect(errorType).toBe(ToolErrorType.POLICY_VIOLATION);
        });
    });
});
describe('Plan Mode Denial Consistency', () => {
    let mockConfig;
    let mockMessageBus;
    let mockPolicyEngine;
    let mockToolRegistry;
    let mockTool;
    let mockInvocation;
    const req = {
        callId: 'call-1',
        name: 'test-tool',
        args: { foo: 'bar' },
        isClientInitiated: false,
        prompt_id: 'prompt-1',
        schedulerId: ROOT_SCHEDULER_ID,
    };
    beforeEach(() => {
        mockTool = {
            name: 'test-tool',
            build: vi.fn(),
        };
        mockInvocation = {
            shouldConfirmExecute: vi.fn(),
        };
        vi.mocked(mockTool.build).mockReturnValue(mockInvocation);
        mockPolicyEngine = {
            check: vi.fn().mockResolvedValue({ decision: PolicyDecision.DENY }), // Default to DENY for this test
        };
        mockToolRegistry = {
            getTool: vi.fn().mockReturnValue(mockTool),
            getAllToolNames: vi.fn().mockReturnValue(['test-tool']),
        };
        mockMessageBus = {
            publish: vi.fn(),
            subscribe: vi.fn(),
        };
        mockConfig = {
            getPolicyEngine: vi.fn().mockReturnValue(mockPolicyEngine),
            toolRegistry: mockToolRegistry,
            getToolRegistry: () => mockToolRegistry,
            getMessageBus: vi.fn().mockReturnValue(mockMessageBus),
            getHookSystem: vi.fn().mockReturnValue(undefined),
            isInteractive: vi.fn().mockReturnValue(true),
            getEnableHooks: vi.fn().mockReturnValue(false),
            getApprovalMode: vi.fn().mockReturnValue(ApprovalMode.PLAN), // Key: Plan Mode
            getTelemetryLogPromptsEnabled: vi.fn().mockReturnValue(false),
            setApprovalMode: vi.fn(),
            getUsageStatisticsEnabled: vi.fn().mockReturnValue(false),
        };
        mockConfig.config = mockConfig;
        mockConfig.messageBus =
            mockMessageBus;
    });
    afterEach(() => {
        vi.clearAllMocks();
    });
    it('should return the correct Plan Mode denial message when policy denies execution', async () => {
        let resultMessage;
        let resultErrorType;
        const signal = new AbortController().signal;
        const scheduler = new Scheduler({
            context: {
                config: mockConfig,
                messageBus: mockMessageBus,
                toolRegistry: mockToolRegistry,
            },
            getPreferredEditor: () => undefined,
            schedulerId: ROOT_SCHEDULER_ID,
        });
        const results = await scheduler.schedule(req, signal);
        const result = results[0];
        expect(result.status).toBe('error');
        if (result.status === 'error') {
            resultMessage = result.response.error?.message;
            resultErrorType = result.response.errorType;
        }
        expect(resultMessage).toBe('Tool execution denied by policy.');
        expect(resultErrorType).toBe(ToolErrorType.POLICY_VIOLATION);
    });
});
//# sourceMappingURL=policy.test.js.map