Files
openchamber/packages/web/server/lib/opencode/skills.test.js
T
Cursor AgentandSerhii Dziupin f0591515fd fix(skills): preserve SKILL.md content when renaming
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>
2026-08-03 07:02:24 +00:00

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