Skip to content
164 linesCodeBlameRaw
1import assert from "node:assert/strict";
2import { test } from "node:test";
3
4import { PLUGIN_SKILL_LIMITS, checkSkillFolder as checkFolder } from "./skill-format.ts";
5
6test("a plugin's skill is checked leniently: its name keeps its case, a long description is shortened, Claude Code's tools are kept aside, and it may be larger", () => {
7 const long = "Use when ".padEnd(1500, "x");
8 const checked = checkFolder([{ path: "SKILL.md", content: `---\nname: Claude-API\ndescription: ${long}\ntools: Read, read_file\n---\n\nBody.\n` }], { lenient: true, limits: PLUGIN_SKILL_LIMITS });
9 assert.ok(checked.ok, !checked.ok ? checked.message : "");
10 assert.equal(checked.skill.name, "Claude-API");
11 assert.equal(checked.skill.description.length, 1024);
12 assert.ok(checked.skill.description.endsWith("…"));
13 assert.deepEqual(checked.skill.tools, ["read_file"]);
14 assert.deepEqual(checked.skill.extra.tools, ["Read"]);
15 const strict = checkFolder([{ path: "SKILL.md", content: `---\nname: claude-api\ndescription: ${long}\n---\n\nBody.\n` }]);
16 assert.equal(strict.ok, false);
17});
18
19import {
20 AGENT_TOOL_NAMES,
21 RESERVED_SKILL_NAMES,
22 SKILL_FOLDER_MAX_BYTES,
23 checkSkillFolder,
24 foundationalSkillMd,
25 parseFrontMatter,
26 renderSkillMd,
27 skillFileBytes,
28 skillNameProblem,
29 splitFrontMatter,
30} from "./skill-format.ts";
31import { FOUNDATIONAL_SKILLS, FOUNDATIONAL_SKILL_IDS } from "./skills.ts";
32
33test("front-matter as SKILL.md files write it", () => {
34 const front = parseFrontMatter(
35 [
36 "name: pdf-forms",
37 "description: >",
38 " Use when filling PDF forms:",
39 " text fields and checkboxes.",
40 "",
41 " Not for scans.",
42 "license: 'Apache-2.0'",
43 "allowed-tools: Read Write",
44 "tools:",
45 " - make_file",
46 " - read_artifact",
47 "requires_computer: true",
48 "version: 2",
49 "metadata:",
50 " author: \"Ana \\\"A\\\" Lima\"",
51 " tags: [forms, \"pdf, docs\"]",
52 "notes: |",
53 " line one",
54 " indented",
55 "# a comment",
56 "plain: words: with a colon # and a comment",
57 "",
58 ].join("\n"),
59 );
60 assert.deepEqual(front, {
61 name: "pdf-forms",
62 description: "Use when filling PDF forms: text fields and checkboxes.\nNot for scans.\n",
63 license: "Apache-2.0",
64 "allowed-tools": "Read Write",
65 tools: ["make_file", "read_artifact"],
66 requires_computer: true,
67 version: 2,
68 metadata: { author: 'Ana "A" Lima', tags: ["forms", "pdf, docs"] },
69 notes: "line one\n indented\n",
70 plain: "words: with a colon",
71 });
72 // Lists at the key's own indentation, and folded plain values.
73 assert.deepEqual(parseFrontMatter("tools:\n- read_file\n- search_code\ndescription: Use when\n reading code.\n"), { tools: ["read_file", "search_code"], description: "Use when reading code." });
74 assert.throws(() => parseFrontMatter("name: a\nname: b\n"), /name is given twice/);
75 assert.throws(() => parseFrontMatter("just words\n"), /line 2: expected "key: value"/);
76 assert.throws(() => parseFrontMatter('name: "open\n'), /isn't closed/);
77});
78
79test("SKILL.md splits into front-matter and body, CRLF and BOM included", () => {
80 const split = splitFrontMatter("---\r\nname: a\r\ndescription: b\r\n---\r\n\r\n# Body\r\n");
81 assert.ok(split.ok);
82 assert.equal(split.ok && split.yaml, "name: a\ndescription: b\n");
83 assert.equal(split.ok && split.body, "\n# Body\n");
84 assert.equal(splitFrontMatter("# No front-matter").ok, false);
85 assert.equal(splitFrontMatter("---\nname: a\n").ok, false);
86});
87
88test("a skill folder is checked: names, description, tools agents have, paths, size, scripts", () => {
89 const md = (front: string, body = "Do the thing.") => `---\n${front}\n---\n\n${body}\n`;
90 const good = checkSkillFolder([
91 { path: "SKILL.md", content: md("name: release-notes\ndescription: Use when someone asks for release notes.\ntools: make_file, read_file\nlicense: MIT") },
92 { path: "./resources/template.md", content: "## Added" },
93 { path: "scripts/collect.sh", content: "echo hi" },
94 { path: "assets/logo.png", content: "iVBORw0KGgo=", encoding: "base64" },
95 ]);
96 assert.ok(good.ok);
97 if (!good.ok) return;
98 assert.equal(good.skill.name, "release-notes");
99 assert.deepEqual(good.skill.tools, ["make_file", "read_file"]);
100 assert.equal(good.skill.requires_computer, true, "scripts need a computer");
101 assert.deepEqual(good.skill.scripts, ["scripts/collect.sh"]);
102 assert.deepEqual(good.skill.files.map((f) => f.path), ["assets/logo.png", "resources/template.md", "scripts/collect.sh"]);
103 assert.deepEqual(good.skill.extra, { license: "MIT" });
104 assert.equal(good.skill.body, "Do the thing.");
105
106 const bad = (files: Parameters<typeof checkSkillFolder>[0], pattern: RegExp, expectName?: string) => {
107 const checked = checkSkillFolder(files, { expectName });
108 assert.equal(checked.ok, false, pattern.source);
109 assert.match(!checked.ok ? checked.message : "", pattern);
110 };
111 bad([], /a folder with a SKILL\.md/);
112 bad([{ path: "README.md", content: "x" }], /needs a SKILL\.md at its top/);
113 bad([{ path: "SKILL.md", content: md("description: x") }], /needs a name/);
114 bad([{ path: "SKILL.md", content: md("name: Release_Notes\ndescription: x") }], /lowercase letters, digits and single hyphens/);
115 bad([{ path: "SKILL.md", content: md("name: code\ndescription: x") }], /one of g1t's foundational skills/);
116 bad([{ path: "SKILL.md", content: md("name: a-b") }], /needs a description/);
117 bad([{ path: "SKILL.md", content: md(`name: a\ndescription: ${"x".repeat(1025)}`) }], /at most 1024 characters/);
118 bad([{ path: "SKILL.md", content: md("name: a\ndescription: x\ntools: [bash]") }], /names bash, which isn't a tool agents have/);
119 bad([{ path: "SKILL.md", content: md("name: a\ndescription: x\nrequires_computer: maybe") }], /requires_computer: is true or false/);
120 bad([{ path: "SKILL.md", content: md("name: a\ndescription: x", "") }], /needs instructions/);
121 bad([{ path: "SKILL.md", content: md("name: a\ndescription: x") }, { path: "../etc/passwd", content: "x" }], /isn't a path a skill can hold/);
122 bad([{ path: "SKILL.md", content: md("name: a\ndescription: x") }, { path: "skill.md", content: "x" }], /twice/);
123 bad([{ path: "SKILL.md", content: md("name: a\ndescription: x") }, { path: "big.txt", content: "x".repeat(SKILL_FOLDER_MAX_BYTES) }], /at most 1 MB/);
124 bad([{ path: "SKILL.md", content: md("name: a\ndescription: x") }], /The folder is b, but its SKILL\.md is named a/, "b");
125 assert.equal(skillFileBytes({ path: "x", content: "aGk=", encoding: "base64" }), 2);
126 assert.equal(skillFileBytes({ path: "x", content: "é" }), 2);
127 assert.equal(skillNameProblem("x".repeat(65))?.includes("at most 64"), true);
128});
129
130test("the editor writes SKILL.md that reads back the same", () => {
131 const written = renderSkillMd({
132 name: "brand-voice",
133 description: "Use when writing: posts, emails # and docs",
134 tools: ["create_artifact"],
135 requires_computer: true,
136 body: "# Voice\r\n\r\nWarm.",
137 extra: { license: "MIT", metadata: { version: "1.0", author: "acme" } },
138 });
139 assert.equal(
140 written,
141 '---\nname: brand-voice\ndescription: "Use when writing: posts, emails # and docs"\ntools: [create_artifact]\nrequires_computer: true\nlicense: MIT\nmetadata:\n version: "1.0"\n author: acme\n---\n\n# Voice\n\nWarm.\n',
142 );
143 const checked = checkSkillFolder([{ path: "SKILL.md", content: written }]);
144 assert.ok(checked.ok);
145 assert.equal(checked.ok && checked.skill.description, "Use when writing: posts, emails # and docs");
146 assert.deepEqual(checked.ok && checked.skill.extra, { license: "MIT", metadata: { version: "1.0", author: "acme" } });
147});
148
149test("g1t's foundational skills are written in the same format, and read back as themselves", () => {
150 assert.deepEqual(RESERVED_SKILL_NAMES, FOUNDATIONAL_SKILL_IDS);
151 for (const skill of FOUNDATIONAL_SKILLS) {
152 const md = foundationalSkillMd(skill);
153 // Checked as a library skill would be, apart from the reserved name.
154 const front = parseFrontMatter((splitFrontMatter(md) as { yaml: string }).yaml);
155 assert.equal(front.name, skill.id);
156 assert.equal(front.description, skill.when);
157 assert.match(skill.when, /^Use when /);
158 for (const tool of front.tools as string[]) assert.ok(AGENT_TOOL_NAMES.includes(tool), `${skill.id}: ${tool}`);
159 assert.deepEqual(front.metadata, { source: "g1t", version: skill.version });
160 assert.ok(md.includes(skill.instructions), `${skill.id} keeps its playbook`);
161 assert.match(md, /## What works today/);
162 if (skill.abilities.some((a) => a.status === "coming")) assert.match(md, /## Not yet in g1t/);
163 }
164});