2026-01-01 22:52:37 +09:00
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 ( ) )
2026-01-21 13:12:27 +01:00
const SKILLS_DIR = join ( TEST_DIR , ".opencode" , "skills" )
2026-01-01 22:52:37 +09:00
2026-01-01 23:01:06 +09:00
function createTestSkill ( name : string , content : string , mcpJson? : object ) : string {
2026-01-01 22:52:37 +09:00
const skillDir = join ( SKILLS_DIR , name )
mkdirSync ( skillDir , { recursive : true } )
const skillPath = join ( skillDir , "SKILL.md" )
writeFileSync ( skillPath , content )
2026-01-01 23:01:06 +09:00
if ( mcpJson ) {
writeFileSync ( join ( skillDir , "mcp.json" ) , JSON . stringify ( mcpJson , null , 2 ) )
}
2026-01-01 22:52:37 +09:00
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 ( ) = > {
2026-02-01 16:47:50 +09:00
// given
2026-01-01 22:52:37 +09:00
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 )
2026-02-01 16:47:50 +09:00
// when
2026-01-01 22:52:37 +09:00
const { discoverSkills } = await import ( "./loader" )
const originalCwd = process . cwd ( )
process . chdir ( TEST_DIR )
try {
2026-01-05 14:18:42 +09:00
const skills = await discoverSkills ( { includeClaudeCodePaths : false } )
2026-01-01 22:52:37 +09:00
const skill = skills . find ( s = > s . name === "test-skill" )
2026-02-01 16:47:50 +09:00
// then
2026-01-01 22:52:37 +09:00
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 ( ) = > {
2026-02-01 16:47:50 +09:00
// given
2026-01-01 22:52:37 +09:00
const skillContent = ` ---
name: simple-skill
description: A simple skill without MCP
---
This is a simple skill.
`
createTestSkill ( "simple-skill" , skillContent )
2026-02-01 16:47:50 +09:00
// when
2026-01-01 22:52:37 +09:00
const { discoverSkills } = await import ( "./loader" )
const originalCwd = process . cwd ( )
process . chdir ( TEST_DIR )
try {
2026-01-05 14:18:42 +09:00
const skills = await discoverSkills ( { includeClaudeCodePaths : false } )
2026-01-01 22:52:37 +09:00
const skill = skills . find ( s = > s . name === "simple-skill" )
2026-02-01 16:47:50 +09:00
// then
2026-01-01 22:52:37 +09:00
expect ( skill ) . toBeDefined ( )
expect ( skill ? . mcpConfig ) . toBeUndefined ( )
} finally {
process . chdir ( originalCwd )
}
} )
it ( "preserves env var placeholders without expansion" , async ( ) = > {
2026-02-01 16:47:50 +09:00
// given
2026-01-01 22:52:37 +09:00
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 )
2026-02-01 16:47:50 +09:00
// when
2026-01-01 22:52:37 +09:00
const { discoverSkills } = await import ( "./loader" )
const originalCwd = process . cwd ( )
process . chdir ( TEST_DIR )
try {
2026-01-05 14:18:42 +09:00
const skills = await discoverSkills ( { includeClaudeCodePaths : false } )
2026-01-01 22:52:37 +09:00
const skill = skills . find ( s = > s . name === "env-skill" )
2026-02-01 16:47:50 +09:00
// then
2026-01-01 22:52:37 +09:00
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 ( ) = > {
2026-02-01 16:47:50 +09:00
// given - malformed YAML causes entire frontmatter to fail parsing
2026-01-01 22:52:37 +09:00
const skillContent = ` ---
name: bad-yaml
mcp: [this is not valid yaml for mcp
---
Skill body.
`
createTestSkill ( "bad-yaml-skill" , skillContent )
2026-02-01 16:47:50 +09:00
// when
2026-01-01 22:52:37 +09:00
const { discoverSkills } = await import ( "./loader" )
const originalCwd = process . cwd ( )
process . chdir ( TEST_DIR )
try {
2026-01-05 14:18:42 +09:00
const skills = await discoverSkills ( { includeClaudeCodePaths : false } )
2026-02-01 16:47:50 +09:00
// then - when YAML fails, skill uses directory name as fallback
2026-01-02 15:11:14 +09:00
const skill = skills . find ( s = > s . name === "bad-yaml-skill" )
2026-01-01 22:52:37 +09:00
expect ( skill ) . toBeDefined ( )
expect ( skill ? . mcpConfig ) . toBeUndefined ( )
} finally {
process . chdir ( originalCwd )
}
} )
} )
2026-01-01 23:01:06 +09:00
describe ( "mcp.json file loading (AmpCode compat)" , ( ) = > {
it ( "loads MCP config from mcp.json with mcpServers format" , async ( ) = > {
2026-02-01 16:47:50 +09:00
// given
2026-01-01 23:01:06 +09:00
const skillContent = ` ---
name: ampcode-skill
description: Skill with mcp.json
---
Skill body.
`
const mcpJson = {
mcpServers : {
playwright : {
command : "npx" ,
args : [ "@playwright/mcp@latest" ]
}
}
}
createTestSkill ( "ampcode-skill" , skillContent , mcpJson )
2026-02-01 16:47:50 +09:00
// when
2026-01-01 23:01:06 +09:00
const { discoverSkills } = await import ( "./loader" )
const originalCwd = process . cwd ( )
process . chdir ( TEST_DIR )
try {
2026-01-05 14:18:42 +09:00
const skills = await discoverSkills ( { includeClaudeCodePaths : false } )
2026-01-01 23:01:06 +09:00
const skill = skills . find ( s = > s . name === "ampcode-skill" )
2026-02-01 16:47:50 +09:00
// then
2026-01-01 23:01:06 +09:00
expect ( skill ) . toBeDefined ( )
expect ( skill ? . mcpConfig ) . toBeDefined ( )
expect ( skill ? . mcpConfig ? . playwright ) . toBeDefined ( )
expect ( skill ? . mcpConfig ? . playwright ? . command ) . toBe ( "npx" )
expect ( skill ? . mcpConfig ? . playwright ? . args ) . toEqual ( [ "@playwright/mcp@latest" ] )
} finally {
process . chdir ( originalCwd )
}
} )
it ( "mcp.json takes priority over YAML frontmatter" , async ( ) = > {
2026-02-01 16:47:50 +09:00
// given
2026-01-01 23:01:06 +09:00
const skillContent = ` ---
name: priority-skill
mcp:
from-yaml:
command: yaml-cmd
args: [yaml-arg]
---
Skill body.
`
const mcpJson = {
mcpServers : {
"from-json" : {
command : "json-cmd" ,
args : [ "json-arg" ]
}
}
}
createTestSkill ( "priority-skill" , skillContent , mcpJson )
2026-02-01 16:47:50 +09:00
// when
2026-01-01 23:01:06 +09:00
const { discoverSkills } = await import ( "./loader" )
const originalCwd = process . cwd ( )
process . chdir ( TEST_DIR )
try {
2026-01-05 14:18:42 +09:00
const skills = await discoverSkills ( { includeClaudeCodePaths : false } )
2026-01-01 23:01:06 +09:00
const skill = skills . find ( s = > s . name === "priority-skill" )
2026-02-01 16:47:50 +09:00
// then - mcp.json should take priority
2026-01-01 23:01:06 +09:00
expect ( skill ? . mcpConfig ? . [ "from-json" ] ) . toBeDefined ( )
expect ( skill ? . mcpConfig ? . [ "from-yaml" ] ) . toBeUndefined ( )
} finally {
process . chdir ( originalCwd )
}
} )
it ( "supports direct format without mcpServers wrapper" , async ( ) = > {
2026-02-01 16:47:50 +09:00
// given
2026-01-01 23:01:06 +09:00
const skillContent = ` ---
name: direct-format
---
Skill body.
`
const mcpJson = {
sqlite : {
command : "uvx" ,
args : [ "mcp-server-sqlite" ]
}
}
createTestSkill ( "direct-format" , skillContent , mcpJson )
2026-02-01 16:47:50 +09:00
// when
2026-01-01 23:01:06 +09:00
const { discoverSkills } = await import ( "./loader" )
const originalCwd = process . cwd ( )
process . chdir ( TEST_DIR )
try {
2026-01-05 14:18:42 +09:00
const skills = await discoverSkills ( { includeClaudeCodePaths : false } )
2026-01-01 23:01:06 +09:00
const skill = skills . find ( s = > s . name === "direct-format" )
2026-02-01 16:47:50 +09:00
// then
2026-01-01 23:01:06 +09:00
expect ( skill ? . mcpConfig ? . sqlite ) . toBeDefined ( )
expect ( skill ? . mcpConfig ? . sqlite ? . command ) . toBe ( "uvx" )
} finally {
process . chdir ( originalCwd )
}
2026-01-28 14:26:34 +07:00
} )
} )
describe ( "allowed-tools parsing" , ( ) = > {
it ( "parses space-separated allowed-tools string" , async ( ) = > {
2026-02-01 16:47:50 +09:00
// given
2026-01-28 14:26:34 +07:00
const skillContent = ` ---
name: space-separated-tools
description: Skill with space-separated allowed-tools
allowed-tools: Read Write Edit Bash
---
Skill body.
`
createTestSkill ( "space-separated-tools" , skillContent )
2026-02-01 16:47:50 +09:00
// when
2026-01-28 14:26:34 +07:00
const { discoverSkills } = await import ( "./loader" )
const originalCwd = process . cwd ( )
process . chdir ( TEST_DIR )
try {
const skills = await discoverSkills ( { includeClaudeCodePaths : false } )
const skill = skills . find ( s = > s . name === "space-separated-tools" )
2026-02-01 16:47:50 +09:00
// then
2026-01-28 14:26:34 +07:00
expect ( skill ) . toBeDefined ( )
expect ( skill ? . allowedTools ) . toEqual ( [ "Read" , "Write" , "Edit" , "Bash" ] )
} finally {
process . chdir ( originalCwd )
}
} )
it ( "parses YAML inline array allowed-tools" , async ( ) = > {
2026-02-01 16:47:50 +09:00
// given
2026-01-28 14:26:34 +07:00
const skillContent = ` ---
name: yaml-inline-array
description: Skill with YAML inline array allowed-tools
allowed-tools: [Read, Write, Edit, Bash]
---
Skill body.
`
createTestSkill ( "yaml-inline-array" , skillContent )
2026-02-01 16:47:50 +09:00
// when
2026-01-28 14:26:34 +07:00
const { discoverSkills } = await import ( "./loader" )
const originalCwd = process . cwd ( )
process . chdir ( TEST_DIR )
try {
const skills = await discoverSkills ( { includeClaudeCodePaths : false } )
const skill = skills . find ( s = > s . name === "yaml-inline-array" )
2026-02-01 16:47:50 +09:00
// then
2026-01-28 14:26:34 +07:00
expect ( skill ) . toBeDefined ( )
expect ( skill ? . allowedTools ) . toEqual ( [ "Read" , "Write" , "Edit" , "Bash" ] )
} finally {
process . chdir ( originalCwd )
}
} )
it ( "parses YAML multi-line array allowed-tools" , async ( ) = > {
2026-02-01 16:47:50 +09:00
// given
2026-01-28 14:26:34 +07:00
const skillContent = ` ---
name: yaml-multiline-array
description: Skill with YAML multi-line array allowed-tools
allowed-tools:
- Read
- Write
- Edit
- Bash
---
Skill body.
`
createTestSkill ( "yaml-multiline-array" , skillContent )
2026-02-01 16:47:50 +09:00
// when
2026-01-28 14:26:34 +07:00
const { discoverSkills } = await import ( "./loader" )
const originalCwd = process . cwd ( )
process . chdir ( TEST_DIR )
try {
const skills = await discoverSkills ( { includeClaudeCodePaths : false } )
const skill = skills . find ( s = > s . name === "yaml-multiline-array" )
2026-02-01 16:47:50 +09:00
// then
2026-01-28 14:26:34 +07:00
expect ( skill ) . toBeDefined ( )
expect ( skill ? . allowedTools ) . toEqual ( [ "Read" , "Write" , "Edit" , "Bash" ] )
} finally {
process . chdir ( originalCwd )
}
} )
it ( "returns undefined for skill without allowed-tools" , async ( ) = > {
2026-02-01 16:47:50 +09:00
// given
2026-01-28 14:26:34 +07:00
const skillContent = ` ---
name: no-allowed-tools
description: Skill without allowed-tools field
---
Skill body.
`
createTestSkill ( "no-allowed-tools" , skillContent )
2026-02-01 16:47:50 +09:00
// when
2026-01-28 14:26:34 +07:00
const { discoverSkills } = await import ( "./loader" )
const originalCwd = process . cwd ( )
process . chdir ( TEST_DIR )
try {
const skills = await discoverSkills ( { includeClaudeCodePaths : false } )
const skill = skills . find ( s = > s . name === "no-allowed-tools" )
2026-02-01 16:47:50 +09:00
// then
2026-01-28 14:26:34 +07:00
expect ( skill ) . toBeDefined ( )
expect ( skill ? . allowedTools ) . toBeUndefined ( )
} finally {
process . chdir ( originalCwd )
}
2026-01-01 23:01:06 +09:00
} )
} )
2026-01-30 00:39:43 +08:00
2026-02-05 01:54:33 +09:00
describe ( "deduplication" , ( ) = > {
2026-02-05 02:36:43 +09:00
it ( "deduplicates skills by name across scopes, keeping higher priority (opencode-project > opencode > project)" , async ( ) = > {
const originalCwd = process . cwd ( )
const originalOpenCodeConfigDir = process . env . OPENCODE_CONFIG_DIR
const originalClaudeConfigDir = process . env . CLAUDE_CONFIG_DIR
// given: same skill name in multiple scopes
2026-02-05 01:54:33 +09:00
const opencodeProjectSkillsDir = join ( TEST_DIR , ".opencode" , "skills" )
2026-02-05 02:36:43 +09:00
const opencodeConfigDir = join ( TEST_DIR , "opencode-global" )
const opencodeGlobalSkillsDir = join ( opencodeConfigDir , "skills" )
const projectClaudeSkillsDir = join ( TEST_DIR , ".claude" , "skills" )
process . env . OPENCODE_CONFIG_DIR = opencodeConfigDir
process . env . CLAUDE_CONFIG_DIR = join ( TEST_DIR , "claude-user" )
2026-02-05 01:54:33 +09:00
mkdirSync ( join ( opencodeProjectSkillsDir , "duplicate-skill" ) , { recursive : true } )
2026-02-05 02:36:43 +09:00
mkdirSync ( join ( opencodeGlobalSkillsDir , "duplicate-skill" ) , { recursive : true } )
mkdirSync ( join ( projectClaudeSkillsDir , "duplicate-skill" ) , { recursive : true } )
2026-02-05 01:54:33 +09:00
writeFileSync (
join ( opencodeProjectSkillsDir , "duplicate-skill" , "SKILL.md" ) ,
` ---
name: duplicate-skill
2026-02-05 02:36:43 +09:00
description: From opencode-project (highest priority)
---
opencode-project body.
`
)
writeFileSync (
join ( opencodeGlobalSkillsDir , "duplicate-skill" , "SKILL.md" ) ,
` ---
name: duplicate-skill
description: From opencode-global (middle priority)
---
opencode-global body.
`
)
writeFileSync (
join ( projectClaudeSkillsDir , "duplicate-skill" , "SKILL.md" ) ,
` ---
name: duplicate-skill
description: From claude project (lowest priority among these)
2026-01-30 00:39:43 +08:00
---
2026-02-05 02:36:43 +09:00
claude project body.
2026-01-30 00:39:43 +08:00
`
2026-02-05 01:54:33 +09:00
)
2026-01-30 00:39:43 +08:00
2026-02-05 02:36:43 +09:00
// when
2026-02-05 02:33:56 +09:00
const { discoverSkills } = await import ( "./loader" )
2026-02-05 02:36:43 +09:00
process . chdir ( TEST_DIR )
try {
const skills = await discoverSkills ( )
const duplicates = skills . filter ( s = > s . name === "duplicate-skill" )
// then
expect ( duplicates ) . toHaveLength ( 1 )
expect ( duplicates [ 0 ] ? . scope ) . toBe ( "opencode-project" )
expect ( duplicates [ 0 ] ? . definition . description ) . toContain ( "opencode-project" )
} finally {
process . chdir ( originalCwd )
2026-02-05 09:37:42 +09:00
if ( originalOpenCodeConfigDir === undefined ) {
delete process . env . OPENCODE_CONFIG_DIR
} else {
process . env . OPENCODE_CONFIG_DIR = originalOpenCodeConfigDir
}
if ( originalClaudeConfigDir === undefined ) {
delete process . env . CLAUDE_CONFIG_DIR
} else {
process . env . CLAUDE_CONFIG_DIR = originalClaudeConfigDir
}
2026-02-05 02:36:43 +09:00
}
} )
it ( "prioritizes OpenCode global skills over legacy Claude project skills" , async ( ) = > {
2026-01-30 00:39:43 +08:00
const originalCwd = process . cwd ( )
2026-02-05 02:36:43 +09:00
const originalOpenCodeConfigDir = process . env . OPENCODE_CONFIG_DIR
const originalClaudeConfigDir = process . env . CLAUDE_CONFIG_DIR
const opencodeConfigDir = join ( TEST_DIR , "opencode-global" )
const opencodeGlobalSkillsDir = join ( opencodeConfigDir , "skills" )
const projectClaudeSkillsDir = join ( TEST_DIR , ".claude" , "skills" )
process . env . OPENCODE_CONFIG_DIR = opencodeConfigDir
process . env . CLAUDE_CONFIG_DIR = join ( TEST_DIR , "claude-user" )
mkdirSync ( join ( opencodeGlobalSkillsDir , "global-over-project" ) , { recursive : true } )
mkdirSync ( join ( projectClaudeSkillsDir , "global-over-project" ) , { recursive : true } )
writeFileSync (
join ( opencodeGlobalSkillsDir , "global-over-project" , "SKILL.md" ) ,
` ---
name: global-over-project
description: From opencode-global (should win)
---
opencode-global body.
`
)
writeFileSync (
join ( projectClaudeSkillsDir , "global-over-project" , "SKILL.md" ) ,
` ---
name: global-over-project
description: From claude project (should lose)
---
claude project body.
`
)
const { discoverSkills } = await import ( "./loader" )
2026-01-30 00:39:43 +08:00
process . chdir ( TEST_DIR )
try {
2026-02-05 02:36:43 +09:00
const skills = await discoverSkills ( )
const matches = skills . filter ( s = > s . name === "global-over-project" )
2026-01-30 00:39:43 +08:00
2026-02-05 02:36:43 +09:00
expect ( matches ) . toHaveLength ( 1 )
expect ( matches [ 0 ] ? . scope ) . toBe ( "opencode" )
expect ( matches [ 0 ] ? . definition . description ) . toContain ( "opencode-global" )
2026-01-30 00:39:43 +08:00
} finally {
process . chdir ( originalCwd )
2026-02-05 09:37:42 +09:00
if ( originalOpenCodeConfigDir === undefined ) {
delete process . env . OPENCODE_CONFIG_DIR
} else {
process . env . OPENCODE_CONFIG_DIR = originalOpenCodeConfigDir
}
if ( originalClaudeConfigDir === undefined ) {
delete process . env . CLAUDE_CONFIG_DIR
} else {
process . env . CLAUDE_CONFIG_DIR = originalClaudeConfigDir
}
2026-01-30 00:39:43 +08:00
}
} )
2026-02-05 01:54:33 +09:00
it ( "returns no duplicates from discoverSkills" , async ( ) = > {
2026-02-05 02:36:43 +09:00
const originalCwd = process . cwd ( )
const originalOpenCodeConfigDir = process . env . OPENCODE_CONFIG_DIR
process . env . OPENCODE_CONFIG_DIR = join ( TEST_DIR , "opencode-global" )
// given
2026-02-05 01:54:33 +09:00
const skillContent = ` ---
name: unique-test-skill
description: A unique skill for dedup test
2026-02-04 13:52:06 +09:00
---
2026-02-05 01:54:33 +09:00
Skill body.
`
createTestSkill ( "unique-test-skill" , skillContent )
2026-02-04 13:52:06 +09:00
2026-02-05 01:54:33 +09:00
// when
2026-02-04 13:52:06 +09:00
const { discoverSkills } = await import ( "./loader" )
process . chdir ( TEST_DIR )
try {
const skills = await discoverSkills ( { includeClaudeCodePaths : false } )
2026-02-05 02:36:43 +09:00
// then
2026-02-05 01:54:33 +09:00
const names = skills . map ( s = > s . name )
const uniqueNames = [ . . . new Set ( names ) ]
expect ( names . length ) . toBe ( uniqueNames . length )
2026-02-04 13:52:06 +09:00
} finally {
process . chdir ( originalCwd )
2026-02-14 14:44:12 +09:00
if ( originalOpenCodeConfigDir === undefined ) {
2026-02-05 09:37:42 +09:00
delete process . env . OPENCODE_CONFIG_DIR
} else {
process . env . OPENCODE_CONFIG_DIR = originalOpenCodeConfigDir
}
2026-02-04 13:52:06 +09:00
}
} )
2026-01-30 00:39:43 +08:00
} )
2026-02-14 14:44:12 +09:00
describe ( "agents skills discovery (.agents/skills/)" , ( ) = > {
it ( "#given a skill in .agents/skills/ #when discoverProjectAgentsSkills is called #then it discovers the skill" , async ( ) = > {
//#given
const skillContent = ` ---
name: agent-project-skill
description: A skill from project .agents/skills directory
---
Skill body.
`
const agentsProjectSkillsDir = join ( TEST_DIR , ".agents" , "skills" )
const skillDir = join ( agentsProjectSkillsDir , "agent-project-skill" )
mkdirSync ( skillDir , { recursive : true } )
writeFileSync ( join ( skillDir , "SKILL.md" ) , skillContent )
//#when
const { discoverProjectAgentsSkills } = await import ( "./loader" )
const originalCwd = process . cwd ( )
process . chdir ( TEST_DIR )
try {
const skills = await discoverProjectAgentsSkills ( )
const skill = skills . find ( s = > s . name === "agent-project-skill" )
//#then
expect ( skill ) . toBeDefined ( )
expect ( skill ? . scope ) . toBe ( "project" )
expect ( skill ? . definition . description ) . toContain ( "A skill from project .agents/skills directory" )
} finally {
process . chdir ( originalCwd )
}
} )
it ( "#given a skill in .agents/skills/ #when discoverProjectAgentsSkills is called with directory #then it discovers the skill" , async ( ) = > {
//#given
const skillContent = ` ---
name: agent-dir-skill
description: A skill via explicit directory param
---
Skill body.
`
const agentsProjectSkillsDir = join ( TEST_DIR , ".agents" , "skills" )
const skillDir = join ( agentsProjectSkillsDir , "agent-dir-skill" )
mkdirSync ( skillDir , { recursive : true } )
writeFileSync ( join ( skillDir , "SKILL.md" ) , skillContent )
//#when
const { discoverProjectAgentsSkills } = await import ( "./loader" )
const skills = await discoverProjectAgentsSkills ( TEST_DIR )
const skill = skills . find ( s = > s . name === "agent-dir-skill" )
//#then
expect ( skill ) . toBeDefined ( )
expect ( skill ? . scope ) . toBe ( "project" )
} )
2026-03-26 11:22:00 +09:00
it ( "#given a skill in ancestor .agents/skills/ #when discoverProjectAgentsSkills is called from child directory #then it discovers the ancestor skill" , async ( ) = > {
// given
const skillContent = ` ---
name: ancestor-agent-skill
description: A skill from ancestor .agents/skills directory
---
Skill body.
`
const projectDir = join ( TEST_DIR , "project" )
const childDir = join ( projectDir , "apps" , "worker" )
const agentsProjectSkillsDir = join ( projectDir , ".agents" , "skills" )
const skillDir = join ( agentsProjectSkillsDir , "ancestor-agent-skill" )
mkdirSync ( childDir , { recursive : true } )
mkdirSync ( skillDir , { recursive : true } )
writeFileSync ( join ( skillDir , "SKILL.md" ) , skillContent )
// when
const { discoverProjectAgentsSkills } = await import ( "./loader" )
const skills = await discoverProjectAgentsSkills ( childDir )
const skill = skills . find ( ( candidate ) = > candidate . name === "ancestor-agent-skill" )
// then
expect ( skill ) . toBeDefined ( )
expect ( skill ? . scope ) . toBe ( "project" )
} )
} )
describe ( "opencode project skill discovery" , ( ) = > {
it ( "#given a skill in ancestor .opencode/skills/ #when discoverOpencodeProjectSkills is called from child directory #then it discovers the ancestor skill" , async ( ) = > {
// given
const skillContent = ` ---
name: ancestor-opencode-skill
description: A skill from ancestor .opencode/skills directory
---
Skill body.
`
const projectDir = join ( TEST_DIR , "project" )
const childDir = join ( projectDir , "packages" , "cli" )
const skillsDir = join ( projectDir , ".opencode" , "skills" , "ancestor-opencode-skill" )
mkdirSync ( childDir , { recursive : true } )
mkdirSync ( skillsDir , { recursive : true } )
writeFileSync ( join ( skillsDir , "SKILL.md" ) , skillContent )
// when
const { discoverOpencodeProjectSkills } = await import ( "./loader" )
const skills = await discoverOpencodeProjectSkills ( childDir )
const skill = skills . find ( ( candidate ) = > candidate . name === "ancestor-opencode-skill" )
// then
expect ( skill ) . toBeDefined ( )
expect ( skill ? . scope ) . toBe ( "opencode-project" )
} )
it ( "#given a skill in .opencode/skill/ #when discoverOpencodeProjectSkills is called #then it discovers the singular alias directory" , async ( ) = > {
// given
const skillContent = ` ---
name: singular-opencode-skill
description: A skill from .opencode/skill directory
---
Skill body.
`
const singularSkillDir = join (
TEST_DIR ,
".opencode" ,
"skill" ,
"singular-opencode-skill" ,
)
mkdirSync ( singularSkillDir , { recursive : true } )
writeFileSync ( join ( singularSkillDir , "SKILL.md" ) , skillContent )
// when
const { discoverOpencodeProjectSkills } = await import ( "./loader" )
const originalCwd = process . cwd ( )
process . chdir ( TEST_DIR )
try {
const skills = await discoverOpencodeProjectSkills ( )
const skill = skills . find ( ( candidate ) = > candidate . name === "singular-opencode-skill" )
// then
expect ( skill ) . toBeDefined ( )
expect ( skill ? . scope ) . toBe ( "opencode-project" )
} finally {
process . chdir ( originalCwd )
}
} )
2026-02-14 14:44:12 +09:00
} )
2026-01-01 22:52:37 +09:00
} )