Files
waggle-os/tests/sidecar/skill-loader.test.ts
Oleg Maslov 0c3e2ead3b
Some checks failed
Installer Smoke / installer-smoke (push) Has been cancelled
moving
2026-09-02 10:10:29 +02:00

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