import { describe, it, expect, beforeEach, vi } from 'vitest'; import { FilePermissionChecker } from '../../../src/permission/checkers/file.js'; import type { PermissionDecision, FilePermissionContext } from '../../../src/permission/types.js'; // Mock fs 以避免实际文件操作 vi.mock('fs', () => ({ existsSync: vi.fn(() => false), readFileSync: vi.fn(), writeFileSync: vi.fn(), mkdirSync: vi.fn(), })); // Mock file-prompt vi.mock('../../../src/permission/file-prompt.js', () => ({ promptFilePermission: vi.fn().mockResolvedValue({ allow: true, remember: false }), })); describe('FilePermissionChecker - 文件权限检查器', () => { let checker: FilePermissionChecker; const testProjectRoot = '/test/project'; beforeEach(() => { checker = new FilePermissionChecker(testProjectRoot); vi.clearAllMocks(); }); describe('默认配置', () => { it('加载默认配置', () => { const config = checker.getConfig(); expect(config.operations).toBeDefined(); expect(config.sensitivePaths).toBeDefined(); expect(config.externalDirectory).toBe('ask'); }); it('读操作默认允许', () => { const config = checker.getConfig(); expect(config.operations.read).toBe('allow'); expect(config.operations.list).toBe('allow'); expect(config.operations.search).toBe('allow'); expect(config.operations.grep).toBe('allow'); expect(config.operations.info).toBe('allow'); }); it('写操作默认需要确认', () => { const config = checker.getConfig(); expect(config.operations.write).toBe('ask'); expect(config.operations.edit).toBe('ask'); expect(config.operations.move).toBe('ask'); expect(config.operations.copy).toBe('ask'); expect(config.operations.delete).toBe('ask'); expect(config.operations.mkdir).toBe('ask'); }); }); describe('读操作权限', () => { const readOperations: FilePermissionContext['operation'][] = [ 'read', 'list', 'search', 'grep', 'info', ]; for (const operation of readOperations) { it(`${operation} 操作在项目内默认允许`, async () => { const result = await checker.checkFilePermission({ operation, path: './src/index.ts', workdir: testProjectRoot, }); expect(result.allowed).toBe(true); expect(result.action).toBe('allow'); }); } }); describe('写操作权限', () => { const writeOperations: FilePermissionContext['operation'][] = [ 'write', 'edit', 'move', 'copy', 'delete', 'mkdir', ]; for (const operation of writeOperations) { it(`${operation} 操作无回调时需要确认`, async () => { const result = await checker.checkFilePermission({ operation, path: './src/new-file.ts', workdir: testProjectRoot, }); expect(result.action).toBe('ask'); expect(result.needsConfirmation).toBe(true); }); } }); describe('敏感路径检查', () => { it('系统路径拒绝访问', async () => { const sensitivePaths = [ '/etc/passwd', '/usr/bin/node', '/bin/sh', '/var/log/syslog', ]; for (const testPath of sensitivePaths) { const result = await checker.checkFilePermission({ operation: 'read', path: testPath, workdir: testProjectRoot, }); expect(result.allowed).toBe(false); expect(result.action).toBe('deny'); } }); it('用户敏感文件需要确认', async () => { const sensitivePaths = [ '~/.ssh/id_rsa', '~/.aws/credentials', './.env', ]; for (const testPath of sensitivePaths) { const result = await checker.checkFilePermission({ operation: 'read', path: testPath, workdir: testProjectRoot, }); // 敏感路径会触发 ask expect(result.action === 'ask' || result.action === 'deny').toBe(true); } }); }); describe('外部目录访问', () => { it('项目外路径需要确认', async () => { const result = await checker.checkFilePermission({ operation: 'read', path: '/home/other/file.txt', workdir: testProjectRoot, }); // 外部目录会触发 ask 或 deny expect(result.action === 'ask' || result.action === 'deny').toBe(true); }); it('项目内路径正常检查', async () => { const result = await checker.checkFilePermission({ operation: 'read', path: './src/index.ts', workdir: testProjectRoot, }); expect(result.allowed).toBe(true); }); }); describe('波浪号展开', () => { it('展开 ~ 为 home 目录', async () => { const result = await checker.checkFilePermission({ operation: 'read', path: '~/projects/file.txt', workdir: testProjectRoot, }); // 外部路径会触发 ask expect(result).toBeDefined(); }); }); describe('回调处理', () => { it('用户允许时返回允许', async () => { const mockCallback = vi.fn().mockResolvedValue({ allow: true, remember: false, } as PermissionDecision); checker.setAskCallback(mockCallback); const result = await checker.checkFilePermission({ operation: 'write', path: './src/new-file.ts', workdir: testProjectRoot, }); expect(result.allowed).toBe(true); }); it('用户拒绝时返回拒绝', async () => { const mockCallback = vi.fn().mockResolvedValue({ allow: false, remember: false, } as PermissionDecision); checker.setAskCallback(mockCallback); const result = await checker.checkFilePermission({ operation: 'delete', path: './src/file.ts', workdir: testProjectRoot, }); expect(result.allowed).toBe(false); expect(result.action).toBe('deny'); }); }); describe('会话权限管理', () => { it('记住允许决定', async () => { const mockCallback = vi.fn().mockResolvedValue({ allow: true, remember: true, } as PermissionDecision); checker.setAskCallback(mockCallback); // 第一次调用 await checker.checkFilePermission({ operation: 'write', path: './src/test.ts', workdir: testProjectRoot, }); // 第二次调用同一操作和路径 const result = await checker.checkFilePermission({ operation: 'write', path: './src/test.ts', workdir: testProjectRoot, }); expect(result.allowed).toBe(true); // 第二次不应该调用回调 expect(mockCallback).toHaveBeenCalledTimes(1); }); it('记住拒绝决定', async () => { const mockCallback = vi.fn().mockResolvedValue({ allow: false, remember: true, } as PermissionDecision); checker.setAskCallback(mockCallback); // 第一次调用 await checker.checkFilePermission({ operation: 'delete', path: './src/important.ts', workdir: testProjectRoot, }); // 第二次调用 const result = await checker.checkFilePermission({ operation: 'delete', path: './src/important.ts', workdir: testProjectRoot, }); expect(result.allowed).toBe(false); expect(mockCallback).toHaveBeenCalledTimes(1); }); it('清除会话权限后重新询问', async () => { const mockCallback = vi.fn().mockResolvedValue({ allow: true, remember: true, } as PermissionDecision); checker.setAskCallback(mockCallback); // 第一次调用 await checker.checkFilePermission({ operation: 'write', path: './src/test.ts', workdir: testProjectRoot, }); // 清除权限 checker.clearSessionPermissions(); // 再次调用 await checker.checkFilePermission({ operation: 'write', path: './src/test.ts', workdir: testProjectRoot, }); // 应该调用两次 expect(mockCallback).toHaveBeenCalledTimes(2); }); }); describe('check 接口(兼容 PermissionChecker)', () => { it('解析 read 操作', async () => { const result = await checker.check({ command: 'read ./src/index.ts', workdir: testProjectRoot, }); expect(result.allowed).toBe(true); }); it('解析 write 操作', async () => { const result = await checker.check({ command: 'write ./src/new.ts', workdir: testProjectRoot, }); expect(result.action).toBe('ask'); }); }); });