From 98fbbebf6a1fc826eaaa0419e4cd4e5476149cf1 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Tue, 17 Mar 2026 23:33:15 -0700 Subject: [PATCH] Tests: add Claude bundle plugin inspect integration test --- src/plugins/bundle-claude-inspect.test.ts | 180 ++++++++++++++++++++++ 1 file changed, 180 insertions(+) create mode 100644 src/plugins/bundle-claude-inspect.test.ts diff --git a/src/plugins/bundle-claude-inspect.test.ts b/src/plugins/bundle-claude-inspect.test.ts new file mode 100644 index 00000000000..87d48c0eff2 --- /dev/null +++ b/src/plugins/bundle-claude-inspect.test.ts @@ -0,0 +1,180 @@ +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { afterAll, beforeAll, describe, expect, it } from "vitest"; +import { loadBundleManifest } from "./bundle-manifest.js"; +import { inspectBundleMcpRuntimeSupport } from "./bundle-mcp.js"; + +/** + * Integration test: builds a Claude Code bundle plugin fixture on disk + * and verifies manifest parsing, capability detection, hook resolution, + * MCP server discovery, and settings detection all work end-to-end. + */ +describe("Claude bundle plugin inspect integration", () => { + let rootDir: string; + + beforeAll(() => { + rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-claude-bundle-")); + + // .claude-plugin/plugin.json + const manifestDir = path.join(rootDir, ".claude-plugin"); + fs.mkdirSync(manifestDir, { recursive: true }); + fs.writeFileSync( + path.join(manifestDir, "plugin.json"), + JSON.stringify({ + name: "Test Claude Plugin", + description: "Integration test fixture for Claude bundle inspection", + version: "1.0.0", + skills: ["skill-packs"], + commands: "extra-commands", + agents: "agents", + hooks: "custom-hooks", + mcpServers: ".mcp.json", + lspServers: ".lsp.json", + outputStyles: "output-styles", + }), + "utf-8", + ); + + // skills/demo/SKILL.md + const skillDir = path.join(rootDir, "skill-packs", "demo"); + fs.mkdirSync(skillDir, { recursive: true }); + fs.writeFileSync( + path.join(skillDir, "SKILL.md"), + "---\nname: demo\ndescription: A demo skill\n---\nDo something useful.", + "utf-8", + ); + + // commands/cmd/SKILL.md + const cmdDir = path.join(rootDir, "extra-commands", "cmd"); + fs.mkdirSync(cmdDir, { recursive: true }); + fs.writeFileSync( + path.join(cmdDir, "SKILL.md"), + "---\nname: cmd\ndescription: A command skill\n---\nRun a command.", + "utf-8", + ); + + // hooks/hooks.json (default hook path) + const hooksDir = path.join(rootDir, "hooks"); + fs.mkdirSync(hooksDir, { recursive: true }); + fs.writeFileSync(path.join(hooksDir, "hooks.json"), '{"hooks":[]}', "utf-8"); + + // custom-hooks/ (manifest-declared hook path) + fs.mkdirSync(path.join(rootDir, "custom-hooks"), { recursive: true }); + + // .mcp.json with a stdio MCP server + fs.writeFileSync( + path.join(rootDir, ".mcp.json"), + JSON.stringify({ + mcpServers: { + "test-stdio-server": { + command: "echo", + args: ["hello"], + }, + "test-sse-server": { + url: "http://localhost:3000/sse", + }, + }, + }), + "utf-8", + ); + + // settings.json + fs.writeFileSync( + path.join(rootDir, "settings.json"), + JSON.stringify({ thinkingLevel: "high" }), + "utf-8", + ); + + // agents/ directory + fs.mkdirSync(path.join(rootDir, "agents"), { recursive: true }); + + // .lsp.json + fs.writeFileSync(path.join(rootDir, ".lsp.json"), '{"lspServers":{}}', "utf-8"); + + // output-styles/ directory + fs.mkdirSync(path.join(rootDir, "output-styles"), { recursive: true }); + }); + + afterAll(() => { + fs.rmSync(rootDir, { recursive: true, force: true }); + }); + + it("loads the full Claude bundle manifest with all capabilities", () => { + const result = loadBundleManifest({ rootDir, bundleFormat: "claude" }); + expect(result.ok).toBe(true); + if (!result.ok) { + return; + } + + const m = result.manifest; + expect(m.name).toBe("Test Claude Plugin"); + expect(m.description).toBe("Integration test fixture for Claude bundle inspection"); + expect(m.version).toBe("1.0.0"); + expect(m.bundleFormat).toBe("claude"); + }); + + it("resolves skills from both skills and commands paths", () => { + const result = loadBundleManifest({ rootDir, bundleFormat: "claude" }); + expect(result.ok).toBe(true); + if (!result.ok) { + return; + } + + expect(result.manifest.skills).toContain("skill-packs"); + expect(result.manifest.skills).toContain("extra-commands"); + }); + + it("resolves hooks from default and declared paths", () => { + const result = loadBundleManifest({ rootDir, bundleFormat: "claude" }); + expect(result.ok).toBe(true); + if (!result.ok) { + return; + } + + // Default hooks/hooks.json path + declared custom-hooks + expect(result.manifest.hooks).toContain("hooks/hooks.json"); + expect(result.manifest.hooks).toContain("custom-hooks"); + }); + + it("detects settings files", () => { + const result = loadBundleManifest({ rootDir, bundleFormat: "claude" }); + expect(result.ok).toBe(true); + if (!result.ok) { + return; + } + + expect(result.manifest.settingsFiles).toEqual(["settings.json"]); + }); + + it("detects all bundle capabilities", () => { + const result = loadBundleManifest({ rootDir, bundleFormat: "claude" }); + expect(result.ok).toBe(true); + if (!result.ok) { + return; + } + + const caps = result.manifest.capabilities; + expect(caps).toContain("skills"); + expect(caps).toContain("commands"); + expect(caps).toContain("agents"); + expect(caps).toContain("hooks"); + expect(caps).toContain("mcpServers"); + expect(caps).toContain("lspServers"); + expect(caps).toContain("outputStyles"); + expect(caps).toContain("settings"); + }); + + it("inspects MCP runtime support with supported and unsupported servers", () => { + const mcp = inspectBundleMcpRuntimeSupport({ + pluginId: "test-claude-plugin", + rootDir, + bundleFormat: "claude", + }); + + expect(mcp.hasSupportedStdioServer).toBe(true); + expect(mcp.supportedServerNames).toContain("test-stdio-server"); + expect(mcp.unsupportedServerNames).toContain("test-sse-server"); + expect(mcp.diagnostics).toEqual([]); + }); +});