/** * Permission Handler 测试 * * 测试权限类型检测、请求构建、响应处理、超时处理等功能 */ import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; // Mock broadcastToSession before importing handler vi.mock('../../../src/ws.js', () => ({ broadcastToSession: vi.fn(), })); import { createServerPermissionCallback, handlePermissionResponse, cancelPendingRequests, getPendingRequestCount, } from '../../../src/permission/handler.js'; import { broadcastToSession } from '../../../src/ws.js'; describe('Permission Handler', () => { beforeEach(() => { vi.clearAllMocks(); vi.useFakeTimers(); }); afterEach(() => { vi.useRealTimers(); }); describe('detectPermissionType - 权限类型检测', () => { it('检测 git 命令', async () => { const callback = createServerPermissionCallback('session-1'); // 触发一个 git 命令的权限请求 const promise = callback({ command: 'git push origin main', workdir: '/test' }); // 检查发送的消息类型 expect(broadcastToSession).toHaveBeenCalledWith( 'session-1', expect.objectContaining({ type: 'permission_request', payload: expect.objectContaining({ permissionType: 'git', context: expect.objectContaining({ gitOperation: 'push', }), }), }) ); // 模拟响应 const call = vi.mocked(broadcastToSession).mock.calls[0]; const requestId = (call[1] as any).payload.requestId; handlePermissionResponse(requestId, true); await promise; }); it('检测文件操作命令', async () => { const callback = createServerPermissionCallback('session-1'); const promise = callback({ command: 'read /etc/passwd', workdir: '/test' }); expect(broadcastToSession).toHaveBeenCalledWith( 'session-1', expect.objectContaining({ payload: expect.objectContaining({ permissionType: 'file', context: expect.objectContaining({ operation: 'read', path: '/etc/passwd', }), }), }) ); const call = vi.mocked(broadcastToSession).mock.calls[0]; const requestId = (call[1] as any).payload.requestId; handlePermissionResponse(requestId, true); await promise; }); it('检测 write 文件操作', async () => { const callback = createServerPermissionCallback('session-1'); const promise = callback({ command: 'write /tmp/test.txt', workdir: '/test' }); expect(broadcastToSession).toHaveBeenCalledWith( 'session-1', expect.objectContaining({ payload: expect.objectContaining({ permissionType: 'file', context: expect.objectContaining({ operation: 'write', }), }), }) ); const call = vi.mocked(broadcastToSession).mock.calls[0]; const requestId = (call[1] as any).payload.requestId; handlePermissionResponse(requestId, true); await promise; }); it('检测 web 操作', async () => { const callback = createServerPermissionCallback('session-1'); const promise = callback({ command: 'fetch https://api.example.com', workdir: '/test' }); expect(broadcastToSession).toHaveBeenCalledWith( 'session-1', expect.objectContaining({ payload: expect.objectContaining({ permissionType: 'web', }), }) ); const call = vi.mocked(broadcastToSession).mock.calls[0]; const requestId = (call[1] as any).payload.requestId; handlePermissionResponse(requestId, true); await promise; }); it('默认为 bash 类型', async () => { const callback = createServerPermissionCallback('session-1'); const promise = callback({ command: 'npm install', workdir: '/test' }); expect(broadcastToSession).toHaveBeenCalledWith( 'session-1', expect.objectContaining({ payload: expect.objectContaining({ permissionType: 'bash', }), }) ); const call = vi.mocked(broadcastToSession).mock.calls[0]; const requestId = (call[1] as any).payload.requestId; handlePermissionResponse(requestId, true); await promise; }); }); describe('buildRequestContext - 请求上下文构建', () => { it('git 命令包含 gitOperation', async () => { const callback = createServerPermissionCallback('session-1'); const promise = callback({ command: 'git commit -m "test"', workdir: '/test' }); expect(broadcastToSession).toHaveBeenCalledWith( 'session-1', expect.objectContaining({ payload: expect.objectContaining({ context: expect.objectContaining({ command: 'git commit -m "test"', gitOperation: 'commit', }), }), }) ); const call = vi.mocked(broadcastToSession).mock.calls[0]; const requestId = (call[1] as any).payload.requestId; handlePermissionResponse(requestId, true); await promise; }); it('文件操作包含 patterns 和 externalPaths', async () => { const callback = createServerPermissionCallback('session-1'); const promise = callback({ command: 'edit /test/file.ts', workdir: '/test', patterns: ['*.ts'], externalPaths: ['/external'], }); expect(broadcastToSession).toHaveBeenCalledWith( 'session-1', expect.objectContaining({ payload: expect.objectContaining({ context: expect.objectContaining({ operation: 'edit', patterns: ['*.ts'], externalPaths: ['/external'], }), }), }) ); const call = vi.mocked(broadcastToSession).mock.calls[0]; const requestId = (call[1] as any).payload.requestId; handlePermissionResponse(requestId, true); await promise; }); it('bash 命令包含 command', async () => { const callback = createServerPermissionCallback('session-1'); const promise = callback({ command: 'rm -rf /tmp/test', workdir: '/test' }); expect(broadcastToSession).toHaveBeenCalledWith( 'session-1', expect.objectContaining({ payload: expect.objectContaining({ context: expect.objectContaining({ command: 'rm -rf /tmp/test', }), }), }) ); const call = vi.mocked(broadcastToSession).mock.calls[0]; const requestId = (call[1] as any).payload.requestId; handlePermissionResponse(requestId, true); await promise; }); }); describe('createServerPermissionCallback - 权限回调', () => { it('发送权限请求消息', async () => { const callback = createServerPermissionCallback('session-1'); const promise = callback({ command: 'ls -la', workdir: '/test' }); expect(broadcastToSession).toHaveBeenCalledTimes(1); expect(broadcastToSession).toHaveBeenCalledWith( 'session-1', expect.objectContaining({ type: 'permission_request', sessionId: 'session-1', payload: expect.objectContaining({ requestId: expect.any(String), permissionType: 'bash', }), }) ); // 响应以完成 promise const call = vi.mocked(broadcastToSession).mock.calls[0]; const requestId = (call[1] as any).payload.requestId; handlePermissionResponse(requestId, true); await promise; }); it('返回 allow: true 当响应允许时', async () => { const callback = createServerPermissionCallback('session-1'); const promise = callback({ command: 'ls', workdir: '/test' }); const call = vi.mocked(broadcastToSession).mock.calls[0]; const requestId = (call[1] as any).payload.requestId; handlePermissionResponse(requestId, true, true); const result = await promise; expect(result).toEqual({ allow: true, remember: true }); }); it('返回 allow: false 当响应拒绝时', async () => { const callback = createServerPermissionCallback('session-1'); const promise = callback({ command: 'rm -rf /', workdir: '/test' }); const call = vi.mocked(broadcastToSession).mock.calls[0]; const requestId = (call[1] as any).payload.requestId; handlePermissionResponse(requestId, false, false); const result = await promise; expect(result).toEqual({ allow: false, remember: false }); }); }); describe('handlePermissionResponse - 权限响应处理', () => { it('处理存在的请求返回 true', async () => { const callback = createServerPermissionCallback('session-1'); const promise = callback({ command: 'ls', workdir: '/test' }); const call = vi.mocked(broadcastToSession).mock.calls[0]; const requestId = (call[1] as any).payload.requestId; const result = handlePermissionResponse(requestId, true); expect(result).toBe(true); await promise; }); it('处理不存在的请求返回 false', () => { const result = handlePermissionResponse('non-existent-request-id', true); expect(result).toBe(false); }); it('清除超时定时器', async () => { const callback = createServerPermissionCallback('session-1'); const promise = callback({ command: 'ls', workdir: '/test' }); const call = vi.mocked(broadcastToSession).mock.calls[0]; const requestId = (call[1] as any).payload.requestId; // 响应请求 handlePermissionResponse(requestId, true); await promise; // 快进超过超时时间,不应有任何错误 vi.advanceTimersByTime(70000); }); }); describe('超时处理', () => { it('超时后返回 allow: false', async () => { const callback = createServerPermissionCallback('session-1'); const promise = callback({ command: 'dangerous-command', workdir: '/test' }); // 快进 60 秒(超时时间) vi.advanceTimersByTime(60000); const result = await promise; expect(result).toEqual({ allow: false, remember: false }); }); it('超时后从 pending 中移除', async () => { const initialCount = getPendingRequestCount(); const callback = createServerPermissionCallback('session-1'); const promise = callback({ command: 'test', workdir: '/test' }); expect(getPendingRequestCount()).toBe(initialCount + 1); // 超时 vi.advanceTimersByTime(60000); await promise; expect(getPendingRequestCount()).toBe(initialCount); }); }); describe('getPendingRequestCount - 待处理请求计数', () => { it('返回正确的待处理请求数', async () => { const initialCount = getPendingRequestCount(); const callback = createServerPermissionCallback('session-1'); const promise1 = callback({ command: 'cmd1', workdir: '/test' }); const promise2 = callback({ command: 'cmd2', workdir: '/test' }); expect(getPendingRequestCount()).toBe(initialCount + 2); // 响应第一个 const call1 = vi.mocked(broadcastToSession).mock.calls[0]; const requestId1 = (call1[1] as any).payload.requestId; handlePermissionResponse(requestId1, true); await promise1; expect(getPendingRequestCount()).toBe(initialCount + 1); // 响应第二个 const call2 = vi.mocked(broadcastToSession).mock.calls[1]; const requestId2 = (call2[1] as any).payload.requestId; handlePermissionResponse(requestId2, true); await promise2; expect(getPendingRequestCount()).toBe(initialCount); }); }); describe('cancelPendingRequests - 取消待处理请求', () => { it('调用不抛出错误', () => { // 目前这个函数是占位符,只是确保不抛出错误 expect(() => cancelPendingRequests('session-1')).not.toThrow(); }); }); });