tests
parent
dc64ce8ad4
commit
65f03894a8
|
|
@ -12,7 +12,8 @@ const state = path.join(xdgState!, app)
|
|||
|
||||
export namespace Global {
|
||||
export const Path = {
|
||||
home: os.homedir(),
|
||||
// Allow override via OPENCODE_TEST_HOME for test isolation
|
||||
home: process.env.OPENCODE_TEST_HOME || os.homedir(),
|
||||
data,
|
||||
bin: path.join(data, "bin"),
|
||||
log: path.join(data, "log"),
|
||||
|
|
|
|||
|
|
@ -5,6 +5,8 @@ import { NamedError } from "@opencode-ai/util/error"
|
|||
import { ConfigMarkdown } from "../config/markdown"
|
||||
import { Log } from "../util/log"
|
||||
import { Global } from "@/global"
|
||||
import { Filesystem } from "@/util/filesystem"
|
||||
import { exists } from "fs/promises"
|
||||
|
||||
export namespace Skill {
|
||||
const log = Log.create({ service: "skill" })
|
||||
|
|
@ -34,13 +36,9 @@ export namespace Skill {
|
|||
)
|
||||
|
||||
const OPENCODE_SKILL_GLOB = new Bun.Glob("skill/**/SKILL.md")
|
||||
const CLAUDE_SKILL_GLOB = new Bun.Glob(".claude/skills/**/SKILL.md")
|
||||
const CLAUDE_SKILL_GLOB = new Bun.Glob("skills/**/SKILL.md")
|
||||
|
||||
export const state = Instance.state(async () => {
|
||||
const directories = await Config.directories()
|
||||
// include the global claude skills
|
||||
directories.push(Global.Path.home)
|
||||
|
||||
const skills: Record<string, Info> = {}
|
||||
|
||||
const addSkill = async (match: string) => {
|
||||
|
|
@ -68,7 +66,33 @@ export namespace Skill {
|
|||
}
|
||||
}
|
||||
|
||||
for (const dir of directories) {
|
||||
// Scan .opencode/skill/ directories
|
||||
for (const dir of await Config.directories()) {
|
||||
for await (const match of OPENCODE_SKILL_GLOB.scan({
|
||||
cwd: dir,
|
||||
absolute: true,
|
||||
onlyFiles: true,
|
||||
followSymlinks: true,
|
||||
})) {
|
||||
await addSkill(match)
|
||||
}
|
||||
}
|
||||
|
||||
// Scan .claude/skills/ directories (project-level)
|
||||
const claudeDirs = await Array.fromAsync(
|
||||
Filesystem.up({
|
||||
targets: [".claude"],
|
||||
start: Instance.directory,
|
||||
stop: Instance.worktree,
|
||||
}),
|
||||
)
|
||||
// Also include global ~/.claude/skills/
|
||||
const globalClaude = `${Global.Path.home}/.claude`
|
||||
if (await exists(globalClaude)) {
|
||||
claudeDirs.push(globalClaude)
|
||||
}
|
||||
|
||||
for (const dir of claudeDirs) {
|
||||
for await (const match of CLAUDE_SKILL_GLOB.scan({
|
||||
cwd: dir,
|
||||
absolute: true,
|
||||
|
|
@ -78,15 +102,6 @@ export namespace Skill {
|
|||
})) {
|
||||
await addSkill(match)
|
||||
}
|
||||
|
||||
for await (const match of OPENCODE_SKILL_GLOB.scan({
|
||||
cwd: dir,
|
||||
absolute: true,
|
||||
onlyFiles: true,
|
||||
followSymlinks: true,
|
||||
})) {
|
||||
await addSkill(match)
|
||||
}
|
||||
}
|
||||
|
||||
return skills
|
||||
|
|
|
|||
|
|
@ -11,6 +11,27 @@ await fs.mkdir(dir, { recursive: true })
|
|||
afterAll(() => {
|
||||
fsSync.rmSync(dir, { recursive: true, force: true })
|
||||
})
|
||||
// Set test home directory to isolate tests from user's actual home directory
|
||||
// This prevents tests from picking up real user configs/skills from ~/.claude/skills
|
||||
const testHome = path.join(dir, "home")
|
||||
await fs.mkdir(testHome, { recursive: true })
|
||||
process.env["OPENCODE_TEST_HOME"] = testHome
|
||||
|
||||
// Create a global skill in ~/.claude/skills/ for testing
|
||||
const globalSkillDir = path.join(testHome, ".claude", "skills", "global-test-skill")
|
||||
await fs.mkdir(globalSkillDir, { recursive: true })
|
||||
await fs.writeFile(
|
||||
path.join(globalSkillDir, "SKILL.md"),
|
||||
`---
|
||||
name: global-test-skill
|
||||
description: A global skill from ~/.claude/skills for testing.
|
||||
---
|
||||
|
||||
# Global Test Skill
|
||||
|
||||
This skill is loaded from the global home directory.
|
||||
`,
|
||||
)
|
||||
process.env["XDG_DATA_HOME"] = path.join(dir, "share")
|
||||
process.env["XDG_CACHE_HOME"] = path.join(dir, "cache")
|
||||
process.env["XDG_CONFIG_HOME"] = path.join(dir, "config")
|
||||
|
|
|
|||
|
|
@ -29,10 +29,12 @@ Instructions here.
|
|||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const skills = await Skill.all()
|
||||
expect(skills.length).toBe(1)
|
||||
expect(skills[0].name).toBe("test-skill")
|
||||
expect(skills[0].description).toBe("A test skill for verification.")
|
||||
expect(skills[0].location).toContain("skill/test-skill/SKILL.md")
|
||||
// Should find local skill + global skill from test home
|
||||
expect(skills.length).toBe(2)
|
||||
const testSkill = skills.find((s) => s.name === "test-skill")
|
||||
expect(testSkill).toBeDefined()
|
||||
expect(testSkill!.description).toBe("A test skill for verification.")
|
||||
expect(testSkill!.location).toContain("skill/test-skill/SKILL.md")
|
||||
},
|
||||
})
|
||||
})
|
||||
|
|
@ -59,8 +61,10 @@ description: Another test skill.
|
|||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const skills = await Skill.all()
|
||||
expect(skills.length).toBe(1)
|
||||
expect(skills[0].name).toBe("my-skill")
|
||||
// Should find local skill + global skill from test home
|
||||
expect(skills.length).toBe(2)
|
||||
const mySkill = skills.find((s) => s.name === "my-skill")
|
||||
expect(mySkill).toBeDefined()
|
||||
},
|
||||
})
|
||||
})
|
||||
|
|
@ -84,19 +88,9 @@ Just some content without YAML frontmatter.
|
|||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const skills = await Skill.all()
|
||||
expect(skills).toEqual([])
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("returns empty array when no skills exist", async () => {
|
||||
await using tmp = await tmpdir({ git: true })
|
||||
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const skills = await Skill.all()
|
||||
expect(skills).toEqual([])
|
||||
// Should only find the global skill, not the one without frontmatter
|
||||
expect(skills.length).toBe(1)
|
||||
expect(skills[0].name).toBe("global-test-skill")
|
||||
},
|
||||
})
|
||||
})
|
||||
|
|
@ -123,9 +117,30 @@ description: A skill in the .claude/skills directory.
|
|||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const skills = await Skill.all()
|
||||
expect(skills.length).toBe(1)
|
||||
expect(skills[0].name).toBe("claude-skill")
|
||||
expect(skills[0].location).toContain(".claude/skills/claude-skill/SKILL.md")
|
||||
// Should find both project-local and global skill
|
||||
expect(skills.length).toBe(2)
|
||||
const claudeSkill = skills.find((s) => s.name === "claude-skill")
|
||||
const globalSkill = skills.find((s) => s.name === "global-test-skill")
|
||||
expect(claudeSkill).toBeDefined()
|
||||
expect(claudeSkill!.location).toContain(".claude/skills/claude-skill/SKILL.md")
|
||||
expect(globalSkill).toBeDefined()
|
||||
expect(globalSkill!.description).toBe("A global skill from ~/.claude/skills for testing.")
|
||||
},
|
||||
})
|
||||
})
|
||||
|
||||
test("discovers global skills from ~/.claude/skills/ directory", async () => {
|
||||
// Create a project with no local skills - should still find global skill
|
||||
await using tmp = await tmpdir({ git: true })
|
||||
|
||||
await Instance.provide({
|
||||
directory: tmp.path,
|
||||
fn: async () => {
|
||||
const skills = await Skill.all()
|
||||
expect(skills.length).toBe(1)
|
||||
expect(skills[0].name).toBe("global-test-skill")
|
||||
expect(skills[0].description).toBe("A global skill from ~/.claude/skills for testing.")
|
||||
expect(skills[0].location).toContain(".claude/skills/global-test-skill/SKILL.md")
|
||||
},
|
||||
})
|
||||
})
|
||||
|
|
|
|||
Loading…
Reference in New Issue