import { describe, expect, it } from 'vitest'; import fs from 'fs'; import fsPromises from 'fs/promises'; import os from 'os'; import path from 'path'; import { getSkillSources, mergeDiscoveredSkills, renameSkill } from './skills.js'; describe('skills', () => { it('merges locally discovered skills missing from OpenCode live discovery', () => { const merged = mergeDiscoveredSkills( [ { name: 'existing-opencode-skill', path: '/home/jkker/.config/opencode/skills/existing-opencode-skill/SKILL.md', source: 'opencode' }, { name: 'existing-agent-skill', path: '/home/jkker/.agents/skills/existing-agent-skill/SKILL.md', source: 'agents' }, ], [ { name: 'existing-agent-skill', path: '/home/jkker/.agents/skills/existing-agent-skill/SKILL.md', source: 'agents' }, { name: 'new-agent-skill', path: '/home/jkker/.agents/skills/new-agent-skill/SKILL.md', source: 'agents' }, ], ); expect(merged.map((skill) => skill.name)).toEqual([ 'existing-opencode-skill', 'existing-agent-skill', 'new-agent-skill', ]); }); it('resolves built-in OpenCode skill content without parsing virtual locations as files', () => { const sources = getSkillSources( 'customize-opencode', '/tmp/openchamber-skills-test-missing-project', { name: 'customize-opencode', path: '', scope: 'user', source: 'opencode', description: 'Customize opencode', content: '# Customizing opencode\n\nUse this skill when updating config.', }, ); expect(sources.md.exists).toBe(true); expect(sources.md.path).toBe(null); expect(sources.md.dir).toBe(null); expect(sources.md.scope).toBe('user'); expect(sources.md.source).toBe('opencode'); expect(sources.md.description).toBe('Customize opencode'); expect(sources.md.instructions).toBe('# Customizing opencode\n\nUse this skill when updating config.'); expect(sources.md.fields).toEqual(['description', 'instructions']); }); it('clears file metadata when a discovered skill path is unreadable', () => { const missingPath = path.join(os.tmpdir(), 'openchamber-skills-test-missing-file', 'SKILL.md'); const sources = getSkillSources( 'missing-agent-skill', '/tmp/openchamber-skills-test-missing-project', { name: 'missing-agent-skill', path: missingPath, scope: 'user', source: 'agents', description: 'Missing skill', }, ); expect(sources.md.exists).toBe(false); expect(sources.md.path).toBe(null); expect(sources.md.dir).toBe(null); expect(sources.md.scope).toBe(null); expect(sources.md.source).toBe(null); expect(sources.md.description).toBe('Missing skill'); expect(sources.md.instructions).toBe(''); }); it('enriches discovered skills when their location is a real markdown file', async () => { const tempRoot = await fsPromises.mkdtemp(path.join(os.tmpdir(), 'oc-skills-')); const skillDir = path.join(tempRoot, 'example-skill'); const skillPath = path.join(skillDir, 'SKILL.md'); try { await fsPromises.mkdir(skillDir, { recursive: true }); await fsPromises.writeFile( skillPath, [ '---', 'name: example-skill', 'description: Example from agents', '---', '', 'Use this skill for examples.', '', ].join('\n'), 'utf8', ); const sources = getSkillSources('example-skill', tempRoot, { name: 'example-skill', path: skillPath, scope: 'user', source: 'agents', description: 'Fallback description', }); expect(sources.md.exists).toBe(true); expect(sources.md.path).toBe(skillPath); expect(sources.md.scope).toBe('user'); expect(sources.md.source).toBe('agents'); expect(sources.md.description).toBe('Example from agents'); expect(sources.md.instructions).toBe('Use this skill for examples.'); } finally { await fsPromises.rm(tempRoot, { recursive: true, force: true }); } }); it('renames a skill directory while preserving SKILL.md body and supporting files', async () => { const tempRoot = await fsPromises.mkdtemp(path.join(os.tmpdir(), 'oc-skills-rename-')); const projectRoot = path.join(tempRoot, 'project'); const skillDir = path.join(projectRoot, '.opencode', 'skills', 'original-skill'); const skillPath = path.join(skillDir, 'SKILL.md'); const supportPath = path.join(skillDir, 'notes.md'); const body = [ '# Original Skill', '', 'Preserve this non-trivial body across rename.', '', '## Details', '', '- step one', '- step two', ].join('\n'); try { await fsPromises.mkdir(skillDir, { recursive: true }); await fsPromises.writeFile( skillPath, [ '---', 'name: original-skill', 'description: Original skill description', 'license: MIT', '---', '', body, '', ].join('\n'), 'utf8', ); await fsPromises.writeFile(supportPath, 'supporting file contents\n', 'utf8'); renameSkill('original-skill', 'renamed-skill', projectRoot); const renamedDir = path.join(projectRoot, '.opencode', 'skills', 'renamed-skill'); const renamedPath = path.join(renamedDir, 'SKILL.md'); const renamedSupportPath = path.join(renamedDir, 'notes.md'); expect(fs.existsSync(skillDir)).toBe(false); expect(fs.existsSync(renamedPath)).toBe(true); expect(fs.existsSync(renamedSupportPath)).toBe(true); const sources = getSkillSources('renamed-skill', projectRoot, { name: 'renamed-skill', path: renamedPath, scope: 'project', source: 'opencode', description: 'fallback', }); expect(sources.md.exists).toBe(true); expect(sources.md.name).toBe('renamed-skill'); expect(sources.md.description).toBe('Original skill description'); expect(sources.md.instructions).toBe(body); expect(await fsPromises.readFile(renamedSupportPath, 'utf8')).toBe('supporting file contents\n'); const raw = await fsPromises.readFile(renamedPath, 'utf8'); expect(raw).toContain('license: MIT'); expect(raw).not.toContain('Renamed skill'); } finally { await fsPromises.rm(tempRoot, { recursive: true, force: true }); } }); });