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'); }); }); });