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,193 @@
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { SkillLoader } from '../../../src/skills/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('SkillLoader - Skill 加载器', () => {
|
||||
let loader: SkillLoader;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
loader = new SkillLoader();
|
||||
});
|
||||
|
||||
describe('loadFromFile - 从文件加载', () => {
|
||||
it('加载 YAML 格式的 Skill', async () => {
|
||||
const yamlContent = `
|
||||
skill:
|
||||
name: test-skill
|
||||
description: 测试 Skill
|
||||
promptTemplate: "提示模板 {{param}}"
|
||||
parameters:
|
||||
param:
|
||||
type: string
|
||||
required: true
|
||||
description: 参数描述
|
||||
`;
|
||||
|
||||
vi.mocked(fs.readFile).mockResolvedValue(yamlContent);
|
||||
|
||||
const skill = await loader.loadFromFile('/test/skill.yaml', 'user');
|
||||
|
||||
expect(skill).not.toBeNull();
|
||||
expect(skill?.name).toBe('test-skill');
|
||||
expect(skill?.description).toBe('测试 Skill');
|
||||
expect(skill?.source).toBe('user');
|
||||
expect(skill?.parameters?.param.required).toBe(true);
|
||||
});
|
||||
|
||||
it('加载 JSON 格式的 Skill', async () => {
|
||||
const jsonContent = JSON.stringify({
|
||||
skill: {
|
||||
name: 'json-skill',
|
||||
description: 'JSON Skill',
|
||||
promptTemplate: '提示',
|
||||
},
|
||||
});
|
||||
|
||||
vi.mocked(fs.readFile).mockResolvedValue(jsonContent);
|
||||
|
||||
const skill = await loader.loadFromFile('/test/skill.json', 'project');
|
||||
|
||||
expect(skill).not.toBeNull();
|
||||
expect(skill?.name).toBe('json-skill');
|
||||
expect(skill?.source).toBe('project');
|
||||
});
|
||||
|
||||
it('加载 Markdown 格式的 Skill(带 frontmatter)', async () => {
|
||||
const mdContent = `---
|
||||
name: md-skill
|
||||
description: Markdown Skill
|
||||
parameters:
|
||||
code:
|
||||
type: string
|
||||
required: true
|
||||
description: 代码
|
||||
---
|
||||
|
||||
# 代码审查
|
||||
|
||||
请审查以下代码:
|
||||
|
||||
{{code}}
|
||||
`;
|
||||
|
||||
vi.mocked(fs.readFile).mockResolvedValue(mdContent);
|
||||
|
||||
const skill = await loader.loadFromFile('/test/skill.md', 'user');
|
||||
|
||||
expect(skill).not.toBeNull();
|
||||
expect(skill?.name).toBe('md-skill');
|
||||
expect(skill?.description).toBe('Markdown Skill');
|
||||
expect(skill?.promptTemplate).toContain('{{code}}');
|
||||
expect(skill?.promptTemplate).toContain('代码审查');
|
||||
});
|
||||
|
||||
it('加载 Markdown 格式的 Skill(无 frontmatter)', async () => {
|
||||
const mdContent = `# 简单提示
|
||||
|
||||
这是一个简单的提示模板。
|
||||
|
||||
{{input}}
|
||||
`;
|
||||
|
||||
vi.mocked(fs.readFile).mockResolvedValue(mdContent);
|
||||
|
||||
const skill = await loader.loadFromFile('/test/simple.md', 'user');
|
||||
|
||||
expect(skill).not.toBeNull();
|
||||
expect(skill?.name).toBe('simple'); // 从文件名获取
|
||||
expect(skill?.promptTemplate).toContain('简单提示');
|
||||
});
|
||||
|
||||
it('缺少必需字段时返回 null', async () => {
|
||||
const yamlContent = `
|
||||
skill:
|
||||
description: 没有 name 和 promptTemplate
|
||||
`;
|
||||
|
||||
vi.mocked(fs.readFile).mockResolvedValue(yamlContent);
|
||||
|
||||
const skill = await loader.loadFromFile('/test/invalid.yaml', 'user');
|
||||
|
||||
expect(skill).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe('loadFromDirectory - 从目录加载', () => {
|
||||
it('目录不存在时返回空数组', async () => {
|
||||
vi.mocked(fs.access).mockRejectedValue(new Error('Not found'));
|
||||
|
||||
const skills = await loader.loadFromDirectory('/non-existent', 'user');
|
||||
|
||||
expect(skills).toEqual([]);
|
||||
});
|
||||
|
||||
it('加载目录中的所有 Skill 文件', async () => {
|
||||
vi.mocked(fs.access).mockResolvedValue(undefined);
|
||||
vi.mocked(fs.readdir).mockResolvedValue([
|
||||
{ name: 'skill1.yaml', isFile: () => true, isDirectory: () => false },
|
||||
{ name: 'skill2.json', isFile: () => true, isDirectory: () => false },
|
||||
{ name: 'readme.txt', isFile: () => true, isDirectory: () => false }, // 不支持的格式
|
||||
] as any);
|
||||
|
||||
vi.mocked(fs.readFile)
|
||||
.mockResolvedValueOnce(`skill:\n name: skill1\n description: 1\n promptTemplate: t1`)
|
||||
.mockResolvedValueOnce(JSON.stringify({ skill: { name: 'skill2', description: '2', promptTemplate: 't2' } }));
|
||||
|
||||
const skills = await loader.loadFromDirectory('/test', 'user');
|
||||
|
||||
expect(skills.length).toBe(2);
|
||||
expect(skills.map((s) => s.name)).toContain('skill1');
|
||||
expect(skills.map((s) => s.name)).toContain('skill2');
|
||||
});
|
||||
|
||||
it('递归加载子目录', async () => {
|
||||
vi.mocked(fs.access).mockResolvedValue(undefined);
|
||||
vi.mocked(fs.readdir)
|
||||
.mockResolvedValueOnce([
|
||||
{ name: 'subdir', isFile: () => false, isDirectory: () => true },
|
||||
{ name: 'root.yaml', isFile: () => true, isDirectory: () => false },
|
||||
] as any)
|
||||
.mockResolvedValueOnce([
|
||||
{ name: 'sub.yaml', isFile: () => true, isDirectory: () => false },
|
||||
] as any);
|
||||
|
||||
vi.mocked(fs.readFile)
|
||||
.mockResolvedValueOnce(`skill:\n name: root\n description: root\n promptTemplate: root`)
|
||||
.mockResolvedValueOnce(`skill:\n name: sub\n description: sub\n promptTemplate: sub`);
|
||||
|
||||
const skills = await loader.loadFromDirectory('/test', 'user');
|
||||
|
||||
expect(skills.length).toBe(2);
|
||||
expect(skills.map((s) => s.name)).toContain('root');
|
||||
expect(skills.map((s) => s.name)).toContain('sub');
|
||||
});
|
||||
});
|
||||
|
||||
describe('目录路径', () => {
|
||||
it('getUserSkillsDir 返回用户 Skills 目录', () => {
|
||||
const originalHome = process.env.HOME;
|
||||
process.env.HOME = '/home/testuser';
|
||||
|
||||
const dir = loader.getUserSkillsDir();
|
||||
|
||||
expect(dir).toBe('/home/testuser/.config/ai-terminal/skills');
|
||||
|
||||
process.env.HOME = originalHome;
|
||||
});
|
||||
|
||||
it('getProjectSkillsDir 返回项目 Skills 目录', () => {
|
||||
const dir = loader.getProjectSkillsDir('/workspace');
|
||||
|
||||
expect(dir).toBe('/workspace/.ai-terminal/skills');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user