121 lines
3.7 KiB
TypeScript
121 lines
3.7 KiB
TypeScript
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
import { SkillLoader, type Skill } from '../../sidecar/src/skill-loader.js';
|
|
import fs from 'fs';
|
|
import path from 'path';
|
|
import os from 'os';
|
|
|
|
describe('Skill Loader', () => {
|
|
let skillDir: string;
|
|
let loader: SkillLoader;
|
|
|
|
beforeEach(() => {
|
|
skillDir = path.join(os.tmpdir(), `waggle-skills-${Date.now()}`);
|
|
fs.mkdirSync(skillDir, { recursive: true });
|
|
loader = new SkillLoader([skillDir]);
|
|
});
|
|
|
|
afterEach(() => {
|
|
fs.rmSync(skillDir, { recursive: true, force: true });
|
|
});
|
|
|
|
it('discovers skills from SKILL.md files', () => {
|
|
const skillPath = path.join(skillDir, 'summarize');
|
|
fs.mkdirSync(skillPath, { recursive: true });
|
|
fs.writeFileSync(path.join(skillPath, 'SKILL.md'), `---
|
|
name: summarize
|
|
description: Summarize text into key points
|
|
model: haiku
|
|
---
|
|
|
|
You are a summarization expert. Given text, produce a concise summary.
|
|
`);
|
|
const skills = loader.discover();
|
|
expect(skills).toHaveLength(1);
|
|
expect(skills[0].name).toBe('summarize');
|
|
expect(skills[0].description).toBe('Summarize text into key points');
|
|
expect(skills[0].model).toBe('haiku');
|
|
expect(skills[0].prompt).toContain('summarization expert');
|
|
});
|
|
|
|
it('discovers skills from multiple directories', () => {
|
|
const dir2 = path.join(os.tmpdir(), `waggle-skills2-${Date.now()}`);
|
|
fs.mkdirSync(dir2, { recursive: true });
|
|
|
|
fs.mkdirSync(path.join(skillDir, 'skill-a'), { recursive: true });
|
|
fs.writeFileSync(path.join(skillDir, 'skill-a', 'SKILL.md'), `---
|
|
name: skill-a
|
|
description: First skill
|
|
---
|
|
Prompt A
|
|
`);
|
|
|
|
fs.mkdirSync(path.join(dir2, 'skill-b'), { recursive: true });
|
|
fs.writeFileSync(path.join(dir2, 'skill-b', 'SKILL.md'), `---
|
|
name: skill-b
|
|
description: Second skill
|
|
---
|
|
Prompt B
|
|
`);
|
|
|
|
const multiLoader = new SkillLoader([skillDir, dir2]);
|
|
const skills = multiLoader.discover();
|
|
expect(skills).toHaveLength(2);
|
|
fs.rmSync(dir2, { recursive: true, force: true });
|
|
});
|
|
|
|
it('ignores directories without SKILL.md', () => {
|
|
fs.mkdirSync(path.join(skillDir, 'empty-dir'), { recursive: true });
|
|
const skills = loader.discover();
|
|
expect(skills).toHaveLength(0);
|
|
});
|
|
|
|
it('handles invalid YAML frontmatter gracefully', () => {
|
|
fs.mkdirSync(path.join(skillDir, 'bad-skill'), { recursive: true });
|
|
fs.writeFileSync(path.join(skillDir, 'bad-skill', 'SKILL.md'), `No frontmatter here.`);
|
|
const skills = loader.discover();
|
|
expect(skills).toHaveLength(0);
|
|
});
|
|
|
|
it('parses optional tools field', () => {
|
|
fs.mkdirSync(path.join(skillDir, 'researcher'), { recursive: true });
|
|
fs.writeFileSync(path.join(skillDir, 'researcher', 'SKILL.md'), `---
|
|
name: researcher
|
|
description: Research topics
|
|
tools: WebSearch, WebFetch
|
|
model: sonnet
|
|
---
|
|
|
|
Research the given topic.
|
|
`);
|
|
const skills = loader.discover();
|
|
expect(skills[0].tools).toEqual(['WebSearch', 'WebFetch']);
|
|
});
|
|
|
|
it('deduplicates skills by name (later directory wins)', () => {
|
|
const dir2 = path.join(os.tmpdir(), `waggle-skills-dup-${Date.now()}`);
|
|
fs.mkdirSync(dir2, { recursive: true });
|
|
|
|
fs.mkdirSync(path.join(skillDir, 'dupe'), { recursive: true });
|
|
fs.writeFileSync(path.join(skillDir, 'dupe', 'SKILL.md'), `---
|
|
name: dupe
|
|
description: First version
|
|
---
|
|
First
|
|
`);
|
|
|
|
fs.mkdirSync(path.join(dir2, 'dupe'), { recursive: true });
|
|
fs.writeFileSync(path.join(dir2, 'dupe', 'SKILL.md'), `---
|
|
name: dupe
|
|
description: Second version
|
|
---
|
|
Second
|
|
`);
|
|
|
|
const multiLoader = new SkillLoader([skillDir, dir2]);
|
|
const skills = multiLoader.discover();
|
|
expect(skills).toHaveLength(1);
|
|
expect(skills[0].description).toBe('Second version');
|
|
fs.rmSync(dir2, { recursive: true, force: true });
|
|
});
|
|
});
|