import { readFileSync, existsSync } from "node:fs"; import { resolve } from "node:path"; import { describe, it, expect } from "vitest"; import yaml from "js-yaml"; const SKILLS_ROOT = resolve(__dirname, "../skills"); interface SkillFrontmatter { name?: string; description?: string; "argument-hint"?: string; "user-invocable"?: boolean; [key: string]: unknown; } function parseSkillMd(skillDir: string): { frontmatter: SkillFrontmatter; body: string; } { const filePath = resolve(SKILLS_ROOT, skillDir, "SKILL.md"); const raw = readFileSync(filePath, "utf-8"); const match = raw.match(/^---\n([\s\S]*?)\n---\n([\s\S]*)$/); if (!match) { throw new Error(`Invalid SKILL.md format in ${skillDir}: no frontmatter`); } return { frontmatter: yaml.load(match[1]) as SkillFrontmatter, body: match[2], }; } const INTERACTIVE_SKILLS = [ { dir: "estimate", mcpTool: "fathom_estimate_task_cost", }, { dir: "recommend-model", mcpTool: "fathom_recommend_model", }, { dir: "budget", mcpTool: "fathom_check_budget", }, { dir: "status", mcpTool: "fathom_get_project_status", }, { dir: "projections", mcpTool: "fathom_generate_tool_configs", }, { dir: "gsd", mcpTool: "fathom_estimate_task_cost", }, ]; const BACKGROUND_SKILLS = [ { dir: "cost-preview", mcpTool: "fathom_estimate_task_cost", }, { dir: "budget-warning", mcpTool: "fathom_check_budget", }, ]; const ALL_SKILLS = [...INTERACTIVE_SKILLS, ...BACKGROUND_SKILLS]; for (const skill of INTERACTIVE_SKILLS) { describe(skill.dir, () => { const skillPath = resolve(SKILLS_ROOT, skill.dir, "SKILL.md"); it("SKILL.md exists", () => { expect(existsSync(skillPath)).toBe(true); }); it("has valid frontmatter with required fields", () => { const { frontmatter } = parseSkillMd(skill.dir); expect(frontmatter.name).toBeTypeOf("string"); expect(frontmatter.name!.length).toBeGreaterThan(0); expect(frontmatter.description).toBeTypeOf("string"); expect(frontmatter.description!.length).toBeGreaterThan(0); expect(frontmatter.description!.length).toBeLessThan(300); }); it("is user-invocable (not false)", () => { const { frontmatter } = parseSkillMd(skill.dir); expect(frontmatter["user-invocable"]).not.toBe(false); }); it("has argument-hint for interactive use", () => { const { frontmatter } = parseSkillMd(skill.dir); expect(frontmatter["argument-hint"]).toBeTypeOf("string"); }); it(`references MCP tool ${skill.mcpTool}`, () => { const { body } = parseSkillMd(skill.dir); expect(body).toContain(skill.mcpTool); }); }); } for (const skill of BACKGROUND_SKILLS) { describe(skill.dir, () => { const skillPath = resolve(SKILLS_ROOT, skill.dir, "SKILL.md"); it("SKILL.md exists", () => { expect(existsSync(skillPath)).toBe(true); }); it("has valid frontmatter with required fields", () => { const { frontmatter } = parseSkillMd(skill.dir); expect(frontmatter.name).toBeTypeOf("string"); expect(frontmatter.name!.length).toBeGreaterThan(0); expect(frontmatter.description).toBeTypeOf("string"); expect(frontmatter.description!.length).toBeGreaterThan(0); expect(frontmatter.description!.length).toBeLessThan(300); }); it("user-invocable is exactly false", () => { const { frontmatter } = parseSkillMd(skill.dir); expect(frontmatter["user-invocable"]).toBe(false); }); it(`references MCP tool ${skill.mcpTool}`, () => { const { body } = parseSkillMd(skill.dir); expect(body).toContain(skill.mcpTool); }); }); }