/** * Tests for default-plugin resident skill discovery. * * A default plugin ships inside the assistant's own source tree * (`plugins/defaults//`) rather than under `/plugins/`, so * its `skills/` directory is enumerated from the compiled-in tree. The walk * takes its roots as an argument so these tests can drive it against a temp * tree instead of adding fixture plugins to `src/`. */ import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync, } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { afterEach, beforeEach, describe, expect, test } from "bun:test"; import { getDefaultPluginSkillRoots } from "../../plugins/defaults/main.js"; import { getWorkspacePluginsDir } from "../../util/platform.js"; import { discoverDefaultPluginResidentSkills, discoverPluginResidentSkills, mergePluginResidentSkills, } from "../skills.js"; let tempRoot: string; /** * Create `//skills//SKILL.md` for each skill and return * the root descriptor the discovery walk consumes. */ function defaultPluginRoot( dirName: string, skills: Array<{ id: string; skillMd?: string }>, ): { pluginName: string; skillsDir: string } { const skillsDir = join(tempRoot, dirName, "skills"); for (const skill of skills) { const skillDir = join(skillsDir, skill.id); mkdirSync(skillDir, { recursive: true }); writeFileSync( join(skillDir, "SKILL.md"), skill.skillMd ?? `---\nname: ${skill.id}\ndescription: Description for ${skill.id}.\n---\n\nBody for ${skill.id}.\n`, ); } return { pluginName: `default-${dirName}`, skillsDir }; } /** Install a workspace plugin shipping the given skills. */ function writeWorkspacePlugin(dirName: string, skillIds: string[]): void { const pluginDir = join(getWorkspacePluginsDir(), dirName); mkdirSync(pluginDir, { recursive: true }); writeFileSync( join(pluginDir, "package.json"), JSON.stringify({ name: dirName, version: "1.0.0" }), ); for (const id of skillIds) { const skillDir = join(pluginDir, "skills", id); mkdirSync(skillDir, { recursive: true }); writeFileSync( join(skillDir, "SKILL.md"), `---\nname: ${id}\ndescription: Workspace plugin copy of ${id}.\n---\n\nBody.\n`, ); } } /** Write the `.disabled` sentinel the CLI writes for `pluginName`. */ function disablePlugin(pluginName: string): void { const dir = join(getWorkspacePluginsDir(), pluginName); mkdirSync(dir, { recursive: true }); writeFileSync(join(dir, ".disabled"), ""); } describe("default plugin resident skills", () => { beforeEach(() => { const pluginsDir = getWorkspacePluginsDir(); if (existsSync(pluginsDir)) { rmSync(pluginsDir, { recursive: true, force: true }); } tempRoot = mkdtempSync(join(tmpdir(), "default-plugin-skills-")); }); afterEach(() => { rmSync(tempRoot, { recursive: true, force: true }); }); test("surfaces a default plugin's resident skill, owned by its default- name", () => { const root = defaultPluginRoot("fixture", [{ id: "qa-fixture-skill" }]); const skills = discoverDefaultPluginResidentSkills([root]); expect(skills).toHaveLength(1); expect(skills[0]!.id).toBe("qa-fixture-skill"); expect(skills[0]!.source).toBe("plugin"); expect(skills[0]!.owner).toEqual({ kind: "plugin", id: "default-fixture", }); expect(skills[0]!.description).toBe("Description for qa-fixture-skill."); }); test("hides a default plugin's skills when the plugin is disabled", () => { const root = defaultPluginRoot("fixture", [{ id: "qa-fixture-skill" }]); disablePlugin("default-fixture"); expect(discoverDefaultPluginResidentSkills([root])).toEqual([]); }); test("an installed plugin's skill of the same id overrides the default one", () => { const root = defaultPluginRoot("fixture", [ { id: "qa-shared-skill" }, { id: "qa-default-only" }, ]); writeWorkspacePlugin("installed-fixture", ["qa-shared-skill"]); // The installed side comes from the real zero-arg discovery (the shipped // defaults tree contributes nothing), the default side from the fixture. const skills = mergePluginResidentSkills( discoverDefaultPluginResidentSkills([root]), discoverPluginResidentSkills(), ); const shared = skills.filter((skill) => skill.id === "qa-shared-skill"); expect(shared).toHaveLength(1); expect(shared[0]!.owner).toEqual({ kind: "plugin", id: "installed-fixture", }); // The default plugin's other skills are unaffected by the override. expect( skills.find((skill) => skill.id === "qa-default-only")?.owner, ).toEqual({ kind: "plugin", id: "default-fixture" }); }); test("skips a malformed SKILL.md and still surfaces its valid siblings", () => { const root = defaultPluginRoot("fixture", [ { id: "qa-malformed-skill", skillMd: "No frontmatter here.\n" }, { id: "qa-missing-description", skillMd: "---\nname: x\n---\n\nBody.\n" }, { id: "qa-valid-skill" }, ]); const skills = discoverDefaultPluginResidentSkills([root]); expect(skills.map((skill) => skill.id)).toEqual(["qa-valid-skill"]); }); test("getDefaultPluginSkillRoots reports no roots until a default plugin ships skills", () => { // Every root it reports must be a `default-`-prefixed plugin name; today no // default plugin ships a `skills/` directory, so the set is empty. for (const root of getDefaultPluginSkillRoots()) { expect(root.pluginName).toMatch(/^default-[a-z0-9-]+$/); } expect(getDefaultPluginSkillRoots()).toEqual([]); }); });