/**
 * @license
 * Copyright 2025 Google LLC
 * SPDX-License-Identifier: Apache-2.0
 */
import { describe, it, expect, vi } from 'vitest';
import { ApprovalMode, PolicyDecision, PolicyEngine, } from '@google/gemini-cli-core';
import { createPolicyEngineConfig } from './policy.js';
// Mock Storage to ensure tests are hermetic and don't read from user's home directory
vi.mock('@google/gemini-cli-core', async (importOriginal) => {
    const actual = await importOriginal();
    const Storage = actual.Storage;
    // Monkey-patch static methods
    Storage.getUserPoliciesDir = () => '/non-existent/user/policies';
    Storage.getSystemPoliciesDir = () => '/non-existent/system/policies';
    return {
        ...actual,
        Storage,
    };
});
describe('Policy Engine Integration Tests', () => {
    describe('Policy configuration produces valid PolicyEngine config', () => {
        it('should create a working PolicyEngine from basic settings', async () => {
            const settings = {
                tools: {
                    allowed: ['run_shell_command'],
                    exclude: ['write_file'],
                },
            };
            const config = await createPolicyEngineConfig(settings, ApprovalMode.DEFAULT);
            const engine = new PolicyEngine(config);
            // Allowed tool should be allowed
            expect((await engine.check({ name: 'run_shell_command' }, undefined)).decision).toBe(PolicyDecision.ALLOW);
            // Excluded tool should be denied
            expect((await engine.check({ name: 'write_file' }, undefined)).decision).toBe(PolicyDecision.DENY);
            // Other write tools should ask user
            expect((await engine.check({ name: 'replace' }, undefined)).decision).toBe(PolicyDecision.ASK_USER);
            // Unknown tools should use default
            expect((await engine.check({ name: 'unknown_tool' }, undefined)).decision).toBe(PolicyDecision.ASK_USER);
        });
        it('should handle MCP server wildcard patterns correctly', async () => {
            const settings = {
                mcp: {
                    allowed: ['allowed-server'],
                    excluded: ['blocked-server'],
                },
                mcpServers: {
                    'trusted-server': {
                        command: 'node',
                        args: ['server.js'],
                        trust: true,
                    },
                },
            };
            const config = await createPolicyEngineConfig(settings, ApprovalMode.DEFAULT);
            const engine = new PolicyEngine(config);
            // Tools from allowed server should be allowed
            // Tools from allowed server should be allowed
            expect((await engine.check({ name: 'mcp_allowed-server_tool1' }, undefined))
                .decision).toBe(PolicyDecision.ALLOW);
            expect((await engine.check({ name: 'mcp_allowed-server_another_tool' }, undefined)).decision).toBe(PolicyDecision.ALLOW);
            // Tools from trusted server should be allowed
            expect((await engine.check({ name: 'mcp_trusted-server_tool1' }, undefined))
                .decision).toBe(PolicyDecision.ALLOW);
            expect((await engine.check({ name: 'mcp_trusted-server_special_tool' }, undefined)).decision).toBe(PolicyDecision.ALLOW);
            // Tools from blocked server should be denied
            expect((await engine.check({ name: 'mcp_blocked-server_tool1' }, undefined))
                .decision).toBe(PolicyDecision.DENY);
            expect((await engine.check({ name: 'mcp_blocked-server_any_tool' }, undefined))
                .decision).toBe(PolicyDecision.DENY);
            // Tools from unknown servers should use default
            expect((await engine.check({ name: 'mcp_unknown-server_tool' }, undefined))
                .decision).toBe(PolicyDecision.ASK_USER);
        });
        it('should handle global MCP wildcard (*) in settings', async () => {
            const settings = {
                mcp: {
                    allowed: ['*'],
                },
            };
            const config = await createPolicyEngineConfig(settings, ApprovalMode.DEFAULT);
            const engine = new PolicyEngine(config);
            // ANY tool with a server name should be allowed
            expect((await engine.check({ name: 'mcp_mcp-server_tool' }, 'mcp-server'))
                .decision).toBe(PolicyDecision.ALLOW);
            expect((await engine.check({ name: 'mcp_another-server_tool' }, 'another-server')).decision).toBe(PolicyDecision.ALLOW);
            // Built-in tools should NOT be allowed by the MCP wildcard
            expect((await engine.check({ name: 'run_shell_command' }, undefined)).decision).toBe(PolicyDecision.ASK_USER);
        });
        it('should correctly prioritize specific tool excludes over MCP server wildcards', async () => {
            const settings = {
                mcp: {
                    allowed: ['my-server'],
                },
                tools: {
                    exclude: ['mcp_my-server_dangerous-tool'],
                },
            };
            const config = await createPolicyEngineConfig(settings, ApprovalMode.DEFAULT);
            const engine = new PolicyEngine(config);
            // MCP server allowed (priority 4.1) provides general allow for server
            // MCP server allowed (priority 4.1) provides general allow for server
            expect((await engine.check({ name: 'mcp_my-server_safe-tool' }, undefined))
                .decision).toBe(PolicyDecision.ALLOW);
            // But specific tool exclude (priority 4.4) wins over server allow
            expect((await engine.check({ name: 'mcp_my-server_dangerous-tool' }, undefined)).decision).toBe(PolicyDecision.DENY);
        });
        it('should handle complex mixed configurations', async () => {
            const settings = {
                tools: {
                    allowed: ['custom-tool', 'mcp_my-server_special-tool'],
                    exclude: ['glob', 'dangerous-tool'],
                },
                mcp: {
                    allowed: ['allowed-server'],
                    excluded: ['blocked-server'],
                },
                mcpServers: {
                    'trusted-server': {
                        command: 'node',
                        args: ['server.js'],
                        trust: true,
                    },
                },
            };
            const config = await createPolicyEngineConfig(settings, ApprovalMode.DEFAULT);
            const engine = new PolicyEngine(config);
            // Read-only tools should be allowed (autoAccept)
            expect((await engine.check({ name: 'read_file' }, undefined)).decision).toBe(PolicyDecision.ALLOW);
            expect((await engine.check({ name: 'list_directory' }, undefined)).decision).toBe(PolicyDecision.ALLOW);
            // But glob is explicitly excluded, so it should be denied
            expect((await engine.check({ name: 'glob' }, undefined)).decision).toBe(PolicyDecision.DENY);
            // Replace should ask user (normal write tool behavior)
            expect((await engine.check({ name: 'replace' }, undefined)).decision).toBe(PolicyDecision.ASK_USER);
            // Explicitly allowed tools
            expect((await engine.check({ name: 'custom-tool' }, undefined)).decision).toBe(PolicyDecision.ALLOW);
            expect((await engine.check({ name: 'mcp_my-server_special-tool' }, undefined))
                .decision).toBe(PolicyDecision.ALLOW);
            // MCP server tools
            expect((await engine.check({ name: 'mcp_allowed-server_tool' }, undefined))
                .decision).toBe(PolicyDecision.ALLOW);
            expect((await engine.check({ name: 'mcp_trusted-server_tool' }, undefined))
                .decision).toBe(PolicyDecision.ALLOW);
            expect((await engine.check({ name: 'mcp_blocked-server_tool' }, undefined))
                .decision).toBe(PolicyDecision.DENY);
            // Write tools should ask by default
            expect((await engine.check({ name: 'write_file' }, undefined)).decision).toBe(PolicyDecision.ASK_USER);
        });
        it('should handle YOLO mode correctly', async () => {
            const settings = {
                tools: {
                    exclude: ['dangerous-tool'], // Even in YOLO, excludes should be respected
                },
            };
            const config = await createPolicyEngineConfig(settings, ApprovalMode.YOLO);
            const engine = new PolicyEngine(config);
            // Most tools should be allowed in YOLO mode
            expect((await engine.check({ name: 'run_shell_command' }, undefined)).decision).toBe(PolicyDecision.ALLOW);
            expect((await engine.check({ name: 'write_file' }, undefined)).decision).toBe(PolicyDecision.ALLOW);
            expect((await engine.check({ name: 'unknown_tool' }, undefined)).decision).toBe(PolicyDecision.ALLOW);
            // But explicitly excluded tools should still be denied
            expect((await engine.check({ name: 'dangerous-tool' }, undefined)).decision).toBe(PolicyDecision.DENY);
        });
        it('should handle AUTO_EDIT mode correctly', async () => {
            const settings = {};
            const config = await createPolicyEngineConfig(settings, ApprovalMode.AUTO_EDIT);
            const engine = new PolicyEngine(config);
            // Edit tools should be allowed in AUTO_EDIT mode
            expect((await engine.check({ name: 'replace' }, undefined)).decision).toBe(PolicyDecision.ALLOW);
            expect((await engine.check({ name: 'write_file' }, undefined)).decision).toBe(PolicyDecision.ALLOW);
            // Other tools should follow normal rules
            expect((await engine.check({ name: 'run_shell_command' }, undefined)).decision).toBe(PolicyDecision.ASK_USER);
        });
        it('should handle Plan mode correctly', async () => {
            const settings = {};
            const config = await createPolicyEngineConfig(settings, ApprovalMode.PLAN);
            const engine = new PolicyEngine(config);
            // Read and search tools should be allowed
            expect((await engine.check({ name: 'read_file' }, undefined)).decision).toBe(PolicyDecision.ALLOW);
            expect((await engine.check({ name: 'google_web_search' }, undefined)).decision).toBe(PolicyDecision.ALLOW);
            expect((await engine.check({ name: 'list_directory' }, undefined)).decision).toBe(PolicyDecision.ALLOW);
            // Other tools should be denied via catch all
            expect((await engine.check({ name: 'replace' }, undefined)).decision).toBe(PolicyDecision.DENY);
            expect((await engine.check({ name: 'write_file' }, undefined)).decision).toBe(PolicyDecision.DENY);
            expect((await engine.check({ name: 'run_shell_command' }, undefined)).decision).toBe(PolicyDecision.DENY);
            // Unknown tools should be denied via catch-all
            expect((await engine.check({ name: 'unknown_tool' }, undefined)).decision).toBe(PolicyDecision.DENY);
        });
        it('should correctly match tool annotations', async () => {
            const settings = {};
            const config = await createPolicyEngineConfig(settings, ApprovalMode.DEFAULT);
            // Add a manual rule with annotations to the config
            config.rules = config.rules || [];
            config.rules.push({
                toolAnnotations: { readOnlyHint: true },
                decision: PolicyDecision.ALLOW,
                priority: 10,
            });
            const engine = new PolicyEngine(config);
            // A tool with readOnlyHint=true should be ALLOWED
            const roCall = { name: 'some_tool', args: {} };
            const roMeta = { readOnlyHint: true };
            expect((await engine.check(roCall, undefined, roMeta)).decision).toBe(PolicyDecision.ALLOW);
            // A tool without the hint (or with false) should follow default decision (ASK_USER)
            const rwMeta = { readOnlyHint: false };
            expect((await engine.check(roCall, undefined, rwMeta)).decision).toBe(PolicyDecision.ASK_USER);
        });
        describe.each(['write_file', 'replace'])('Plan Mode policy for %s', (toolName) => {
            it(`should allow ${toolName} to plans directory`, async () => {
                const settings = {};
                const config = await createPolicyEngineConfig(settings, ApprovalMode.PLAN);
                const engine = new PolicyEngine(config);
                // Valid plan file paths
                const validPaths = [
                    '/home/user/.gemini/tmp/a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2/session-1/plans/my-plan.md',
                    '/home/user/.gemini/tmp/a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2/session-1/plans/feature_auth.md',
                    '/home/user/.gemini/tmp/new-temp_dir_123/session-1/plans/plan.md', // new style of temp directory
                    'C:\\Users\\user\\.gemini\\tmp\\project-id\\session-id\\plans\\plan.md',
                    'D:\\gemini-cli\\.gemini\\tmp\\project-id\\session-1\\plans\\plan.md', // no session ID
                ];
                for (const file_path of validPaths) {
                    expect((await engine.check({ name: toolName, args: { file_path } }, undefined)).decision).toBe(PolicyDecision.ALLOW);
                }
            });
            it(`should deny ${toolName} outside plans directory`, async () => {
                const settings = {};
                const config = await createPolicyEngineConfig(settings, ApprovalMode.PLAN);
                const engine = new PolicyEngine(config);
                const invalidPaths = [
                    '/project/src/file.ts', // Workspace
                    '/home/user/.gemini/tmp/a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2/plans/script.js', // Wrong extension
                    '/home/user/.gemini/tmp/a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2/plans/../../../etc/passwd.md', // Path traversal (Unix)
                    'C:\\Users\\user\\.gemini\\tmp\\id\\session\\plans\\..\\..\\..\\Windows\\System32\\config\\SAM', // Path traversal (Windows)
                    '/home/user/.gemini/non-tmp/new-temp_dir_123/plans/plan.md', // outside of temp dir
                ];
                for (const file_path of invalidPaths) {
                    expect((await engine.check({ name: toolName, args: { file_path } }, undefined)).decision).toBe(PolicyDecision.DENY);
                }
            });
        });
        it('should verify priority ordering works correctly in practice', async () => {
            const settings = {
                tools: {
                    allowed: ['specific-tool'], // Priority 100
                    exclude: ['blocked-tool'], // Priority 200
                },
                mcp: {
                    allowed: ['mcp-server'], // Priority 85
                    excluded: ['blocked-server'], // Priority 195
                },
                mcpServers: {
                    'trusted-server': {
                        command: 'node',
                        args: ['server.js'],
                        trust: true, // Priority 90
                    },
                },
            };
            const config = await createPolicyEngineConfig(settings, ApprovalMode.DEFAULT);
            const engine = new PolicyEngine(config);
            // Test that priorities are applied correctly
            const rules = config.rules || [];
            // Find rules and verify their priorities
            const blockedToolRule = rules.find((r) => r.toolName === 'blocked-tool');
            expect(blockedToolRule?.priority).toBe(4.4); // Command line exclude
            const blockedServerRule = rules.find((r) => r.toolName === 'mcp_blocked-server_*');
            expect(blockedServerRule?.priority).toBe(4.9); // MCP server exclude
            const specificToolRule = rules.find((r) => r.toolName === 'specific-tool');
            expect(specificToolRule?.priority).toBe(4.3); // Command line allow
            const trustedServerRule = rules.find((r) => r.toolName === 'mcp_trusted-server_*');
            expect(trustedServerRule?.priority).toBe(4.2); // MCP trusted server
            const mcpServerRule = rules.find((r) => r.toolName === 'mcp_mcp-server_*');
            expect(mcpServerRule?.priority).toBe(4.1); // MCP allowed server
            const readOnlyToolRule = rules.find((r) => r.toolName === 'glob');
            // Priority 70 in default tier → 1.07 (Overriding Plan Mode Deny)
            expect(readOnlyToolRule?.priority).toBeCloseTo(1.07, 5);
            // Verify the engine applies these priorities correctly
            expect((await engine.check({ name: 'blocked-tool' }, undefined)).decision).toBe(PolicyDecision.DENY);
            expect((await engine.check({ name: 'mcp_blocked-server_any' }, undefined))
                .decision).toBe(PolicyDecision.DENY);
            expect((await engine.check({ name: 'specific-tool' }, undefined)).decision).toBe(PolicyDecision.ALLOW);
            expect((await engine.check({ name: 'mcp_trusted-server_any' }, undefined))
                .decision).toBe(PolicyDecision.ALLOW);
            expect((await engine.check({ name: 'mcp_mcp-server_any' }, undefined))
                .decision).toBe(PolicyDecision.ALLOW);
            expect((await engine.check({ name: 'glob' }, undefined)).decision).toBe(PolicyDecision.ALLOW);
        });
        it('should handle edge case: MCP server with both trust and exclusion', async () => {
            const settings = {
                mcpServers: {
                    'conflicted-server': {
                        command: 'node',
                        args: ['server.js'],
                        trust: true, // Priority 90 - ALLOW
                    },
                },
                mcp: {
                    excluded: ['conflicted-server'], // Priority 195 - DENY
                },
            };
            const config = await createPolicyEngineConfig(settings, ApprovalMode.DEFAULT);
            const engine = new PolicyEngine(config);
            // Exclusion (195) should win over trust (90)
            expect((await engine.check({ name: 'mcp_conflicted-server_tool' }, undefined))
                .decision).toBe(PolicyDecision.DENY);
        });
        it('should handle edge case: specific tool allowed but server excluded', async () => {
            const settings = {
                mcp: {
                    excluded: ['my-server'], // Priority 195 - DENY
                },
                tools: {
                    allowed: ['mcp_my-server_special-tool'], // Priority 100 - ALLOW
                },
            };
            const config = await createPolicyEngineConfig(settings, ApprovalMode.DEFAULT);
            const engine = new PolicyEngine(config);
            // Server exclusion (195) wins over specific tool allow (100)
            // This might be counterintuitive but follows the priority system
            expect((await engine.check({ name: 'mcp_my-server_special-tool' }, undefined))
                .decision).toBe(PolicyDecision.DENY);
            expect((await engine.check({ name: 'mcp_my-server_other-tool' }, undefined))
                .decision).toBe(PolicyDecision.DENY);
        });
        it('should verify non-interactive mode transformation', async () => {
            const settings = {};
            const config = await createPolicyEngineConfig(settings, ApprovalMode.DEFAULT);
            // Enable non-interactive mode
            const engineConfig = { ...config, nonInteractive: true };
            const engine = new PolicyEngine(engineConfig);
            // ASK_USER should become DENY in non-interactive mode
            expect((await engine.check({ name: 'unknown_tool' }, undefined)).decision).toBe(PolicyDecision.DENY);
            expect((await engine.check({ name: 'run_shell_command' }, undefined)).decision).toBe(PolicyDecision.DENY);
        });
        it('should handle empty settings gracefully', async () => {
            const settings = {};
            const config = await createPolicyEngineConfig(settings, ApprovalMode.DEFAULT);
            const engine = new PolicyEngine(config);
            // Should have default rules for write tools
            expect((await engine.check({ name: 'write_file' }, undefined)).decision).toBe(PolicyDecision.ASK_USER);
            expect((await engine.check({ name: 'replace' }, undefined)).decision).toBe(PolicyDecision.ASK_USER);
            // Unknown tools should use default
            expect((await engine.check({ name: 'unknown' }, undefined)).decision).toBe(PolicyDecision.ASK_USER);
        });
        it('should verify rules are created with correct priorities', async () => {
            const settings = {
                tools: {
                    allowed: ['tool1', 'tool2'],
                    exclude: ['tool3'],
                },
                mcp: {
                    allowed: ['server1'],
                    excluded: ['server2'],
                },
            };
            const config = await createPolicyEngineConfig(settings, ApprovalMode.DEFAULT);
            const rules = config.rules || [];
            // Verify each rule has the expected priority
            const tool3Rule = rules.find((r) => r.toolName === 'tool3');
            expect(tool3Rule?.priority).toBe(4.4); // Excluded tools (user tier)
            const server2Rule = rules.find((r) => r.toolName === 'mcp_server2_*');
            expect(server2Rule?.priority).toBe(4.9); // Excluded servers (user tier)
            const tool1Rule = rules.find((r) => r.toolName === 'tool1');
            expect(tool1Rule?.priority).toBe(4.3); // Allowed tools (user tier)
            const server1Rule = rules.find((r) => r.toolName === 'mcp_server1_*');
            expect(server1Rule?.priority).toBe(4.1); // Allowed servers (user tier)
            const globRule = rules.find((r) => r.toolName === 'glob');
            // Priority 70 in default tier → 1.07
            expect(globRule?.priority).toBeCloseTo(1.07, 5); // Auto-accept read-only
            // The PolicyEngine will sort these by priority when it's created
            const engine = new PolicyEngine(config);
            const sortedRules = engine.getRules();
            // Verify the engine sorted them correctly
            for (let i = 1; i < sortedRules.length; i++) {
                const prevPriority = sortedRules[i - 1].priority ?? 0;
                const currPriority = sortedRules[i].priority ?? 0;
                expect(prevPriority).toBeGreaterThanOrEqual(currPriority);
            }
        });
    });
});
//# sourceMappingURL=policy-engine.integration.test.js.map