/** * PolicyEngine 单元测试(P1-14 测试基线) * 覆盖:默认策略、deniedPatterns 深度扫描、频率限制、通配符策略 */ import { describe, it, expect } from 'vitest'; import { PolicyEngine, DEFAULT_POLICIES } from '../permissions'; describe('PolicyEngine 默认策略', () => { it('所有内置工具均有策略配置', () => { const knownTools = [ 'read_file', 'write_file', 'list_directory', 'search_files', 'delete_file', 'file_move', 'file_info', 'file_editor', 'code_search', 'diff_viewer', 'web_search', 'web_fetch', 'web_browser', 'http_request', 'memory_store', 'memory_search', 'run_command', 'task_manager', 'delegate_task', 'git_status', 'git_diff', 'git_log', 'git_commit', 'lint_code', 'run_tests', 'project_info', 'think', 'view_image', ]; for (const tool of knownTools) { expect(DEFAULT_POLICIES.some((p) => p.toolName === tool)).toBe(true); } }); it('未配置策略的工具被拒绝(fail-closed)', () => { const engine = new PolicyEngine(); const result = engine.checkAuthorization('unknown_tool_xyz', {}); expect(result.authorized).toBe(false); expect(result.reason).toContain('No policy configured'); }); it('read_file 默认放行', () => { const engine = new PolicyEngine(); expect(engine.checkAuthorization('read_file', { file_path: 'a.ts' }).authorized).toBe(true); }); }); describe('deniedPatterns 深度扫描', () => { it('read_file 访问 /etc/passwd 被拒绝', () => { const engine = new PolicyEngine(); const result = engine.checkAuthorization('read_file', { file_path: '/etc/passwd' }); expect(result.authorized).toBe(false); }); it('嵌套对象中的危险路径被拒绝(deepScanStrings)', () => { const engine = new PolicyEngine(); const result = engine.checkAuthorization('read_file', { nested: { deep: { path: '/etc/passwd' } }, }); expect(result.authorized).toBe(false); }); it('run_command 参数中包含 MEMORY.md 被拒绝', () => { const engine = new PolicyEngine(); const result = engine.checkAuthorization('run_command', { command: 'cat MEMORY.md' }); expect(result.authorized).toBe(false); }); it('正常路径不误判', () => { const engine = new PolicyEngine(); expect(engine.checkAuthorization('read_file', { file_path: 'src/main.ts' }).authorized).toBe( true, ); }); }); describe('频率限制(滑动窗口)', () => { it('超过 maxFrequency 后被限流', () => { const engine = new PolicyEngine(); // web_search 默认 maxFrequency: 10 for (let i = 0; i < 10; i++) { expect(engine.checkAuthorization('web_search', { query: 'x' }).authorized).toBe(true); engine.recordCall('web_search'); } const blocked = engine.checkAuthorization('web_search', { query: 'x' }); expect(blocked.authorized).toBe(false); expect(blocked.reason).toContain('Rate limit exceeded'); }); it('限流只影响对应工具', () => { const engine = new PolicyEngine(); for (let i = 0; i < 10; i++) engine.recordCall('web_search'); // read_file 无频率限制 expect(engine.checkAuthorization('read_file', {}).authorized).toBe(true); }); }); describe('通配符策略(mcp_*)', () => { it('MCP 动态工具命中 mcp_* 通配策略', () => { const engine = new PolicyEngine(); const result = engine.checkAuthorization('mcp_filesystem_read_file', {}); // mcp_* 策略:EXTERNAL_ACTION + requireConfirmation expect(result.authorized).toBe(true); expect(result.requiresConfirmation).toBe(true); }); }); describe('频率限制会话隔离(v0.4.1)', () => { it('不同会话各自独立配额(一个会话耗尽不影响另一个)', () => { const engine = new PolicyEngine(); // web_search 默认 maxFrequency: 10 // 会话 A 耗尽全部配额 for (let i = 0; i < 10; i++) { expect(engine.checkAuthorization('web_search', { query: 'x' }, 'session-A').authorized).toBe( true, ); engine.recordCall('web_search', 'session-A'); } // 会话 A 已被限流 expect(engine.checkAuthorization('web_search', { query: 'x' }, 'session-A').authorized).toBe( false, ); // 会话 B 配额不受影响 expect(engine.checkAuthorization('web_search', { query: 'x' }, 'session-B').authorized).toBe( true, ); expect(engine.recordCall('web_search', 'session-B') === undefined).toBe(true); }); it('recordCall 与 checkAuthorization 使用相同的会话 key', () => { const engine = new PolicyEngine(); // 会话 A 记录 10 次 for (let i = 0; i < 10; i++) engine.recordCall('web_search', 'session-A'); // 会话 A 限流,会话 B 不限 expect(engine.checkAuthorization('web_search', {}, 'session-A').authorized).toBe(false); expect(engine.checkAuthorization('web_search', {}, 'session-B').authorized).toBe(true); }); it('无 sessionId 时计入 global 桶(向后兼容)', () => { const engine = new PolicyEngine(); // 旧式调用(无 sessionId)共享 global 桶 for (let i = 0; i < 10; i++) { engine.recordCall('web_search'); } expect(engine.checkAuthorization('web_search', {}).authorized).toBe(false); expect(engine.checkAuthorization('web_search', {}, 'any-session').authorized).toBe(true); }); });