Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
Blame · Line-by-line history

skills-bundle.test.ts

Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.

skills-bundle.test.tsBlame181 lines · 1 contributor
43095dcClaude1/**
2 * Block L7 — Claude Code skill bundle.
3 *
4 * Coverage:
5 * - Each SKILL.md file exists at the expected `.claude/skills/<name>/` path
6 * - Each SKILL.md has a YAML frontmatter block with `name`, `description`,
7 * `tools` keys
8 * - The frontmatter's `description` mentions Gluecron so the harness
9 * auto-invokes the skill on Gluecron-hosted repos
10 * - The frontmatter's `name` matches the directory name
11 * - The install script (scripts/install.sh) contains the mkdir + write
12 * step for `~/.claude/skills/gluecron-pr/`, gluecron-issue, and
13 * gluecron-review
14 */
15
16import { describe, it, expect } from "bun:test";
17import { readFileSync, existsSync } from "node:fs";
18import { join } from "node:path";
19
20const REPO_ROOT = join(import.meta.dir, "..", "..");
21const SKILLS_DIR = join(REPO_ROOT, ".claude", "skills");
22const INSTALL_SCRIPT = join(REPO_ROOT, "scripts", "install.sh");
23
24const SKILLS = ["gluecron-pr", "gluecron-issue", "gluecron-review"] as const;
25
26// ---------------------------------------------------------------------------
27// Helper: parse the simple YAML frontmatter block at the top of a SKILL.md.
28// ---------------------------------------------------------------------------
29
30function parseFrontmatter(src: string): {
31 raw: string;
32 fields: Record<string, string>;
33} {
34 expect(src.startsWith("---")).toBe(true);
35 const end = src.indexOf("\n---", 3);
36 expect(end).toBeGreaterThan(0);
37 const raw = src.slice(3, end).trim();
38 const fields: Record<string, string> = {};
39 let currentKey = "";
40 let buffer: string[] = [];
41 const flush = () => {
42 if (currentKey) {
43 fields[currentKey] = buffer.join("\n").trim();
44 }
45 buffer = [];
46 };
47 for (const line of raw.split("\n")) {
48 const m = /^([a-zA-Z_][a-zA-Z0-9_-]*):\s*(.*)$/.exec(line);
49 if (m && !line.startsWith(" ") && !line.startsWith("\t")) {
50 flush();
51 currentKey = m[1];
52 buffer = [m[2]];
53 } else {
54 buffer.push(line);
55 }
56 }
57 flush();
58 return { raw, fields };
59}
60
61// ---------------------------------------------------------------------------
62// 1. Each SKILL.md exists at the expected path
63// ---------------------------------------------------------------------------
64
65describe("skills bundle — files exist", () => {
66 for (const name of SKILLS) {
67 it(`${name}/SKILL.md is present`, () => {
68 const path = join(SKILLS_DIR, name, "SKILL.md");
69 expect(existsSync(path)).toBe(true);
70 });
71 }
72});
73
74// ---------------------------------------------------------------------------
75// 2. Each SKILL.md has a valid YAML frontmatter with required keys
76// ---------------------------------------------------------------------------
77
78describe("skills bundle — frontmatter shape", () => {
79 for (const name of SKILLS) {
80 const path = join(SKILLS_DIR, name, "SKILL.md");
81
82 it(`${name} frontmatter has name + description + tools`, () => {
83 const src = readFileSync(path, "utf8");
84 const { fields } = parseFrontmatter(src);
85 expect(fields.name).toBeDefined();
86 expect(fields.description).toBeDefined();
87 expect(fields.tools).toBeDefined();
88 expect(fields.name.length).toBeGreaterThan(0);
89 expect(fields.description.length).toBeGreaterThan(0);
90 expect(fields.tools.length).toBeGreaterThan(0);
91 });
92
93 it(`${name} frontmatter "name" matches the directory`, () => {
94 const src = readFileSync(path, "utf8");
95 const { fields } = parseFrontmatter(src);
96 expect(fields.name).toBe(name);
97 });
98
99 it(`${name} frontmatter "description" mentions Gluecron`, () => {
100 const src = readFileSync(path, "utf8");
101 const { fields } = parseFrontmatter(src);
102 expect(fields.description.toLowerCase()).toContain("gluecron");
103 });
104
105 it(`${name} body is non-trivial (>= 200 chars after frontmatter)`, () => {
106 const src = readFileSync(path, "utf8");
107 const end = src.indexOf("\n---", 3);
108 const body = src.slice(end + 4).trim();
109 expect(body.length).toBeGreaterThanOrEqual(200);
110 });
111 }
112});
113
114// ---------------------------------------------------------------------------
115// 3. Each skill references its MCP tools in the frontmatter `tools:` list
116// ---------------------------------------------------------------------------
117
118describe("skills bundle — tool references", () => {
119 const expectedTools: Record<(typeof SKILLS)[number], string[]> = {
120 "gluecron-pr": [
121 "gluecron_create_pr",
122 "gluecron_get_pr",
123 "gluecron_list_prs",
124 "gluecron_comment_pr",
125 "gluecron_merge_pr",
126 "gluecron_close_pr",
127 ],
128 "gluecron-issue": [
129 "gluecron_create_issue",
130 "gluecron_comment_issue",
131 "gluecron_close_issue",
132 "gluecron_reopen_issue",
133 "gluecron_repo_list_issues",
134 ],
135 "gluecron-review": [
136 "gluecron_get_pr",
137 "gluecron_list_prs",
138 "gluecron_comment_pr",
139 ],
140 };
141
142 for (const name of SKILLS) {
143 it(`${name} lists every expected MCP tool in frontmatter`, () => {
144 const src = readFileSync(join(SKILLS_DIR, name, "SKILL.md"), "utf8");
145 const { fields } = parseFrontmatter(src);
146 for (const tool of expectedTools[name]) {
147 expect(fields.tools).toContain(tool);
148 }
149 });
150 }
151});
152
153// ---------------------------------------------------------------------------
154// 4. The install script copies each skill into ~/.claude/skills/
155// ---------------------------------------------------------------------------
156
157describe("skills bundle — install script wiring", () => {
158 const script = readFileSync(INSTALL_SCRIPT, "utf8");
159
160 it("creates the ~/.claude/skills/ directory tree", () => {
161 expect(script).toContain("$HOME/.claude/skills");
162 expect(script).toMatch(/mkdir -p[^\n]*gluecron-pr/);
163 expect(script).toMatch(/mkdir -p[^\n]*gluecron-issue/);
164 expect(script).toMatch(/mkdir -p[^\n]*gluecron-review/);
165 });
166
167 for (const name of SKILLS) {
168 it(`writes ${name}/SKILL.md to the skills dir`, () => {
169 // Either `cat > .../<name>/SKILL.md` or `cp ... <name>/SKILL.md` is fine.
170 const pattern = new RegExp(`(cat\\s*>|cp\\s+[^\\n]+)[^\\n]*${name}/SKILL\\.md`);
171 expect(script).toMatch(pattern);
172 });
173
174 it(`installed ${name}/SKILL.md heredoc mentions Gluecron`, () => {
175 // The heredoc body should describe the skill — at minimum, mention "Gluecron".
176 // We just check the script contains both the path AND the word Gluecron.
177 expect(script).toContain(`${name}/SKILL.md`);
178 expect(script.toLowerCase()).toContain("gluecron");
179 });
180 }
181});