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,206 @@
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { CommandLoader } from '../../../src/commands/loader.js';
|
||||
|
||||
// Mock fs/promises
|
||||
vi.mock('fs/promises', () => ({
|
||||
access: vi.fn(),
|
||||
readdir: vi.fn(),
|
||||
readFile: vi.fn(),
|
||||
}));
|
||||
|
||||
import * as fs from 'fs/promises';
|
||||
|
||||
describe('CommandLoader - Command 加载器', () => {
|
||||
let loader: CommandLoader;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
loader = new CommandLoader();
|
||||
});
|
||||
|
||||
describe('parseMarkdownCommand - 解析 Markdown', () => {
|
||||
it('解析带 frontmatter 的 Markdown', () => {
|
||||
const content = `---
|
||||
description: 代码审查
|
||||
agent: explore
|
||||
model: sonnet
|
||||
subtask: true
|
||||
---
|
||||
|
||||
You are a code reviewer.
|
||||
|
||||
Input: $ARGUMENTS
|
||||
`;
|
||||
|
||||
const command = loader.parseMarkdownCommand(content, 'review', 'user');
|
||||
|
||||
expect(command).not.toBeNull();
|
||||
expect(command?.name).toBe('review');
|
||||
expect(command?.description).toBe('代码审查');
|
||||
expect(command?.agent).toBe('explore');
|
||||
expect(command?.model).toBe('sonnet');
|
||||
expect(command?.subtask).toBe(true);
|
||||
expect(command?.template).toContain('You are a code reviewer');
|
||||
expect(command?.template).toContain('$ARGUMENTS');
|
||||
expect(command?.source).toBe('user');
|
||||
});
|
||||
|
||||
it('解析不带 frontmatter 的 Markdown', () => {
|
||||
const content = `# Simple Command
|
||||
|
||||
This is a simple prompt template.
|
||||
|
||||
$ARGUMENTS
|
||||
`;
|
||||
|
||||
const command = loader.parseMarkdownCommand(content, 'simple', 'project');
|
||||
|
||||
expect(command).not.toBeNull();
|
||||
expect(command?.name).toBe('simple');
|
||||
expect(command?.description).toBeUndefined();
|
||||
expect(command?.template).toContain('Simple Command');
|
||||
expect(command?.template).toContain('$ARGUMENTS');
|
||||
expect(command?.source).toBe('project');
|
||||
});
|
||||
|
||||
it('空模板返回 null', () => {
|
||||
const content = `---
|
||||
description: Empty
|
||||
---
|
||||
|
||||
`;
|
||||
|
||||
const command = loader.parseMarkdownCommand(content, 'empty', 'user');
|
||||
|
||||
expect(command).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('loadFromFile - 从文件加载', () => {
|
||||
it('从 Markdown 文件加载 Command', async () => {
|
||||
const mdContent = `---
|
||||
description: Test command
|
||||
---
|
||||
|
||||
Test template $ARGUMENTS
|
||||
`;
|
||||
|
||||
vi.mocked(fs.readFile).mockResolvedValue(mdContent);
|
||||
|
||||
const command = await loader.loadFromFile(
|
||||
'/base/commands/test.md',
|
||||
'/base/commands',
|
||||
'user'
|
||||
);
|
||||
|
||||
expect(command).not.toBeNull();
|
||||
expect(command?.name).toBe('test');
|
||||
expect(command?.description).toBe('Test command');
|
||||
});
|
||||
|
||||
it('处理嵌套目录路径', async () => {
|
||||
const mdContent = `---
|
||||
description: Deploy staging
|
||||
---
|
||||
|
||||
Deploy to staging
|
||||
`;
|
||||
|
||||
vi.mocked(fs.readFile).mockResolvedValue(mdContent);
|
||||
|
||||
const command = await loader.loadFromFile(
|
||||
'/base/commands/deploy/staging.md',
|
||||
'/base/commands',
|
||||
'project'
|
||||
);
|
||||
|
||||
expect(command).not.toBeNull();
|
||||
expect(command?.name).toBe('deploy/staging');
|
||||
});
|
||||
});
|
||||
|
||||
describe('loadFromDirectory - 从目录加载', () => {
|
||||
it('目录不存在时返回空数组', async () => {
|
||||
vi.mocked(fs.access).mockRejectedValue(new Error('Not found'));
|
||||
|
||||
const commands = await loader.loadFromDirectory('/non-existent', 'user');
|
||||
|
||||
expect(commands).toEqual([]);
|
||||
});
|
||||
|
||||
it('加载目录中的所有 Markdown 文件', async () => {
|
||||
vi.mocked(fs.access).mockResolvedValue(undefined);
|
||||
vi.mocked(fs.readdir).mockResolvedValue([
|
||||
{ name: 'cmd1.md', isFile: () => true, isDirectory: () => false },
|
||||
{ name: 'cmd2.md', isFile: () => true, isDirectory: () => false },
|
||||
{ name: 'readme.txt', isFile: () => true, isDirectory: () => false }, // 非 .md
|
||||
] as any);
|
||||
|
||||
vi.mocked(fs.readFile)
|
||||
.mockResolvedValueOnce('---\ndescription: Cmd1\n---\nTemplate 1')
|
||||
.mockResolvedValueOnce('---\ndescription: Cmd2\n---\nTemplate 2');
|
||||
|
||||
const commands = await loader.loadFromDirectory('/test', 'user');
|
||||
|
||||
expect(commands.length).toBe(2);
|
||||
expect(commands.map((c) => c.name)).toContain('cmd1');
|
||||
expect(commands.map((c) => c.name)).toContain('cmd2');
|
||||
});
|
||||
|
||||
it('递归加载子目录', async () => {
|
||||
vi.mocked(fs.access).mockResolvedValue(undefined);
|
||||
vi.mocked(fs.readdir)
|
||||
.mockResolvedValueOnce([
|
||||
{ name: 'root.md', isFile: () => true, isDirectory: () => false },
|
||||
{ name: 'subdir', isFile: () => false, isDirectory: () => true },
|
||||
] as any)
|
||||
.mockResolvedValueOnce([
|
||||
{ name: 'sub.md', isFile: () => true, isDirectory: () => false },
|
||||
] as any);
|
||||
|
||||
vi.mocked(fs.readFile)
|
||||
.mockResolvedValueOnce('Root template')
|
||||
.mockResolvedValueOnce('Sub template');
|
||||
|
||||
const commands = await loader.loadFromDirectory('/test', 'project');
|
||||
|
||||
expect(commands.length).toBe(2);
|
||||
expect(commands.map((c) => c.name)).toContain('root');
|
||||
expect(commands.map((c) => c.name)).toContain('subdir/sub');
|
||||
});
|
||||
|
||||
it('跳过隐藏目录', async () => {
|
||||
vi.mocked(fs.access).mockResolvedValue(undefined);
|
||||
vi.mocked(fs.readdir).mockResolvedValue([
|
||||
{ name: '.hidden', isFile: () => false, isDirectory: () => true },
|
||||
{ name: 'visible.md', isFile: () => true, isDirectory: () => false },
|
||||
] as any);
|
||||
|
||||
vi.mocked(fs.readFile).mockResolvedValue('Template');
|
||||
|
||||
const commands = await loader.loadFromDirectory('/test', 'user');
|
||||
|
||||
expect(commands.length).toBe(1);
|
||||
expect(commands[0].name).toBe('visible');
|
||||
});
|
||||
});
|
||||
|
||||
describe('目录路径', () => {
|
||||
it('getUserCommandsDir 返回用户 Commands 目录', () => {
|
||||
const originalHome = process.env.HOME;
|
||||
process.env.HOME = '/home/testuser';
|
||||
|
||||
const dir = loader.getUserCommandsDir();
|
||||
|
||||
expect(dir).toBe('/home/testuser/.config/ai-terminal/commands');
|
||||
|
||||
process.env.HOME = originalHome;
|
||||
});
|
||||
|
||||
it('getProjectCommandsDir 返回项目 Commands 目录', () => {
|
||||
const dir = loader.getProjectCommandsDir('/workspace');
|
||||
|
||||
expect(dir).toBe('/workspace/.ai-terminal/commands');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user