import { describe, it, expect } from "vitest";
import { parseSkillContent, validateSkill, toPromptXML, Skill } from "./skills.ts";
describe("skills", () => {
describe("parseSkillContent", () => {
it("parses a full skill file", () => {
const content = `---
name: pdf-processing
description: Extracts text and tables from PDF files.
license: MIT
compatibility: Requires python 3.8+
metadata:
author: test-org
version: "1.0"
---
# PDF Processing
Use this skill when working with PDFs.
`;
const skill = parseSkillContent(content, "/skills/pdf-processing/SKILL.md");
expect(skill).not.toBeNull();
expect(skill!.name).toBe("pdf-processing");
expect(skill!.description).toBe("Extracts text and tables from PDF files.");
expect(skill!.license).toBe("MIT");
expect(skill!.compatibility).toBe("Requires python 3.8+");
expect(skill!.metadata).toEqual({ author: "test-org", version: "1.0" });
expect(skill!.instructions).toBe(
"# PDF Processing\n\nUse this skill when working with PDFs.",
);
expect(skill!.path).toBe("/skills/pdf-processing");
expect(skill!.skillFilePath).toBe("/skills/pdf-processing/SKILL.md");
});
it("parses a minimal skill file", () => {
const content = `---
name: my-skill
description: A simple skill.
---
Instructions here.
`;
const skill = parseSkillContent(content, "/skills/my-skill/SKILL.md");
expect(skill).not.toBeNull();
expect(skill!.name).toBe("my-skill");
expect(skill!.description).toBe("A simple skill.");
expect(skill!.instructions).toBe("Instructions here.");
});
it("returns null for missing frontmatter", () => {
const content = "# Just Markdown\n\nNo frontmatter here.";
const skill = parseSkillContent(content, "/test/SKILL.md");
expect(skill).toBeNull();
});
it("returns null for unclosed frontmatter", () => {
const content = `---
name: broken
description: Missing closing delimiter
`;
const skill = parseSkillContent(content, "/test/SKILL.md");
expect(skill).toBeNull();
});
it("returns null for missing required fields", () => {
const content = `---
name: no-description
---
Content here.
`;
const skill = parseSkillContent(content, "/test/SKILL.md");
expect(skill).toBeNull();
});
it("handles CRLF line endings", () => {
const content =
"---\r\nname: crlf-skill\r\ndescription: Works with Windows line endings.\r\n---\r\n\r\nInstructions.";
const skill = parseSkillContent(content, "/test/SKILL.md");
expect(skill).not.toBeNull();
expect(skill!.name).toBe("crlf-skill");
});
});
describe("validateSkill", () => {
it("validates a correct skill", () => {
const skill: Skill = {
name: "valid-skill",
description: "A valid skill description.",
instructions: "Do things.",
path: "/skills/valid-skill",
skillFilePath: "/skills/valid-skill/SKILL.md",
};
const errors = validateSkill(skill);
expect(errors).toHaveLength(0);
});
it("requires name", () => {
const skill: Skill = {
name: "",
description: "Has description.",
instructions: "",
path: "",
skillFilePath: "",
};
const errors = validateSkill(skill);
expect(errors).toContain("name is required");
});
it("requires description", () => {
const skill: Skill = {
name: "no-desc",
description: "",
instructions: "",
path: "/skills/no-desc",
skillFilePath: "",
};
const errors = validateSkill(skill);
expect(errors).toContain("description is required");
});
it("validates name format", () => {
const skill: Skill = {
name: "-invalid-name",
description: "Description.",
instructions: "",
path: "",
skillFilePath: "",
};
const errors = validateSkill(skill);
expect(errors.some(e => e.includes("alphanumeric"))).toBe(true);
});
it("validates name matches directory", () => {
const skill: Skill = {
name: "skill-one",
description: "Description.",
instructions: "",
path: "/skills/skill-two",
skillFilePath: "",
};
const errors = validateSkill(skill);
expect(errors.some(e => e.includes("must match directory"))).toBe(true);
});
it("validates name length", () => {
const skill: Skill = {
name: "a".repeat(65),
description: "Description.",
instructions: "",
path: "",
skillFilePath: "",
};
const errors = validateSkill(skill);
expect(errors.some(e => e.includes("exceeds 64"))).toBe(true);
});
it("validates description length", () => {
const skill: Skill = {
name: "test",
description: "a".repeat(1025),
instructions: "",
path: "/skills/test",
skillFilePath: "",
};
const errors = validateSkill(skill);
expect(errors.some(e => e.includes("description exceeds"))).toBe(true);
});
it("validates compatibility length", () => {
const skill: Skill = {
name: "test",
description: "Description.",
compatibility: "a".repeat(501),
instructions: "",
path: "/skills/test",
skillFilePath: "",
};
const errors = validateSkill(skill);
expect(errors.some(e => e.includes("compatibility exceeds"))).toBe(true);
});
it("allows mixed case names", () => {
const skill: Skill = {
name: "MySkill",
description: "Description.",
instructions: "",
path: "/skills/MySkill",
skillFilePath: "",
};
const errors = validateSkill(skill);
expect(errors).toHaveLength(0);
});
});
describe("toPromptXML", () => {
it("generates XML for skills", () => {
const skills: Skill[] = [
{
name: "pdf-processing",
description: "Extracts text from PDFs.",
instructions: "",
path: "/skills/pdf-processing",
skillFilePath: "/skills/pdf-processing/SKILL.md",
},
{
name: "data-analysis",
description: "Analyzes datasets & charts.",
instructions: "",
path: "/skills/data-analysis",
skillFilePath: "/skills/data-analysis/SKILL.md",
},
];
const xml = toPromptXML(skills);
expect(xml).toContain("");
expect(xml).toContain("");
expect(xml).toContain("pdf-processing");
expect(xml).toContain("Extracts text from PDFs.");
expect(xml).toContain("/skills/pdf-processing/SKILL.md");
expect(xml).toContain("&"); // XML escaping for &
});
it("returns empty string for empty skills array", () => {
expect(toPromptXML([])).toBe("");
});
it("escapes XML special characters", () => {
const skills: Skill[] = [
{
name: "test-skill",
description: 'Uses & "quotes"',
instructions: "",
path: "/skills/test-skill",
skillFilePath: "/skills/test-skill/SKILL.md",
},
];
const xml = toPromptXML(skills);
expect(xml).toContain("<tags>");
expect(xml).toContain("&");
expect(xml).toContain(""quotes"");
});
});
});