import { afterEach, beforeEach, describe, expect, it } from "bun:test"; import { mkdir, mkdtemp, readFile, rm } from "node:fs/promises"; import { tmpdir } from "node:os"; import { join } from "node:path"; import { SkillStore } from "../../src/skills/store.js"; describe("SkillStore", () => { let originalCwd: string; let tempDir: string; let alternateCwd: string; let skillsRoot: string; let backupRoot: string; let store: SkillStore; const skillDocument = (name: string, body: string) => [ "---", `name: ${name}`, `description: ${name} workflow.`, "---", "", body, ].join("\n"); beforeEach(async () => { originalCwd = process.cwd(); tempDir = await mkdtemp(join(tmpdir(), "tjwater-skills-")); alternateCwd = join(tempDir, "runtime-cwd"); skillsRoot = join(tempDir, "project", ".opencode", "skills"); backupRoot = join(tempDir, "backup", "skills"); store = new SkillStore(skillsRoot, backupRoot); }); afterEach(async () => { process.chdir(originalCwd); await rm(tempDir, { force: true, recursive: true }); }); it("writes scripts under the configured skills root regardless of process cwd", async () => { await mkdir(alternateCwd, { recursive: true }); process.chdir(alternateCwd); const result = await store.writeScript( "workflow/hydraulic-bottleneck-analysis", "scripts/analyze.py", "print('ok')\n", ); expect(result).toEqual({ changed: true, detail: "script written", target: join( skillsRoot, "workflow", "hydraulic-bottleneck-analysis", "scripts", "analyze.py", ), }); await expect(readFile(result.target, "utf8")).resolves.toBe("print('ok')\n"); }); it("rejects script paths outside scripts/*.py", async () => { const result = await store.writeScript( "workflow/hydraulic-bottleneck-analysis", "analyze.ts", "console.log('ok')\n", ); expect(result).toEqual({ changed: false, detail: "invalid script file_path", target: "", }); }); it("writes and overwrites the main skill file", async () => { const skillPath = "workflow/pressure-review"; const writeResult = await store.writeSkill( skillPath, skillDocument("pressure-review", "# Pressure Review"), ); expect(writeResult).toEqual({ changed: true, detail: "skill written", target: join(skillsRoot, "workflow", "pressure-review", "SKILL.md"), }); const overwriteResult = await store.writeSkill( skillPath, skillDocument("pressure-review", "# Updated Pressure Review"), ); expect(overwriteResult).toEqual({ changed: true, detail: "skill written", target: writeResult.target, }); await expect(readFile(writeResult.target, "utf8")).resolves.toContain( "# Updated Pressure Review\n", ); }); it("writes the root skills index via the reserved alias", async () => { const result = await store.writeSkill( "__root__", [ "---", "name: skills", "description: TJWater Skills root index.", "---", "", "# TJWater Skills", ].join("\n"), ); expect(result).toEqual({ changed: true, detail: "skill written", target: join(skillsRoot, "SKILL.md"), }); await expect(readFile(result.target, "utf8")).resolves.toContain( "# TJWater Skills\n", ); }); it("removes the main skill file", async () => { const writeResult = await store.writeSkill( "workflow/remove-me", skillDocument("remove-me", "# Remove Me"), ); const removeResult = await store.removeSkill("workflow/remove-me"); expect(removeResult).toEqual({ changed: true, detail: "skill removed", target: writeResult.target, }); await expect(readFile(writeResult.target, "utf8")).rejects.toThrow(); }); it("rejects sensitive skill content", async () => { const result = await store.writeSkill( "workflow/unsafe", "access_token=secret-value", ); expect(result).toEqual({ changed: false, detail: "skill content rejected by persistence policy", target: "", }); }); it("rejects skill content without required frontmatter", async () => { const result = await store.writeSkill("workflow/incomplete", "# Incomplete"); expect(result).toEqual({ changed: false, detail: "skill content rejected: expected SKILL.md frontmatter with name and description", target: "", }); }); });