2026-01-01 22:52:37 +09:00
|
|
|
import { describe, it, expect, beforeEach, afterEach } from "bun:test"
|
|
|
|
|
import { mkdirSync, writeFileSync, rmSync } from "fs"
|
|
|
|
|
import { join } from "path"
|
|
|
|
|
import { tmpdir } from "os"
|
|
|
|
|
|
|
|
|
|
const TEST_DIR = join(tmpdir(), "skill-loader-test-" + Date.now())
|
2026-01-21 13:12:27 +01:00
|
|
|
const SKILLS_DIR = join(TEST_DIR, ".opencode", "skills")
|
2026-01-01 22:52:37 +09:00
|
|
|
|
2026-01-01 23:01:06 +09:00
|
|
|
function createTestSkill(name: string, content: string, mcpJson?: object): string {
|
2026-01-01 22:52:37 +09:00
|
|
|
const skillDir = join(SKILLS_DIR, name)
|
|
|
|
|
mkdirSync(skillDir, { recursive: true })
|
|
|
|
|
const skillPath = join(skillDir, "SKILL.md")
|
|
|
|
|
writeFileSync(skillPath, content)
|
2026-01-01 23:01:06 +09:00
|
|
|
if (mcpJson) {
|
|
|
|
|
writeFileSync(join(skillDir, "mcp.json"), JSON.stringify(mcpJson, null, 2))
|
|
|
|
|
}
|
2026-01-01 22:52:37 +09:00
|
|
|
return skillDir
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
describe("skill loader MCP parsing", () => {
|
|
|
|
|
beforeEach(() => {
|
|
|
|
|
mkdirSync(TEST_DIR, { recursive: true })
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
afterEach(() => {
|
|
|
|
|
rmSync(TEST_DIR, { recursive: true, force: true })
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
describe("parseSkillMcpConfig", () => {
|
|
|
|
|
it("parses skill with nested MCP config", async () => {
|
2026-02-01 16:47:50 +09:00
|
|
|
// given
|
2026-01-01 22:52:37 +09:00
|
|
|
const skillContent = `---
|
|
|
|
|
name: test-skill
|
|
|
|
|
description: A test skill with MCP
|
|
|
|
|
mcp:
|
|
|
|
|
sqlite:
|
|
|
|
|
command: uvx
|
|
|
|
|
args:
|
|
|
|
|
- mcp-server-sqlite
|
|
|
|
|
- --db-path
|
|
|
|
|
- ./data.db
|
|
|
|
|
memory:
|
|
|
|
|
command: npx
|
|
|
|
|
args: [-y, "@anthropic-ai/mcp-server-memory"]
|
|
|
|
|
---
|
|
|
|
|
This is the skill body.
|
|
|
|
|
`
|
|
|
|
|
createTestSkill("test-mcp-skill", skillContent)
|
|
|
|
|
|
2026-02-01 16:47:50 +09:00
|
|
|
// when
|
2026-01-01 22:52:37 +09:00
|
|
|
const { discoverSkills } = await import("./loader")
|
|
|
|
|
const originalCwd = process.cwd()
|
|
|
|
|
process.chdir(TEST_DIR)
|
|
|
|
|
|
|
|
|
|
try {
|
2026-01-05 14:18:42 +09:00
|
|
|
const skills = await discoverSkills({ includeClaudeCodePaths: false })
|
2026-01-01 22:52:37 +09:00
|
|
|
const skill = skills.find(s => s.name === "test-skill")
|
|
|
|
|
|
2026-02-01 16:47:50 +09:00
|
|
|
// then
|
2026-01-01 22:52:37 +09:00
|
|
|
expect(skill).toBeDefined()
|
|
|
|
|
expect(skill?.mcpConfig).toBeDefined()
|
|
|
|
|
expect(skill?.mcpConfig?.sqlite).toBeDefined()
|
|
|
|
|
expect(skill?.mcpConfig?.sqlite?.command).toBe("uvx")
|
|
|
|
|
expect(skill?.mcpConfig?.sqlite?.args).toEqual([
|
|
|
|
|
"mcp-server-sqlite",
|
|
|
|
|
"--db-path",
|
|
|
|
|
"./data.db"
|
|
|
|
|
])
|
|
|
|
|
expect(skill?.mcpConfig?.memory).toBeDefined()
|
|
|
|
|
expect(skill?.mcpConfig?.memory?.command).toBe("npx")
|
|
|
|
|
} finally {
|
|
|
|
|
process.chdir(originalCwd)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it("returns undefined mcpConfig for skill without MCP", async () => {
|
2026-02-01 16:47:50 +09:00
|
|
|
// given
|
2026-01-01 22:52:37 +09:00
|
|
|
const skillContent = `---
|
|
|
|
|
name: simple-skill
|
|
|
|
|
description: A simple skill without MCP
|
|
|
|
|
---
|
|
|
|
|
This is a simple skill.
|
|
|
|
|
`
|
|
|
|
|
createTestSkill("simple-skill", skillContent)
|
|
|
|
|
|
2026-02-01 16:47:50 +09:00
|
|
|
// when
|
2026-01-01 22:52:37 +09:00
|
|
|
const { discoverSkills } = await import("./loader")
|
|
|
|
|
const originalCwd = process.cwd()
|
|
|
|
|
process.chdir(TEST_DIR)
|
|
|
|
|
|
|
|
|
|
try {
|
2026-01-05 14:18:42 +09:00
|
|
|
const skills = await discoverSkills({ includeClaudeCodePaths: false })
|
2026-01-01 22:52:37 +09:00
|
|
|
const skill = skills.find(s => s.name === "simple-skill")
|
|
|
|
|
|
2026-02-01 16:47:50 +09:00
|
|
|
// then
|
2026-01-01 22:52:37 +09:00
|
|
|
expect(skill).toBeDefined()
|
|
|
|
|
expect(skill?.mcpConfig).toBeUndefined()
|
|
|
|
|
} finally {
|
|
|
|
|
process.chdir(originalCwd)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it("preserves env var placeholders without expansion", async () => {
|
2026-02-01 16:47:50 +09:00
|
|
|
// given
|
2026-01-01 22:52:37 +09:00
|
|
|
const skillContent = `---
|
|
|
|
|
name: env-skill
|
|
|
|
|
mcp:
|
|
|
|
|
api-server:
|
|
|
|
|
command: node
|
|
|
|
|
args: [server.js]
|
|
|
|
|
env:
|
|
|
|
|
API_KEY: "\${API_KEY}"
|
|
|
|
|
DB_PATH: "\${HOME}/data.db"
|
|
|
|
|
---
|
|
|
|
|
Skill with env vars.
|
|
|
|
|
`
|
|
|
|
|
createTestSkill("env-skill", skillContent)
|
|
|
|
|
|
2026-02-01 16:47:50 +09:00
|
|
|
// when
|
2026-01-01 22:52:37 +09:00
|
|
|
const { discoverSkills } = await import("./loader")
|
|
|
|
|
const originalCwd = process.cwd()
|
|
|
|
|
process.chdir(TEST_DIR)
|
|
|
|
|
|
|
|
|
|
try {
|
2026-01-05 14:18:42 +09:00
|
|
|
const skills = await discoverSkills({ includeClaudeCodePaths: false })
|
2026-01-01 22:52:37 +09:00
|
|
|
const skill = skills.find(s => s.name === "env-skill")
|
|
|
|
|
|
2026-02-01 16:47:50 +09:00
|
|
|
// then
|
2026-01-01 22:52:37 +09:00
|
|
|
expect(skill?.mcpConfig?.["api-server"]?.env?.API_KEY).toBe("${API_KEY}")
|
|
|
|
|
expect(skill?.mcpConfig?.["api-server"]?.env?.DB_PATH).toBe("${HOME}/data.db")
|
|
|
|
|
} finally {
|
|
|
|
|
process.chdir(originalCwd)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it("handles malformed YAML gracefully", async () => {
|
2026-02-01 16:47:50 +09:00
|
|
|
// given - malformed YAML causes entire frontmatter to fail parsing
|
2026-01-01 22:52:37 +09:00
|
|
|
const skillContent = `---
|
|
|
|
|
name: bad-yaml
|
|
|
|
|
mcp: [this is not valid yaml for mcp
|
|
|
|
|
---
|
|
|
|
|
Skill body.
|
|
|
|
|
`
|
|
|
|
|
createTestSkill("bad-yaml-skill", skillContent)
|
|
|
|
|
|
2026-02-01 16:47:50 +09:00
|
|
|
// when
|
2026-01-01 22:52:37 +09:00
|
|
|
const { discoverSkills } = await import("./loader")
|
|
|
|
|
const originalCwd = process.cwd()
|
|
|
|
|
process.chdir(TEST_DIR)
|
|
|
|
|
|
|
|
|
|
try {
|
2026-01-05 14:18:42 +09:00
|
|
|
const skills = await discoverSkills({ includeClaudeCodePaths: false })
|
2026-02-01 16:47:50 +09:00
|
|
|
// then - when YAML fails, skill uses directory name as fallback
|
2026-01-02 15:11:14 +09:00
|
|
|
const skill = skills.find(s => s.name === "bad-yaml-skill")
|
2026-01-01 22:52:37 +09:00
|
|
|
|
|
|
|
|
expect(skill).toBeDefined()
|
|
|
|
|
expect(skill?.mcpConfig).toBeUndefined()
|
|
|
|
|
} finally {
|
|
|
|
|
process.chdir(originalCwd)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
})
|
2026-01-01 23:01:06 +09:00
|
|
|
|
|
|
|
|
describe("mcp.json file loading (AmpCode compat)", () => {
|
|
|
|
|
it("loads MCP config from mcp.json with mcpServers format", async () => {
|
2026-02-01 16:47:50 +09:00
|
|
|
// given
|
2026-01-01 23:01:06 +09:00
|
|
|
const skillContent = `---
|
|
|
|
|
name: ampcode-skill
|
|
|
|
|
description: Skill with mcp.json
|
|
|
|
|
---
|
|
|
|
|
Skill body.
|
|
|
|
|
`
|
|
|
|
|
const mcpJson = {
|
|
|
|
|
mcpServers: {
|
|
|
|
|
playwright: {
|
|
|
|
|
command: "npx",
|
|
|
|
|
args: ["@playwright/mcp@latest"]
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
createTestSkill("ampcode-skill", skillContent, mcpJson)
|
|
|
|
|
|
2026-02-01 16:47:50 +09:00
|
|
|
// when
|
2026-01-01 23:01:06 +09:00
|
|
|
const { discoverSkills } = await import("./loader")
|
|
|
|
|
const originalCwd = process.cwd()
|
|
|
|
|
process.chdir(TEST_DIR)
|
|
|
|
|
|
|
|
|
|
try {
|
2026-01-05 14:18:42 +09:00
|
|
|
const skills = await discoverSkills({ includeClaudeCodePaths: false })
|
2026-01-01 23:01:06 +09:00
|
|
|
const skill = skills.find(s => s.name === "ampcode-skill")
|
|
|
|
|
|
2026-02-01 16:47:50 +09:00
|
|
|
// then
|
2026-01-01 23:01:06 +09:00
|
|
|
expect(skill).toBeDefined()
|
|
|
|
|
expect(skill?.mcpConfig).toBeDefined()
|
|
|
|
|
expect(skill?.mcpConfig?.playwright).toBeDefined()
|
|
|
|
|
expect(skill?.mcpConfig?.playwright?.command).toBe("npx")
|
|
|
|
|
expect(skill?.mcpConfig?.playwright?.args).toEqual(["@playwright/mcp@latest"])
|
|
|
|
|
} finally {
|
|
|
|
|
process.chdir(originalCwd)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it("mcp.json takes priority over YAML frontmatter", async () => {
|
2026-02-01 16:47:50 +09:00
|
|
|
// given
|
2026-01-01 23:01:06 +09:00
|
|
|
const skillContent = `---
|
|
|
|
|
name: priority-skill
|
|
|
|
|
mcp:
|
|
|
|
|
from-yaml:
|
|
|
|
|
command: yaml-cmd
|
|
|
|
|
args: [yaml-arg]
|
|
|
|
|
---
|
|
|
|
|
Skill body.
|
|
|
|
|
`
|
|
|
|
|
const mcpJson = {
|
|
|
|
|
mcpServers: {
|
|
|
|
|
"from-json": {
|
|
|
|
|
command: "json-cmd",
|
|
|
|
|
args: ["json-arg"]
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
createTestSkill("priority-skill", skillContent, mcpJson)
|
|
|
|
|
|
2026-02-01 16:47:50 +09:00
|
|
|
// when
|
2026-01-01 23:01:06 +09:00
|
|
|
const { discoverSkills } = await import("./loader")
|
|
|
|
|
const originalCwd = process.cwd()
|
|
|
|
|
process.chdir(TEST_DIR)
|
|
|
|
|
|
|
|
|
|
try {
|
2026-01-05 14:18:42 +09:00
|
|
|
const skills = await discoverSkills({ includeClaudeCodePaths: false })
|
2026-01-01 23:01:06 +09:00
|
|
|
const skill = skills.find(s => s.name === "priority-skill")
|
|
|
|
|
|
2026-02-01 16:47:50 +09:00
|
|
|
// then - mcp.json should take priority
|
2026-01-01 23:01:06 +09:00
|
|
|
expect(skill?.mcpConfig?.["from-json"]).toBeDefined()
|
|
|
|
|
expect(skill?.mcpConfig?.["from-yaml"]).toBeUndefined()
|
|
|
|
|
} finally {
|
|
|
|
|
process.chdir(originalCwd)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it("supports direct format without mcpServers wrapper", async () => {
|
2026-02-01 16:47:50 +09:00
|
|
|
// given
|
2026-01-01 23:01:06 +09:00
|
|
|
const skillContent = `---
|
|
|
|
|
name: direct-format
|
|
|
|
|
---
|
|
|
|
|
Skill body.
|
|
|
|
|
`
|
|
|
|
|
const mcpJson = {
|
|
|
|
|
sqlite: {
|
|
|
|
|
command: "uvx",
|
|
|
|
|
args: ["mcp-server-sqlite"]
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
createTestSkill("direct-format", skillContent, mcpJson)
|
|
|
|
|
|
2026-02-01 16:47:50 +09:00
|
|
|
// when
|
2026-01-01 23:01:06 +09:00
|
|
|
const { discoverSkills } = await import("./loader")
|
|
|
|
|
const originalCwd = process.cwd()
|
|
|
|
|
process.chdir(TEST_DIR)
|
|
|
|
|
|
|
|
|
|
try {
|
2026-01-05 14:18:42 +09:00
|
|
|
const skills = await discoverSkills({ includeClaudeCodePaths: false })
|
2026-01-01 23:01:06 +09:00
|
|
|
const skill = skills.find(s => s.name === "direct-format")
|
|
|
|
|
|
2026-02-01 16:47:50 +09:00
|
|
|
// then
|
2026-01-01 23:01:06 +09:00
|
|
|
expect(skill?.mcpConfig?.sqlite).toBeDefined()
|
|
|
|
|
expect(skill?.mcpConfig?.sqlite?.command).toBe("uvx")
|
|
|
|
|
} finally {
|
|
|
|
|
process.chdir(originalCwd)
|
|
|
|
|
}
|
2026-01-28 14:26:34 +07:00
|
|
|
})
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
describe("allowed-tools parsing", () => {
|
|
|
|
|
it("parses space-separated allowed-tools string", async () => {
|
2026-02-01 16:47:50 +09:00
|
|
|
// given
|
2026-01-28 14:26:34 +07:00
|
|
|
const skillContent = `---
|
|
|
|
|
name: space-separated-tools
|
|
|
|
|
description: Skill with space-separated allowed-tools
|
|
|
|
|
allowed-tools: Read Write Edit Bash
|
|
|
|
|
---
|
|
|
|
|
Skill body.
|
|
|
|
|
`
|
|
|
|
|
createTestSkill("space-separated-tools", skillContent)
|
|
|
|
|
|
2026-02-01 16:47:50 +09:00
|
|
|
// when
|
2026-01-28 14:26:34 +07:00
|
|
|
const { discoverSkills } = await import("./loader")
|
|
|
|
|
const originalCwd = process.cwd()
|
|
|
|
|
process.chdir(TEST_DIR)
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const skills = await discoverSkills({ includeClaudeCodePaths: false })
|
|
|
|
|
const skill = skills.find(s => s.name === "space-separated-tools")
|
|
|
|
|
|
2026-02-01 16:47:50 +09:00
|
|
|
// then
|
2026-01-28 14:26:34 +07:00
|
|
|
expect(skill).toBeDefined()
|
|
|
|
|
expect(skill?.allowedTools).toEqual(["Read", "Write", "Edit", "Bash"])
|
|
|
|
|
} finally {
|
|
|
|
|
process.chdir(originalCwd)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it("parses YAML inline array allowed-tools", async () => {
|
2026-02-01 16:47:50 +09:00
|
|
|
// given
|
2026-01-28 14:26:34 +07:00
|
|
|
const skillContent = `---
|
|
|
|
|
name: yaml-inline-array
|
|
|
|
|
description: Skill with YAML inline array allowed-tools
|
|
|
|
|
allowed-tools: [Read, Write, Edit, Bash]
|
|
|
|
|
---
|
|
|
|
|
Skill body.
|
|
|
|
|
`
|
|
|
|
|
createTestSkill("yaml-inline-array", skillContent)
|
|
|
|
|
|
2026-02-01 16:47:50 +09:00
|
|
|
// when
|
2026-01-28 14:26:34 +07:00
|
|
|
const { discoverSkills } = await import("./loader")
|
|
|
|
|
const originalCwd = process.cwd()
|
|
|
|
|
process.chdir(TEST_DIR)
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const skills = await discoverSkills({ includeClaudeCodePaths: false })
|
|
|
|
|
const skill = skills.find(s => s.name === "yaml-inline-array")
|
|
|
|
|
|
2026-02-01 16:47:50 +09:00
|
|
|
// then
|
2026-01-28 14:26:34 +07:00
|
|
|
expect(skill).toBeDefined()
|
|
|
|
|
expect(skill?.allowedTools).toEqual(["Read", "Write", "Edit", "Bash"])
|
|
|
|
|
} finally {
|
|
|
|
|
process.chdir(originalCwd)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it("parses YAML multi-line array allowed-tools", async () => {
|
2026-02-01 16:47:50 +09:00
|
|
|
// given
|
2026-01-28 14:26:34 +07:00
|
|
|
const skillContent = `---
|
|
|
|
|
name: yaml-multiline-array
|
|
|
|
|
description: Skill with YAML multi-line array allowed-tools
|
|
|
|
|
allowed-tools:
|
|
|
|
|
- Read
|
|
|
|
|
- Write
|
|
|
|
|
- Edit
|
|
|
|
|
- Bash
|
|
|
|
|
---
|
|
|
|
|
Skill body.
|
|
|
|
|
`
|
|
|
|
|
createTestSkill("yaml-multiline-array", skillContent)
|
|
|
|
|
|
2026-02-01 16:47:50 +09:00
|
|
|
// when
|
2026-01-28 14:26:34 +07:00
|
|
|
const { discoverSkills } = await import("./loader")
|
|
|
|
|
const originalCwd = process.cwd()
|
|
|
|
|
process.chdir(TEST_DIR)
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const skills = await discoverSkills({ includeClaudeCodePaths: false })
|
|
|
|
|
const skill = skills.find(s => s.name === "yaml-multiline-array")
|
|
|
|
|
|
2026-02-01 16:47:50 +09:00
|
|
|
// then
|
2026-01-28 14:26:34 +07:00
|
|
|
expect(skill).toBeDefined()
|
|
|
|
|
expect(skill?.allowedTools).toEqual(["Read", "Write", "Edit", "Bash"])
|
|
|
|
|
} finally {
|
|
|
|
|
process.chdir(originalCwd)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it("returns undefined for skill without allowed-tools", async () => {
|
2026-02-01 16:47:50 +09:00
|
|
|
// given
|
2026-01-28 14:26:34 +07:00
|
|
|
const skillContent = `---
|
|
|
|
|
name: no-allowed-tools
|
|
|
|
|
description: Skill without allowed-tools field
|
|
|
|
|
---
|
|
|
|
|
Skill body.
|
|
|
|
|
`
|
|
|
|
|
createTestSkill("no-allowed-tools", skillContent)
|
|
|
|
|
|
2026-02-01 16:47:50 +09:00
|
|
|
// when
|
2026-01-28 14:26:34 +07:00
|
|
|
const { discoverSkills } = await import("./loader")
|
|
|
|
|
const originalCwd = process.cwd()
|
|
|
|
|
process.chdir(TEST_DIR)
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const skills = await discoverSkills({ includeClaudeCodePaths: false })
|
|
|
|
|
const skill = skills.find(s => s.name === "no-allowed-tools")
|
|
|
|
|
|
2026-02-01 16:47:50 +09:00
|
|
|
// then
|
2026-01-28 14:26:34 +07:00
|
|
|
expect(skill).toBeDefined()
|
|
|
|
|
expect(skill?.allowedTools).toBeUndefined()
|
|
|
|
|
} finally {
|
|
|
|
|
process.chdir(originalCwd)
|
|
|
|
|
}
|
2026-01-01 23:01:06 +09:00
|
|
|
})
|
|
|
|
|
})
|
2026-01-30 00:39:43 +08:00
|
|
|
|
|
|
|
|
describe("nested skill discovery", () => {
|
|
|
|
|
it("discovers skills in nested directories (superpowers pattern)", async () => {
|
|
|
|
|
// #given - simulate superpowers structure: skills/superpowers/brainstorming/SKILL.md
|
|
|
|
|
const nestedDir = join(SKILLS_DIR, "superpowers", "brainstorming")
|
|
|
|
|
mkdirSync(nestedDir, { recursive: true })
|
|
|
|
|
const skillContent = `---
|
|
|
|
|
name: brainstorming
|
|
|
|
|
description: A nested skill for brainstorming
|
|
|
|
|
---
|
|
|
|
|
This is a nested skill.
|
|
|
|
|
`
|
|
|
|
|
writeFileSync(join(nestedDir, "SKILL.md"), skillContent)
|
|
|
|
|
|
|
|
|
|
// #when
|
|
|
|
|
const { discoverSkills } = await import("./loader")
|
|
|
|
|
const originalCwd = process.cwd()
|
|
|
|
|
process.chdir(TEST_DIR)
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const skills = await discoverSkills({ includeClaudeCodePaths: false })
|
|
|
|
|
const skill = skills.find(s => s.name === "superpowers/brainstorming")
|
|
|
|
|
|
|
|
|
|
// #then
|
|
|
|
|
expect(skill).toBeDefined()
|
|
|
|
|
expect(skill?.name).toBe("superpowers/brainstorming")
|
|
|
|
|
expect(skill?.definition.description).toContain("brainstorming")
|
|
|
|
|
} finally {
|
|
|
|
|
process.chdir(originalCwd)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it("discovers multiple skills in nested directories", async () => {
|
|
|
|
|
// #given - multiple nested skills
|
|
|
|
|
const skills = ["brainstorming", "debugging", "testing"]
|
|
|
|
|
for (const skillName of skills) {
|
|
|
|
|
const nestedDir = join(SKILLS_DIR, "superpowers", skillName)
|
|
|
|
|
mkdirSync(nestedDir, { recursive: true })
|
|
|
|
|
writeFileSync(join(nestedDir, "SKILL.md"), `---
|
|
|
|
|
name: ${skillName}
|
|
|
|
|
description: ${skillName} skill
|
|
|
|
|
---
|
|
|
|
|
Content for ${skillName}.
|
|
|
|
|
`)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// #when
|
|
|
|
|
const { discoverSkills } = await import("./loader")
|
|
|
|
|
const originalCwd = process.cwd()
|
|
|
|
|
process.chdir(TEST_DIR)
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const discoveredSkills = await discoverSkills({ includeClaudeCodePaths: false })
|
|
|
|
|
|
|
|
|
|
// #then
|
|
|
|
|
for (const skillName of skills) {
|
|
|
|
|
const skill = discoveredSkills.find(s => s.name === `superpowers/${skillName}`)
|
|
|
|
|
expect(skill).toBeDefined()
|
|
|
|
|
}
|
|
|
|
|
} finally {
|
|
|
|
|
process.chdir(originalCwd)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it("respects max depth limit", async () => {
|
|
|
|
|
// #given - deeply nested skill (3 levels deep, beyond default maxDepth of 2)
|
|
|
|
|
const deepDir = join(SKILLS_DIR, "level1", "level2", "level3", "deep-skill")
|
|
|
|
|
mkdirSync(deepDir, { recursive: true })
|
|
|
|
|
writeFileSync(join(deepDir, "SKILL.md"), `---
|
|
|
|
|
name: deep-skill
|
|
|
|
|
description: A deeply nested skill
|
|
|
|
|
---
|
|
|
|
|
Too deep.
|
|
|
|
|
`)
|
|
|
|
|
|
|
|
|
|
// #when
|
|
|
|
|
const { discoverSkills } = await import("./loader")
|
|
|
|
|
const originalCwd = process.cwd()
|
|
|
|
|
process.chdir(TEST_DIR)
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const skills = await discoverSkills({ includeClaudeCodePaths: false })
|
|
|
|
|
const skill = skills.find(s => s.name.includes("deep-skill"))
|
|
|
|
|
|
|
|
|
|
// #then - should not find skill beyond maxDepth
|
|
|
|
|
expect(skill).toBeUndefined()
|
|
|
|
|
} finally {
|
|
|
|
|
process.chdir(originalCwd)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
it("flat skills still work alongside nested skills", async () => {
|
|
|
|
|
// #given - both flat and nested skills
|
|
|
|
|
const flatSkillDir = join(SKILLS_DIR, "flat-skill")
|
|
|
|
|
mkdirSync(flatSkillDir, { recursive: true })
|
|
|
|
|
writeFileSync(join(flatSkillDir, "SKILL.md"), `---
|
|
|
|
|
name: flat-skill
|
|
|
|
|
description: A flat skill
|
|
|
|
|
---
|
|
|
|
|
Flat content.
|
|
|
|
|
`)
|
|
|
|
|
|
|
|
|
|
const nestedDir = join(SKILLS_DIR, "nested", "nested-skill")
|
|
|
|
|
mkdirSync(nestedDir, { recursive: true })
|
|
|
|
|
writeFileSync(join(nestedDir, "SKILL.md"), `---
|
|
|
|
|
name: nested-skill
|
|
|
|
|
description: A nested skill
|
|
|
|
|
---
|
|
|
|
|
Nested content.
|
|
|
|
|
`)
|
|
|
|
|
|
|
|
|
|
// #when
|
|
|
|
|
const { discoverSkills } = await import("./loader")
|
|
|
|
|
const originalCwd = process.cwd()
|
|
|
|
|
process.chdir(TEST_DIR)
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const skills = await discoverSkills({ includeClaudeCodePaths: false })
|
|
|
|
|
|
|
|
|
|
// #then - both should be found
|
|
|
|
|
const flatSkill = skills.find(s => s.name === "flat-skill")
|
|
|
|
|
const nestedSkill = skills.find(s => s.name === "nested/nested-skill")
|
|
|
|
|
|
|
|
|
|
expect(flatSkill).toBeDefined()
|
|
|
|
|
expect(nestedSkill).toBeDefined()
|
|
|
|
|
} finally {
|
|
|
|
|
process.chdir(originalCwd)
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
})
|
2026-01-01 22:52:37 +09:00
|
|
|
})
|