/**
* Parser Tests
*
* Tests SKILL.md parsing and validation with real skills from otto.
*/
import { describe, it, expect } from "vitest";
import * as fs from "fs";
import * as path from "path";
import {
parseFrontmatter,
validateFrontmatter,
validateSkillDirectory,
extractBody,
estimateTokens,
generateSkillsXml,
} from "../parser";
import type { SkillFrontmatter } from "../types";
// Path to otto skills for testing
const OTTO_SKILLS_PATH = path.resolve(
__dirname,
"../../../../../otto/skills",
);
describe("parseFrontmatter", () => {
it("should parse simple frontmatter", () => {
const content = `---
name: test-skill
description: A test skill for testing purposes.
---
# Test Skill
Instructions here.
`;
const { frontmatter, body, raw } = parseFrontmatter(content);
expect(frontmatter).not.toBeNull();
expect(frontmatter?.name).toBe("test-skill");
expect(frontmatter?.description).toBe("A test skill for testing purposes.");
expect(body).toContain("# Test Skill");
expect(raw).toContain("name: test-skill");
});
it("should parse frontmatter with optional fields", () => {
const content = `---
name: advanced-skill
description: An advanced skill with all optional fields.
license: MIT
compatibility: Requires Python 3.10+
homepage: https://example.com
---
# Advanced Skill
`;
const { frontmatter } = parseFrontmatter(content);
expect(frontmatter?.license).toBe("MIT");
expect(frontmatter?.compatibility).toBe("Requires Python 3.10+");
expect(frontmatter?.homepage).toBe("https://example.com");
});
it("should return null frontmatter for content without frontmatter", () => {
const content = `# No Frontmatter
Just regular markdown.
`;
const { frontmatter, body } = parseFrontmatter(content);
expect(frontmatter).toBeNull();
expect(body).toContain("# No Frontmatter");
});
it("should parse real otto github skill", () => {
const skillPath = path.join(OTTO_SKILLS_PATH, "github", "SKILL.md");
if (fs.existsSync(skillPath)) {
const content = fs.readFileSync(skillPath, "utf-8");
const { frontmatter, body } = parseFrontmatter(content);
expect(frontmatter).not.toBeNull();
expect(frontmatter?.name).toBe("github");
expect(frontmatter?.description).toContain("gh");
expect(body).toContain("# GitHub Skill");
// Check Otto metadata
const ottoMeta = frontmatter?.metadata?.otto;
expect(ottoMeta).toBeDefined();
expect(ottoMeta?.requires?.bins).toContain("gh");
}
});
it("should parse real otto 1password skill with references", () => {
const skillPath = path.join(OTTO_SKILLS_PATH, "1password", "SKILL.md");
if (fs.existsSync(skillPath)) {
const content = fs.readFileSync(skillPath, "utf-8");
const { frontmatter, body } = parseFrontmatter(content);
expect(frontmatter).not.toBeNull();
expect(frontmatter?.name).toBe("1password");
const ottoMeta = frontmatter?.metadata?.otto;
expect(ottoMeta?.requires?.bins).toContain("op");
expect(body).toContain("references/");
}
});
});
describe("validateFrontmatter", () => {
it("should validate correct frontmatter", () => {
const fm: SkillFrontmatter = {
name: "valid-skill",
description: "A valid skill description that explains what it does.",
};
const result = validateFrontmatter(fm);
expect(result.valid).toBe(true);
expect(result.errors).toHaveLength(0);
});
it("should reject missing name", () => {
const fm: SkillFrontmatter = {
name: "",
description: "A description.",
};
const result = validateFrontmatter(fm);
expect(result.valid).toBe(false);
expect(result.errors.some((e) => e.code === "MISSING_NAME")).toBe(true);
});
it("should reject invalid name format", () => {
const fm: SkillFrontmatter = {
name: "Invalid-Name", // uppercase not allowed
description: "A description.",
};
const result = validateFrontmatter(fm);
expect(result.valid).toBe(false);
expect(result.errors.some((e) => e.code === "INVALID_NAME_FORMAT")).toBe(
true,
);
});
it("should reject name with consecutive hyphens", () => {
const fm: SkillFrontmatter = {
name: "invalid--name",
description: "A description.",
};
const result = validateFrontmatter(fm);
expect(result.valid).toBe(false);
expect(
result.errors.some((e) => e.code === "NAME_CONSECUTIVE_HYPHENS"),
).toBe(true);
});
it("should reject missing description", () => {
const fm: SkillFrontmatter = {
name: "valid-name",
description: "",
};
const result = validateFrontmatter(fm);
expect(result.valid).toBe(false);
expect(result.errors.some((e) => e.code === "MISSING_DESCRIPTION")).toBe(
true,
);
});
it("should warn about short description", () => {
const fm: SkillFrontmatter = {
name: "valid-name",
description: "Too short.",
};
const result = validateFrontmatter(fm);
expect(result.valid).toBe(true); // warnings don't make it invalid
expect(
result.warnings.some((w) => w.code === "DESCRIPTION_TOO_SHORT"),
).toBe(true);
});
it("should validate directory name match", () => {
const fm: SkillFrontmatter = {
name: "skill-name",
description: "A valid description that is long enough.",
};
const result = validateFrontmatter(fm, "different-name");
expect(result.valid).toBe(false);
expect(result.errors.some((e) => e.code === "NAME_MISMATCH")).toBe(true);
});
});
describe("extractBody", () => {
it("should extract body without frontmatter", () => {
const content = `---
name: test
description: Test skill.
---
# Main Content
This is the body.
`;
const body = extractBody(content);
expect(body).toBe("# Main Content\n\nThis is the body.");
expect(body).not.toContain("---");
expect(body).not.toContain("name: test");
});
});
describe("estimateTokens", () => {
it("should estimate tokens based on character count", () => {
const text = "This is some text that should be approximately some tokens.";
const tokens = estimateTokens(text);
expect(tokens).toBeGreaterThan(0);
expect(tokens).toBeLessThan(text.length); // ~4 chars per token
});
});
describe("generateSkillsXml", () => {
it("should generate valid XML with locations", () => {
const skills = [
{
name: "skill-one",
description: "First skill.",
location: "/path/to/skill-one/SKILL.md",
},
{
name: "skill-two",
description: "Second skill.",
location: "/path/to/skill-two/SKILL.md",
},
];
const xml = generateSkillsXml(skills, { includeLocation: true });
expect(xml).toContain("");
expect(xml).toContain("skill-one");
expect(xml).toContain("First skill.");
expect(xml).toContain("/path/to/skill-one/SKILL.md");
expect(xml).toContain("");
});
it("should generate XML without locations", () => {
const skills = [
{ name: "skill-one", description: "First skill.", location: "/path" },
];
const xml = generateSkillsXml(skills, { includeLocation: false });
expect(xml).not.toContain("");
});
it("should escape XML special characters", () => {
const skills = [
{ name: "test", description: 'Use when & "situation".' },
];
const xml = generateSkillsXml(skills);
expect(xml).toContain("<condition>");
expect(xml).toContain("&");
expect(xml).toContain(""");
});
it("should return empty string for empty skills array", () => {
const xml = generateSkillsXml([]);
expect(xml).toBe("");
});
});
describe("real otto skills validation", () => {
const skillDirs = ["github", "1password", "clawhub", "skill-creator", "tmux"];
for (const skillDir of skillDirs) {
const skillPath = path.join(OTTO_SKILLS_PATH, skillDir);
if (fs.existsSync(skillPath)) {
it(`should validate ${skillDir} skill`, () => {
const skillMdPath = path.join(skillPath, "SKILL.md");
const content = fs.readFileSync(skillMdPath, "utf-8");
const result = validateSkillDirectory(skillPath, content, skillDir);
// Most otto skills should be valid
if (!result.valid) {
console.log(`${skillDir} validation errors:`, result.errors);
}
// Note: Some skills might have minor issues, so we just log them
expect(result).toBeDefined();
});
}
}
});