Connect the existing plugin loader infrastructure to both slash command dispatch paths (executor and slashcommand tool), enabling namespaced commands like /daplug:run-prompt to resolve and execute. - Add plugin discovery to executor.ts discoverAllCommands() - Add plugin discovery to command-discovery.ts discoverCommandsSync() - Add "plugin" to CommandScope type - Remove blanket colon-rejection error (replaced with standard not-found) - Update slash command regex to accept namespaced commands - Thread claude_code.plugins config toggle through dispatch chain - Add unit tests for plugin command discovery and dispatch Closes #2019 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com> Co-Authored-By: Codex <noreply@openai.com>
169 lines
4.6 KiB
TypeScript
169 lines
4.6 KiB
TypeScript
import { afterEach, beforeEach, describe, expect, it } from "bun:test"
|
|
import { mkdtempSync, mkdirSync, rmSync, writeFileSync } from "node:fs"
|
|
import { tmpdir } from "node:os"
|
|
import { join } from "node:path"
|
|
import { executeSlashCommand } from "./executor"
|
|
|
|
const ENV_KEYS = [
|
|
"CLAUDE_CONFIG_DIR",
|
|
"CLAUDE_PLUGINS_HOME",
|
|
"CLAUDE_SETTINGS_PATH",
|
|
"OPENCODE_CONFIG_DIR",
|
|
] as const
|
|
|
|
type EnvKey = (typeof ENV_KEYS)[number]
|
|
type EnvSnapshot = Record<EnvKey, string | undefined>
|
|
|
|
function writePluginFixture(baseDir: string): void {
|
|
const claudeConfigDir = join(baseDir, "claude-config")
|
|
const pluginsHome = join(claudeConfigDir, "plugins")
|
|
const settingsPath = join(claudeConfigDir, "settings.json")
|
|
const opencodeConfigDir = join(baseDir, "opencode-config")
|
|
const pluginInstallPath = join(baseDir, "installed-plugins", "daplug")
|
|
const pluginKey = "daplug@1.0.0"
|
|
|
|
mkdirSync(join(pluginInstallPath, ".claude-plugin"), { recursive: true })
|
|
mkdirSync(join(pluginInstallPath, "commands"), { recursive: true })
|
|
|
|
writeFileSync(
|
|
join(pluginInstallPath, ".claude-plugin", "plugin.json"),
|
|
JSON.stringify({ name: "daplug", version: "1.0.0" }, null, 2),
|
|
)
|
|
writeFileSync(
|
|
join(pluginInstallPath, "commands", "run-prompt.md"),
|
|
`---
|
|
description: Run prompt from daplug
|
|
---
|
|
Execute daplug prompt flow.
|
|
`,
|
|
)
|
|
|
|
mkdirSync(pluginsHome, { recursive: true })
|
|
writeFileSync(
|
|
join(pluginsHome, "installed_plugins.json"),
|
|
JSON.stringify(
|
|
{
|
|
version: 2,
|
|
plugins: {
|
|
[pluginKey]: [
|
|
{
|
|
scope: "user",
|
|
installPath: pluginInstallPath,
|
|
version: "1.0.0",
|
|
installedAt: "2026-01-01T00:00:00.000Z",
|
|
lastUpdated: "2026-01-01T00:00:00.000Z",
|
|
},
|
|
],
|
|
},
|
|
},
|
|
null,
|
|
2,
|
|
),
|
|
)
|
|
|
|
mkdirSync(claudeConfigDir, { recursive: true })
|
|
writeFileSync(
|
|
settingsPath,
|
|
JSON.stringify(
|
|
{
|
|
enabledPlugins: {
|
|
[pluginKey]: true,
|
|
},
|
|
},
|
|
null,
|
|
2,
|
|
),
|
|
)
|
|
mkdirSync(opencodeConfigDir, { recursive: true })
|
|
|
|
process.env.CLAUDE_CONFIG_DIR = claudeConfigDir
|
|
process.env.CLAUDE_PLUGINS_HOME = pluginsHome
|
|
process.env.CLAUDE_SETTINGS_PATH = settingsPath
|
|
process.env.OPENCODE_CONFIG_DIR = opencodeConfigDir
|
|
}
|
|
|
|
describe("auto-slash command executor plugin dispatch", () => {
|
|
let tempDir = ""
|
|
let envSnapshot: EnvSnapshot
|
|
|
|
beforeEach(() => {
|
|
tempDir = mkdtempSync(join(tmpdir(), "omo-executor-plugin-test-"))
|
|
envSnapshot = {
|
|
CLAUDE_CONFIG_DIR: process.env.CLAUDE_CONFIG_DIR,
|
|
CLAUDE_PLUGINS_HOME: process.env.CLAUDE_PLUGINS_HOME,
|
|
CLAUDE_SETTINGS_PATH: process.env.CLAUDE_SETTINGS_PATH,
|
|
OPENCODE_CONFIG_DIR: process.env.OPENCODE_CONFIG_DIR,
|
|
}
|
|
writePluginFixture(tempDir)
|
|
})
|
|
|
|
afterEach(() => {
|
|
for (const key of ENV_KEYS) {
|
|
const previousValue = envSnapshot[key]
|
|
if (previousValue === undefined) {
|
|
delete process.env[key]
|
|
} else {
|
|
process.env[key] = previousValue
|
|
}
|
|
}
|
|
rmSync(tempDir, { recursive: true, force: true })
|
|
})
|
|
|
|
it("resolves marketplace plugin commands when plugin loading is enabled", async () => {
|
|
const result = await executeSlashCommand(
|
|
{
|
|
command: "daplug:run-prompt",
|
|
args: "ship it",
|
|
raw: "/daplug:run-prompt ship it",
|
|
},
|
|
{
|
|
skills: [],
|
|
pluginsEnabled: true,
|
|
},
|
|
)
|
|
|
|
expect(result.success).toBe(true)
|
|
expect(result.replacementText).toContain("# /daplug:run-prompt Command")
|
|
expect(result.replacementText).toContain("**Scope**: plugin")
|
|
})
|
|
|
|
it("excludes marketplace commands when plugins are disabled via config toggle", async () => {
|
|
const result = await executeSlashCommand(
|
|
{
|
|
command: "daplug:run-prompt",
|
|
args: "",
|
|
raw: "/daplug:run-prompt",
|
|
},
|
|
{
|
|
skills: [],
|
|
pluginsEnabled: false,
|
|
},
|
|
)
|
|
|
|
expect(result.success).toBe(false)
|
|
expect(result.error).toBe(
|
|
'Command "/daplug:run-prompt" not found. Use the skill tool to list available skills and commands.',
|
|
)
|
|
})
|
|
|
|
it("returns standard not-found for unknown namespaced commands", async () => {
|
|
const result = await executeSlashCommand(
|
|
{
|
|
command: "daplug:missing",
|
|
args: "",
|
|
raw: "/daplug:missing",
|
|
},
|
|
{
|
|
skills: [],
|
|
pluginsEnabled: true,
|
|
},
|
|
)
|
|
|
|
expect(result.success).toBe(false)
|
|
expect(result.error).toBe(
|
|
'Command "/daplug:missing" not found. Use the skill tool to list available skills and commands.',
|
|
)
|
|
expect(result.error).not.toContain("Marketplace plugin commands")
|
|
})
|
|
})
|