251148c3ff
## Summary - add repository-wide quality tooling and verification scaffolding, including CI workflows, pnpm workspace setup, ESLint/Prettier/markdown checks, and generated-output verification helpers - reorganize skill sources and generation flow by introducing canonical `_source` variants, generator/manifests, reusable helper abstractions, and shared web-automation/browser utilities - clean up and expand documentation so the root README flows into docs and skill docs, with clearer development, reviewer, installer, and workflow guidance ## Notable changes - docs flow and consistency cleanup across `README.md`, `docs/README.md`, and related docs - new scripts for `check`, docs verification, generated-file verification, shell portability, and safe directory replacement - refactors in Atlassian and web-automation skill runtimes to reduce duplication and centralize reusable code - changelog, development documentation, and CI surface updates ## Test Plan - [ ] `pnpm run check` - [ ] review generated/manifests and skill sync outputs - [ ] smoke-check docs flow from `README.md` to `docs/README.md` to skill docs ## Notes - this branch currently includes tracked `skills/web-automation/shared/node_modules` content that should be reviewed carefully as potentially noisy/accidental committed artifacts Co-authored-by: Stefano Fiorini <stefano.fiorini@firsthorizon.com> Reviewed-on: #1
430 lines
20 KiB
JavaScript
430 lines
20 KiB
JavaScript
import assert from "node:assert/strict";
|
|
import { execFileSync } from "node:child_process";
|
|
import { mkdtemp, mkdir, writeFile, rm } from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import path from "node:path";
|
|
import test from "node:test";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
import {
|
|
CLIENTS,
|
|
SKILLS,
|
|
buildOperationPlan,
|
|
detectInstalledSkills,
|
|
findInstalledSuperpowers,
|
|
getSkillSource,
|
|
piPackageCommand,
|
|
parseReviewerShorthand,
|
|
validateRemoveTarget,
|
|
} from "../lib/skill-manager-core.mjs";
|
|
|
|
const REPO_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
|
|
|
test("manifest records supported variants and helper allowlists", () => {
|
|
assert.deepEqual(SKILLS["web-automation"].variants, ["codex", "claude-code", "cursor", "opencode", "pi"]);
|
|
assert.deepEqual(CLIENTS.codex.reviewerRuntime.files, ["run-review.sh", "notify-telegram.sh"]);
|
|
assert.deepEqual(CLIENTS.pi.reviewerRuntime.files, ["run-review.sh", "notify-telegram.sh"]);
|
|
});
|
|
|
|
test("parseReviewerShorthand keeps provider-qualified model ids verbatim", () => {
|
|
assert.deepEqual(parseReviewerShorthand("pi/claude-opus-4-7"), {
|
|
reviewerCli: "pi",
|
|
reviewerModel: "claude-opus-4-7",
|
|
});
|
|
assert.deepEqual(parseReviewerShorthand("pi/anthropic/claude-opus-4-7"), {
|
|
reviewerCli: "pi",
|
|
reviewerModel: "anthropic/claude-opus-4-7",
|
|
});
|
|
assert.equal(parseReviewerShorthand("codex/gpt-5"), null);
|
|
});
|
|
|
|
test("unsupported skill variant is reported as unsupported", () => {
|
|
assert.ok(getSkillSource("web-automation", "cursor").endsWith("skills/web-automation/cursor"));
|
|
assert.ok(getSkillSource("web-automation", "pi").endsWith("pi-package/skills/web-automation"));
|
|
});
|
|
|
|
test("detectInstalledSkills reports installed and missing skills", async () => {
|
|
const dir = await mkdtemp(path.join(tmpdir(), "skill-manager-detect-"));
|
|
try {
|
|
await mkdir(path.join(dir, "skills", "create-plan"), { recursive: true });
|
|
await writeFile(path.join(dir, "skills", "create-plan", "SKILL.md"), "---\nname: create-plan\n---\n");
|
|
const state = await detectInstalledSkills({ skillsRoot: path.join(dir, "skills"), clientId: "codex" });
|
|
assert.equal(state["create-plan"].state, "installed");
|
|
assert.equal(state["web-automation"].state, "not-installed");
|
|
} finally {
|
|
await rm(dir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("plan install workflow skill includes reviewer-runtime and missing Superpowers prompt", async () => {
|
|
const dir = await mkdtemp(path.join(tmpdir(), "skill-manager-plan-"));
|
|
try {
|
|
await mkdir(path.join(dir, "repo", "skills", "create-plan", "codex"), { recursive: true });
|
|
await writeFile(path.join(dir, "repo", "skills", "create-plan", "codex", "SKILL.md"), "---\nname: create-plan\n---\n");
|
|
const plan = await buildOperationPlan({
|
|
selections: [{ clientId: "codex", scope: "global", skillsRoot: path.join(dir, "install"), actions: { "create-plan": "install" } }],
|
|
assumeYes: true,
|
|
repoRoot: path.join(dir, "repo"),
|
|
superpowersByClient: { codex: [] },
|
|
});
|
|
assert.equal(plan.operations.some((op) => op.kind === "skill" && op.skill === "create-plan" && op.action === "install"), true);
|
|
assert.equal(plan.operations.some((op) => op.kind === "helper" && op.helper === "reviewer-runtime"), true);
|
|
assert.equal(plan.prompts.some((prompt) => prompt.kind === "missing-superpowers" && prompt.clientId === "codex"), true);
|
|
} finally {
|
|
await rm(dir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("plan skips already current reviewer-runtime helper for workflow skill updates", async () => {
|
|
const dir = await mkdtemp(path.join(tmpdir(), "skill-manager-helper-current-"));
|
|
try {
|
|
const repo = path.join(dir, "repo");
|
|
const install = path.join(dir, "install");
|
|
await mkdir(path.join(repo, "skills", "create-plan", "cursor"), { recursive: true });
|
|
await mkdir(path.join(repo, "skills", "do-task", "cursor"), { recursive: true });
|
|
await writeFile(path.join(repo, "skills", "create-plan", "cursor", "SKILL.md"), "---\nname: create-plan\n---\n");
|
|
await writeFile(path.join(repo, "skills", "do-task", "cursor", "SKILL.md"), "---\nname: do-task\n---\n");
|
|
await mkdir(path.join(repo, "skills", "reviewer-runtime"), { recursive: true });
|
|
await mkdir(path.join(install, "reviewer-runtime"), { recursive: true });
|
|
for (const file of CLIENTS.cursor.reviewerRuntime.files) {
|
|
await writeFile(path.join(repo, "skills", "reviewer-runtime", file), `${file}\n`);
|
|
await writeFile(path.join(install, "reviewer-runtime", file), `${file}\n`);
|
|
}
|
|
|
|
const plan = await buildOperationPlan({
|
|
selections: [{ clientId: "cursor", scope: "global", skillsRoot: install, actions: { "create-plan": "update", "do-task": "update" } }],
|
|
repoRoot: repo,
|
|
superpowersByClient: { cursor: [path.join(dir, "superpowers")] },
|
|
});
|
|
|
|
const helperRows = plan.reportRows.filter((row) => row.item === "reviewer-runtime");
|
|
assert.equal(helperRows.length, 1);
|
|
assert.equal(helperRows[0].action, "install");
|
|
assert.equal(helperRows[0].status, "skipped");
|
|
assert.match(helperRows[0].details, /already installed/);
|
|
} finally {
|
|
await rm(dir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("plan auto-updates stale reviewer-runtime helper for workflow skill updates", async () => {
|
|
const dir = await mkdtemp(path.join(tmpdir(), "skill-manager-helper-stale-"));
|
|
try {
|
|
const repo = path.join(dir, "repo");
|
|
const install = path.join(dir, "install");
|
|
await mkdir(path.join(repo, "skills", "create-plan", "cursor"), { recursive: true });
|
|
await writeFile(path.join(repo, "skills", "create-plan", "cursor", "SKILL.md"), "---\nname: create-plan\n---\n");
|
|
await mkdir(path.join(repo, "skills", "reviewer-runtime"), { recursive: true });
|
|
await mkdir(path.join(install, "reviewer-runtime"), { recursive: true });
|
|
for (const file of CLIENTS.cursor.reviewerRuntime.files) {
|
|
await writeFile(path.join(repo, "skills", "reviewer-runtime", file), `${file}:new\n`);
|
|
await writeFile(path.join(install, "reviewer-runtime", file), `${file}:old\n`);
|
|
}
|
|
|
|
const plan = await buildOperationPlan({
|
|
selections: [{ clientId: "cursor", scope: "global", skillsRoot: install, actions: { "create-plan": "update" } }],
|
|
repoRoot: repo,
|
|
superpowersByClient: { cursor: [path.join(dir, "superpowers")] },
|
|
});
|
|
|
|
const helperRows = plan.reportRows.filter((row) => row.item === "reviewer-runtime");
|
|
assert.equal(helperRows.length, 1);
|
|
assert.equal(helperRows[0].action, "update");
|
|
assert.equal(helperRows[0].status, "planned");
|
|
} finally {
|
|
await rm(dir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("plan honors explicit reviewer-runtime helper actions", async () => {
|
|
const dir = await mkdtemp(path.join(tmpdir(), "skill-manager-helper-explicit-"));
|
|
try {
|
|
const repo = path.join(dir, "repo");
|
|
const install = path.join(dir, "install");
|
|
await mkdir(path.join(repo, "skills", "reviewer-runtime"), { recursive: true });
|
|
await mkdir(path.join(install, "reviewer-runtime"), { recursive: true });
|
|
for (const file of CLIENTS.cursor.reviewerRuntime.files) {
|
|
await writeFile(path.join(repo, "skills", "reviewer-runtime", file), `${file}\n`);
|
|
await writeFile(path.join(install, "reviewer-runtime", file), `${file}\n`);
|
|
}
|
|
|
|
const plan = await buildOperationPlan({
|
|
selections: [{ clientId: "cursor", scope: "global", skillsRoot: install, actions: {}, helperActions: { "reviewer-runtime": "reinstall" } }],
|
|
repoRoot: repo,
|
|
});
|
|
|
|
assert.deepEqual(plan.reportRows.map((row) => [row.item, row.action, row.status]), [
|
|
["reviewer-runtime", "reinstall", "planned"],
|
|
]);
|
|
} finally {
|
|
await rm(dir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("plan labels skill bootstrap rows as dependency rows", async () => {
|
|
const dir = await mkdtemp(path.join(tmpdir(), "skill-manager-bootstrap-label-"));
|
|
try {
|
|
const repo = path.join(dir, "repo");
|
|
const install = path.join(dir, "install");
|
|
await mkdir(path.join(repo, "skills", "web-automation", "claude-code"), { recursive: true });
|
|
await writeFile(path.join(repo, "skills", "web-automation", "claude-code", "SKILL.md"), "---\nname: web-automation\n---\n");
|
|
|
|
const plan = await buildOperationPlan({
|
|
selections: [{ clientId: "claude-code", scope: "global", skillsRoot: install, actions: { "web-automation": "update" } }],
|
|
repoRoot: repo,
|
|
});
|
|
|
|
assert.deepEqual(plan.reportRows.map((row) => [row.item, row.action]), [
|
|
["web-automation", "update"],
|
|
["web-automation deps", "bootstrap-deps"],
|
|
]);
|
|
} finally {
|
|
await rm(dir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("findInstalledSuperpowers detects Claude Code Superpowers plugin installs", async () => {
|
|
const dir = await mkdtemp(path.join(tmpdir(), "skill-manager-claude-superpowers-"));
|
|
try {
|
|
const installPath = path.join(dir, ".claude", "plugins", "cache", "claude-plugins-official", "superpowers", "4.2.0");
|
|
await mkdir(path.join(installPath, "skills", "brainstorming"), { recursive: true });
|
|
await writeFile(path.join(installPath, "skills", "brainstorming", "SKILL.md"), "---\nname: brainstorming\n---\n");
|
|
await mkdir(path.join(dir, ".claude", "plugins"), { recursive: true });
|
|
await writeFile(path.join(dir, ".claude", "settings.json"), JSON.stringify({
|
|
enabledPlugins: {
|
|
"superpowers@claude-plugins-official": true,
|
|
},
|
|
}));
|
|
await writeFile(path.join(dir, ".claude", "plugins", "installed_plugins.json"), JSON.stringify({
|
|
plugins: {
|
|
"superpowers@claude-plugins-official": [
|
|
{
|
|
scope: "user",
|
|
installPath,
|
|
version: "4.2.0",
|
|
},
|
|
],
|
|
},
|
|
}));
|
|
|
|
assert.deepEqual(await findInstalledSuperpowers("claude-code", process.cwd(), { homeDir: dir }), [
|
|
path.join(installPath, "skills"),
|
|
]);
|
|
} finally {
|
|
await rm(dir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("findInstalledSuperpowers detects OpenCode shared agents Superpowers installs", async () => {
|
|
const dir = await mkdtemp(path.join(tmpdir(), "skill-manager-opencode-superpowers-"));
|
|
try {
|
|
const sharedRoot = path.join(dir, ".agents", "skills", "superpowers");
|
|
await mkdir(path.join(sharedRoot, "brainstorming"), { recursive: true });
|
|
await writeFile(path.join(sharedRoot, "brainstorming", "SKILL.md"), "---\nname: brainstorming\n---\n");
|
|
|
|
assert.deepEqual(await findInstalledSuperpowers("opencode", process.cwd(), { homeDir: dir }), [
|
|
sharedRoot,
|
|
]);
|
|
} finally {
|
|
await rm(dir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("findInstalledSuperpowers detects Cursor Superpowers plugin installs", async () => {
|
|
const dir = await mkdtemp(path.join(tmpdir(), "skill-manager-cursor-superpowers-"));
|
|
try {
|
|
const pluginSkills = path.join(dir, ".cursor", "plugins", "cache", "cursor-public", "superpowers", "abc123", "skills");
|
|
await mkdir(path.join(pluginSkills, "brainstorming"), { recursive: true });
|
|
await writeFile(path.join(pluginSkills, "brainstorming", "SKILL.md"), "---\nname: brainstorming\n---\n");
|
|
|
|
assert.deepEqual(await findInstalledSuperpowers("cursor", process.cwd(), { homeDir: dir }), [
|
|
pluginSkills,
|
|
]);
|
|
} finally {
|
|
await rm(dir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("plan removing last workflow skill prompts for optional Superpowers removal", async () => {
|
|
const dir = await mkdtemp(path.join(tmpdir(), "skill-manager-remove-"));
|
|
try {
|
|
await mkdir(path.join(dir, "skills", "do-task"), { recursive: true });
|
|
await writeFile(path.join(dir, "skills", "do-task", "SKILL.md"), "---\nname: do-task\n---\n");
|
|
const plan = await buildOperationPlan({
|
|
selections: [{ clientId: "codex", scope: "global", skillsRoot: path.join(dir, "skills"), actions: { "do-task": "remove" } }],
|
|
assumeYes: true,
|
|
repoRoot: process.cwd(),
|
|
});
|
|
assert.equal(plan.prompts.some((prompt) => prompt.kind === "remove-superpowers" && prompt.clientId === "codex"), true);
|
|
} finally {
|
|
await rm(dir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("pi package mode plans full package install instead of per-skill copy", async () => {
|
|
const dir = await mkdtemp(path.join(tmpdir(), "skill-manager-pi-package-"));
|
|
try {
|
|
const plan = await buildOperationPlan({
|
|
selections: [{ clientId: "pi", scope: "packageLocal", action: "install", actions: { "create-plan": "skip", atlassian: "remove" } }],
|
|
repoRoot: dir,
|
|
});
|
|
assert.equal(plan.operations.some((op) => op.kind === "pi-package" && op.piInstallArg === "-l"), true);
|
|
assert.equal(plan.operations.some((op) => op.kind === "skill"), false);
|
|
} finally {
|
|
await rm(dir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("pi package mode surfaces bundled skills and skips already installed package resources", async () => {
|
|
const dir = await mkdtemp(path.join(tmpdir(), "skill-manager-pi-package-installed-"));
|
|
try {
|
|
const repo = path.join(dir, "repo");
|
|
await mkdir(path.join(repo, ".pi"), { recursive: true });
|
|
await writeFile(path.join(repo, ".pi", "settings.json"), JSON.stringify({ packages: [".."] }));
|
|
for (const skill of Object.keys(SKILLS)) {
|
|
await mkdir(path.join(repo, "pi-package", "skills", skill), { recursive: true });
|
|
}
|
|
await mkdir(path.join(repo, "pi-package", "skills", "atlassian", "scripts", "node_modules"), { recursive: true });
|
|
await mkdir(path.join(repo, "pi-package", "skills", "web-automation", "scripts", "node_modules", ".bin"), { recursive: true });
|
|
await writeFile(path.join(repo, "pi-package", "skills", "web-automation", "scripts", "node_modules", ".bin", "cloakbrowser"), "");
|
|
|
|
const plan = await buildOperationPlan({
|
|
selections: [{ clientId: "pi", scope: "packageLocal", action: "install", actions: {} }],
|
|
repoRoot: repo,
|
|
});
|
|
|
|
const packageInstall = plan.operations.find((op) => op.kind === "pi-package");
|
|
assert.equal(packageInstall.status, "skipped");
|
|
assert.match(packageInstall.details, /already installed/);
|
|
assert.deepEqual(
|
|
plan.reportRows.filter((row) => row.action === "included").map((row) => row.item).sort(),
|
|
Object.keys(SKILLS).sort()
|
|
);
|
|
assert.deepEqual(
|
|
plan.reportRows.filter((row) => row.action === "bootstrap-deps").map((row) => [row.item, row.status]).sort(),
|
|
[["atlassian", "skipped"], ["web-automation", "skipped"]]
|
|
);
|
|
} finally {
|
|
await rm(dir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("pi package mode remove skips when package is not installed", async () => {
|
|
const dir = await mkdtemp(path.join(tmpdir(), "skill-manager-pi-package-remove-"));
|
|
try {
|
|
const plan = await buildOperationPlan({
|
|
selections: [{ clientId: "pi", scope: "packageLocal", action: "remove", actions: {} }],
|
|
repoRoot: dir,
|
|
});
|
|
|
|
assert.deepEqual(plan.operations.map((op) => op.kind), ["pi-package"]);
|
|
assert.equal(plan.operations[0].action, "remove");
|
|
assert.equal(plan.operations[0].status, "skipped");
|
|
assert.match(plan.operations[0].details, /not installed/);
|
|
assert.equal(plan.reportRows[0].item, "pi-package");
|
|
assert.equal(plan.reportRows[0].action, "remove");
|
|
assert.equal(plan.reportRows[0].status, "skipped");
|
|
} finally {
|
|
await rm(dir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("pi package mode remove plans removal when package is installed", async () => {
|
|
const dir = await mkdtemp(path.join(tmpdir(), "skill-manager-pi-package-remove-installed-"));
|
|
try {
|
|
const repo = path.join(dir, "repo");
|
|
await mkdir(path.join(repo, ".pi"), { recursive: true });
|
|
await writeFile(path.join(repo, ".pi", "settings.json"), JSON.stringify({ packages: [".."] }));
|
|
const plan = await buildOperationPlan({
|
|
selections: [{ clientId: "pi", scope: "packageLocal", action: "remove", actions: {} }],
|
|
repoRoot: repo,
|
|
});
|
|
|
|
assert.deepEqual(plan.operations.map((op) => op.kind), ["pi-package"]);
|
|
assert.equal(plan.operations[0].action, "remove");
|
|
assert.equal(plan.operations[0].status, undefined);
|
|
assert.equal(plan.reportRows[0].status, "planned");
|
|
} finally {
|
|
await rm(dir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("pi package mode update syncs and forces package reinstall plus dependency bootstrap", async () => {
|
|
const dir = await mkdtemp(path.join(tmpdir(), "skill-manager-pi-package-update-"));
|
|
try {
|
|
const repo = path.join(dir, "repo");
|
|
await mkdir(path.join(repo, ".pi"), { recursive: true });
|
|
await writeFile(path.join(repo, ".pi", "settings.json"), JSON.stringify({ packages: [".."] }));
|
|
await mkdir(path.join(repo, "pi-package", "skills", "atlassian", "scripts", "node_modules"), { recursive: true });
|
|
await mkdir(path.join(repo, "pi-package", "skills", "web-automation", "scripts", "node_modules", ".bin"), { recursive: true });
|
|
await writeFile(path.join(repo, "pi-package", "skills", "web-automation", "scripts", "node_modules", ".bin", "cloakbrowser"), "");
|
|
|
|
const plan = await buildOperationPlan({
|
|
selections: [{ clientId: "pi", scope: "packageLocal", action: "update", actions: {} }],
|
|
repoRoot: repo,
|
|
});
|
|
|
|
assert.equal(plan.operations[0].kind, "sync-pi-package");
|
|
const packageUpdate = plan.operations.find((op) => op.kind === "pi-package");
|
|
assert.equal(packageUpdate.action, "update");
|
|
assert.equal(packageUpdate.status, undefined);
|
|
assert.deepEqual(
|
|
plan.reportRows.filter((row) => row.action === "bootstrap-deps").map((row) => [row.item, row.status]).sort(),
|
|
[["atlassian", "planned"], ["web-automation", "planned"]]
|
|
);
|
|
} finally {
|
|
await rm(dir, { recursive: true, force: true });
|
|
}
|
|
});
|
|
|
|
test("pi package command helper builds exact install and remove argv", () => {
|
|
assert.deepEqual(piPackageCommand({ action: "install", repoRoot: "/repo", piInstallArg: "" }), ["pi", ["install", "/repo"]]);
|
|
assert.deepEqual(piPackageCommand({ action: "update", repoRoot: "/repo", piInstallArg: "-l" }), ["pi", ["install", "-l", "/repo"]]);
|
|
assert.deepEqual(piPackageCommand({ action: "reinstall", repoRoot: "/repo", piInstallArg: "-l" }), ["pi", ["install", "-l", "/repo"]]);
|
|
assert.deepEqual(piPackageCommand({ action: "remove", repoRoot: "/repo", piInstallArg: "" }), ["pi", ["remove", "/repo"]]);
|
|
assert.deepEqual(piPackageCommand({ action: "remove", repoRoot: "/repo", piInstallArg: "-l" }), ["pi", ["remove", "-l", "/repo"]]);
|
|
});
|
|
|
|
test("cli package mode preserves package action and ignores skill narrowing", () => {
|
|
const output = execFileSync(process.execPath, [
|
|
path.join(REPO_ROOT, "scripts", "manage-skills.mjs"),
|
|
"--client", "pi",
|
|
"--scope", "packageGlobal",
|
|
"--pi-package",
|
|
"--skill", "create-plan",
|
|
"--action", "remove",
|
|
"--plan-only",
|
|
], { cwd: REPO_ROOT, encoding: "utf8" });
|
|
const plan = JSON.parse(output);
|
|
assert.deepEqual(plan.operations.map((op) => op.kind), ["pi-package"]);
|
|
assert.equal(plan.operations[0].action, "remove");
|
|
});
|
|
|
|
test("cli exits without confirmation when no operations are planned", () => {
|
|
const output = execFileSync(process.execPath, [
|
|
path.join(REPO_ROOT, "scripts", "manage-skills.mjs"),
|
|
"--answers",
|
|
"-",
|
|
], {
|
|
cwd: REPO_ROOT,
|
|
encoding: "utf8",
|
|
input: JSON.stringify({ selections: [{ clientId: "pi", scope: "packageGlobal", action: "skip", actions: {} }] }),
|
|
});
|
|
assert.match(output, /No operations planned\./);
|
|
assert.doesNotMatch(output, /Proceed with these operations/);
|
|
assert.doesNotMatch(output, /Final report/);
|
|
});
|
|
|
|
test("validateRemoveTarget rejects paths outside the manifest root", async () => {
|
|
const dir = await mkdtemp(path.join(tmpdir(), "skill-manager-safe-"));
|
|
try {
|
|
await mkdir(path.join(dir, "skills", "create-plan"), { recursive: true });
|
|
await mkdir(path.join(dir, "outside"), { recursive: true });
|
|
assert.equal(await validateRemoveTarget(path.join(dir, "skills", "create-plan"), path.join(dir, "skills")), true);
|
|
await assert.rejects(() => validateRemoveTarget(path.join(dir, "outside"), path.join(dir, "skills")), /outside skills root/);
|
|
await assert.rejects(() => validateRemoveTarget(dir, dir), /refusing to remove skills root itself/);
|
|
} finally {
|
|
await rm(dir, { recursive: true, force: true });
|
|
}
|
|
});
|