feat: sync shared skills contract and graph-first discipline
This commit is contained in:
@@ -0,0 +1,149 @@
|
||||
import fs from 'node:fs';
|
||||
import os from 'node:os';
|
||||
import path from 'node:path';
|
||||
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
||||
|
||||
const SKILL_FIXTURES = {
|
||||
onboarding: `---
|
||||
name: Onboarding
|
||||
description: "Initial setup guidance."
|
||||
---
|
||||
|
||||
# Onboarding
|
||||
`,
|
||||
'create-skill': `---
|
||||
name: Create Skill
|
||||
description: "Create or rewrite a reusable skill."
|
||||
---
|
||||
|
||||
# Create Skill
|
||||
`,
|
||||
refine: `---
|
||||
name: Refine
|
||||
description: "Refine a node or small set of nodes."
|
||||
---
|
||||
|
||||
# Refine
|
||||
`,
|
||||
};
|
||||
|
||||
const LEGACY_AUDIT_SKILL = `---
|
||||
name: Audit
|
||||
description: "Legacy audit skill."
|
||||
---
|
||||
|
||||
# Audit
|
||||
`;
|
||||
|
||||
const CUSTOM_SKILL = `---
|
||||
name: Custom Capture
|
||||
description: "A custom user skill."
|
||||
---
|
||||
|
||||
# Custom Capture
|
||||
`;
|
||||
|
||||
let originalCwd: string;
|
||||
let tempRoot: string;
|
||||
let tempHome: string;
|
||||
|
||||
function writeFile(filepath: string, content: string) {
|
||||
fs.mkdirSync(path.dirname(filepath), { recursive: true });
|
||||
fs.writeFileSync(filepath, content, 'utf8');
|
||||
}
|
||||
|
||||
function createCanonicalSkillSet(rootDir: string) {
|
||||
const skillDir = path.join(rootDir, 'src/config/skills');
|
||||
for (const [name, content] of Object.entries(SKILL_FIXTURES)) {
|
||||
writeFile(path.join(skillDir, `${name}.md`), content);
|
||||
}
|
||||
}
|
||||
|
||||
async function loadSkillService() {
|
||||
vi.resetModules();
|
||||
vi.doMock('os', () => ({
|
||||
default: { homedir: () => tempHome },
|
||||
homedir: () => tempHome,
|
||||
}));
|
||||
|
||||
return import('@/services/skills/skillService');
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
originalCwd = process.cwd();
|
||||
tempRoot = fs.mkdtempSync(path.join(os.tmpdir(), 'rah-skill-root-'));
|
||||
tempHome = fs.mkdtempSync(path.join(os.tmpdir(), 'rah-skill-home-'));
|
||||
createCanonicalSkillSet(tempRoot);
|
||||
process.chdir(tempRoot);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
process.chdir(originalCwd);
|
||||
vi.doUnmock('os');
|
||||
vi.resetModules();
|
||||
fs.rmSync(tempRoot, { recursive: true, force: true });
|
||||
fs.rmSync(tempHome, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
describe('skillService', () => {
|
||||
it('seeds the canonical three-skill set into the live skills folder', async () => {
|
||||
const skillService = await loadSkillService();
|
||||
|
||||
const skills = skillService.listSkills();
|
||||
|
||||
expect(skills.map((skill) => skill.name)).toEqual([
|
||||
'Create Skill',
|
||||
'Onboarding',
|
||||
'Refine',
|
||||
]);
|
||||
|
||||
const liveSkillDir = path.join(tempHome, 'Library/Application Support/RA-H/skills');
|
||||
expect(fs.readdirSync(liveSkillDir).sort()).toEqual([
|
||||
'.seed-migrated-2026-03-07-skills-overhaul',
|
||||
'create-skill.md',
|
||||
'onboarding.md',
|
||||
'refine.md',
|
||||
]);
|
||||
});
|
||||
|
||||
it('hard-prunes retired built-in skills while preserving non-retired custom skills', async () => {
|
||||
const liveSkillDir = path.join(tempHome, 'Library/Application Support/RA-H/skills');
|
||||
writeFile(path.join(liveSkillDir, 'audit.md'), LEGACY_AUDIT_SKILL);
|
||||
writeFile(path.join(liveSkillDir, 'custom-capture.md'), CUSTOM_SKILL);
|
||||
|
||||
const skillService = await loadSkillService();
|
||||
|
||||
const skills = skillService.listSkills();
|
||||
|
||||
expect(skills.map((skill) => skill.name)).toEqual([
|
||||
'Create Skill',
|
||||
'Custom Capture',
|
||||
'Onboarding',
|
||||
'Refine',
|
||||
]);
|
||||
expect(fs.existsSync(path.join(liveSkillDir, 'audit.md'))).toBe(false);
|
||||
expect(fs.existsSync(path.join(liveSkillDir, 'custom-capture.md'))).toBe(true);
|
||||
});
|
||||
|
||||
it('supports normalized skill CRUD against the live skills folder', async () => {
|
||||
const skillService = await loadSkillService();
|
||||
|
||||
const content = `---
|
||||
name: Capture Source
|
||||
description: "Preserve raw source while writing a strong description."
|
||||
---
|
||||
|
||||
# Capture Source
|
||||
`;
|
||||
|
||||
expect(skillService.writeSkill('Capture Source', content)).toEqual({ success: true });
|
||||
|
||||
const created = skillService.readSkill('capture-source');
|
||||
expect(created?.name).toBe('Capture Source');
|
||||
expect(created?.description).toContain('Preserve raw source');
|
||||
expect(created?.content).toContain('# Capture Source');
|
||||
|
||||
expect(skillService.deleteSkill('Capture Source')).toEqual({ success: true });
|
||||
expect(skillService.readSkill('capture-source')).toBeNull();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user