import { describe, it, expect, beforeEach, vi } from 'vitest'; import { BashPermissionChecker } from '../../../src/permission/checkers/bash.js'; import type { PermissionDecision, PermissionContext } from '../../../src/permission/types.js'; // Mock fs 和 path 以避免实际文件操作 vi.mock('fs', () => ({ existsSync: vi.fn(() => false), readFileSync: vi.fn(), writeFileSync: vi.fn(), mkdirSync: vi.fn(), })); describe('BashPermissionChecker - Bash 权限检查器', () => { let checker: BashPermissionChecker; const testProjectRoot = '/test/project'; beforeEach(() => { checker = new BashPermissionChecker(testProjectRoot); vi.clearAllMocks(); }); describe('默认配置', () => { it('加载默认配置', () => { const config = checker.getConfig(); expect(config.rules).toBeDefined(); expect(config.rules.length).toBeGreaterThan(0); expect(config.default).toBe('ask'); expect(config.externalDirectory).toBe('ask'); }); it('默认规则包含安全命令', () => { const config = checker.getConfig(); // 检查一些默认允许的规则 const allowRules = config.rules.filter(r => r.action === 'allow'); const patterns = allowRules.map(r => r.pattern); expect(patterns.some(p => p.startsWith('ls'))).toBe(true); expect(patterns.some(p => p.startsWith('cat'))).toBe(true); expect(patterns.some(p => p.startsWith('git status'))).toBe(true); }); it('默认规则包含危险命令', () => { const config = checker.getConfig(); const denyRules = config.rules.filter(r => r.action === 'deny'); const patterns = denyRules.map(r => r.pattern); expect(patterns.some(p => p.includes('rm -rf'))).toBe(true); expect(patterns.some(p => p.includes('sudo'))).toBe(true); }); }); describe('安全命令检查(默认允许)', () => { const safeCommands = [ 'ls -la', 'cat file.txt', 'head -n 10 log.txt', 'tail -f server.log', 'grep pattern file.txt', 'find . -name "*.js"', 'echo hello', 'pwd', 'which node', 'git status', 'git log --oneline', 'git diff HEAD', ]; for (const command of safeCommands) { it(`允许安全命令: ${command}`, async () => { const result = await checker.check({ command, workdir: testProjectRoot, }); expect(result.allowed).toBe(true); expect(result.action).toBe('allow'); }); } }); describe('危险命令检查(默认拒绝)', () => { // 这些命令精确匹配默认拒绝规则 const denyCommands = [ 'sudo rm file', ]; for (const command of denyCommands) { it(`拒绝危险命令: ${command}`, async () => { const result = await checker.check({ command, workdir: testProjectRoot, }); expect(result.allowed).toBe(false); expect(result.action).toBe('deny'); }); } // 这些命令可能匹配 ask 规则或默认行为 const askCommands = [ 'rm -rf /', 'rm -rf /*', 'chmod 777 /', ]; for (const command of askCommands) { it(`危险命令需要确认或拒绝: ${command}`, async () => { const result = await checker.check({ command, workdir: testProjectRoot, }); // 这些命令应该不允许直接执行 expect(result.allowed).toBe(false); // 可能是 deny 或 ask(取决于具体规则匹配) expect(['deny', 'ask']).toContain(result.action); }); } }); describe('需要确认的命令', () => { const askCommands = [ 'git push origin main', 'git commit -m "test"', 'git checkout feature', 'npm install lodash', ]; for (const command of askCommands) { it(`需要确认: ${command}`, async () => { // 不设置回调,应该返回 ask const result = await checker.check({ command, workdir: testProjectRoot, }); expect(result.action).toBe('ask'); expect(result.needsConfirmation).toBe(true); }); } }); describe('回调处理', () => { it('用户允许时返回允许', async () => { const mockCallback = vi.fn().mockResolvedValue({ allow: true, remember: false, } as PermissionDecision); checker.setAskCallback(mockCallback); const result = await checker.check({ command: 'git push origin main', workdir: testProjectRoot, }); expect(result.allowed).toBe(true); expect(mockCallback).toHaveBeenCalled(); }); it('用户拒绝时返回拒绝', async () => { const mockCallback = vi.fn().mockResolvedValue({ allow: false, remember: false, } as PermissionDecision); checker.setAskCallback(mockCallback); const result = await checker.check({ command: 'git push origin main', workdir: testProjectRoot, }); expect(result.allowed).toBe(false); expect(result.action).toBe('deny'); }); it('remember=true 时记住决定', async () => { const mockCallback = vi.fn().mockResolvedValue({ allow: true, remember: true, } as PermissionDecision); checker.setAskCallback(mockCallback); // 第一次调用 await checker.check({ command: 'git push origin main', workdir: testProjectRoot, }); // 第二次调用应该不再询问 const result = await checker.check({ command: 'git commit -m "test"', workdir: testProjectRoot, }); // 记住的是整个模式,第二次可能仍需询问(取决于实现) expect(result.allowed).toBeDefined(); }); }); describe('会话权限管理', () => { it('清除会话权限', async () => { const mockCallback = vi.fn().mockResolvedValue({ allow: true, remember: true, } as PermissionDecision); checker.setAskCallback(mockCallback); // 第一次调用 await checker.check({ command: 'git push origin main', workdir: testProjectRoot, }); // 清除会话权限 checker.clearSessionPermissions(); // 再次调用应该重新询问 await checker.check({ command: 'git push origin main', workdir: testProjectRoot, }); // 应该被调用两次 expect(mockCallback).toHaveBeenCalledTimes(2); }); }); describe('规则管理', () => { it('添加新规则', () => { checker.addRule({ pattern: 'custom-cmd *', action: 'allow', }); const config = checker.getConfig(); const hasRule = config.rules.some(r => r.pattern === 'custom-cmd *'); expect(hasRule).toBe(true); }); it('更新已有规则', () => { // 添加规则 checker.addRule({ pattern: 'test-cmd', action: 'allow', }); // 更新规则 checker.addRule({ pattern: 'test-cmd', action: 'deny', }); const config = checker.getConfig(); const rule = config.rules.find(r => r.pattern === 'test-cmd'); expect(rule?.action).toBe('deny'); }); }); describe('项目目录检查', () => { it('识别项目内路径', async () => { const result = await checker.check({ command: 'cat ./src/index.ts', workdir: testProjectRoot, }); // 项目内路径应该正常检查 expect(result).toBeDefined(); }); it('识别项目外路径', async () => { // 访问项目外的绝对路径 const result = await checker.check({ command: 'cat /etc/passwd', workdir: testProjectRoot, }); // 外部路径可能需要确认或拒绝 expect(result).toBeDefined(); }); }); });