723558ff22
- 新增 Skill 类型定义和加载器,支持 YAML/JSON/Markdown 格式 - 实现 Skill 注册表,支持搜索、分类和优先级覆盖 - 添加 8 个内置 Skills: code-review, explain-code, generate-docs 等 - 创建 skill 和 skill_search 工具供 Agent 调用 - 支持从用户目录和项目目录加载自定义 Skills - 添加完整的单元测试覆盖
408 lines
10 KiB
TypeScript
408 lines
10 KiB
TypeScript
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);
|
|
});
|
|
});
|