Rename skills by moving the skill directory and updating frontmatter name instead of recreate-with-stub-description, which wiped the body and supporting files. Co-authored-by: Serhii Dziupin <makeittech@users.noreply.github.com>
182 lines
6.4 KiB
JavaScript
182 lines
6.4 KiB
JavaScript
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: '<built-in>',
|
|
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 });
|
|
}
|
|
});
|
|
});
|