/**
 * @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 { ToolConfirmationOutcome, } from '../tools/tools.js';
import { DiscoveredMCPTool } from '../tools/mcp-tool.js';
import { CoreToolScheduler } from '../core/coreToolScheduler.js';
import { Scheduler } from './scheduler.js';
import { ROOT_SCHEDULER_ID } from './types.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),
            };
            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);
        });
        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),
            };
            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 });
        });
        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),
            };
            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),
            };
            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),
            };
            const toolCall = {
                request: { name: 'test-tool', args: {} },
                tool: { name: 'test-tool' },
            };
            const result = await checkPolicy(toolCall, mockConfig);
            expect(result.decision).toBe(PolicyDecision.ASK_USER);
        });
    });
    describe('updatePolicy', () => {
        it('should set AUTO_EDIT mode for auto-edit transition tools', async () => {
            const mockConfig = {
                setApprovalMode: vi.fn(),
            };
            const mockMessageBus = {
                publish: vi.fn(),
            };
            const tool = { name: 'replace' }; // 'replace' is in EDIT_TOOL_NAMES
            await updatePolicy(tool, ToolConfirmationOutcome.ProceedAlways, undefined, { config: mockConfig, messageBus: 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(),
            };
            const mockMessageBus = {
                publish: vi.fn(),
            };
            const tool = { name: 'test-tool' };
            await updatePolicy(tool, ToolConfirmationOutcome.ProceedAlways, undefined, { config: mockConfig, messageBus: 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 = {
                setApprovalMode: vi.fn(),
            };
            const mockMessageBus = {
                publish: vi.fn(),
            };
            const tool = { name: 'test-tool' };
            await updatePolicy(tool, ToolConfirmationOutcome.ProceedAlwaysAndSave, undefined, { config: mockConfig, messageBus: 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(),
            };
            const mockMessageBus = {
                publish: vi.fn(),
            };
            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, {
                config: mockConfig,
                messageBus: 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(),
            };
            const mockMessageBus = {
                publish: vi.fn(),
            };
            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, { config: mockConfig, messageBus: mockMessageBus });
            expect(mockMessageBus.publish).toHaveBeenCalledWith(expect.objectContaining({
                type: MessageBusType.UPDATE_POLICY,
                toolName: 'my-server__*',
                mcpName: 'my-server',
                persist: false,
            }));
        });
        it('should NOT publish update for ProceedOnce', async () => {
            const mockConfig = {
                setApprovalMode: vi.fn(),
            };
            const mockMessageBus = {
                publish: vi.fn(),
            };
            const tool = { name: 'test-tool' };
            await updatePolicy(tool, ToolConfirmationOutcome.ProceedOnce, undefined, {
                config: mockConfig,
                messageBus: mockMessageBus,
            });
            expect(mockMessageBus.publish).not.toHaveBeenCalled();
            expect(mockConfig.setApprovalMode).not.toHaveBeenCalled();
        });
        it('should NOT publish update for Cancel', async () => {
            const mockConfig = {
                setApprovalMode: vi.fn(),
            };
            const mockMessageBus = {
                publish: vi.fn(),
            };
            const tool = { name: 'test-tool' };
            await updatePolicy(tool, ToolConfirmationOutcome.Cancel, undefined, {
                config: mockConfig,
                messageBus: mockMessageBus,
            });
            expect(mockMessageBus.publish).not.toHaveBeenCalled();
        });
        it('should NOT publish update for ModifyWithEditor', async () => {
            const mockConfig = {
                setApprovalMode: vi.fn(),
            };
            const mockMessageBus = {
                publish: vi.fn(),
            };
            const tool = { name: 'test-tool' };
            await updatePolicy(tool, ToolConfirmationOutcome.ModifyWithEditor, undefined, { config: mockConfig, messageBus: mockMessageBus });
            expect(mockMessageBus.publish).not.toHaveBeenCalled();
        });
        it('should handle MCP ProceedAlwaysTool (specific tool name)', async () => {
            const mockConfig = {
                setApprovalMode: vi.fn(),
            };
            const mockMessageBus = {
                publish: vi.fn(),
            };
            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, { config: mockConfig, messageBus: 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(),
            };
            const mockMessageBus = {
                publish: vi.fn(),
            };
            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, {
                config: mockConfig,
                messageBus: 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 = {
                setApprovalMode: vi.fn(),
            };
            const mockMessageBus = {
                publish: vi.fn(),
            };
            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, { config: mockConfig, messageBus: mockMessageBus });
            expect(mockMessageBus.publish).toHaveBeenCalledWith(expect.objectContaining({
                type: MessageBusType.UPDATE_POLICY,
                toolName: 'mcp-tool',
                mcpName: 'my-server',
                persist: true,
            }));
        });
    });
    describe('getPolicyDenialError', () => {
        it('should return default denial message when no rule provided', () => {
            const mockConfig = {
                getApprovalMode: vi.fn().mockReturnValue(ApprovalMode.DEFAULT),
            };
            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),
            };
            const rule = {
                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),
            getToolRegistry: vi.fn().mockReturnValue(mockToolRegistry),
            getMessageBus: vi.fn().mockReturnValue(mockMessageBus),
            isInteractive: vi.fn().mockReturnValue(true),
            getEnableHooks: vi.fn().mockReturnValue(false),
            getApprovalMode: vi.fn().mockReturnValue(ApprovalMode.PLAN), // Key: Plan Mode
            setApprovalMode: vi.fn(),
            getUsageStatisticsEnabled: vi.fn().mockReturnValue(false),
        };
    });
    afterEach(() => {
        vi.clearAllMocks();
    });
    describe.each([
        { enableEventDrivenScheduler: false, name: 'Legacy CoreToolScheduler' },
        { enableEventDrivenScheduler: true, name: 'Event-Driven Scheduler' },
    ])('$name', ({ enableEventDrivenScheduler }) => {
        it('should return the correct Plan Mode denial message when policy denies execution', async () => {
            let resultMessage;
            let resultErrorType;
            const signal = new AbortController().signal;
            if (enableEventDrivenScheduler) {
                const scheduler = new Scheduler({
                    config: mockConfig,
                    messageBus: mockMessageBus,
                    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;
                }
            }
            else {
                let capturedCalls = [];
                const scheduler = new CoreToolScheduler({
                    config: mockConfig,
                    getPreferredEditor: () => undefined,
                    onAllToolCallsComplete: async (calls) => {
                        capturedCalls = calls;
                    },
                });
                await scheduler.schedule(req, signal);
                expect(capturedCalls.length).toBeGreaterThan(0);
                const call = capturedCalls[0];
                if (call.status === 'error') {
                    resultMessage = call.response.error?.message;
                    resultErrorType = call.response.errorType;
                }
            }
            expect(resultMessage).toBe('Tool execution denied by policy.');
            expect(resultErrorType).toBe(ToolErrorType.POLICY_VIOLATION);
        });
    });
});
//# sourceMappingURL=policy.test.js.map