test(skills): add unit tests for parser and eligibility checker
Add tests for: - YAML frontmatter parsing from SKILL.md files - Skill eligibility checking (platform, binary, env requirements) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
9f5424eb5c
commit
de8ff730fb
2 changed files with 575 additions and 0 deletions
332
src/agent/skills/eligibility.test.ts
Normal file
332
src/agent/skills/eligibility.test.ts
Normal file
|
|
@ -0,0 +1,332 @@
|
|||
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest";
|
||||
import { checkEligibility, filterEligibleSkills } from "./eligibility.js";
|
||||
import type { Skill, SkillFrontmatter, EligibilityResult } from "./types.js";
|
||||
|
||||
// Helper to create a skill for testing
|
||||
function createSkill(
|
||||
id: string,
|
||||
frontmatter: Partial<SkillFrontmatter> & { name: string },
|
||||
): Skill {
|
||||
return {
|
||||
id,
|
||||
frontmatter: frontmatter as SkillFrontmatter,
|
||||
instructions: "Test instructions",
|
||||
source: "bundled",
|
||||
filePath: `/path/to/${id}/SKILL.md`,
|
||||
};
|
||||
}
|
||||
|
||||
describe("eligibility", () => {
|
||||
describe("checkEligibility", () => {
|
||||
describe("platform requirements", () => {
|
||||
it("should be eligible when no platform requirement specified", () => {
|
||||
const skill = createSkill("test", {
|
||||
name: "Test Skill",
|
||||
});
|
||||
|
||||
const result = checkEligibility(skill, "darwin");
|
||||
expect(result.eligible).toBe(true);
|
||||
expect(result.reasons).toBeUndefined();
|
||||
});
|
||||
|
||||
it("should be eligible when current platform matches", () => {
|
||||
const skill = createSkill("test", {
|
||||
name: "Test Skill",
|
||||
metadata: {
|
||||
platforms: ["darwin", "linux"],
|
||||
},
|
||||
});
|
||||
|
||||
expect(checkEligibility(skill, "darwin").eligible).toBe(true);
|
||||
expect(checkEligibility(skill, "linux").eligible).toBe(true);
|
||||
});
|
||||
|
||||
it("should be ineligible when platform does not match", () => {
|
||||
const skill = createSkill("test", {
|
||||
name: "Test Skill",
|
||||
metadata: {
|
||||
platforms: ["darwin"],
|
||||
},
|
||||
});
|
||||
|
||||
const result = checkEligibility(skill, "win32");
|
||||
expect(result.eligible).toBe(false);
|
||||
expect(result.reasons).toContain(
|
||||
"Platform 'win32' not supported (requires: darwin)",
|
||||
);
|
||||
});
|
||||
|
||||
it("should handle empty platforms array as no requirement", () => {
|
||||
const skill = createSkill("test", {
|
||||
name: "Test Skill",
|
||||
metadata: {
|
||||
platforms: [],
|
||||
},
|
||||
});
|
||||
|
||||
const result = checkEligibility(skill, "darwin");
|
||||
expect(result.eligible).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("binary requirements", () => {
|
||||
it("should be eligible when required binary exists", () => {
|
||||
const skill = createSkill("test", {
|
||||
name: "Test Skill",
|
||||
metadata: {
|
||||
requiresBinaries: ["node"],
|
||||
},
|
||||
});
|
||||
|
||||
// node should exist in the test environment
|
||||
const result = checkEligibility(skill, "darwin");
|
||||
expect(result.eligible).toBe(true);
|
||||
});
|
||||
|
||||
it("should be ineligible when required binary does not exist", () => {
|
||||
const skill = createSkill("test", {
|
||||
name: "Test Skill",
|
||||
metadata: {
|
||||
requiresBinaries: ["nonexistent-binary-xyz-123"],
|
||||
},
|
||||
});
|
||||
|
||||
const result = checkEligibility(skill, "darwin");
|
||||
expect(result.eligible).toBe(false);
|
||||
expect(result.reasons).toContainEqual(
|
||||
expect.stringContaining("Required binary not found: nonexistent-binary-xyz-123"),
|
||||
);
|
||||
});
|
||||
|
||||
it("should check all binaries and report all missing", () => {
|
||||
const skill = createSkill("test", {
|
||||
name: "Test Skill",
|
||||
metadata: {
|
||||
requiresBinaries: ["node", "missing-bin-1", "missing-bin-2"],
|
||||
},
|
||||
});
|
||||
|
||||
const result = checkEligibility(skill, "darwin");
|
||||
expect(result.eligible).toBe(false);
|
||||
expect(result.reasons?.length).toBe(2);
|
||||
expect(result.reasons).toContainEqual(
|
||||
expect.stringContaining("missing-bin-1"),
|
||||
);
|
||||
expect(result.reasons).toContainEqual(
|
||||
expect.stringContaining("missing-bin-2"),
|
||||
);
|
||||
});
|
||||
|
||||
it("should handle empty binaries array", () => {
|
||||
const skill = createSkill("test", {
|
||||
name: "Test Skill",
|
||||
metadata: {
|
||||
requiresBinaries: [],
|
||||
},
|
||||
});
|
||||
|
||||
const result = checkEligibility(skill, "darwin");
|
||||
expect(result.eligible).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("environment variable requirements", () => {
|
||||
const originalEnv = process.env;
|
||||
|
||||
beforeEach(() => {
|
||||
process.env = { ...originalEnv };
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
process.env = originalEnv;
|
||||
});
|
||||
|
||||
it("should be eligible when required env vars exist", () => {
|
||||
process.env.TEST_VAR = "value";
|
||||
|
||||
const skill = createSkill("test", {
|
||||
name: "Test Skill",
|
||||
metadata: {
|
||||
requiresEnv: ["TEST_VAR"],
|
||||
},
|
||||
});
|
||||
|
||||
const result = checkEligibility(skill, "darwin");
|
||||
expect(result.eligible).toBe(true);
|
||||
});
|
||||
|
||||
it("should be eligible even if env var is empty string", () => {
|
||||
process.env.EMPTY_VAR = "";
|
||||
|
||||
const skill = createSkill("test", {
|
||||
name: "Test Skill",
|
||||
metadata: {
|
||||
requiresEnv: ["EMPTY_VAR"],
|
||||
},
|
||||
});
|
||||
|
||||
const result = checkEligibility(skill, "darwin");
|
||||
expect(result.eligible).toBe(true);
|
||||
});
|
||||
|
||||
it("should be ineligible when required env var does not exist", () => {
|
||||
delete process.env.MISSING_VAR;
|
||||
|
||||
const skill = createSkill("test", {
|
||||
name: "Test Skill",
|
||||
metadata: {
|
||||
requiresEnv: ["MISSING_VAR"],
|
||||
},
|
||||
});
|
||||
|
||||
const result = checkEligibility(skill, "darwin");
|
||||
expect(result.eligible).toBe(false);
|
||||
expect(result.reasons).toContainEqual(
|
||||
expect.stringContaining("Required environment variable not set: MISSING_VAR"),
|
||||
);
|
||||
});
|
||||
|
||||
it("should check all env vars and report all missing", () => {
|
||||
process.env.EXISTS = "yes";
|
||||
delete process.env.MISSING_1;
|
||||
delete process.env.MISSING_2;
|
||||
|
||||
const skill = createSkill("test", {
|
||||
name: "Test Skill",
|
||||
metadata: {
|
||||
requiresEnv: ["EXISTS", "MISSING_1", "MISSING_2"],
|
||||
},
|
||||
});
|
||||
|
||||
const result = checkEligibility(skill, "darwin");
|
||||
expect(result.eligible).toBe(false);
|
||||
expect(result.reasons?.length).toBe(2);
|
||||
});
|
||||
});
|
||||
|
||||
describe("combined requirements", () => {
|
||||
const originalEnv = process.env;
|
||||
|
||||
beforeEach(() => {
|
||||
process.env = { ...originalEnv };
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
process.env = originalEnv;
|
||||
});
|
||||
|
||||
it("should collect all failure reasons", () => {
|
||||
delete process.env.MISSING_VAR;
|
||||
|
||||
const skill = createSkill("test", {
|
||||
name: "Test Skill",
|
||||
metadata: {
|
||||
platforms: ["win32"],
|
||||
requiresBinaries: ["missing-binary"],
|
||||
requiresEnv: ["MISSING_VAR"],
|
||||
},
|
||||
});
|
||||
|
||||
const result = checkEligibility(skill, "darwin");
|
||||
expect(result.eligible).toBe(false);
|
||||
expect(result.reasons?.length).toBe(3);
|
||||
});
|
||||
|
||||
it("should be eligible when all requirements met", () => {
|
||||
process.env.REQUIRED_VAR = "value";
|
||||
|
||||
const skill = createSkill("test", {
|
||||
name: "Test Skill",
|
||||
metadata: {
|
||||
platforms: ["darwin", "linux"],
|
||||
requiresBinaries: ["node"],
|
||||
requiresEnv: ["REQUIRED_VAR"],
|
||||
},
|
||||
});
|
||||
|
||||
const result = checkEligibility(skill, "darwin");
|
||||
expect(result.eligible).toBe(true);
|
||||
expect(result.reasons).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
it("should use process.platform by default", () => {
|
||||
const skill = createSkill("test", {
|
||||
name: "Test Skill",
|
||||
metadata: {
|
||||
platforms: [process.platform],
|
||||
},
|
||||
});
|
||||
|
||||
// Call without platform argument
|
||||
const result = checkEligibility(skill);
|
||||
expect(result.eligible).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe("filterEligibleSkills", () => {
|
||||
const originalEnv = process.env;
|
||||
|
||||
beforeEach(() => {
|
||||
process.env = { ...originalEnv };
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
process.env = originalEnv;
|
||||
});
|
||||
|
||||
it("should return only eligible skills", () => {
|
||||
const skills = new Map<string, Skill>([
|
||||
["darwin-only", createSkill("darwin-only", {
|
||||
name: "Darwin Only",
|
||||
metadata: { platforms: ["darwin"] },
|
||||
})],
|
||||
["linux-only", createSkill("linux-only", {
|
||||
name: "Linux Only",
|
||||
metadata: { platforms: ["linux"] },
|
||||
})],
|
||||
["all-platforms", createSkill("all-platforms", {
|
||||
name: "All Platforms",
|
||||
})],
|
||||
]);
|
||||
|
||||
const eligible = filterEligibleSkills(skills, "darwin");
|
||||
|
||||
expect(eligible.size).toBe(2);
|
||||
expect(eligible.has("darwin-only")).toBe(true);
|
||||
expect(eligible.has("all-platforms")).toBe(true);
|
||||
expect(eligible.has("linux-only")).toBe(false);
|
||||
});
|
||||
|
||||
it("should return empty map when no skills are eligible", () => {
|
||||
const skills = new Map<string, Skill>([
|
||||
["win-only", createSkill("win-only", {
|
||||
name: "Windows Only",
|
||||
metadata: { platforms: ["win32"] },
|
||||
})],
|
||||
]);
|
||||
|
||||
const eligible = filterEligibleSkills(skills, "darwin");
|
||||
|
||||
expect(eligible.size).toBe(0);
|
||||
});
|
||||
|
||||
it("should return all skills when all are eligible", () => {
|
||||
const skills = new Map<string, Skill>([
|
||||
["skill-1", createSkill("skill-1", { name: "Skill 1" })],
|
||||
["skill-2", createSkill("skill-2", { name: "Skill 2" })],
|
||||
["skill-3", createSkill("skill-3", { name: "Skill 3" })],
|
||||
]);
|
||||
|
||||
const eligible = filterEligibleSkills(skills, "darwin");
|
||||
|
||||
expect(eligible.size).toBe(3);
|
||||
});
|
||||
|
||||
it("should handle empty input map", () => {
|
||||
const skills = new Map<string, Skill>();
|
||||
const eligible = filterEligibleSkills(skills, "darwin");
|
||||
expect(eligible.size).toBe(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
243
src/agent/skills/parser.test.ts
Normal file
243
src/agent/skills/parser.test.ts
Normal file
|
|
@ -0,0 +1,243 @@
|
|||
import { describe, it, expect } from "vitest";
|
||||
import { parseFrontmatter } from "./parser.js";
|
||||
|
||||
describe("parser", () => {
|
||||
describe("parseFrontmatter", () => {
|
||||
it("should parse valid frontmatter with all fields", () => {
|
||||
const content = `---
|
||||
name: Test Skill
|
||||
description: A test skill
|
||||
version: 1.0.0
|
||||
author: Test Author
|
||||
homepage: https://example.com
|
||||
metadata:
|
||||
emoji: "test"
|
||||
requiresEnv:
|
||||
- API_KEY
|
||||
- SECRET
|
||||
requiresBinaries:
|
||||
- git
|
||||
- node
|
||||
platforms:
|
||||
- darwin
|
||||
- linux
|
||||
tags:
|
||||
- testing
|
||||
- development
|
||||
---
|
||||
# Skill Instructions
|
||||
|
||||
This is the body content.
|
||||
`;
|
||||
|
||||
const [frontmatter, body] = parseFrontmatter(content);
|
||||
|
||||
expect(frontmatter).toEqual({
|
||||
name: "Test Skill",
|
||||
description: "A test skill",
|
||||
version: "1.0.0",
|
||||
author: "Test Author",
|
||||
homepage: "https://example.com",
|
||||
metadata: {
|
||||
emoji: "test",
|
||||
requiresEnv: ["API_KEY", "SECRET"],
|
||||
requiresBinaries: ["git", "node"],
|
||||
platforms: ["darwin", "linux"],
|
||||
tags: ["testing", "development"],
|
||||
},
|
||||
});
|
||||
expect(body).toBe("# Skill Instructions\n\nThis is the body content.");
|
||||
});
|
||||
|
||||
it("should parse minimal frontmatter with only required name field", () => {
|
||||
const content = `---
|
||||
name: Minimal Skill
|
||||
---
|
||||
Body content here.
|
||||
`;
|
||||
|
||||
const [frontmatter, body] = parseFrontmatter(content);
|
||||
|
||||
expect(frontmatter).toEqual({ name: "Minimal Skill" });
|
||||
expect(body).toBe("Body content here.");
|
||||
});
|
||||
|
||||
it("should return null frontmatter when no frontmatter present", () => {
|
||||
const content = `# Just Markdown
|
||||
|
||||
No frontmatter here.
|
||||
`;
|
||||
|
||||
const [frontmatter, body] = parseFrontmatter(content);
|
||||
|
||||
expect(frontmatter).toBeNull();
|
||||
expect(body).toBe("# Just Markdown\n\nNo frontmatter here.");
|
||||
});
|
||||
|
||||
it("should return null frontmatter for invalid YAML", () => {
|
||||
const content = `---
|
||||
name: Test
|
||||
invalid: yaml: syntax: here
|
||||
- broken
|
||||
indentation
|
||||
---
|
||||
Body content.
|
||||
`;
|
||||
|
||||
const [frontmatter, body] = parseFrontmatter(content);
|
||||
|
||||
// Note: YAML parser may or may not fail depending on the exact syntax
|
||||
// This tests that the function handles errors gracefully
|
||||
if (frontmatter === null) {
|
||||
expect(body).toBe(content.trim());
|
||||
} else {
|
||||
expect(frontmatter).toBeDefined();
|
||||
}
|
||||
});
|
||||
|
||||
it("should handle empty frontmatter block", () => {
|
||||
const content = `---
|
||||
|
||||
---
|
||||
Body content.
|
||||
`;
|
||||
|
||||
const [frontmatter, body] = parseFrontmatter(content);
|
||||
|
||||
// Empty frontmatter returns null because YAML parses empty content as null
|
||||
// The regex still matches, so body is extracted, but frontmatter check fails
|
||||
if (frontmatter === null) {
|
||||
// When frontmatter is null, body contains trimmed original content
|
||||
expect(body).toContain("Body content.");
|
||||
} else {
|
||||
// If YAML returns empty object, frontmatter would be defined
|
||||
expect(body).toBe("Body content.");
|
||||
}
|
||||
});
|
||||
|
||||
it("should handle CRLF line endings", () => {
|
||||
const content = "---\r\nname: Windows Skill\r\n---\r\nBody with CRLF.";
|
||||
|
||||
const [frontmatter, body] = parseFrontmatter(content);
|
||||
|
||||
expect(frontmatter).toEqual({ name: "Windows Skill" });
|
||||
expect(body).toBe("Body with CRLF.");
|
||||
});
|
||||
|
||||
it("should handle content with multiple --- markers in body", () => {
|
||||
const content = `---
|
||||
name: Test
|
||||
---
|
||||
# Body
|
||||
|
||||
---
|
||||
|
||||
More content after horizontal rule.
|
||||
|
||||
---
|
||||
Another section.
|
||||
`;
|
||||
|
||||
const [frontmatter, body] = parseFrontmatter(content);
|
||||
|
||||
expect(frontmatter).toEqual({ name: "Test" });
|
||||
expect(body).toContain("---");
|
||||
expect(body).toContain("More content after horizontal rule.");
|
||||
});
|
||||
|
||||
it("should handle frontmatter that doesn't start at beginning", () => {
|
||||
const content = `
|
||||
---
|
||||
name: Test
|
||||
---
|
||||
Body.
|
||||
`;
|
||||
|
||||
const [frontmatter, body] = parseFrontmatter(content);
|
||||
|
||||
// Frontmatter must start at the very beginning
|
||||
expect(frontmatter).toBeNull();
|
||||
});
|
||||
|
||||
it("should handle frontmatter with nested metadata object", () => {
|
||||
const content = `---
|
||||
name: Nested Test
|
||||
metadata:
|
||||
emoji: "rocket"
|
||||
platforms:
|
||||
- darwin
|
||||
requiresBinaries: []
|
||||
requiresEnv: []
|
||||
---
|
||||
Instructions here.
|
||||
`;
|
||||
|
||||
const [frontmatter, body] = parseFrontmatter(content);
|
||||
|
||||
expect(frontmatter?.metadata).toEqual({
|
||||
emoji: "rocket",
|
||||
platforms: ["darwin"],
|
||||
requiresBinaries: [],
|
||||
requiresEnv: [],
|
||||
});
|
||||
});
|
||||
|
||||
it("should handle multiline string values", () => {
|
||||
const content = `---
|
||||
name: Multiline
|
||||
description: |
|
||||
This is a multiline
|
||||
description that spans
|
||||
multiple lines.
|
||||
---
|
||||
Body.
|
||||
`;
|
||||
|
||||
const [frontmatter, body] = parseFrontmatter(content);
|
||||
|
||||
expect(frontmatter?.description).toContain("multiline");
|
||||
expect(body).toBe("Body.");
|
||||
});
|
||||
|
||||
it("should trim whitespace from body", () => {
|
||||
const content = `---
|
||||
name: Test
|
||||
---
|
||||
|
||||
Body with extra whitespace.
|
||||
|
||||
`;
|
||||
|
||||
const [frontmatter, body] = parseFrontmatter(content);
|
||||
|
||||
expect(body).toBe("Body with extra whitespace.");
|
||||
});
|
||||
|
||||
it("should handle empty body", () => {
|
||||
const content = `---
|
||||
name: No Body
|
||||
---
|
||||
`;
|
||||
|
||||
const [frontmatter, body] = parseFrontmatter(content);
|
||||
|
||||
expect(frontmatter).toEqual({ name: "No Body" });
|
||||
expect(body).toBe("");
|
||||
});
|
||||
|
||||
it("should handle special characters in values", () => {
|
||||
const content = `---
|
||||
name: "Special: Characters & Symbols"
|
||||
description: 'Quotes and colons: work'
|
||||
---
|
||||
Body.
|
||||
`;
|
||||
|
||||
const [frontmatter, body] = parseFrontmatter(content);
|
||||
|
||||
expect(frontmatter?.name).toBe("Special: Characters & Symbols");
|
||||
expect(frontmatter?.description).toBe("Quotes and colons: work");
|
||||
expect(body).toBe("Body.");
|
||||
});
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Add a link
Reference in a new issue