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 - 配置管理
This commit is contained in:
@@ -0,0 +1,297 @@
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { GitPermissionChecker } from '../../../src/permission/checkers/git.js';
|
||||
import type { GitPermissionContext, PermissionDecision } from '../../../src/permission/types.js';
|
||||
|
||||
describe('GitPermissionChecker - Git 权限检查器', () => {
|
||||
let checker: GitPermissionChecker;
|
||||
|
||||
beforeEach(() => {
|
||||
checker = new GitPermissionChecker();
|
||||
});
|
||||
|
||||
describe('读操作(默认允许)', () => {
|
||||
const readOperations: GitPermissionContext['operation'][] = [
|
||||
'status',
|
||||
'diff',
|
||||
'log',
|
||||
'branch_list',
|
||||
'show',
|
||||
];
|
||||
|
||||
for (const operation of readOperations) {
|
||||
it(`${operation} 默认允许`, async () => {
|
||||
const result = await checker.checkGitPermission({ operation });
|
||||
|
||||
expect(result.allowed).toBe(true);
|
||||
expect(result.action).toBe('allow');
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe('写操作(默认询问)', () => {
|
||||
const writeOperations: GitPermissionContext['operation'][] = [
|
||||
'add',
|
||||
'commit',
|
||||
'push',
|
||||
'pull',
|
||||
'checkout',
|
||||
'branch_create',
|
||||
'branch_delete',
|
||||
'stash',
|
||||
'stash_pop',
|
||||
'merge',
|
||||
'rebase',
|
||||
];
|
||||
|
||||
for (const operation of writeOperations) {
|
||||
it(`${operation} 无回调时需要确认`, async () => {
|
||||
const result = await checker.checkGitPermission({ operation });
|
||||
|
||||
expect(result.action).toBe('ask');
|
||||
expect(result.needsConfirmation).toBe(true);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
describe('危险操作', () => {
|
||||
it('reset 总是危险操作', async () => {
|
||||
const result = await checker.checkGitPermission({ operation: 'reset' });
|
||||
|
||||
expect(result.action).toBe('ask');
|
||||
expect(result.needsConfirmation).toBe(true);
|
||||
});
|
||||
|
||||
it('push --force 是危险操作', async () => {
|
||||
const result = await checker.checkGitPermission({
|
||||
operation: 'push',
|
||||
force: true,
|
||||
});
|
||||
|
||||
expect(result.action).toBe('ask');
|
||||
});
|
||||
|
||||
it('checkout --force 是危险操作', async () => {
|
||||
const result = await checker.checkGitPermission({
|
||||
operation: 'checkout',
|
||||
force: true,
|
||||
});
|
||||
|
||||
expect(result.action).toBe('ask');
|
||||
});
|
||||
|
||||
it('rebase --force 是危险操作', async () => {
|
||||
const result = await checker.checkGitPermission({
|
||||
operation: 'rebase',
|
||||
force: true,
|
||||
});
|
||||
|
||||
expect(result.action).toBe('ask');
|
||||
});
|
||||
});
|
||||
|
||||
describe('回调处理', () => {
|
||||
it('用户允许时返回允许', async () => {
|
||||
const mockCallback = vi.fn().mockResolvedValue({
|
||||
allow: true,
|
||||
remember: false,
|
||||
} as PermissionDecision);
|
||||
|
||||
checker.setAskCallback(mockCallback);
|
||||
|
||||
const result = await checker.checkGitPermission({ operation: 'commit' });
|
||||
|
||||
expect(result.allowed).toBe(true);
|
||||
expect(mockCallback).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('用户拒绝时返回拒绝', async () => {
|
||||
const mockCallback = vi.fn().mockResolvedValue({
|
||||
allow: false,
|
||||
remember: false,
|
||||
} as PermissionDecision);
|
||||
|
||||
checker.setAskCallback(mockCallback);
|
||||
|
||||
const result = await checker.checkGitPermission({ operation: 'push' });
|
||||
|
||||
expect(result.allowed).toBe(false);
|
||||
expect(result.action).toBe('deny');
|
||||
});
|
||||
|
||||
it('remember=true 时记住决定', async () => {
|
||||
const mockCallback = vi.fn().mockResolvedValue({
|
||||
allow: true,
|
||||
remember: true,
|
||||
} as PermissionDecision);
|
||||
|
||||
checker.setAskCallback(mockCallback);
|
||||
|
||||
// 第一次调用
|
||||
await checker.checkGitPermission({ operation: 'commit' });
|
||||
|
||||
// 第二次调用不应该再询问
|
||||
const secondResult = await checker.checkGitPermission({ operation: 'push' });
|
||||
|
||||
expect(secondResult.allowed).toBe(true);
|
||||
// 第二次不需要回调(因为记住了写操作的权限)
|
||||
expect(mockCallback).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('会话权限管理', () => {
|
||||
it('清除会话权限后重新询问', async () => {
|
||||
const mockCallback = vi.fn().mockResolvedValue({
|
||||
allow: true,
|
||||
remember: true,
|
||||
} as PermissionDecision);
|
||||
|
||||
checker.setAskCallback(mockCallback);
|
||||
|
||||
// 第一次调用并记住
|
||||
await checker.checkGitPermission({ operation: 'commit' });
|
||||
|
||||
// 清除会话权限
|
||||
checker.clearSessionPermissions();
|
||||
|
||||
// 再次调用应该重新询问
|
||||
await checker.checkGitPermission({ operation: 'commit' });
|
||||
|
||||
expect(mockCallback).toHaveBeenCalledTimes(2);
|
||||
});
|
||||
|
||||
it('拒绝决定也被记住', async () => {
|
||||
const mockCallback = vi.fn().mockResolvedValue({
|
||||
allow: false,
|
||||
remember: true,
|
||||
} as PermissionDecision);
|
||||
|
||||
checker.setAskCallback(mockCallback);
|
||||
|
||||
// 第一次拒绝并记住
|
||||
await checker.checkGitPermission({ operation: 'push' });
|
||||
|
||||
// 第二次直接拒绝
|
||||
const result = await checker.checkGitPermission({ operation: 'commit' });
|
||||
|
||||
expect(result.allowed).toBe(false);
|
||||
expect(mockCallback).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('配置管理', () => {
|
||||
it('获取默认配置', () => {
|
||||
const config = checker.getConfig();
|
||||
|
||||
expect(config.readOperations).toBe('allow');
|
||||
expect(config.writeOperations).toBe('ask');
|
||||
expect(config.dangerousOperations).toBe('ask');
|
||||
});
|
||||
|
||||
it('更新配置', () => {
|
||||
checker.setConfig({ writeOperations: 'allow' });
|
||||
|
||||
const config = checker.getConfig();
|
||||
expect(config.writeOperations).toBe('allow');
|
||||
});
|
||||
|
||||
it('配置更改后影响权限检查', async () => {
|
||||
checker.setConfig({ writeOperations: 'allow' });
|
||||
|
||||
const result = await checker.checkGitPermission({ operation: 'commit' });
|
||||
|
||||
expect(result.allowed).toBe(true);
|
||||
});
|
||||
|
||||
it('配置危险操作为拒绝', async () => {
|
||||
checker.setConfig({ dangerousOperations: 'deny' });
|
||||
|
||||
const result = await checker.checkGitPermission({ operation: 'reset' });
|
||||
|
||||
expect(result.allowed).toBe(false);
|
||||
expect(result.action).toBe('deny');
|
||||
});
|
||||
});
|
||||
|
||||
describe('操作描述生成', () => {
|
||||
it('带 target 的操作描述', async () => {
|
||||
checker.setAskCallback(vi.fn().mockResolvedValue({ allow: true, remember: false }));
|
||||
|
||||
await checker.checkGitPermission({
|
||||
operation: 'checkout',
|
||||
target: 'feature-branch',
|
||||
});
|
||||
|
||||
// 验证回调收到正确的描述
|
||||
const callArg = vi.mocked(checker['askCallback']!).mock.calls[0][0];
|
||||
expect(callArg.command).toContain('feature-branch');
|
||||
});
|
||||
|
||||
it('带 remote 的操作描述', async () => {
|
||||
checker.setAskCallback(vi.fn().mockResolvedValue({ allow: true, remember: false }));
|
||||
|
||||
await checker.checkGitPermission({
|
||||
operation: 'push',
|
||||
remote: 'origin',
|
||||
});
|
||||
|
||||
const callArg = vi.mocked(checker['askCallback']!).mock.calls[0][0];
|
||||
expect(callArg.command).toContain('origin');
|
||||
});
|
||||
|
||||
it('带 commit message 的操作描述(截断)', async () => {
|
||||
checker.setAskCallback(vi.fn().mockResolvedValue({ allow: true, remember: false }));
|
||||
|
||||
const longMessage = 'a'.repeat(100);
|
||||
await checker.checkGitPermission({
|
||||
operation: 'commit',
|
||||
message: longMessage,
|
||||
});
|
||||
|
||||
const callArg = vi.mocked(checker['askCallback']!).mock.calls[0][0];
|
||||
expect(callArg.command).toContain('...');
|
||||
expect(callArg.command.length).toBeLessThan(longMessage.length + 50);
|
||||
});
|
||||
|
||||
it('--force 显示在描述中', async () => {
|
||||
checker.setAskCallback(vi.fn().mockResolvedValue({ allow: true, remember: false }));
|
||||
|
||||
await checker.checkGitPermission({
|
||||
operation: 'push',
|
||||
force: true,
|
||||
});
|
||||
|
||||
const callArg = vi.mocked(checker['askCallback']!).mock.calls[0][0];
|
||||
expect(callArg.command).toContain('--force');
|
||||
});
|
||||
});
|
||||
|
||||
describe('check 接口(从命令解析)', () => {
|
||||
it('解析 git status', async () => {
|
||||
const result = await checker.check({
|
||||
command: 'git status',
|
||||
workdir: '/test',
|
||||
});
|
||||
|
||||
expect(result.allowed).toBe(true);
|
||||
});
|
||||
|
||||
it('解析 git_commit', async () => {
|
||||
const result = await checker.check({
|
||||
command: 'git_commit',
|
||||
workdir: '/test',
|
||||
});
|
||||
|
||||
expect(result.action).toBe('ask');
|
||||
});
|
||||
|
||||
it('无法解析的命令返回拒绝', async () => {
|
||||
const result = await checker.check({
|
||||
command: 'invalid command',
|
||||
workdir: '/test',
|
||||
});
|
||||
|
||||
expect(result.allowed).toBe(false);
|
||||
expect(result.reason).toContain('无法解析');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user