openclaw/src/plugins/bundle-claude-inspect.test.ts

230 lines
7.0 KiB
TypeScript

import fs from "node:fs";
import os from "node:os";
import path from "node:path";
import { afterAll, beforeAll, describe, expect, it } from "vitest";
import { inspectBundleLspRuntimeSupport } from "./bundle-lsp.js";
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;
function writeFixtureText(relativePath: string, value: string) {
fs.mkdirSync(path.dirname(path.join(rootDir, relativePath)), { recursive: true });
fs.writeFileSync(path.join(rootDir, relativePath), value, "utf-8");
}
function writeFixtureJson(relativePath: string, value: unknown) {
writeFixtureText(relativePath, JSON.stringify(value));
}
function writeFixtureEntries(
entries: Readonly<Record<string, string | Record<string, unknown>>>,
) {
Object.entries(entries).forEach(([relativePath, value]) => {
if (typeof value === "string") {
writeFixtureText(relativePath, value);
return;
}
writeFixtureJson(relativePath, value);
});
}
function setupClaudeInspectFixture() {
for (const relativeDir of [
".claude-plugin",
"skill-packs/demo",
"extra-commands/cmd",
"hooks",
"custom-hooks",
"agents",
"output-styles",
]) {
fs.mkdirSync(path.join(rootDir, relativeDir), { recursive: true });
}
writeFixtureEntries({
".claude-plugin/plugin.json": {
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",
},
"skill-packs/demo/SKILL.md":
"---\nname: demo\ndescription: A demo skill\n---\nDo something useful.",
"extra-commands/cmd/SKILL.md":
"---\nname: cmd\ndescription: A command skill\n---\nRun a command.",
"hooks/hooks.json": '{"hooks":[]}',
".mcp.json": {
mcpServers: {
"test-stdio-server": {
command: "echo",
args: ["hello"],
},
"test-sse-server": {
url: "http://localhost:3000/sse",
},
},
},
"settings.json": { thinkingLevel: "high" },
".lsp.json": {
lspServers: {
"typescript-lsp": {
command: "typescript-language-server",
args: ["--stdio"],
},
},
},
});
}
function expectLoadedClaudeManifest() {
const result = loadBundleManifest({ rootDir, bundleFormat: "claude" });
expect(result.ok).toBe(true);
if (!result.ok) {
throw new Error("expected Claude bundle manifest to load");
}
return result.manifest;
}
function expectClaudeManifestField(params: {
field: "skills" | "hooks" | "settingsFiles" | "capabilities";
includes: readonly string[];
}) {
const manifest = expectLoadedClaudeManifest();
const values = manifest[params.field];
expect(values).toEqual(expect.arrayContaining([...params.includes]));
}
function expectNoDiagnostics(diagnostics: unknown[]) {
expect(diagnostics).toEqual([]);
}
function expectBundleRuntimeSupport(params: {
actual: {
supportedServerNames: string[];
unsupportedServerNames: string[];
diagnostics: unknown[];
} & Record<string, unknown>;
supportedServerNames: readonly string[];
unsupportedServerNames: readonly string[];
hasSupportedKey: "hasSupportedStdioServer" | "hasStdioServer";
}) {
expect(params.actual[params.hasSupportedKey]).toBe(true);
expect(params.actual.supportedServerNames).toEqual(
expect.arrayContaining([...params.supportedServerNames]),
);
expect(params.actual.unsupportedServerNames).toEqual([...params.unsupportedServerNames]);
expectNoDiagnostics(params.actual.diagnostics);
}
function inspectClaudeBundleRuntimeSupport(kind: "mcp" | "lsp"): {
supportedServerNames: string[];
unsupportedServerNames: string[];
diagnostics: unknown[];
hasSupportedStdioServer?: boolean;
hasStdioServer?: boolean;
} {
if (kind === "mcp") {
return inspectBundleMcpRuntimeSupport({
pluginId: "test-claude-plugin",
rootDir,
bundleFormat: "claude",
});
}
return inspectBundleLspRuntimeSupport({
pluginId: "test-claude-plugin",
rootDir,
bundleFormat: "claude",
});
}
beforeAll(() => {
rootDir = fs.mkdtempSync(path.join(os.tmpdir(), "openclaw-claude-bundle-"));
setupClaudeInspectFixture();
});
afterAll(() => {
fs.rmSync(rootDir, { recursive: true, force: true });
});
it("loads the full Claude bundle manifest with all capabilities", () => {
const m = expectLoadedClaudeManifest();
expect(m).toMatchObject({
name: "Test Claude Plugin",
description: "Integration test fixture for Claude bundle inspection",
version: "1.0.0",
bundleFormat: "claude",
});
});
it.each([
{
name: "resolves skills from skills, commands, and agents paths",
field: "skills" as const,
includes: ["skill-packs", "extra-commands", "agents", "output-styles"],
},
{
name: "resolves hooks from default and declared paths",
field: "hooks" as const,
includes: ["hooks/hooks.json", "custom-hooks"],
},
{
name: "detects settings files",
field: "settingsFiles" as const,
includes: ["settings.json"],
},
{
name: "detects all bundle capabilities",
field: "capabilities" as const,
includes: [
"skills",
"commands",
"agents",
"hooks",
"mcpServers",
"lspServers",
"outputStyles",
"settings",
],
},
] as const)("$name", ({ field, includes }) => {
expectClaudeManifestField({ field, includes });
});
it.each([
{
name: "inspects MCP runtime support with supported and unsupported servers",
kind: "mcp" as const,
supportedServerNames: ["test-stdio-server"],
unsupportedServerNames: ["test-sse-server"],
hasSupportedKey: "hasSupportedStdioServer" as const,
},
{
name: "inspects LSP runtime support with stdio server",
kind: "lsp" as const,
supportedServerNames: ["typescript-lsp"],
unsupportedServerNames: [],
hasSupportedKey: "hasStdioServer" as const,
},
])("$name", ({ kind, supportedServerNames, unsupportedServerNames, hasSupportedKey }) => {
expectBundleRuntimeSupport({
actual: inspectClaudeBundleRuntimeSupport(kind),
supportedServerNames,
unsupportedServerNames,
hasSupportedKey,
});
});
});