Files
kurihada 5e32375f0e feat: 重构为 Monorepo 架构并实现 HTTP Server
架构变更:
- 采用 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 - 配置管理
2025-12-12 10:42:20 +08:00

323 lines
8.6 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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');
});
});
});