mirror of
https://github.com/openclaw/openclaw.git
synced 2026-04-06 06:41:08 +00:00
203 lines
6.1 KiB
TypeScript
203 lines
6.1 KiB
TypeScript
import fs from "node:fs";
|
|
import path from "node:path";
|
|
import { afterAll, afterEach, describe, expect, it } from "vitest";
|
|
import { withEnv } from "../test-utils/env.js";
|
|
import { loadOpenClawPlugins } from "./loader.js";
|
|
import {
|
|
cleanupPluginLoaderFixturesForTest,
|
|
loadBundleFixture,
|
|
makeTempDir,
|
|
mkdirSafe,
|
|
resetPluginLoaderTestStateForTest,
|
|
useNoBundledPlugins,
|
|
} from "./loader.test-fixtures.js";
|
|
|
|
function expectNoUnwiredBundleDiagnostic(
|
|
registry: ReturnType<typeof loadOpenClawPlugins>,
|
|
pluginId: string,
|
|
) {
|
|
expect(
|
|
registry.diagnostics.some(
|
|
(diag) =>
|
|
diag.pluginId === pluginId &&
|
|
diag.message.includes("bundle capability detected but not wired"),
|
|
),
|
|
).toBe(false);
|
|
}
|
|
|
|
afterEach(() => {
|
|
resetPluginLoaderTestStateForTest();
|
|
});
|
|
|
|
afterAll(() => {
|
|
cleanupPluginLoaderFixturesForTest();
|
|
});
|
|
|
|
describe("bundle plugins", () => {
|
|
it("reports Codex bundles as loaded bundle plugins without importing runtime code", () => {
|
|
useNoBundledPlugins();
|
|
const workspaceDir = makeTempDir();
|
|
const stateDir = makeTempDir();
|
|
const bundleRoot = path.join(workspaceDir, ".openclaw", "extensions", "sample-bundle");
|
|
mkdirSafe(path.join(bundleRoot, ".codex-plugin"));
|
|
mkdirSafe(path.join(bundleRoot, "skills"));
|
|
fs.writeFileSync(
|
|
path.join(bundleRoot, ".codex-plugin", "plugin.json"),
|
|
JSON.stringify({
|
|
name: "Sample Bundle",
|
|
description: "Codex bundle fixture",
|
|
skills: "skills",
|
|
}),
|
|
"utf-8",
|
|
);
|
|
fs.writeFileSync(
|
|
path.join(bundleRoot, "skills", "SKILL.md"),
|
|
"---\ndescription: fixture\n---\n",
|
|
);
|
|
|
|
const registry = withEnv({ OPENCLAW_STATE_DIR: stateDir }, () =>
|
|
loadOpenClawPlugins({
|
|
workspaceDir,
|
|
onlyPluginIds: ["sample-bundle"],
|
|
config: {
|
|
plugins: {
|
|
entries: {
|
|
"sample-bundle": {
|
|
enabled: true,
|
|
},
|
|
},
|
|
},
|
|
},
|
|
cache: false,
|
|
}),
|
|
);
|
|
|
|
const plugin = registry.plugins.find((entry) => entry.id === "sample-bundle");
|
|
expect(plugin?.status).toBe("loaded");
|
|
expect(plugin?.format).toBe("bundle");
|
|
expect(plugin?.bundleFormat).toBe("codex");
|
|
expect(plugin?.bundleCapabilities).toContain("skills");
|
|
});
|
|
|
|
it.each([
|
|
{
|
|
name: "treats Claude command roots and settings as supported bundle surfaces",
|
|
pluginId: "claude-skills",
|
|
expectedFormat: "claude",
|
|
expectedCapabilities: ["skills", "commands", "settings"],
|
|
build: (bundleRoot: string) => {
|
|
mkdirSafe(path.join(bundleRoot, "commands"));
|
|
fs.writeFileSync(
|
|
path.join(bundleRoot, "commands", "review.md"),
|
|
"---\ndescription: fixture\n---\n",
|
|
);
|
|
fs.writeFileSync(
|
|
path.join(bundleRoot, "settings.json"),
|
|
'{"hideThinkingBlock":true}',
|
|
"utf-8",
|
|
);
|
|
},
|
|
},
|
|
{
|
|
name: "treats bundle MCP as a supported bundle surface",
|
|
pluginId: "claude-mcp",
|
|
expectedFormat: "claude",
|
|
expectedCapabilities: ["mcpServers"],
|
|
build: (bundleRoot: string) => {
|
|
mkdirSafe(path.join(bundleRoot, ".claude-plugin"));
|
|
fs.writeFileSync(
|
|
path.join(bundleRoot, ".claude-plugin", "plugin.json"),
|
|
JSON.stringify({
|
|
name: "Claude MCP",
|
|
}),
|
|
"utf-8",
|
|
);
|
|
fs.writeFileSync(
|
|
path.join(bundleRoot, ".mcp.json"),
|
|
JSON.stringify({
|
|
mcpServers: {
|
|
probe: {
|
|
command: "node",
|
|
args: ["./probe.mjs"],
|
|
},
|
|
},
|
|
}),
|
|
"utf-8",
|
|
);
|
|
},
|
|
},
|
|
{
|
|
name: "treats Cursor command roots as supported bundle skill surfaces",
|
|
pluginId: "cursor-skills",
|
|
expectedFormat: "cursor",
|
|
expectedCapabilities: ["skills", "commands"],
|
|
build: (bundleRoot: string) => {
|
|
mkdirSafe(path.join(bundleRoot, ".cursor-plugin"));
|
|
mkdirSafe(path.join(bundleRoot, ".cursor", "commands"));
|
|
fs.writeFileSync(
|
|
path.join(bundleRoot, ".cursor-plugin", "plugin.json"),
|
|
JSON.stringify({
|
|
name: "Cursor Skills",
|
|
}),
|
|
"utf-8",
|
|
);
|
|
fs.writeFileSync(
|
|
path.join(bundleRoot, ".cursor", "commands", "review.md"),
|
|
"---\ndescription: fixture\n---\n",
|
|
);
|
|
},
|
|
},
|
|
])("$name", ({ pluginId, expectedFormat, expectedCapabilities, build }) => {
|
|
const registry = loadBundleFixture({ pluginId, build });
|
|
const plugin = registry.plugins.find((entry) => entry.id === pluginId);
|
|
|
|
expect(plugin?.status).toBe("loaded");
|
|
expect(plugin?.bundleFormat).toBe(expectedFormat);
|
|
expect(plugin?.bundleCapabilities).toEqual(expect.arrayContaining(expectedCapabilities));
|
|
expectNoUnwiredBundleDiagnostic(registry, pluginId);
|
|
});
|
|
|
|
it("warns when bundle MCP only declares unsupported non-stdio transports", () => {
|
|
const stateDir = makeTempDir();
|
|
const registry = loadBundleFixture({
|
|
pluginId: "claude-mcp-url",
|
|
env: {
|
|
OPENCLAW_HOME: stateDir,
|
|
},
|
|
build: (bundleRoot) => {
|
|
mkdirSafe(path.join(bundleRoot, ".claude-plugin"));
|
|
fs.writeFileSync(
|
|
path.join(bundleRoot, ".claude-plugin", "plugin.json"),
|
|
JSON.stringify({
|
|
name: "Claude MCP URL",
|
|
}),
|
|
"utf-8",
|
|
);
|
|
fs.writeFileSync(
|
|
path.join(bundleRoot, ".mcp.json"),
|
|
JSON.stringify({
|
|
mcpServers: {
|
|
remoteProbe: {
|
|
url: "http://127.0.0.1:8787/mcp",
|
|
},
|
|
},
|
|
}),
|
|
"utf-8",
|
|
);
|
|
},
|
|
});
|
|
|
|
const plugin = registry.plugins.find((entry) => entry.id === "claude-mcp-url");
|
|
expect(plugin?.status).toBe("loaded");
|
|
expect(plugin?.bundleCapabilities).toEqual(expect.arrayContaining(["mcpServers"]));
|
|
expect(
|
|
registry.diagnostics.some(
|
|
(diag) =>
|
|
diag.pluginId === "claude-mcp-url" &&
|
|
diag.message.includes("stdio only today") &&
|
|
diag.message.includes("remoteProbe"),
|
|
),
|
|
).toBe(true);
|
|
});
|
|
});
|