Add async versions of skill loading functions with concurrency control: - mapWithConcurrency: Generic concurrent mapper with limit (16) - loadSkillFromPathAsync: Async skill file parsing - loadMcpJsonFromDirAsync: Async mcp.json loading - discoverSkillsInDirAsync: Async directory scanner Tests: 20 new tests covering all async functions 🤖 GENERATED WITH ASSISTANCE OF [OhMyOpenCode](https://github.com/code-yeongyu/oh-my-opencode)
449 lines
13 KiB
TypeScript
449 lines
13 KiB
TypeScript
import { describe, it, expect, beforeEach, afterEach } from "bun:test"
|
|
import { mkdirSync, writeFileSync, rmSync, chmodSync } from "fs"
|
|
import { join } from "path"
|
|
import { tmpdir } from "os"
|
|
import type { LoadedSkill } from "./types"
|
|
|
|
const TEST_DIR = join(tmpdir(), "async-loader-test-" + Date.now())
|
|
const SKILLS_DIR = join(TEST_DIR, ".opencode", "skill")
|
|
|
|
function createTestSkill(name: string, content: string, mcpJson?: object): string {
|
|
const skillDir = join(SKILLS_DIR, name)
|
|
mkdirSync(skillDir, { recursive: true })
|
|
const skillPath = join(skillDir, "SKILL.md")
|
|
writeFileSync(skillPath, content)
|
|
if (mcpJson) {
|
|
writeFileSync(join(skillDir, "mcp.json"), JSON.stringify(mcpJson, null, 2))
|
|
}
|
|
return skillDir
|
|
}
|
|
|
|
function createDirectSkill(name: string, content: string): string {
|
|
mkdirSync(SKILLS_DIR, { recursive: true })
|
|
const skillPath = join(SKILLS_DIR, `${name}.md`)
|
|
writeFileSync(skillPath, content)
|
|
return skillPath
|
|
}
|
|
|
|
describe("async-loader", () => {
|
|
beforeEach(() => {
|
|
mkdirSync(TEST_DIR, { recursive: true })
|
|
})
|
|
|
|
afterEach(() => {
|
|
rmSync(TEST_DIR, { recursive: true, force: true })
|
|
})
|
|
|
|
describe("discoverSkillsInDirAsync", () => {
|
|
it("returns empty array for non-existent directory", async () => {
|
|
// #given - non-existent directory
|
|
const nonExistentDir = join(TEST_DIR, "does-not-exist")
|
|
|
|
// #when
|
|
const { discoverSkillsInDirAsync } = await import("./async-loader")
|
|
const skills = await discoverSkillsInDirAsync(nonExistentDir)
|
|
|
|
// #then - should return empty array, not throw
|
|
expect(skills).toEqual([])
|
|
})
|
|
|
|
it("discovers skills from SKILL.md in directory", async () => {
|
|
// #given
|
|
const skillContent = `---
|
|
name: test-skill
|
|
description: A test skill
|
|
---
|
|
This is the skill body.
|
|
`
|
|
createTestSkill("test-skill", skillContent)
|
|
|
|
// #when
|
|
const { discoverSkillsInDirAsync } = await import("./async-loader")
|
|
const skills = await discoverSkillsInDirAsync(SKILLS_DIR)
|
|
|
|
// #then
|
|
expect(skills).toHaveLength(1)
|
|
expect(skills[0].name).toBe("test-skill")
|
|
expect(skills[0].definition.description).toContain("A test skill")
|
|
})
|
|
|
|
it("discovers skills from {name}.md pattern in directory", async () => {
|
|
// #given
|
|
const skillContent = `---
|
|
name: named-skill
|
|
description: Named pattern skill
|
|
---
|
|
Skill body.
|
|
`
|
|
const skillDir = join(SKILLS_DIR, "named-skill")
|
|
mkdirSync(skillDir, { recursive: true })
|
|
writeFileSync(join(skillDir, "named-skill.md"), skillContent)
|
|
|
|
// #when
|
|
const { discoverSkillsInDirAsync } = await import("./async-loader")
|
|
const skills = await discoverSkillsInDirAsync(SKILLS_DIR)
|
|
|
|
// #then
|
|
expect(skills).toHaveLength(1)
|
|
expect(skills[0].name).toBe("named-skill")
|
|
})
|
|
|
|
it("discovers direct .md files", async () => {
|
|
// #given
|
|
const skillContent = `---
|
|
name: direct-skill
|
|
description: Direct markdown file
|
|
---
|
|
Direct skill.
|
|
`
|
|
createDirectSkill("direct-skill", skillContent)
|
|
|
|
// #when
|
|
const { discoverSkillsInDirAsync } = await import("./async-loader")
|
|
const skills = await discoverSkillsInDirAsync(SKILLS_DIR)
|
|
|
|
// #then
|
|
expect(skills).toHaveLength(1)
|
|
expect(skills[0].name).toBe("direct-skill")
|
|
})
|
|
|
|
it("skips entries starting with dot", async () => {
|
|
// #given
|
|
const validContent = `---
|
|
name: valid-skill
|
|
---
|
|
Valid.
|
|
`
|
|
const hiddenContent = `---
|
|
name: hidden-skill
|
|
---
|
|
Hidden.
|
|
`
|
|
createTestSkill("valid-skill", validContent)
|
|
createTestSkill(".hidden-skill", hiddenContent)
|
|
|
|
// #when
|
|
const { discoverSkillsInDirAsync } = await import("./async-loader")
|
|
const skills = await discoverSkillsInDirAsync(SKILLS_DIR)
|
|
|
|
// #then - only valid-skill should be discovered
|
|
expect(skills).toHaveLength(1)
|
|
expect(skills[0]?.name).toBe("valid-skill")
|
|
})
|
|
|
|
it("skips invalid files and continues with valid ones", async () => {
|
|
// #given - one valid, one invalid (unreadable)
|
|
const validContent = `---
|
|
name: valid-skill
|
|
---
|
|
Valid skill.
|
|
`
|
|
const invalidContent = `---
|
|
name: invalid-skill
|
|
---
|
|
Invalid skill.
|
|
`
|
|
createTestSkill("valid-skill", validContent)
|
|
const invalidDir = createTestSkill("invalid-skill", invalidContent)
|
|
const invalidFile = join(invalidDir, "SKILL.md")
|
|
|
|
// Make file unreadable on Unix systems
|
|
if (process.platform !== "win32") {
|
|
chmodSync(invalidFile, 0o000)
|
|
}
|
|
|
|
// #when
|
|
const { discoverSkillsInDirAsync } = await import("./async-loader")
|
|
const skills = await discoverSkillsInDirAsync(SKILLS_DIR)
|
|
|
|
// #then - should skip invalid and return only valid
|
|
expect(skills.length).toBeGreaterThanOrEqual(1)
|
|
expect(skills.some((s: LoadedSkill) => s.name === "valid-skill")).toBe(true)
|
|
|
|
// Cleanup: restore permissions before cleanup
|
|
if (process.platform !== "win32") {
|
|
chmodSync(invalidFile, 0o644)
|
|
}
|
|
})
|
|
|
|
it("discovers multiple skills correctly", async () => {
|
|
// #given
|
|
const skill1 = `---
|
|
name: skill-one
|
|
description: First skill
|
|
---
|
|
Skill one.
|
|
`
|
|
const skill2 = `---
|
|
name: skill-two
|
|
description: Second skill
|
|
---
|
|
Skill two.
|
|
`
|
|
createTestSkill("skill-one", skill1)
|
|
createTestSkill("skill-two", skill2)
|
|
|
|
// #when
|
|
const { discoverSkillsInDirAsync } = await import("./async-loader")
|
|
const asyncSkills = await discoverSkillsInDirAsync(SKILLS_DIR)
|
|
|
|
// #then
|
|
expect(asyncSkills.length).toBe(2)
|
|
expect(asyncSkills.map((s: LoadedSkill) => s.name).sort()).toEqual(["skill-one", "skill-two"])
|
|
|
|
const skill1Result = asyncSkills.find((s: LoadedSkill) => s.name === "skill-one")
|
|
expect(skill1Result?.definition.description).toContain("First skill")
|
|
})
|
|
|
|
it("loads MCP config from frontmatter", async () => {
|
|
// #given
|
|
const skillContent = `---
|
|
name: mcp-skill
|
|
description: Skill with MCP
|
|
mcp:
|
|
sqlite:
|
|
command: uvx
|
|
args: [mcp-server-sqlite]
|
|
---
|
|
MCP skill.
|
|
`
|
|
createTestSkill("mcp-skill", skillContent)
|
|
|
|
// #when
|
|
const { discoverSkillsInDirAsync } = await import("./async-loader")
|
|
const skills = await discoverSkillsInDirAsync(SKILLS_DIR)
|
|
|
|
// #then
|
|
const skill = skills.find((s: LoadedSkill) => s.name === "mcp-skill")
|
|
expect(skill?.mcpConfig).toBeDefined()
|
|
expect(skill?.mcpConfig?.sqlite).toBeDefined()
|
|
expect(skill?.mcpConfig?.sqlite?.command).toBe("uvx")
|
|
})
|
|
|
|
it("loads MCP config from mcp.json file", async () => {
|
|
// #given
|
|
const skillContent = `---
|
|
name: json-mcp-skill
|
|
description: Skill with mcp.json
|
|
---
|
|
Skill body.
|
|
`
|
|
const mcpJson = {
|
|
mcpServers: {
|
|
playwright: {
|
|
command: "npx",
|
|
args: ["@playwright/mcp"]
|
|
}
|
|
}
|
|
}
|
|
createTestSkill("json-mcp-skill", skillContent, mcpJson)
|
|
|
|
// #when
|
|
const { discoverSkillsInDirAsync } = await import("./async-loader")
|
|
const skills = await discoverSkillsInDirAsync(SKILLS_DIR)
|
|
|
|
// #then
|
|
const skill = skills.find((s: LoadedSkill) => s.name === "json-mcp-skill")
|
|
expect(skill?.mcpConfig?.playwright).toBeDefined()
|
|
expect(skill?.mcpConfig?.playwright?.command).toBe("npx")
|
|
})
|
|
|
|
it("prioritizes mcp.json over frontmatter MCP", async () => {
|
|
// #given
|
|
const skillContent = `---
|
|
name: priority-test
|
|
mcp:
|
|
from-yaml:
|
|
command: yaml-cmd
|
|
---
|
|
Skill.
|
|
`
|
|
const mcpJson = {
|
|
mcpServers: {
|
|
"from-json": {
|
|
command: "json-cmd"
|
|
}
|
|
}
|
|
}
|
|
createTestSkill("priority-test", skillContent, mcpJson)
|
|
|
|
// #when
|
|
const { discoverSkillsInDirAsync } = await import("./async-loader")
|
|
const skills = await discoverSkillsInDirAsync(SKILLS_DIR)
|
|
|
|
// #then - mcp.json should take priority
|
|
const skill = skills.find((s: LoadedSkill) => s.name === "priority-test")
|
|
expect(skill?.mcpConfig?.["from-json"]).toBeDefined()
|
|
expect(skill?.mcpConfig?.["from-yaml"]).toBeUndefined()
|
|
})
|
|
})
|
|
|
|
describe("mapWithConcurrency", () => {
|
|
it("processes items with concurrency limit", async () => {
|
|
// #given
|
|
const { mapWithConcurrency } = await import("./async-loader")
|
|
const items = Array.from({ length: 50 }, (_, i) => i)
|
|
let maxConcurrent = 0
|
|
let currentConcurrent = 0
|
|
|
|
const mapper = async (item: number) => {
|
|
currentConcurrent++
|
|
maxConcurrent = Math.max(maxConcurrent, currentConcurrent)
|
|
await new Promise(resolve => setTimeout(resolve, 10))
|
|
currentConcurrent--
|
|
return item * 2
|
|
}
|
|
|
|
// #when
|
|
const results = await mapWithConcurrency(items, mapper, 16)
|
|
|
|
// #then
|
|
expect(results).toEqual(items.map(i => i * 2))
|
|
expect(maxConcurrent).toBeLessThanOrEqual(16)
|
|
expect(maxConcurrent).toBeGreaterThan(1) // Should actually run concurrently
|
|
})
|
|
|
|
it("handles empty array", async () => {
|
|
// #given
|
|
const { mapWithConcurrency } = await import("./async-loader")
|
|
|
|
// #when
|
|
const results = await mapWithConcurrency([], async (x: number) => x * 2, 16)
|
|
|
|
// #then
|
|
expect(results).toEqual([])
|
|
})
|
|
|
|
it("handles single item", async () => {
|
|
// #given
|
|
const { mapWithConcurrency } = await import("./async-loader")
|
|
|
|
// #when
|
|
const results = await mapWithConcurrency([42], async (x: number) => x * 2, 16)
|
|
|
|
// #then
|
|
expect(results).toEqual([84])
|
|
})
|
|
})
|
|
|
|
describe("loadSkillFromPathAsync", () => {
|
|
it("loads skill from valid path", async () => {
|
|
// #given
|
|
const skillContent = `---
|
|
name: path-skill
|
|
description: Loaded from path
|
|
---
|
|
Path skill.
|
|
`
|
|
const skillDir = createTestSkill("path-skill", skillContent)
|
|
const skillPath = join(skillDir, "SKILL.md")
|
|
|
|
// #when
|
|
const { loadSkillFromPathAsync } = await import("./async-loader")
|
|
const skill = await loadSkillFromPathAsync(skillPath, skillDir, "path-skill", "opencode-project")
|
|
|
|
// #then
|
|
expect(skill).not.toBeNull()
|
|
expect(skill?.name).toBe("path-skill")
|
|
expect(skill?.scope).toBe("opencode-project")
|
|
})
|
|
|
|
it("returns null for invalid path", async () => {
|
|
// #given
|
|
const invalidPath = join(TEST_DIR, "nonexistent.md")
|
|
|
|
// #when
|
|
const { loadSkillFromPathAsync } = await import("./async-loader")
|
|
const skill = await loadSkillFromPathAsync(invalidPath, TEST_DIR, "invalid", "opencode")
|
|
|
|
// #then
|
|
expect(skill).toBeNull()
|
|
})
|
|
|
|
it("returns null for malformed skill file", async () => {
|
|
// #given
|
|
const malformedContent = "This is not valid frontmatter content\nNo YAML here!"
|
|
mkdirSync(SKILLS_DIR, { recursive: true })
|
|
const malformedPath = join(SKILLS_DIR, "malformed.md")
|
|
writeFileSync(malformedPath, malformedContent)
|
|
|
|
// #when
|
|
const { loadSkillFromPathAsync } = await import("./async-loader")
|
|
const skill = await loadSkillFromPathAsync(malformedPath, SKILLS_DIR, "malformed", "user")
|
|
|
|
// #then
|
|
expect(skill).not.toBeNull() // parseFrontmatter handles missing frontmatter gracefully
|
|
})
|
|
})
|
|
|
|
describe("loadMcpJsonFromDirAsync", () => {
|
|
it("loads mcp.json with mcpServers format", async () => {
|
|
// #given
|
|
mkdirSync(SKILLS_DIR, { recursive: true })
|
|
const mcpJson = {
|
|
mcpServers: {
|
|
test: {
|
|
command: "test-cmd",
|
|
args: ["arg1"]
|
|
}
|
|
}
|
|
}
|
|
writeFileSync(join(SKILLS_DIR, "mcp.json"), JSON.stringify(mcpJson))
|
|
|
|
// #when
|
|
const { loadMcpJsonFromDirAsync } = await import("./async-loader")
|
|
const config = await loadMcpJsonFromDirAsync(SKILLS_DIR)
|
|
|
|
// #then
|
|
expect(config).toBeDefined()
|
|
expect(config?.test).toBeDefined()
|
|
expect(config?.test?.command).toBe("test-cmd")
|
|
})
|
|
|
|
it("returns undefined for non-existent mcp.json", async () => {
|
|
// #given
|
|
mkdirSync(SKILLS_DIR, { recursive: true })
|
|
|
|
// #when
|
|
const { loadMcpJsonFromDirAsync } = await import("./async-loader")
|
|
const config = await loadMcpJsonFromDirAsync(SKILLS_DIR)
|
|
|
|
// #then
|
|
expect(config).toBeUndefined()
|
|
})
|
|
|
|
it("returns undefined for invalid JSON", async () => {
|
|
// #given
|
|
mkdirSync(SKILLS_DIR, { recursive: true })
|
|
writeFileSync(join(SKILLS_DIR, "mcp.json"), "{ invalid json }")
|
|
|
|
// #when
|
|
const { loadMcpJsonFromDirAsync } = await import("./async-loader")
|
|
const config = await loadMcpJsonFromDirAsync(SKILLS_DIR)
|
|
|
|
// #then
|
|
expect(config).toBeUndefined()
|
|
})
|
|
|
|
it("supports direct format without mcpServers", async () => {
|
|
// #given
|
|
mkdirSync(SKILLS_DIR, { recursive: true })
|
|
const mcpJson = {
|
|
direct: {
|
|
command: "direct-cmd",
|
|
args: ["arg"]
|
|
}
|
|
}
|
|
writeFileSync(join(SKILLS_DIR, "mcp.json"), JSON.stringify(mcpJson))
|
|
|
|
// #when
|
|
const { loadMcpJsonFromDirAsync } = await import("./async-loader")
|
|
const config = await loadMcpJsonFromDirAsync(SKILLS_DIR)
|
|
|
|
// #then
|
|
expect(config?.direct).toBeDefined()
|
|
expect(config?.direct?.command).toBe("direct-cmd")
|
|
})
|
|
})
|
|
})
|