import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; import { promptPermission, showPermissionDenied, showPermissionAllowed } from '../../../src/permission/prompt.js'; import type { PermissionContext } from '../../../src/permission/types.js'; // Mock readline vi.mock('readline', () => ({ createInterface: vi.fn(() => ({ question: vi.fn(), close: vi.fn(), })), })); // Mock chalk vi.mock('chalk', () => ({ default: { yellow: (s: string) => s, cyan: (s: string) => s, white: (s: string) => s, gray: (s: string) => s, red: (s: string) => s, green: (s: string) => s, }, })); import * as readline from 'readline'; describe('Permission Prompt - 权限提示模块', () => { let consoleLogSpy: ReturnType; beforeEach(() => { vi.clearAllMocks(); consoleLogSpy = vi.spyOn(console, 'log').mockImplementation(() => {}); }); afterEach(() => { consoleLogSpy.mockRestore(); }); describe('showPermissionDenied - 显示权限被拒绝', () => { it('显示命令和原因', () => { showPermissionDenied('rm -rf /', '危险命令'); expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('权限被拒绝')); expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('rm -rf /')); expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('危险命令')); }); it('输出包含空行', () => { showPermissionDenied('test', 'reason'); // 第一个和最后一个调用是空行 expect(consoleLogSpy).toHaveBeenCalledWith(''); }); }); describe('showPermissionAllowed - 显示权限允许', () => { it('显示执行的命令', () => { showPermissionAllowed('npm install'); expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('执行')); expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('npm install')); }); }); describe('promptPermission - 交互式权限提示', () => { const mockContext: PermissionContext = { command: 'git push', workdir: '/project', toolName: 'bash', }; it('用户输入 y 返回允许不记住', async () => { const mockRl = { question: vi.fn((_, callback) => callback('y')), close: vi.fn(), }; vi.mocked(readline.createInterface).mockReturnValue(mockRl as any); const result = await promptPermission(mockContext); expect(result).toEqual({ allow: true, remember: false }); expect(mockRl.close).toHaveBeenCalled(); }); it('用户输入 Y 返回允许并记住', async () => { const mockRl = { question: vi.fn((_, callback) => callback('Y')), close: vi.fn(), }; vi.mocked(readline.createInterface).mockReturnValue(mockRl as any); const result = await promptPermission(mockContext); expect(result).toEqual({ allow: true, remember: true }); }); it('用户输入 n 返回拒绝不记住', async () => { const mockRl = { question: vi.fn((_, callback) => callback('n')), close: vi.fn(), }; vi.mocked(readline.createInterface).mockReturnValue(mockRl as any); const result = await promptPermission(mockContext); expect(result).toEqual({ allow: false, remember: false }); }); it('用户输入 N 返回拒绝并记住', async () => { const mockRl = { question: vi.fn((_, callback) => callback('N')), close: vi.fn(), }; vi.mocked(readline.createInterface).mockReturnValue(mockRl as any); const result = await promptPermission(mockContext); expect(result).toEqual({ allow: false, remember: true }); }); it('无效输入默认为拒绝', async () => { const mockRl = { question: vi.fn((_, callback) => callback('invalid')), close: vi.fn(), }; vi.mocked(readline.createInterface).mockReturnValue(mockRl as any); const result = await promptPermission(mockContext); expect(result).toEqual({ allow: false, remember: false }); }); it('空输入默认为拒绝', async () => { const mockRl = { question: vi.fn((_, callback) => callback('')), close: vi.fn(), }; vi.mocked(readline.createInterface).mockReturnValue(mockRl as any); const result = await promptPermission(mockContext); expect(result).toEqual({ allow: false, remember: false }); }); it('带空格的输入会被 trim', async () => { const mockRl = { question: vi.fn((_, callback) => callback(' y ')), close: vi.fn(), }; vi.mocked(readline.createInterface).mockReturnValue(mockRl as any); const result = await promptPermission(mockContext); expect(result).toEqual({ allow: true, remember: false }); }); it('显示命令和工作目录', async () => { const mockRl = { question: vi.fn((_, callback) => callback('y')), close: vi.fn(), }; vi.mocked(readline.createInterface).mockReturnValue(mockRl as any); await promptPermission(mockContext); expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('git push')); expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('/project')); }); it('显示外部路径警告', async () => { const contextWithExternal: PermissionContext = { ...mockContext, externalPaths: ['/etc/passwd', '/root/.ssh'], }; const mockRl = { question: vi.fn((_, callback) => callback('n')), close: vi.fn(), }; vi.mocked(readline.createInterface).mockReturnValue(mockRl as any); await promptPermission(contextWithExternal); expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('项目目录外的路径')); expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('/etc/passwd')); expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('/root/.ssh')); }); it('显示匹配模式', async () => { const contextWithPatterns: PermissionContext = { ...mockContext, patterns: ['*.js', '*.ts'], }; const mockRl = { question: vi.fn((_, callback) => callback('y')), close: vi.fn(), }; vi.mocked(readline.createInterface).mockReturnValue(mockRl as any); await promptPermission(contextWithPatterns); expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('*.js')); expect(consoleLogSpy).toHaveBeenCalledWith(expect.stringContaining('*.ts')); }); it('不显示空的外部路径', async () => { const contextEmptyExternal: PermissionContext = { ...mockContext, externalPaths: [], }; const mockRl = { question: vi.fn((_, callback) => callback('y')), close: vi.fn(), }; vi.mocked(readline.createInterface).mockReturnValue(mockRl as any); await promptPermission(contextEmptyExternal); // 不应该显示外部路径相关的警告 const calls = consoleLogSpy.mock.calls.flat().join('\n'); expect(calls).not.toContain('项目目录外的路径'); }); }); });