import assert from "node:assert/strict"; import { mkdtemp, mkdir, writeFile, rm, lstat, symlink } from "node:fs/promises"; import { tmpdir } from "node:os"; import path from "node:path"; import test from "node:test"; import { removeTarget } from "../lib/skill-manager-core.mjs"; // ── Happy path: remove existing directory ───────────────────────────────── test("removeTarget removes an installed skill directory", async () => { const dir = await mkdtemp(path.join(tmpdir(), "smc-remove-dir-")); try { const skillsRoot = path.join(dir, "skills"); const target = path.join(skillsRoot, "create-plan"); await mkdir(target, { recursive: true }); await writeFile(path.join(target, "SKILL.md"), "---\nname: create-plan\n---\n"); const op = { kind: "skill", action: "remove", target, skillsRoot }; const result = await removeTarget(op); assert.equal(result.status, "ok"); let exists = true; try { await lstat(target); } catch { exists = false; } assert.equal(exists, false, "target directory should be gone"); } finally { await rm(dir, { recursive: true, force: true }); } }); // ── Happy path: remove symbolic link ───────────────────────────────────── test("removeTarget removes a symlink without following it", async () => { const dir = await mkdtemp(path.join(tmpdir(), "smc-remove-sym-")); try { const skillsRoot = path.join(dir, "skills"); const realDir = path.join(dir, "real-skill"); const target = path.join(skillsRoot, "create-plan"); await mkdir(skillsRoot, { recursive: true }); await mkdir(realDir, { recursive: true }); await writeFile(path.join(realDir, "SKILL.md"), "---\nname: create-plan\n---\n"); await symlink(realDir, target, "dir"); const op = { kind: "skill", action: "remove", target, skillsRoot }; const result = await removeTarget(op); assert.equal(result.status, "ok"); // symlink itself should be gone let symlinkExists = true; try { await lstat(target); } catch { symlinkExists = false; } assert.equal(symlinkExists, false, "symlink should be removed"); // real directory should still exist const realStat = await lstat(realDir); assert.ok(realStat.isDirectory(), "real directory must not be touched"); } finally { await rm(dir, { recursive: true, force: true }); } }); // ── Missing skill (partial state): target does not exist ───────────────── test("removeTarget succeeds when target does not exist (idempotent)", async () => { const dir = await mkdtemp(path.join(tmpdir(), "smc-remove-missing-")); try { const skillsRoot = path.join(dir, "skills"); const target = path.join(skillsRoot, "create-plan"); await mkdir(skillsRoot, { recursive: true }); // target intentionally NOT created const op = { kind: "skill", action: "remove", target, skillsRoot }; const result = await removeTarget(op); assert.equal(result.status, "ok"); } finally { await rm(dir, { recursive: true, force: true }); } }); // ── Partial state: directory exists but is empty ───────────────────────── test("removeTarget removes an empty skill directory", async () => { const dir = await mkdtemp(path.join(tmpdir(), "smc-remove-empty-")); try { const skillsRoot = path.join(dir, "skills"); const target = path.join(skillsRoot, "create-plan"); await mkdir(target, { recursive: true }); // directory exists but has no SKILL.md (partial install state) const op = { kind: "skill", action: "remove", target, skillsRoot }; const result = await removeTarget(op); assert.equal(result.status, "ok"); let exists = true; try { await lstat(target); } catch { exists = false; } assert.equal(exists, false); } finally { await rm(dir, { recursive: true, force: true }); } }); // ── Safety: refuses to remove path outside skills root ──────────────────── test("removeTarget refuses to remove a path outside the skills root", async () => { const dir = await mkdtemp(path.join(tmpdir(), "smc-remove-outside-")); try { const skillsRoot = path.join(dir, "skills"); const outsideTarget = path.join(dir, "outside"); await mkdir(skillsRoot, { recursive: true }); await mkdir(outsideTarget, { recursive: true }); const op = { kind: "skill", action: "remove", target: outsideTarget, skillsRoot, }; await assert.rejects(() => removeTarget(op), /outside skills root/); } finally { await rm(dir, { recursive: true, force: true }); } });