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()) const SKILLS_DIR = join(TEST_DIR, ".opencode", "skill") function createTestSkill(name: string, content: string): string { const skillDir = join(SKILLS_DIR, name) mkdirSync(skillDir, { recursive: true }) const skillPath = join(skillDir, "SKILL.md") writeFileSync(skillPath, content) 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 () => { // #given 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) // #when const { discoverSkills } = await import("./loader") const originalCwd = process.cwd() process.chdir(TEST_DIR) try { const skills = discoverSkills({ includeClaudeCodePaths: false }) const skill = skills.find(s => s.name === "test-skill") // #then 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 () => { // #given const skillContent = `--- name: simple-skill description: A simple skill without MCP --- This is a simple skill. ` createTestSkill("simple-skill", skillContent) // #when const { discoverSkills } = await import("./loader") const originalCwd = process.cwd() process.chdir(TEST_DIR) try { const skills = discoverSkills({ includeClaudeCodePaths: false }) const skill = skills.find(s => s.name === "simple-skill") // #then expect(skill).toBeDefined() expect(skill?.mcpConfig).toBeUndefined() } finally { process.chdir(originalCwd) } }) it("preserves env var placeholders without expansion", async () => { // #given 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) // #when const { discoverSkills } = await import("./loader") const originalCwd = process.cwd() process.chdir(TEST_DIR) try { const skills = discoverSkills({ includeClaudeCodePaths: false }) const skill = skills.find(s => s.name === "env-skill") // #then 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 () => { // #given const skillContent = `--- name: bad-yaml mcp: [this is not valid yaml for mcp --- Skill body. ` createTestSkill("bad-yaml-skill", skillContent) // #when const { discoverSkills } = await import("./loader") const originalCwd = process.cwd() process.chdir(TEST_DIR) try { const skills = discoverSkills({ includeClaudeCodePaths: false }) const skill = skills.find(s => s.name === "bad-yaml") // #then - should still load skill but without MCP config expect(skill).toBeDefined() expect(skill?.mcpConfig).toBeUndefined() } finally { process.chdir(originalCwd) } }) }) })