From 65f03894a859e5db2059a80e7757d62bf4584b58 Mon Sep 17 00:00:00 2001 From: Aiden Cline Date: Mon, 29 Dec 2025 23:24:17 -0600 Subject: [PATCH] tests --- packages/opencode/src/global/index.ts | 3 +- packages/opencode/src/skill/skill.ts | 45 +++++++++++------ packages/opencode/test/preload.ts | 21 ++++++++ packages/opencode/test/skill/skill.test.ts | 57 ++++++++++++++-------- 4 files changed, 89 insertions(+), 37 deletions(-) diff --git a/packages/opencode/src/global/index.ts b/packages/opencode/src/global/index.ts index 2504a47dc..9621cadd3 100644 --- a/packages/opencode/src/global/index.ts +++ b/packages/opencode/src/global/index.ts @@ -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"), diff --git a/packages/opencode/src/skill/skill.ts b/packages/opencode/src/skill/skill.ts index 772a0d742..33158c4d9 100644 --- a/packages/opencode/src/skill/skill.ts +++ b/packages/opencode/src/skill/skill.ts @@ -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 = {} 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 diff --git a/packages/opencode/test/preload.ts b/packages/opencode/test/preload.ts index b6b6a66cf..1161f8661 100644 --- a/packages/opencode/test/preload.ts +++ b/packages/opencode/test/preload.ts @@ -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") diff --git a/packages/opencode/test/skill/skill.test.ts b/packages/opencode/test/skill/skill.test.ts index 1da8105bd..5c9f6bc7c 100644 --- a/packages/opencode/test/skill/skill.test.ts +++ b/packages/opencode/test/skill/skill.test.ts @@ -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() + // 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("claude-skill") - expect(skills[0].location).toContain(".claude/skills/claude-skill/SKILL.md") + 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") }, }) })