mirror of https://github.com/openclaw/openclaw.git
230 lines
7.0 KiB
TypeScript
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,
|
|
});
|
|
});
|
|
});
|