5e32375f0e
架构变更: - 采用 pnpm workspaces 实现 Monorepo 结构 - 将现有代码迁移到 packages/core - 新增 packages/server HTTP 服务层 Server 功能: - REST API: 会话管理、工具管理、配置管理 - WebSocket: 实时双向通信支持 - SSE: 服务端事件推送 - Hono + Bun 作为运行时 API 端点: - GET/POST /api/sessions - 会话 CRUD - GET/POST /api/sessions/:id/messages - 消息管理 - GET /api/sessions/:id/events - SSE 事件流 - WS /api/ws/:sessionId - WebSocket 连接 - GET/POST /api/tools - 工具管理 - GET/PUT /api/config - 配置管理
227 lines
7.1 KiB
TypeScript
227 lines
7.1 KiB
TypeScript
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<typeof vi.spyOn>;
|
|
|
|
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('项目目录外的路径');
|
|
});
|
|
});
|
|
});
|