feat: 添加 Skills 系统支持可复用提示模板
- 新增 Skill 类型定义和加载器,支持 YAML/JSON/Markdown 格式 - 实现 Skill 注册表,支持搜索、分类和优先级覆盖 - 添加 8 个内置 Skills: code-review, explain-code, generate-docs 等 - 创建 skill 和 skill_search 工具供 Agent 调用 - 支持从用户目录和项目目录加载自定义 Skills - 添加完整的单元测试覆盖
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');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,407 @@
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import {
|
||||
SkillRegistry,
|
||||
getSkillRegistry,
|
||||
resetSkillRegistry,
|
||||
} from '../../../src/skills/registry.js';
|
||||
import type { Skill } from '../../../src/skills/types.js';
|
||||
|
||||
// Mock loader to prevent file system access
|
||||
vi.mock('../../../src/skills/loader.js', () => ({
|
||||
skillLoader: {
|
||||
loadFromDirectory: vi.fn().mockResolvedValue([]),
|
||||
getUserSkillsDir: vi.fn().mockReturnValue('/mock/user/skills'),
|
||||
getProjectSkillsDir: vi.fn().mockReturnValue('/mock/project/skills'),
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock builtin skills
|
||||
vi.mock('../../../src/skills/builtin/index.js', () => ({
|
||||
builtinSkills: [
|
||||
{
|
||||
name: 'test-builtin',
|
||||
description: '内置测试 Skill',
|
||||
promptTemplate: '测试提示: {{param1}}',
|
||||
parameters: {
|
||||
param1: { type: 'string', required: true, description: '参数1' },
|
||||
},
|
||||
source: 'builtin',
|
||||
enabled: true,
|
||||
},
|
||||
],
|
||||
}));
|
||||
|
||||
describe('SkillRegistry - Skill 注册表', () => {
|
||||
let registry: SkillRegistry;
|
||||
|
||||
beforeEach(() => {
|
||||
resetSkillRegistry();
|
||||
registry = new SkillRegistry({ autoLoad: false });
|
||||
});
|
||||
|
||||
describe('register - 注册', () => {
|
||||
it('成功注册 Skill', () => {
|
||||
const skill: Skill = {
|
||||
name: 'test-skill',
|
||||
description: '测试 Skill',
|
||||
promptTemplate: '测试提示',
|
||||
source: 'user',
|
||||
};
|
||||
|
||||
registry.register(skill);
|
||||
expect(registry.has('test-skill')).toBe(true);
|
||||
});
|
||||
|
||||
it('高优先级 Skill 可以覆盖低优先级', () => {
|
||||
const builtinSkill: Skill = {
|
||||
name: 'same-name',
|
||||
description: '内置版本',
|
||||
promptTemplate: '内置提示',
|
||||
source: 'builtin',
|
||||
};
|
||||
|
||||
const projectSkill: Skill = {
|
||||
name: 'same-name',
|
||||
description: '项目版本',
|
||||
promptTemplate: '项目提示',
|
||||
source: 'project',
|
||||
};
|
||||
|
||||
registry.register(builtinSkill);
|
||||
registry.register(projectSkill);
|
||||
|
||||
const result = registry.get('same-name');
|
||||
expect(result?.source).toBe('project');
|
||||
});
|
||||
|
||||
it('低优先级 Skill 不能覆盖高优先级', () => {
|
||||
const projectSkill: Skill = {
|
||||
name: 'same-name',
|
||||
description: '项目版本',
|
||||
promptTemplate: '项目提示',
|
||||
source: 'project',
|
||||
};
|
||||
|
||||
const builtinSkill: Skill = {
|
||||
name: 'same-name',
|
||||
description: '内置版本',
|
||||
promptTemplate: '内置提示',
|
||||
source: 'builtin',
|
||||
};
|
||||
|
||||
registry.register(projectSkill);
|
||||
registry.register(builtinSkill);
|
||||
|
||||
const result = registry.get('same-name');
|
||||
expect(result?.source).toBe('project');
|
||||
});
|
||||
});
|
||||
|
||||
describe('get - 获取', () => {
|
||||
it('获取存在的 Skill', () => {
|
||||
const skill: Skill = {
|
||||
name: 'test-skill',
|
||||
description: '测试',
|
||||
promptTemplate: '提示',
|
||||
source: 'user',
|
||||
};
|
||||
|
||||
registry.register(skill);
|
||||
const result = registry.get('test-skill');
|
||||
|
||||
expect(result).toBeDefined();
|
||||
expect(result?.name).toBe('test-skill');
|
||||
});
|
||||
|
||||
it('获取不存在的 Skill 返回 undefined', () => {
|
||||
const result = registry.get('non-existent');
|
||||
expect(result).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe('search - 搜索', () => {
|
||||
beforeEach(() => {
|
||||
const skills: Skill[] = [
|
||||
{
|
||||
name: 'code-review',
|
||||
description: '代码审查',
|
||||
promptTemplate: '审查代码',
|
||||
keywords: ['review', 'quality'],
|
||||
category: 'development',
|
||||
source: 'builtin',
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
name: 'generate-tests',
|
||||
description: '生成测试',
|
||||
promptTemplate: '生成测试代码',
|
||||
keywords: ['test', 'unit'],
|
||||
category: 'testing',
|
||||
source: 'builtin',
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
name: 'explain-code',
|
||||
description: '解释代码功能',
|
||||
promptTemplate: '解释代码',
|
||||
category: 'development',
|
||||
source: 'builtin',
|
||||
enabled: true,
|
||||
},
|
||||
];
|
||||
|
||||
for (const skill of skills) {
|
||||
registry.register(skill);
|
||||
}
|
||||
});
|
||||
|
||||
it('按名称精确匹配', () => {
|
||||
const results = registry.search('code-review');
|
||||
expect(results.length).toBeGreaterThan(0);
|
||||
expect(results[0].skill.name).toBe('code-review');
|
||||
expect(results[0].score).toBe(100);
|
||||
});
|
||||
|
||||
it('按名称前缀匹配', () => {
|
||||
const results = registry.search('code');
|
||||
expect(results.length).toBe(2); // code-review, explain-code
|
||||
});
|
||||
|
||||
it('按描述匹配', () => {
|
||||
const results = registry.search('审查');
|
||||
expect(results.length).toBeGreaterThan(0);
|
||||
expect(results[0].skill.name).toBe('code-review');
|
||||
});
|
||||
|
||||
it('按关键词匹配', () => {
|
||||
const results = registry.search('quality');
|
||||
expect(results.length).toBeGreaterThan(0);
|
||||
expect(results[0].skill.name).toBe('code-review');
|
||||
});
|
||||
|
||||
it('按分类匹配', () => {
|
||||
const results = registry.search('testing');
|
||||
expect(results.length).toBeGreaterThan(0);
|
||||
expect(results[0].skill.name).toBe('generate-tests');
|
||||
});
|
||||
|
||||
it('限制结果数量', () => {
|
||||
const results = registry.search('code', 1);
|
||||
expect(results.length).toBe(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe('renderPrompt - 渲染模板', () => {
|
||||
it('成功渲染模板', () => {
|
||||
const skill: Skill = {
|
||||
name: 'test',
|
||||
description: '测试',
|
||||
promptTemplate: '你好 {{name}},请{{action}}',
|
||||
source: 'user',
|
||||
};
|
||||
|
||||
const result = registry.renderPrompt(skill, {
|
||||
name: '世界',
|
||||
action: '测试',
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.prompt).toBe('你好 世界,请测试');
|
||||
});
|
||||
|
||||
it('缺少必需参数时返回错误', () => {
|
||||
const skill: Skill = {
|
||||
name: 'test',
|
||||
description: '测试',
|
||||
promptTemplate: '{{param}}',
|
||||
parameters: {
|
||||
param: { type: 'string', required: true, description: '必需参数' },
|
||||
},
|
||||
source: 'user',
|
||||
};
|
||||
|
||||
const result = registry.renderPrompt(skill, {});
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toContain('缺少必需参数');
|
||||
});
|
||||
|
||||
it('使用默认值', () => {
|
||||
const skill: Skill = {
|
||||
name: 'test',
|
||||
description: '测试',
|
||||
promptTemplate: '值: {{param}}',
|
||||
parameters: {
|
||||
param: {
|
||||
type: 'string',
|
||||
required: false,
|
||||
default: '默认值',
|
||||
description: '可选参数',
|
||||
},
|
||||
},
|
||||
source: 'user',
|
||||
};
|
||||
|
||||
const result = registry.renderPrompt(skill, {});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.prompt).toBe('值: 默认值');
|
||||
});
|
||||
|
||||
it('保留未匹配的变量', () => {
|
||||
const skill: Skill = {
|
||||
name: 'test',
|
||||
description: '测试',
|
||||
promptTemplate: '{{known}} 和 {{unknown}}',
|
||||
source: 'user',
|
||||
};
|
||||
|
||||
const result = registry.renderPrompt(skill, { known: '已知' });
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.prompt).toBe('已知 和 {{unknown}}');
|
||||
});
|
||||
});
|
||||
|
||||
describe('execute - 执行', () => {
|
||||
it('成功执行 Skill', () => {
|
||||
const skill: Skill = {
|
||||
name: 'test',
|
||||
description: '测试',
|
||||
promptTemplate: '提示: {{param}}',
|
||||
source: 'user',
|
||||
enabled: true,
|
||||
};
|
||||
|
||||
registry.register(skill);
|
||||
const result = registry.execute('test', { param: '值' });
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.prompt).toBe('提示: 值');
|
||||
});
|
||||
|
||||
it('Skill 不存在时返回错误', () => {
|
||||
const result = registry.execute('non-existent', {});
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toContain('不存在');
|
||||
});
|
||||
|
||||
it('Skill 禁用时返回错误', () => {
|
||||
const skill: Skill = {
|
||||
name: 'disabled',
|
||||
description: '禁用的 Skill',
|
||||
promptTemplate: '提示',
|
||||
source: 'user',
|
||||
enabled: false,
|
||||
};
|
||||
|
||||
registry.register(skill);
|
||||
const result = registry.execute('disabled', {});
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toContain('禁用');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getByCategory - 按分类获取', () => {
|
||||
it('返回指定分类的 Skills', () => {
|
||||
const skills: Skill[] = [
|
||||
{
|
||||
name: 'skill1',
|
||||
description: '1',
|
||||
promptTemplate: '1',
|
||||
category: 'dev',
|
||||
source: 'builtin',
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
name: 'skill2',
|
||||
description: '2',
|
||||
promptTemplate: '2',
|
||||
category: 'dev',
|
||||
source: 'builtin',
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
name: 'skill3',
|
||||
description: '3',
|
||||
promptTemplate: '3',
|
||||
category: 'test',
|
||||
source: 'builtin',
|
||||
enabled: true,
|
||||
},
|
||||
];
|
||||
|
||||
for (const skill of skills) {
|
||||
registry.register(skill);
|
||||
}
|
||||
|
||||
const devSkills = registry.getByCategory('dev');
|
||||
expect(devSkills.length).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getStats - 统计信息', () => {
|
||||
it('返回正确的统计信息', () => {
|
||||
const skills: Skill[] = [
|
||||
{
|
||||
name: 'builtin1',
|
||||
description: '1',
|
||||
promptTemplate: '1',
|
||||
category: 'dev',
|
||||
source: 'builtin',
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
name: 'user1',
|
||||
description: '2',
|
||||
promptTemplate: '2',
|
||||
category: 'dev',
|
||||
source: 'user',
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
name: 'disabled1',
|
||||
description: '3',
|
||||
promptTemplate: '3',
|
||||
source: 'builtin',
|
||||
enabled: false,
|
||||
},
|
||||
];
|
||||
|
||||
for (const skill of skills) {
|
||||
registry.register(skill);
|
||||
}
|
||||
|
||||
const stats = registry.getStats();
|
||||
|
||||
expect(stats.total).toBe(3);
|
||||
expect(stats.enabled).toBe(2);
|
||||
expect(stats.bySource.builtin).toBe(2);
|
||||
expect(stats.bySource.user).toBe(1);
|
||||
expect(stats.byCategory.dev).toBe(2);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getSkillRegistry - 全局注册表', () => {
|
||||
beforeEach(() => {
|
||||
resetSkillRegistry();
|
||||
});
|
||||
|
||||
it('返回单例实例', () => {
|
||||
const registry1 = getSkillRegistry();
|
||||
const registry2 = getSkillRegistry();
|
||||
|
||||
expect(registry1).toBe(registry2);
|
||||
});
|
||||
|
||||
it('resetSkillRegistry 重置实例', () => {
|
||||
const registry1 = getSkillRegistry();
|
||||
resetSkillRegistry();
|
||||
const registry2 = getSkillRegistry();
|
||||
|
||||
expect(registry1).not.toBe(registry2);
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,147 @@
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { skillTool } from '../../../../src/tools/skill/skill.js';
|
||||
import { getSkillRegistry, resetSkillRegistry } from '../../../../src/skills/registry.js';
|
||||
import type { Skill } from '../../../../src/skills/types.js';
|
||||
|
||||
// Mock loader to prevent file system access
|
||||
vi.mock('../../../../src/skills/loader.js', () => ({
|
||||
skillLoader: {
|
||||
loadFromDirectory: vi.fn().mockResolvedValue([]),
|
||||
getUserSkillsDir: vi.fn().mockReturnValue('/mock/user/skills'),
|
||||
getProjectSkillsDir: vi.fn().mockReturnValue('/mock/project/skills'),
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock builtin skills
|
||||
vi.mock('../../../../src/skills/builtin/index.js', () => ({
|
||||
builtinSkills: [
|
||||
{
|
||||
name: 'code-review',
|
||||
description: '代码审查',
|
||||
promptTemplate: '请审查以下代码:\n\n{{code}}\n\n重点:{{focus}}',
|
||||
parameters: {
|
||||
code: { type: 'string', required: true, description: '代码' },
|
||||
focus: { type: 'string', required: false, default: '代码质量', description: '审查重点' },
|
||||
},
|
||||
category: 'development',
|
||||
source: 'builtin',
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
name: 'disabled-skill',
|
||||
description: '禁用的 Skill',
|
||||
promptTemplate: '不应该执行',
|
||||
source: 'builtin',
|
||||
enabled: false,
|
||||
},
|
||||
],
|
||||
}));
|
||||
|
||||
describe('skillTool - Skill 工具', () => {
|
||||
beforeEach(async () => {
|
||||
vi.clearAllMocks();
|
||||
resetSkillRegistry();
|
||||
// 初始化注册表
|
||||
const registry = getSkillRegistry();
|
||||
await registry.initialize();
|
||||
});
|
||||
|
||||
describe('工具定义', () => {
|
||||
it('有正确的名称', () => {
|
||||
expect(skillTool.name).toBe('skill');
|
||||
});
|
||||
|
||||
it('有正确的元数据', () => {
|
||||
expect(skillTool.metadata.category).toBe('agent');
|
||||
expect(skillTool.metadata.keywords).toContain('skill');
|
||||
});
|
||||
|
||||
it('skill_name 参数是必须的', () => {
|
||||
expect(skillTool.parameters.skill_name.required).toBe(true);
|
||||
});
|
||||
|
||||
it('params 参数是可选的', () => {
|
||||
expect(skillTool.parameters.params.required).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('execute - 执行', () => {
|
||||
it('成功执行 Skill 并渲染模板', async () => {
|
||||
const result = await skillTool.execute({
|
||||
skill_name: 'code-review',
|
||||
params: {
|
||||
code: 'function add(a, b) { return a + b; }',
|
||||
focus: '性能',
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.output).toContain('function add');
|
||||
expect(result.output).toContain('性能');
|
||||
expect(result.metadata?.skill).toBe('code-review');
|
||||
});
|
||||
|
||||
it('使用默认参数值', async () => {
|
||||
const result = await skillTool.execute({
|
||||
skill_name: 'code-review',
|
||||
params: {
|
||||
code: 'const x = 1;',
|
||||
},
|
||||
});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.output).toContain('代码质量'); // 默认值
|
||||
});
|
||||
|
||||
it('Skill 不存在时返回错误', async () => {
|
||||
const result = await skillTool.execute({
|
||||
skill_name: 'non-existent',
|
||||
params: {},
|
||||
});
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toContain('不存在');
|
||||
});
|
||||
|
||||
it('Skill 不存在时提供建议', async () => {
|
||||
const result = await skillTool.execute({
|
||||
skill_name: 'code-rev', // 类似 code-review
|
||||
params: {},
|
||||
});
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toContain('code-review'); // 建议
|
||||
});
|
||||
|
||||
it('缺少必需参数时返回错误', async () => {
|
||||
const result = await skillTool.execute({
|
||||
skill_name: 'code-review',
|
||||
params: {}, // 缺少 code 参数
|
||||
});
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toContain('缺少必需参数');
|
||||
});
|
||||
|
||||
it('禁用的 Skill 返回错误', async () => {
|
||||
const result = await skillTool.execute({
|
||||
skill_name: 'disabled-skill',
|
||||
params: {},
|
||||
});
|
||||
|
||||
expect(result.success).toBe(false);
|
||||
expect(result.error).toContain('禁用');
|
||||
});
|
||||
|
||||
it('返回正确的 metadata', async () => {
|
||||
const result = await skillTool.execute({
|
||||
skill_name: 'code-review',
|
||||
params: { code: 'test' },
|
||||
});
|
||||
|
||||
expect(result.metadata?.skill).toBe('code-review');
|
||||
expect(result.metadata?.category).toBe('development');
|
||||
expect(result.metadata?.source).toBe('builtin');
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,122 @@
|
||||
import { describe, it, expect, beforeEach, vi } from 'vitest';
|
||||
import { skillSearchTool } from '../../../../src/tools/skill/skill_search.js';
|
||||
import { getSkillRegistry, resetSkillRegistry } from '../../../../src/skills/registry.js';
|
||||
|
||||
// Mock loader to prevent file system access
|
||||
vi.mock('../../../../src/skills/loader.js', () => ({
|
||||
skillLoader: {
|
||||
loadFromDirectory: vi.fn().mockResolvedValue([]),
|
||||
getUserSkillsDir: vi.fn().mockReturnValue('/mock/user/skills'),
|
||||
getProjectSkillsDir: vi.fn().mockReturnValue('/mock/project/skills'),
|
||||
},
|
||||
}));
|
||||
|
||||
// Mock builtin skills
|
||||
vi.mock('../../../../src/skills/builtin/index.js', () => ({
|
||||
builtinSkills: [
|
||||
{
|
||||
name: 'code-review',
|
||||
description: '代码审查',
|
||||
promptTemplate: '审查代码',
|
||||
keywords: ['review', 'quality'],
|
||||
category: 'development',
|
||||
source: 'builtin',
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
name: 'generate-tests',
|
||||
description: '生成测试',
|
||||
promptTemplate: '生成测试',
|
||||
keywords: ['test', 'unit'],
|
||||
category: 'testing',
|
||||
source: 'builtin',
|
||||
enabled: true,
|
||||
},
|
||||
{
|
||||
name: 'explain-code',
|
||||
description: '解释代码功能',
|
||||
promptTemplate: '解释代码',
|
||||
category: 'development',
|
||||
source: 'builtin',
|
||||
enabled: true,
|
||||
},
|
||||
],
|
||||
}));
|
||||
|
||||
describe('skillSearchTool - Skill 搜索工具', () => {
|
||||
beforeEach(async () => {
|
||||
vi.clearAllMocks();
|
||||
resetSkillRegistry();
|
||||
const registry = getSkillRegistry();
|
||||
await registry.initialize();
|
||||
});
|
||||
|
||||
describe('工具定义', () => {
|
||||
it('有正确的名称', () => {
|
||||
expect(skillSearchTool.name).toBe('skill_search');
|
||||
});
|
||||
|
||||
it('有正确的元数据', () => {
|
||||
expect(skillSearchTool.metadata.category).toBe('agent');
|
||||
expect(skillSearchTool.metadata.keywords).toContain('search');
|
||||
});
|
||||
|
||||
it('所有参数都是可选的', () => {
|
||||
expect(skillSearchTool.parameters.query.required).toBe(false);
|
||||
expect(skillSearchTool.parameters.category.required).toBe(false);
|
||||
expect(skillSearchTool.parameters.list_all.required).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('execute - 执行', () => {
|
||||
it('list_all 列出所有 Skills', async () => {
|
||||
const result = await skillSearchTool.execute({ list_all: true });
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.output).toContain('code-review');
|
||||
expect(result.output).toContain('generate-tests');
|
||||
expect(result.output).toContain('explain-code');
|
||||
expect(result.output).toContain('development');
|
||||
expect(result.output).toContain('testing');
|
||||
});
|
||||
|
||||
it('按关键词搜索', async () => {
|
||||
const result = await skillSearchTool.execute({ query: 'review' });
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.output).toContain('code-review');
|
||||
expect(result.metadata?.query).toBe('review');
|
||||
});
|
||||
|
||||
it('按分类筛选', async () => {
|
||||
const result = await skillSearchTool.execute({ category: 'testing' });
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.output).toContain('generate-tests');
|
||||
expect(result.output).not.toContain('code-review');
|
||||
});
|
||||
|
||||
it('分类不存在时显示可用分类', async () => {
|
||||
const result = await skillSearchTool.execute({ category: 'non-existent' });
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.output).toContain('没有 Skills');
|
||||
expect(result.output).toContain('可用分类');
|
||||
});
|
||||
|
||||
it('搜索无结果时提示', async () => {
|
||||
const result = await skillSearchTool.execute({ query: 'xxxxxxxxx' });
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.output).toContain('没有找到');
|
||||
});
|
||||
|
||||
it('无参数时显示概览', async () => {
|
||||
const result = await skillSearchTool.execute({});
|
||||
|
||||
expect(result.success).toBe(true);
|
||||
expect(result.output).toContain('概览');
|
||||
expect(result.output).toContain('使用方法');
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user