/** * @file Tests the `Skill plugins` row of scripts/plugin-doctor.js — the check * that tells a fresh install its pipelines name slash commands no installed * plugin provides (today: `superpowers`, which `/agenthub:ship-feature-lane` drives * step by step). The row must WARN and never FAIL: without that plugin the * dashboard, hooks, CLI and MCP all still work, so failing a doctor run over it * would cry wolf. Every case runs against an injected temp CLAUDE_HOME. * @author Nguyễn Ngọc Trí Vĩ */ const { describe, it, after } = require("node:test"); const assert = require("node:assert/strict"); const fs = require("node:fs"); const os = require("node:os"); const path = require("node:path"); const TMP_HOME = fs.mkdtempSync(path.join(os.tmpdir(), "agenthub-doctor-skills-")); process.env.CLAUDE_HOME = TMP_HOME; const { diagnose } = require("../../scripts/plugin-doctor"); after(() => { fs.rmSync(TMP_HOME, { recursive: true, force: true }); delete process.env.CLAUDE_HOME; }); /** Rewrite the injected Claude home, then read back just the row under test. */ function rowFor({ plugins = {}, enabledPlugins = {}, userSkills = [], installDirs = [] }) { fs.rmSync(TMP_HOME, { recursive: true, force: true }); fs.mkdirSync(path.join(TMP_HOME, "plugins"), { recursive: true }); fs.mkdirSync(path.join(TMP_HOME, "skills"), { recursive: true }); fs.writeFileSync( path.join(TMP_HOME, "plugins", "installed_plugins.json"), JSON.stringify({ plugins }) ); fs.writeFileSync(path.join(TMP_HOME, "settings.json"), JSON.stringify({ enabledPlugins })); // Created AFTER the wipe above: readPlugins() only counts an instance whose // installPath still exists on disk. for (const dir of installDirs) fs.mkdirSync(dir, { recursive: true }); for (const name of userSkills) { fs.mkdirSync(path.join(TMP_HOME, "skills", name), { recursive: true }); fs.writeFileSync( path.join(TMP_HOME, "skills", name, "SKILL.md"), `---\nname: ${name}\n---\n\nbody\n` ); } const row = diagnose().find((r) => r.label === "Skill plugins"); assert.ok(row, "the doctor must always emit a Skill plugins row"); return row; } const SP_DIR = path.join(TMP_HOME, "plugins", "sp"); /** An installed superpowers instance; `enabled` mirrors settings.json. */ function installedSuperpowers(enabled) { return { plugins: { "superpowers@official": [{ installPath: SP_DIR, scope: "user", version: "1.0.0" }] }, enabledPlugins: { "superpowers@official": enabled }, installDirs: [SP_DIR], }; } /** The skills the built-in templates name, read from the templates themselves. */ function requiredSuperpowersSkills() { const { listPipelines } = require("../lib/pipelines"); const names = new Set(); for (const p of listPipelines()) { for (const cmd of [p.skill, ...p.nodes.map((n) => n.command)]) { const m = /^\/superpowers:([A-Za-z0-9_-]+)/.exec(cmd || ""); if (m) names.add(m[1]); } } return [...names]; } describe("plugin-doctor: Skill plugins row", () => { it("warns, never fails, when a plugin the pipelines name is not installed", () => { const row = rowFor({}); assert.equal(row.level, "warn", "a missing skill plugin must not fail the doctor run"); assert.match(row.detail, /superpowers/); assert.match(row.detail, /claude plugin install superpowers/); }); it("names the specific skills that are unavailable, so the reader can verify the claim", () => { const row = rowFor({}); for (const skill of requiredSuperpowersSkills()) { assert.match(row.detail, new RegExp(skill.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))); } }); it("passes once the plugin is installed and enabled", () => { const row = rowFor(installedSuperpowers(true)); assert.equal(row.level, "ok", row.detail); assert.match(row.detail, /superpowers available/); }); it("treats an explicitly disabled plugin as missing - Claude Code will not resolve its skills", () => { const row = rowFor(installedSuperpowers(false)); assert.equal(row.level, "warn", row.detail); assert.match(row.detail, /superpowers/); }); it("accepts the skills copied into ~/.claude/skills instead of installed as a plugin", () => { // A legitimate setup. Reporting it as missing would train the reader to // ignore this row, which is the only failure mode a warning really has. const row = rowFor({ userSkills: requiredSuperpowersSkills() }); assert.equal(row.level, "ok", row.detail); }); it("still warns when only SOME of the required skills are present locally", () => { const [first, ...rest] = requiredSuperpowersSkills(); const row = rowFor({ userSkills: [first] }); assert.equal(row.level, "warn", row.detail); assert.ok(!row.detail.includes(`${first},`), `${first} is present and must not be listed`); for (const skill of rest) assert.match(row.detail, new RegExp(skill)); }); });