import assert from "node:assert/strict"; import { spawnSync } from "node:child_process"; import { mkdtempSync, mkdirSync, rmSync, writeFileSync, } from "node:fs"; import { tmpdir } from "node:os"; import path from "node:path"; import test from "node:test"; import { fileURLToPath } from "node:url"; const skillDir = path.dirname(path.dirname(fileURLToPath(import.meta.url))); const checker = path.join(skillDir, "scripts", "check.mjs"); const temporaryDirectories = []; function fixture(name, files) { const directory = mkdtempSync(path.join(tmpdir(), "writing-skills-check-")); temporaryDirectories.push(directory); const root = path.join(directory, name); mkdirSync(root, { recursive: true }); for (const [relativePath, contents] of Object.entries(files)) { const file = path.join(root, relativePath); mkdirSync(path.dirname(file), { recursive: true }); writeFileSync(file, contents); } return root; } function skill(name = "fixture", body = "") { return `---\nname: ${name}\ndescription: Fixture skill\n---\n\n# Fixture\n\n${body}`; } function check(directory) { const result = spawnSync(process.execPath, [checker, directory], { encoding: "utf8", }); assert.equal(result.error, undefined, result.error?.message); return { status: result.status, output: JSON.parse(result.stdout), }; } function codes(output) { return output.issues.map((issue) => issue.code).sort(); } test.after(() => { for (const directory of temporaryDirectories) { rmSync(directory, { recursive: true, force: true }); } }); test("minimal prose-only skill needs no package", () => { const directory = fixture("fixture", { "SKILL.md": skill() }); const result = check(directory); assert.equal(result.status, 0); assert.equal(result.output.ok, true); assert.deepEqual(result.output.issues, []); }); test("scripted skill requires package and test file", () => { const directory = fixture("fixture", { "SKILL.md": skill(), "scripts/helper.mjs": "// fixture helper; never execute\n", }); const result = check(directory); assert.equal(result.status, 1); assert.equal(result.output.ok, false); assert.deepEqual(codes(result.output), ["package.missing", "tests.missing"]); }); test("scripted skill rejects a missing test file", () => { const directory = fixture("fixture", { "SKILL.md": skill(), "package.json": JSON.stringify({ scripts: { test: "node --test" } }), "scripts/helper.mjs": "// fixture helper; never execute\n", }); const result = check(directory); assert.equal(result.status, 1); assert.deepEqual(codes(result.output), ["tests.missing"]); }); test("scripted skill accepts direct node test packaging", () => { const directory = fixture("fixture", { "SKILL.md": skill(), "package.json": JSON.stringify({ scripts: { test: "node --test scripts/*.test.mjs" }, }), "scripts/helper.mjs": "// fixture helper; never execute\n", "scripts/helper.test.mjs": "// fixture test; never execute\n", }); const result = check(directory); assert.equal(result.status, 0); assert.equal(result.output.ok, true); }); test("CLI reports usage errors with status 2", () => { const result = spawnSync(process.execPath, [checker], { encoding: "utf8" }); assert.equal(result.status, 2); assert.deepEqual(JSON.parse(result.stdout), { error: "Usage: node scripts/check.mjs ", }); }); test("owning skill passes its structural self-check", () => { const result = check(skillDir); assert.equal(result.status, 0); assert.equal(result.output.skill, skillDir); assert.equal(result.output.ok, true); assert.deepEqual(result.output.issues, []); });