354 lines
8.9 KiB
TypeScript
354 lines
8.9 KiB
TypeScript
/**
|
|
* Tests for the bundled skill-creator scripts
|
|
*/
|
|
|
|
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
import { existsSync, mkdirSync, rmSync, writeFileSync } from "node:fs";
|
|
import { join } from "node:path";
|
|
import {
|
|
initSkill,
|
|
titleCaseSkillName,
|
|
} from "../../skills/builtin/creating-skills/scripts/init-skill";
|
|
import { packageSkill } from "../../skills/builtin/creating-skills/scripts/package-skill";
|
|
import { validateSkill } from "../../skills/builtin/creating-skills/scripts/validate-skill";
|
|
|
|
const TEST_DIR = join(import.meta.dir, ".test-skill-creator");
|
|
|
|
describe("validate-skill", () => {
|
|
beforeEach(() => {
|
|
if (existsSync(TEST_DIR)) {
|
|
rmSync(TEST_DIR, { recursive: true });
|
|
}
|
|
mkdirSync(TEST_DIR, { recursive: true });
|
|
});
|
|
|
|
afterEach(() => {
|
|
if (existsSync(TEST_DIR)) {
|
|
rmSync(TEST_DIR, { recursive: true });
|
|
}
|
|
});
|
|
|
|
test("validates a valid skill", () => {
|
|
const skillDir = join(TEST_DIR, "valid-skill");
|
|
mkdirSync(skillDir);
|
|
writeFileSync(
|
|
join(skillDir, "SKILL.md"),
|
|
`---
|
|
name: valid-skill
|
|
description: A valid test skill
|
|
---
|
|
|
|
# Valid Skill
|
|
|
|
This is a valid skill.
|
|
`,
|
|
);
|
|
|
|
const result = validateSkill(skillDir);
|
|
expect(result.valid).toBe(true);
|
|
expect(result.message).toBe("Skill is valid!");
|
|
});
|
|
|
|
test("fails when SKILL.md is missing", () => {
|
|
const skillDir = join(TEST_DIR, "missing-skill");
|
|
mkdirSync(skillDir);
|
|
|
|
const result = validateSkill(skillDir);
|
|
expect(result.valid).toBe(false);
|
|
expect(result.message).toBe("SKILL.md not found");
|
|
});
|
|
|
|
test("fails when frontmatter is missing", () => {
|
|
const skillDir = join(TEST_DIR, "no-frontmatter");
|
|
mkdirSync(skillDir);
|
|
writeFileSync(
|
|
join(skillDir, "SKILL.md"),
|
|
`# No Frontmatter
|
|
|
|
This skill has no frontmatter.
|
|
`,
|
|
);
|
|
|
|
const result = validateSkill(skillDir);
|
|
expect(result.valid).toBe(false);
|
|
expect(result.message).toBe("No YAML frontmatter found");
|
|
});
|
|
|
|
test("fails when name is missing", () => {
|
|
const skillDir = join(TEST_DIR, "no-name");
|
|
mkdirSync(skillDir);
|
|
writeFileSync(
|
|
join(skillDir, "SKILL.md"),
|
|
`---
|
|
description: A skill without a name
|
|
---
|
|
|
|
# No Name
|
|
`,
|
|
);
|
|
|
|
const result = validateSkill(skillDir);
|
|
expect(result.valid).toBe(false);
|
|
expect(result.message).toBe("Missing 'name' in frontmatter");
|
|
});
|
|
|
|
test("fails when description is missing", () => {
|
|
const skillDir = join(TEST_DIR, "no-description");
|
|
mkdirSync(skillDir);
|
|
writeFileSync(
|
|
join(skillDir, "SKILL.md"),
|
|
`---
|
|
name: no-description
|
|
---
|
|
|
|
# No Description
|
|
`,
|
|
);
|
|
|
|
const result = validateSkill(skillDir);
|
|
expect(result.valid).toBe(false);
|
|
expect(result.message).toBe("Missing 'description' in frontmatter");
|
|
});
|
|
|
|
test("fails when name has invalid characters", () => {
|
|
const skillDir = join(TEST_DIR, "invalid-name");
|
|
mkdirSync(skillDir);
|
|
writeFileSync(
|
|
join(skillDir, "SKILL.md"),
|
|
`---
|
|
name: Invalid_Name
|
|
description: A skill with invalid name
|
|
---
|
|
|
|
# Invalid Name
|
|
`,
|
|
);
|
|
|
|
const result = validateSkill(skillDir);
|
|
expect(result.valid).toBe(false);
|
|
expect(result.message).toContain("should be hyphen-case");
|
|
});
|
|
|
|
test("fails when name starts with hyphen", () => {
|
|
const skillDir = join(TEST_DIR, "hyphen-start");
|
|
mkdirSync(skillDir);
|
|
writeFileSync(
|
|
join(skillDir, "SKILL.md"),
|
|
`---
|
|
name: -invalid-start
|
|
description: A skill with invalid name
|
|
---
|
|
|
|
# Invalid Start
|
|
`,
|
|
);
|
|
|
|
const result = validateSkill(skillDir);
|
|
expect(result.valid).toBe(false);
|
|
expect(result.message).toContain("cannot start/end with hyphen");
|
|
});
|
|
|
|
test("fails when description contains angle brackets", () => {
|
|
const skillDir = join(TEST_DIR, "angle-brackets");
|
|
mkdirSync(skillDir);
|
|
writeFileSync(
|
|
join(skillDir, "SKILL.md"),
|
|
`---
|
|
name: angle-brackets
|
|
description: A skill with <invalid> description
|
|
---
|
|
|
|
# Angle Brackets
|
|
`,
|
|
);
|
|
|
|
const result = validateSkill(skillDir);
|
|
expect(result.valid).toBe(false);
|
|
expect(result.message).toContain("cannot contain angle brackets");
|
|
});
|
|
|
|
test("warns but passes when unknown frontmatter keys are present", () => {
|
|
const skillDir = join(TEST_DIR, "unknown-keys");
|
|
mkdirSync(skillDir);
|
|
writeFileSync(
|
|
join(skillDir, "SKILL.md"),
|
|
`---
|
|
name: unknown-keys
|
|
description: A skill with unknown keys
|
|
author: Someone
|
|
version: 1.0.0
|
|
---
|
|
|
|
# Unknown Keys
|
|
`,
|
|
);
|
|
|
|
const result = validateSkill(skillDir);
|
|
expect(result.valid).toBe(true);
|
|
expect(result.warnings).toBeDefined();
|
|
expect(result.warnings?.length).toBeGreaterThan(0);
|
|
expect(result.warnings?.[0]).toContain("Unknown frontmatter key(s)");
|
|
});
|
|
|
|
test("accepts all official spec frontmatter fields without warnings", () => {
|
|
const skillDir = join(TEST_DIR, "full-spec");
|
|
mkdirSync(skillDir);
|
|
writeFileSync(
|
|
join(skillDir, "SKILL.md"),
|
|
`---
|
|
name: full-spec
|
|
description: A skill with all official spec fields
|
|
license: MIT
|
|
compatibility: Requires Node.js 18+
|
|
metadata:
|
|
author: test
|
|
version: "1.0"
|
|
allowed-tools: Bash Read Write
|
|
---
|
|
|
|
# Full Spec Skill
|
|
`,
|
|
);
|
|
|
|
const result = validateSkill(skillDir);
|
|
expect(result.valid).toBe(true);
|
|
expect(result.warnings).toBeUndefined();
|
|
});
|
|
|
|
test("warns when name doesn't match directory name", () => {
|
|
const skillDir = join(TEST_DIR, "my-directory");
|
|
mkdirSync(skillDir);
|
|
writeFileSync(
|
|
join(skillDir, "SKILL.md"),
|
|
`---
|
|
name: different-name
|
|
description: Name doesn't match directory
|
|
---
|
|
|
|
# Mismatched Name
|
|
`,
|
|
);
|
|
|
|
const result = validateSkill(skillDir);
|
|
expect(result.valid).toBe(true);
|
|
expect(result.warnings).toBeDefined();
|
|
expect(result.warnings?.[0]).toContain("doesn't match directory name");
|
|
});
|
|
});
|
|
|
|
describe("init-skill", () => {
|
|
beforeEach(() => {
|
|
if (existsSync(TEST_DIR)) {
|
|
rmSync(TEST_DIR, { recursive: true });
|
|
}
|
|
mkdirSync(TEST_DIR, { recursive: true });
|
|
});
|
|
|
|
afterEach(() => {
|
|
if (existsSync(TEST_DIR)) {
|
|
rmSync(TEST_DIR, { recursive: true });
|
|
}
|
|
});
|
|
|
|
test("titleCaseSkillName converts hyphenated names", () => {
|
|
expect(titleCaseSkillName("my-skill")).toBe("My Skill");
|
|
expect(titleCaseSkillName("pdf-editor")).toBe("Pdf Editor");
|
|
expect(titleCaseSkillName("big-query-helper")).toBe("Big Query Helper");
|
|
});
|
|
|
|
test("creates a new skill directory with all files", () => {
|
|
const result = initSkill("test-skill", TEST_DIR);
|
|
|
|
expect(result).not.toBeNull();
|
|
expect(existsSync(join(TEST_DIR, "test-skill"))).toBe(true);
|
|
expect(existsSync(join(TEST_DIR, "test-skill", "SKILL.md"))).toBe(true);
|
|
expect(existsSync(join(TEST_DIR, "test-skill", "scripts"))).toBe(true);
|
|
expect(existsSync(join(TEST_DIR, "test-skill", "references"))).toBe(true);
|
|
expect(existsSync(join(TEST_DIR, "test-skill", "assets"))).toBe(true);
|
|
});
|
|
|
|
test("created skill passes validation", () => {
|
|
initSkill("valid-init", TEST_DIR);
|
|
|
|
// The initialized skill should pass validation (except for TODO in description)
|
|
const skillDir = join(TEST_DIR, "valid-init");
|
|
expect(existsSync(join(skillDir, "SKILL.md"))).toBe(true);
|
|
});
|
|
|
|
test("fails when directory already exists", () => {
|
|
const skillDir = join(TEST_DIR, "existing-skill");
|
|
mkdirSync(skillDir, { recursive: true });
|
|
|
|
const result = initSkill("existing-skill", TEST_DIR);
|
|
expect(result).toBeNull();
|
|
});
|
|
});
|
|
|
|
describe("package-skill", () => {
|
|
beforeEach(() => {
|
|
if (existsSync(TEST_DIR)) {
|
|
rmSync(TEST_DIR, { recursive: true });
|
|
}
|
|
mkdirSync(TEST_DIR, { recursive: true });
|
|
});
|
|
|
|
afterEach(() => {
|
|
if (existsSync(TEST_DIR)) {
|
|
rmSync(TEST_DIR, { recursive: true });
|
|
}
|
|
});
|
|
|
|
test("packages a valid skill into a .skill file", () => {
|
|
// Create a valid skill
|
|
const skillDir = join(TEST_DIR, "packagable-skill");
|
|
mkdirSync(skillDir);
|
|
writeFileSync(
|
|
join(skillDir, "SKILL.md"),
|
|
`---
|
|
name: packagable-skill
|
|
description: A skill that can be packaged
|
|
---
|
|
|
|
# Packagable Skill
|
|
|
|
This skill can be packaged.
|
|
`,
|
|
);
|
|
|
|
const result = packageSkill(skillDir, TEST_DIR);
|
|
expect(result).not.toBeNull();
|
|
expect(existsSync(join(TEST_DIR, "packagable-skill.skill"))).toBe(true);
|
|
});
|
|
|
|
test("fails when skill directory does not exist", () => {
|
|
const result = packageSkill(join(TEST_DIR, "nonexistent"), TEST_DIR);
|
|
expect(result).toBeNull();
|
|
});
|
|
|
|
test("fails when SKILL.md is missing", () => {
|
|
const skillDir = join(TEST_DIR, "no-skill-md");
|
|
mkdirSync(skillDir);
|
|
|
|
const result = packageSkill(skillDir, TEST_DIR);
|
|
expect(result).toBeNull();
|
|
});
|
|
|
|
test("fails when skill validation fails", () => {
|
|
const skillDir = join(TEST_DIR, "invalid-skill");
|
|
mkdirSync(skillDir);
|
|
writeFileSync(
|
|
join(skillDir, "SKILL.md"),
|
|
`---
|
|
name: Invalid_Name
|
|
description: Invalid skill
|
|
---
|
|
|
|
# Invalid
|
|
`,
|
|
);
|
|
|
|
const result = packageSkill(skillDir, TEST_DIR);
|
|
expect(result).toBeNull();
|
|
});
|
|
});
|