fix: correct skill priority order and improve test coverage

- Changed priority order to: opencode-project > opencode > project > user
  (OpenCode Global skills now take precedence over legacy Claude project skills)
- Updated JSDoc comments to reflect correct priority order
- Fixed test to use actual discoverSkills() for deduplication verification
- Changed test assertion from 'source' to 'scope' (correct field name)
This commit is contained in:
YeonGyu-Kim 2026-02-05 02:33:56 +09:00
parent 86ac39fb78
commit 18e941b6be
2 changed files with 17 additions and 40 deletions

View File

@ -389,13 +389,11 @@ Skill body.
}) })
describe("deduplication", () => { describe("deduplication", () => {
it("deduplicates skills with same name, keeping higher priority", async () => { it("deduplicates skills with same name, keeping higher priority (opencode-project > opencode)", async () => {
// given: same skill name in both opencode-project and opencode scopes // given: same skill name in both opencode-project and opencode scopes
const opencodeProjectSkillsDir = join(TEST_DIR, ".opencode", "skills") const opencodeProjectSkillsDir = join(TEST_DIR, ".opencode", "skills")
const opencodeGlobalSkillsDir = join(TEST_DIR, "opencode-global", "skills")
mkdirSync(join(opencodeProjectSkillsDir, "duplicate-skill"), { recursive: true }) mkdirSync(join(opencodeProjectSkillsDir, "duplicate-skill"), { recursive: true })
mkdirSync(join(opencodeGlobalSkillsDir, "duplicate-skill"), { recursive: true })
writeFileSync( writeFileSync(
join(opencodeProjectSkillsDir, "duplicate-skill", "SKILL.md"), join(opencodeProjectSkillsDir, "duplicate-skill", "SKILL.md"),
@ -407,43 +405,21 @@ Project skill body.
` `
) )
writeFileSync( // when: use discoverSkills which performs actual deduplication
join(opencodeGlobalSkillsDir, "duplicate-skill", "SKILL.md"), const { discoverSkills } = await import("./loader")
`---
name: duplicate-skill
description: From opencode-global (lower priority)
---
Global skill body.
`
)
// when
const { discoverOpencodeProjectSkills } = await import("./loader")
const originalCwd = process.cwd() const originalCwd = process.cwd()
process.chdir(TEST_DIR) process.chdir(TEST_DIR)
// Manually test deduplication logic
const { deduplicateSkills } = await import("./loader").then(m => ({
deduplicateSkills: (skills: any[]) => {
const seen = new Set<string>()
const result: any[] = []
for (const skill of skills) {
if (!seen.has(skill.name)) {
seen.add(skill.name)
result.push(skill)
}
}
return result
}
}))
try { try {
const projectSkills = await discoverOpencodeProjectSkills() // discoverSkills with includeClaudeCodePaths: false only loads opencode-project and opencode-global
const projectSkill = projectSkills.find(s => s.name === "duplicate-skill") const skills = await discoverSkills({ includeClaudeCodePaths: false })
const duplicateSkills = skills.filter(s => s.name === "duplicate-skill")
// then: opencode-project skill should exist // then: should have exactly one skill (deduplicated)
expect(projectSkill).toBeDefined() expect(duplicateSkills).toHaveLength(1)
expect(projectSkill?.definition.description).toContain("opencode-project") // and it should be from opencode-project (higher priority)
expect(duplicateSkills[0]?.definition.description).toContain("opencode-project")
expect(duplicateSkills[0]?.scope).toBe("opencode-project")
} finally { } finally {
process.chdir(originalCwd) process.chdir(originalCwd)
} }

View File

@ -235,7 +235,8 @@ export interface DiscoverSkillsOptions {
/** /**
* Deduplicates skills by name, keeping the first occurrence (higher priority). * Deduplicates skills by name, keeping the first occurrence (higher priority).
* Priority order: opencode-project > project > opencode > user * Priority order: opencode-project > opencode > project > user
* (OpenCode Global skills take precedence over legacy Claude project skills)
*/ */
function deduplicateSkills(skills: LoadedSkill[]): LoadedSkill[] { function deduplicateSkills(skills: LoadedSkill[]): LoadedSkill[] {
const seen = new Set<string>() const seen = new Set<string>()
@ -257,8 +258,8 @@ export async function discoverAllSkills(): Promise<LoadedSkill[]> {
discoverUserClaudeSkills(), discoverUserClaudeSkills(),
]) ])
// Priority: opencode-project > project > opencode > user // Priority: opencode-project > opencode > project > user
return deduplicateSkills([...opencodeProjectSkills, ...projectSkills, ...opencodeGlobalSkills, ...userSkills]) return deduplicateSkills([...opencodeProjectSkills, ...opencodeGlobalSkills, ...projectSkills, ...userSkills])
} }
export async function discoverSkills(options: DiscoverSkillsOptions = {}): Promise<LoadedSkill[]> { export async function discoverSkills(options: DiscoverSkillsOptions = {}): Promise<LoadedSkill[]> {
@ -279,8 +280,8 @@ export async function discoverSkills(options: DiscoverSkillsOptions = {}): Promi
discoverUserClaudeSkills(), discoverUserClaudeSkills(),
]) ])
// Priority: opencode-project > project > opencode > user // Priority: opencode-project > opencode > project > user
return deduplicateSkills([...opencodeProjectSkills, ...projectSkills, ...opencodeGlobalSkills, ...userSkills]) return deduplicateSkills([...opencodeProjectSkills, ...opencodeGlobalSkills, ...projectSkills, ...userSkills])
} }
export async function getSkillByName(name: string, options: DiscoverSkillsOptions = {}): Promise<LoadedSkill | undefined> { export async function getSkillByName(name: string, options: DiscoverSkillsOptions = {}): Promise<LoadedSkill | undefined> {