2026-03-26 12:59:39 +09:00
import { execFileSync } from "node:child_process"
2026-04-18 14:10:49 +09:00
import { promises as fs } from "node:fs"
import { afterEach , beforeEach , describe , expect , it , spyOn } from "bun:test"
2026-03-26 11:22:00 +09:00
import { mkdirSync , rmSync , writeFileSync } from "node:fs"
import { tmpdir } from "node:os"
import { join } from "node:path"
2026-04-18 14:10:49 +09:00
import * as loader from "./loader"
2026-03-26 11:22:00 +09:00
const TEST_DIR = join ( tmpdir ( ) , ` claude-code-command-loader- ${ Date . now ( ) } ` )
function writeCommand ( directory : string , name : string , description : string ) : void {
mkdirSync ( directory , { recursive : true } )
writeFileSync (
join ( directory , ` ${ name } .md ` ) ,
` --- \ ndescription: ${ description } \ n--- \ nRun ${ name } . \ n ` ,
)
}
describe ( "claude-code command loader" , ( ) = > {
2026-04-18 14:10:49 +09:00
let originalClaudeConfigDir : string | undefined
2026-03-26 11:22:00 +09:00
let originalOpencodeConfigDir : string | undefined
beforeEach ( ( ) = > {
mkdirSync ( TEST_DIR , { recursive : true } )
2026-04-18 14:10:49 +09:00
originalClaudeConfigDir = process . env . CLAUDE_CONFIG_DIR
2026-03-26 11:22:00 +09:00
originalOpencodeConfigDir = process . env . OPENCODE_CONFIG_DIR
2026-04-18 14:10:49 +09:00
const claudeConfigDir = join ( TEST_DIR , "claude-config" )
const opencodeConfigDir = join ( TEST_DIR , "opencode-config" )
process . env . CLAUDE_CONFIG_DIR = claudeConfigDir
process . env . OPENCODE_CONFIG_DIR = opencodeConfigDir
if ( "clearCommandLoaderCache" in loader && typeof loader . clearCommandLoaderCache === "function" ) {
loader . clearCommandLoaderCache ( )
}
2026-03-26 11:22:00 +09:00
} )
afterEach ( ( ) = > {
2026-04-18 14:10:49 +09:00
if ( originalClaudeConfigDir === undefined ) {
delete process . env . CLAUDE_CONFIG_DIR
} else {
process . env . CLAUDE_CONFIG_DIR = originalClaudeConfigDir
}
2026-03-26 11:22:00 +09:00
if ( originalOpencodeConfigDir === undefined ) {
delete process . env . OPENCODE_CONFIG_DIR
} else {
process . env . OPENCODE_CONFIG_DIR = originalOpencodeConfigDir
}
2026-04-18 14:10:49 +09:00
if ( "clearCommandLoaderCache" in loader && typeof loader . clearCommandLoaderCache === "function" ) {
loader . clearCommandLoaderCache ( )
}
2026-03-26 11:22:00 +09:00
rmSync ( TEST_DIR , { recursive : true , force : true } )
} )
it ( "#given a parent .opencode/commands directory #when loadOpencodeProjectCommands is called from child directory #then it loads the ancestor command" , async ( ) = > {
// given
const projectDir = join ( TEST_DIR , "project" )
const childDir = join ( projectDir , "apps" , "desktop" )
writeCommand ( join ( projectDir , ".opencode" , "commands" ) , "ancestor" , "Ancestor command" )
// when
2026-04-18 14:10:49 +09:00
const commands = await loader . loadOpencodeProjectCommands ( childDir )
2026-03-26 11:22:00 +09:00
// then
expect ( commands . ancestor ? . description ) . toBe ( "(opencode-project) Ancestor command" )
} )
it ( "#given a .opencode/command directory #when loadOpencodeProjectCommands is called #then it loads the singular alias directory" , async ( ) = > {
// given
writeCommand ( join ( TEST_DIR , ".opencode" , "command" ) , "singular" , "Singular command" )
// when
2026-04-18 14:10:49 +09:00
const commands = await loader . loadOpencodeProjectCommands ( TEST_DIR )
2026-03-26 11:22:00 +09:00
// then
expect ( commands . singular ? . description ) . toBe ( "(opencode-project) Singular command" )
} )
2026-03-26 11:36:59 +09:00
it ( "#given duplicate project command names across ancestors #when loadOpencodeProjectCommands is called #then the nearest directory wins" , async ( ) = > {
// given
const projectRoot = join ( TEST_DIR , "project" )
const childDir = join ( projectRoot , "apps" , "desktop" )
const ancestorDir = join ( TEST_DIR , ".opencode" , "commands" )
const projectDir = join ( projectRoot , ".opencode" , "commands" )
writeCommand ( ancestorDir , "duplicate" , "Ancestor command" )
writeCommand ( projectDir , "duplicate" , "Nearest command" )
// when
2026-04-18 14:10:49 +09:00
const commands = await loader . loadOpencodeProjectCommands ( childDir )
2026-03-26 11:36:59 +09:00
// then
expect ( commands . duplicate ? . description ) . toBe ( "(opencode-project) Nearest command" )
} )
2026-03-26 11:22:00 +09:00
it ( "#given a global .opencode/commands directory #when loadOpencodeGlobalCommands is called #then it loads the plural alias directory" , async ( ) = > {
// given
const opencodeConfigDir = join ( TEST_DIR , "opencode-config" )
process . env . OPENCODE_CONFIG_DIR = opencodeConfigDir
writeCommand ( join ( opencodeConfigDir , "commands" ) , "global-plural" , "Global plural command" )
// when
2026-04-18 14:10:49 +09:00
const commands = await loader . loadOpencodeGlobalCommands ( )
2026-03-26 11:22:00 +09:00
// then
expect ( commands [ "global-plural" ] ? . description ) . toBe ( "(opencode) Global plural command" )
} )
2026-03-26 11:36:59 +09:00
it ( "#given duplicate global command names across profile and parent dirs #when loadOpencodeGlobalCommands is called #then the profile dir wins" , async ( ) = > {
// given
const opencodeRootDir = join ( TEST_DIR , "opencode-root" )
const profileConfigDir = join ( opencodeRootDir , "profiles" , "codex" )
process . env . OPENCODE_CONFIG_DIR = profileConfigDir
writeCommand ( join ( opencodeRootDir , "commands" ) , "duplicate-global" , "Parent global command" )
writeCommand ( join ( profileConfigDir , "commands" ) , "duplicate-global" , "Profile global command" )
// when
2026-04-18 14:10:49 +09:00
const commands = await loader . loadOpencodeGlobalCommands ( )
2026-03-26 11:36:59 +09:00
// then
expect ( commands [ "duplicate-global" ] ? . description ) . toBe ( "(opencode) Profile global command" )
} )
2026-03-26 12:59:39 +09:00
it ( "#given nested project opencode commands in a worktree #when loadOpencodeProjectCommands is called #then it preserves slash names and stops at the worktree root" , async ( ) = > {
// given
const repositoryDir = join ( TEST_DIR , "repo" )
const nestedDirectory = join ( repositoryDir , "packages" , "app" , "src" )
mkdirSync ( nestedDirectory , { recursive : true } )
execFileSync ( "git" , [ "init" ] , {
cwd : repositoryDir ,
stdio : [ "ignore" , "ignore" , "ignore" ] ,
} )
writeCommand ( join ( repositoryDir , ".opencode" , "commands" , "deploy" ) , "staging" , "Deploy staging" )
writeCommand ( join ( repositoryDir , ".opencode" , "command" ) , "release" , "Release command" )
writeCommand ( join ( TEST_DIR , ".opencode" , "commands" ) , "outside" , "Outside command" )
// when
2026-04-18 14:10:49 +09:00
const commands = await loader . loadOpencodeProjectCommands ( nestedDirectory )
2026-03-26 12:59:39 +09:00
// then
expect ( commands [ "deploy/staging" ] ? . description ) . toBe ( "(opencode-project) Deploy staging" )
expect ( commands . release ? . description ) . toBe ( "(opencode-project) Release command" )
expect ( commands . outside ) . toBeUndefined ( )
expect ( commands [ "deploy:staging" ] ) . toBeUndefined ( )
} )
2026-04-18 14:10:49 +09:00
it ( "#given commands nested under an excluded basename #when loadProjectCommands is called #then it skips the excluded directory contents" , async ( ) = > {
// given
writeCommand ( join ( TEST_DIR , ".claude" , "commands" ) , "real" , "Real command" )
writeCommand (
join ( TEST_DIR , ".claude" , "commands" , "node_modules" ) ,
"fake" ,
"Fake command" ,
)
// when
const commands = await loader . loadProjectCommands ( TEST_DIR )
// then
expect ( commands . real ? . description ) . toBe ( "(project) Real command" )
expect ( commands . fake ) . toBeUndefined ( )
} )
it ( "#given a previously loaded directory #when loadAllCommands is called twice #then the second call reuses the cached result without readdir calls" , async ( ) = > {
// given
writeCommand ( join ( TEST_DIR , ".claude" , "commands" ) , "cached" , "Cached command" )
const readdirSpy = spyOn ( fs , "readdir" )
// when
const firstCommands = await loader . loadAllCommands ( TEST_DIR )
const firstReaddirCount = readdirSpy . mock . calls . length
const secondCommands = await loader . loadAllCommands ( TEST_DIR )
// then
expect ( firstCommands . cached ? . description ) . toBe ( "(project) Cached command" )
expect ( secondCommands ) . toEqual ( firstCommands )
expect ( firstReaddirCount ) . toBeGreaterThan ( 0 )
expect ( readdirSpy . mock . calls . length ) . toBe ( firstReaddirCount )
} )
2026-03-26 11:22:00 +09:00
} )