/* Tests for mcp/claude module behavior. */ import { describe, expect, test } from "bun:test"; import { execSync } from "node:child_process"; import { mkdirSync, mkdtempSync, readFileSync, rmSync, statSync, writeFileSync } from "node:fs"; import { tmpdir } from "node:os"; import { join } from "node:path"; import type { CliProgram } from "../core/types.ts"; import { defaultClaudePluginPaths, generatePluginManifest, generatePluginMcpJson, packClaudePlugin } from "./claude.ts"; import { pluginName } from "./plugin-shared.ts"; const configFixture: CliProgram = { key: "myapp", version: "1.0.0", description: "Demo.", mcpServer: { enabled: true }, appConfig: { entries: { apiToken: { description: "Token from settings.", env: "API_TOKEN", sensitive: true, }, }, }, commands: [{ key: "run", description: "Run.", handler: () => {} }], }; /** Tests for claude plugin. */ describe("claude plugin", () => { test("pluginName is kebab-case", () => { expect(pluginName({ ...configFixture, key: "MyApp" })).toBe("my-app"); }); test("generatePluginManifest includes userConfig from program.appConfig", () => { const manifest = generatePluginManifest(configFixture, "myapp"); expect(manifest.name).toBe("myapp"); expect(manifest.mcpServers).toBe(".mcp.json"); const userConfig = manifest.userConfig as Record>; expect(userConfig.api_token?.description).toBe("Token from settings."); expect(userConfig.api_token?.sensitive).toBe(true); }); test("generatePluginMcpJson uses CLAUDE_PLUGIN_ROOT and env mapping", () => { const json = generatePluginMcpJson(configFixture, "myapp"); const entry = json.myapp as { command: string; args: string[]; env: Record }; expect(entry.command).toBe("${CLAUDE_PLUGIN_ROOT}/bin/myapp"); expect(entry.args).toEqual(["mcp"]); expect(entry.env.API_TOKEN).toBe("${user_config.api_token}"); }); test("defaultClaudePluginPaths", () => { const cwd = "/tmp/work"; const paths = defaultClaudePluginPaths(configFixture, cwd); expect(paths.pluginZipPath).toBe(join(cwd, "dist", "claude-plugin", "myapp.zip")); }); /** Tests that packClaudePlugin writes zip with MCP pointer skill only. */ test("packClaudePlugin writes zip with MCP pointer skill only", () => { const work = mkdtempSync(join(tmpdir(), "claude-plugin-test-")); try { const dist = join(work, "dist"); mkdirSync(dist, { recursive: true }); const binaryPath = join(dist, "myapp"); writeFileSync(binaryPath, "#!/bin/sh\n", { mode: 0o755 }); const paths = defaultClaudePluginPaths(configFixture, work); packClaudePlugin(configFixture, { cwd: work, binaryPath }); const zip = readFileSync(paths.pluginZipPath); expect(zip.length).toBeGreaterThan(0); const zipText = zip.toString("utf8"); expect(zipText).toContain("skills/myapp/SKILL.md"); expect(zipText).toContain("MCP toolset"); expect(zipText).toContain("Server id: `myapp`"); expect(zipText).not.toContain("skills/myapp/reference.md"); expect(zipText).not.toContain("Invoke via shell"); writeFileSync(join(work, "out.zip"), zip); const extract = join(work, "extract"); mkdirSync(extract, { recursive: true }); execSync("unzip -o -q ../out.zip", { cwd: extract }); const binMode = statSync(join(extract, "bin", "myapp")).mode & 0o777; expect(binMode & 0o111).not.toBe(0); const pluginJson = JSON.parse(readFileSync(join(extract, ".claude-plugin", "plugin.json"), "utf8")) as { mcpServers: string; }; expect(pluginJson.mcpServers).toBe(".mcp.json"); } finally { rmSync(work, { recursive: true, force: true }); } }); });