import { describe, it, expect, beforeEach, afterEach } from 'vitest'; import * as fs from 'node:fs'; import * as path from 'node:path'; import * as os from 'node:os'; import { createSkillTools } from '../src/skill-tools.js'; describe('skill-tools', () => { let tmpDir: string; let skillsDir: string; let tools: ReturnType; let reloadCount: number; beforeEach(() => { tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'waggle-skill-test-')); skillsDir = path.join(tmpDir, 'skills'); reloadCount = 0; tools = createSkillTools({ waggleHome: tmpDir, onSkillsChanged: () => { reloadCount++; }, }); }); afterEach(() => { fs.rmSync(tmpDir, { recursive: true, force: true }); }); function run(name: string, args: Record = {}) { const tool = tools.find(t => t.name === name); if (!tool) throw new Error(`Tool ${name} not found`); return tool.execute(args); } it('list_skills returns empty when no skills installed', async () => { const result = await run('list_skills'); expect(result).toContain('No skills or plugins installed'); }); it('create_skill creates a skill file', async () => { const result = await run('create_skill', { name: 'test-skill', content: '# Test Skill\nDo something useful.', }); expect(result).toContain('Created skill "test-skill"'); expect(fs.existsSync(path.join(skillsDir, 'test-skill.md'))).toBe(true); expect(reloadCount).toBe(1); }); it('create_skill rejects invalid names', async () => { const result = await run('create_skill', { name: '../etc/passwd', content: 'malicious', }); expect(result).toContain('Error'); }); it('list_skills shows created skills', async () => { fs.mkdirSync(skillsDir, { recursive: true }); fs.writeFileSync(path.join(skillsDir, 'my-skill.md'), '# My Skill\nInstructions here.'); const result = await run('list_skills'); expect(result).toContain('my-skill'); expect(result).toContain('Installed Skills (1)'); }); it('read_skill returns full content', async () => { fs.mkdirSync(skillsDir, { recursive: true }); const content = '# Full Content\nLots of instructions...'; fs.writeFileSync(path.join(skillsDir, 'readable.md'), content); const result = await run('read_skill', { name: 'readable' }); expect(result).toBe(content); }); it('delete_skill removes a skill', async () => { fs.mkdirSync(skillsDir, { recursive: true }); fs.writeFileSync(path.join(skillsDir, 'to-delete.md'), 'temp'); const result = await run('delete_skill', { name: 'to-delete' }); expect(result).toContain('Deleted'); expect(fs.existsSync(path.join(skillsDir, 'to-delete.md'))).toBe(false); expect(reloadCount).toBe(1); }); it('delete_skill rejects path traversal', async () => { const result = await run('delete_skill', { name: '../../etc' }); expect(result).toContain('Error'); }); it('search_skills finds matching installed skills', async () => { fs.mkdirSync(skillsDir, { recursive: true }); fs.writeFileSync(path.join(skillsDir, 'code-review.md'), '# Code Review\nReview code for quality issues.'); const result = await run('search_skills', { query: 'code review' }); expect(result).toContain('code-review'); expect(result).toContain('Matching Installed Skills'); }); it('search_skills suggests built-in tools', async () => { const result = await run('search_skills', { query: 'web search internet' }); expect(result).toContain('web_search'); expect(result).toContain('Built-in Tools'); }); it('search_skills shows suggestions when nothing matches', async () => { const result = await run('search_skills', { query: 'quantum physics simulation' }); expect(result).toContain('Suggestions'); expect(result).toContain('create_skill'); }); it('create_skill updates existing skill', async () => { await run('create_skill', { name: 'updatable', content: 'v1' }); const result = await run('create_skill', { name: 'updatable', content: 'v2' }); expect(result).toContain('Updated'); // P5/D4(iii): the shared service stamps a provenance frontmatter block, so // the body — not the whole file — carries the content. const written = fs.readFileSync(path.join(skillsDir, 'updatable.md'), 'utf-8'); expect(written).toContain('v2'); expect(written).toContain('initiator: agent'); }); });