mirror of https://github.com/openclaw/openclaw.git
test: split plugin loader coverage by concern
This commit is contained in:
parent
b53dcb9380
commit
db76dbc546
|
|
@ -32,6 +32,7 @@ describe("bundled capability metadata", () => {
|
|||
manifest.contracts?.mediaUnderstandingProviders,
|
||||
),
|
||||
imageGenerationProviderIds: uniqueStrings(manifest.contracts?.imageGenerationProviders),
|
||||
webFetchProviderIds: uniqueStrings(manifest.contracts?.webFetchProviders),
|
||||
webSearchProviderIds: uniqueStrings(manifest.contracts?.webSearchProviders),
|
||||
toolNames: uniqueStrings(manifest.contracts?.tools),
|
||||
}))
|
||||
|
|
@ -42,6 +43,7 @@ describe("bundled capability metadata", () => {
|
|||
entry.speechProviderIds.length > 0 ||
|
||||
entry.mediaUnderstandingProviderIds.length > 0 ||
|
||||
entry.imageGenerationProviderIds.length > 0 ||
|
||||
entry.webFetchProviderIds.length > 0 ||
|
||||
entry.webSearchProviderIds.length > 0 ||
|
||||
entry.toolNames.length > 0,
|
||||
)
|
||||
|
|
|
|||
|
|
@ -0,0 +1,202 @@
|
|||
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);
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,417 @@
|
|||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { afterAll, afterEach, describe, expect, it } from "vitest";
|
||||
import { loadOpenClawPluginCliRegistry, loadOpenClawPlugins } from "./loader.js";
|
||||
import {
|
||||
cleanupPluginLoaderFixturesForTest,
|
||||
EMPTY_PLUGIN_SCHEMA,
|
||||
makeTempDir,
|
||||
resetPluginLoaderTestStateForTest,
|
||||
useNoBundledPlugins,
|
||||
writePlugin,
|
||||
} from "./loader.test-fixtures.js";
|
||||
|
||||
afterEach(() => {
|
||||
resetPluginLoaderTestStateForTest();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
cleanupPluginLoaderFixturesForTest();
|
||||
});
|
||||
|
||||
describe("plugin loader CLI metadata", () => {
|
||||
it("passes validated plugin config into non-activating CLI metadata loads", async () => {
|
||||
useNoBundledPlugins();
|
||||
const plugin = writePlugin({
|
||||
id: "config-cli",
|
||||
filename: "config-cli.cjs",
|
||||
body: `module.exports = {
|
||||
id: "config-cli",
|
||||
register(api) {
|
||||
if (!api.pluginConfig || api.pluginConfig.token !== "ok") {
|
||||
throw new Error("missing plugin config");
|
||||
}
|
||||
api.registerCli(() => {}, {
|
||||
descriptors: [
|
||||
{
|
||||
name: "cfg",
|
||||
description: "Config-backed CLI command",
|
||||
hasSubcommands: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
};`,
|
||||
});
|
||||
fs.writeFileSync(
|
||||
path.join(plugin.dir, "openclaw.plugin.json"),
|
||||
JSON.stringify(
|
||||
{
|
||||
id: "config-cli",
|
||||
configSchema: {
|
||||
type: "object",
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
token: { type: "string" },
|
||||
},
|
||||
required: ["token"],
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const registry = await loadOpenClawPluginCliRegistry({
|
||||
config: {
|
||||
plugins: {
|
||||
load: { paths: [plugin.file] },
|
||||
allow: ["config-cli"],
|
||||
entries: {
|
||||
"config-cli": {
|
||||
config: {
|
||||
token: "ok",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(registry.cliRegistrars.flatMap((entry) => entry.commands)).toContain("cfg");
|
||||
expect(registry.plugins.find((entry) => entry.id === "config-cli")?.status).toBe("loaded");
|
||||
});
|
||||
|
||||
it("uses the real channel entry in cli-metadata mode for CLI metadata capture", async () => {
|
||||
useNoBundledPlugins();
|
||||
const pluginDir = makeTempDir();
|
||||
const fullMarker = path.join(pluginDir, "full-loaded.txt");
|
||||
const modeMarker = path.join(pluginDir, "registration-mode.txt");
|
||||
const runtimeMarker = path.join(pluginDir, "runtime-set.txt");
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(pluginDir, "package.json"),
|
||||
JSON.stringify(
|
||||
{
|
||||
name: "@openclaw/cli-metadata-channel",
|
||||
openclaw: { extensions: ["./index.cjs"], setupEntry: "./setup-entry.cjs" },
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf-8",
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(pluginDir, "openclaw.plugin.json"),
|
||||
JSON.stringify(
|
||||
{
|
||||
id: "cli-metadata-channel",
|
||||
configSchema: EMPTY_PLUGIN_SCHEMA,
|
||||
channels: ["cli-metadata-channel"],
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf-8",
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(pluginDir, "index.cjs"),
|
||||
`const { defineChannelPluginEntry } = require("openclaw/plugin-sdk/core");
|
||||
require("node:fs").writeFileSync(${JSON.stringify(fullMarker)}, "loaded", "utf-8");
|
||||
module.exports = {
|
||||
...defineChannelPluginEntry({
|
||||
id: "cli-metadata-channel",
|
||||
name: "CLI Metadata Channel",
|
||||
description: "cli metadata channel",
|
||||
setRuntime() {
|
||||
require("node:fs").writeFileSync(${JSON.stringify(runtimeMarker)}, "loaded", "utf-8");
|
||||
},
|
||||
plugin: {
|
||||
id: "cli-metadata-channel",
|
||||
meta: {
|
||||
id: "cli-metadata-channel",
|
||||
label: "CLI Metadata Channel",
|
||||
selectionLabel: "CLI Metadata Channel",
|
||||
docsPath: "/channels/cli-metadata-channel",
|
||||
blurb: "cli metadata channel",
|
||||
},
|
||||
capabilities: { chatTypes: ["direct"] },
|
||||
config: {
|
||||
listAccountIds: () => [],
|
||||
resolveAccount: () => ({ accountId: "default" }),
|
||||
},
|
||||
outbound: { deliveryMode: "direct" },
|
||||
},
|
||||
registerCliMetadata(api) {
|
||||
require("node:fs").writeFileSync(
|
||||
${JSON.stringify(modeMarker)},
|
||||
String(api.registrationMode),
|
||||
"utf-8",
|
||||
);
|
||||
api.registerCli(() => {}, {
|
||||
descriptors: [
|
||||
{
|
||||
name: "cli-metadata-channel",
|
||||
description: "Channel CLI metadata",
|
||||
hasSubcommands: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
registerFull() {
|
||||
throw new Error("full channel entry should not run during CLI metadata capture");
|
||||
},
|
||||
}),
|
||||
};`,
|
||||
"utf-8",
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(pluginDir, "setup-entry.cjs"),
|
||||
`throw new Error("setup entry should not load during CLI metadata capture");`,
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const registry = await loadOpenClawPluginCliRegistry({
|
||||
config: {
|
||||
plugins: {
|
||||
load: { paths: [pluginDir] },
|
||||
allow: ["cli-metadata-channel"],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(fs.existsSync(fullMarker)).toBe(true);
|
||||
expect(fs.existsSync(runtimeMarker)).toBe(false);
|
||||
expect(fs.readFileSync(modeMarker, "utf-8")).toBe("cli-metadata");
|
||||
expect(registry.cliRegistrars.flatMap((entry) => entry.commands)).toContain(
|
||||
"cli-metadata-channel",
|
||||
);
|
||||
});
|
||||
|
||||
it("collects channel CLI metadata during full plugin loads", () => {
|
||||
useNoBundledPlugins();
|
||||
const pluginDir = makeTempDir();
|
||||
const modeMarker = path.join(pluginDir, "registration-mode.txt");
|
||||
const fullMarker = path.join(pluginDir, "full-loaded.txt");
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(pluginDir, "package.json"),
|
||||
JSON.stringify(
|
||||
{
|
||||
name: "@openclaw/full-cli-metadata-channel",
|
||||
openclaw: { extensions: ["./index.cjs"] },
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf-8",
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(pluginDir, "openclaw.plugin.json"),
|
||||
JSON.stringify(
|
||||
{
|
||||
id: "full-cli-metadata-channel",
|
||||
configSchema: EMPTY_PLUGIN_SCHEMA,
|
||||
channels: ["full-cli-metadata-channel"],
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf-8",
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(pluginDir, "index.cjs"),
|
||||
`const { defineChannelPluginEntry } = require("openclaw/plugin-sdk/core");
|
||||
module.exports = {
|
||||
...defineChannelPluginEntry({
|
||||
id: "full-cli-metadata-channel",
|
||||
name: "Full CLI Metadata Channel",
|
||||
description: "full cli metadata channel",
|
||||
plugin: {
|
||||
id: "full-cli-metadata-channel",
|
||||
meta: {
|
||||
id: "full-cli-metadata-channel",
|
||||
label: "Full CLI Metadata Channel",
|
||||
selectionLabel: "Full CLI Metadata Channel",
|
||||
docsPath: "/channels/full-cli-metadata-channel",
|
||||
blurb: "full cli metadata channel",
|
||||
},
|
||||
capabilities: { chatTypes: ["direct"] },
|
||||
config: {
|
||||
listAccountIds: () => [],
|
||||
resolveAccount: () => ({ accountId: "default" }),
|
||||
},
|
||||
outbound: { deliveryMode: "direct" },
|
||||
},
|
||||
registerCliMetadata(api) {
|
||||
require("node:fs").writeFileSync(
|
||||
${JSON.stringify(modeMarker)},
|
||||
String(api.registrationMode),
|
||||
"utf-8",
|
||||
);
|
||||
api.registerCli(() => {}, {
|
||||
descriptors: [
|
||||
{
|
||||
name: "full-cli-metadata-channel",
|
||||
description: "Full-load channel CLI metadata",
|
||||
hasSubcommands: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
registerFull() {
|
||||
require("node:fs").writeFileSync(${JSON.stringify(fullMarker)}, "loaded", "utf-8");
|
||||
},
|
||||
}),
|
||||
};`,
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const registry = loadOpenClawPlugins({
|
||||
cache: false,
|
||||
config: {
|
||||
plugins: {
|
||||
load: { paths: [pluginDir] },
|
||||
allow: ["full-cli-metadata-channel"],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(fs.readFileSync(modeMarker, "utf-8")).toBe("full");
|
||||
expect(fs.existsSync(fullMarker)).toBe(true);
|
||||
expect(registry.cliRegistrars.flatMap((entry) => entry.commands)).toContain(
|
||||
"full-cli-metadata-channel",
|
||||
);
|
||||
});
|
||||
|
||||
it("awaits async plugin registration when collecting CLI metadata", async () => {
|
||||
useNoBundledPlugins();
|
||||
const plugin = writePlugin({
|
||||
id: "async-cli",
|
||||
filename: "async-cli.cjs",
|
||||
body: `module.exports = {
|
||||
id: "async-cli",
|
||||
async register(api) {
|
||||
await Promise.resolve();
|
||||
api.registerCli(() => {}, {
|
||||
descriptors: [
|
||||
{
|
||||
name: "async-cli",
|
||||
description: "Async CLI metadata",
|
||||
hasSubcommands: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
};`,
|
||||
});
|
||||
|
||||
const registry = await loadOpenClawPluginCliRegistry({
|
||||
config: {
|
||||
plugins: {
|
||||
load: { paths: [plugin.file] },
|
||||
allow: ["async-cli"],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(registry.cliRegistrars.flatMap((entry) => entry.commands)).toContain("async-cli");
|
||||
expect(
|
||||
registry.diagnostics.some((entry) => entry.message.includes("async registration is ignored")),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("applies memory slot gating to non-bundled CLI metadata loads", async () => {
|
||||
useNoBundledPlugins();
|
||||
const plugin = writePlugin({
|
||||
id: "memory-external",
|
||||
filename: "memory-external.cjs",
|
||||
body: `module.exports = {
|
||||
id: "memory-external",
|
||||
kind: "memory",
|
||||
register(api) {
|
||||
api.registerCli(() => {}, {
|
||||
descriptors: [
|
||||
{
|
||||
name: "memory-external",
|
||||
description: "External memory CLI metadata",
|
||||
hasSubcommands: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
};`,
|
||||
});
|
||||
fs.writeFileSync(
|
||||
path.join(plugin.dir, "openclaw.plugin.json"),
|
||||
JSON.stringify(
|
||||
{
|
||||
id: "memory-external",
|
||||
kind: "memory",
|
||||
configSchema: EMPTY_PLUGIN_SCHEMA,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const registry = await loadOpenClawPluginCliRegistry({
|
||||
config: {
|
||||
plugins: {
|
||||
load: { paths: [plugin.file] },
|
||||
allow: ["memory-external"],
|
||||
slots: { memory: "memory-other" },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(registry.cliRegistrars.flatMap((entry) => entry.commands)).not.toContain(
|
||||
"memory-external",
|
||||
);
|
||||
const memory = registry.plugins.find((entry) => entry.id === "memory-external");
|
||||
expect(memory?.status).toBe("disabled");
|
||||
expect(String(memory?.error ?? "")).toContain('memory slot set to "memory-other"');
|
||||
});
|
||||
|
||||
it("re-evaluates memory slot gating after resolving exported plugin kind", async () => {
|
||||
useNoBundledPlugins();
|
||||
const plugin = writePlugin({
|
||||
id: "memory-export-only",
|
||||
filename: "memory-export-only.cjs",
|
||||
body: `module.exports = {
|
||||
id: "memory-export-only",
|
||||
kind: "memory",
|
||||
register(api) {
|
||||
api.registerCli(() => {}, {
|
||||
descriptors: [
|
||||
{
|
||||
name: "memory-export-only",
|
||||
description: "Export-only memory CLI metadata",
|
||||
hasSubcommands: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
};`,
|
||||
});
|
||||
|
||||
const registry = await loadOpenClawPluginCliRegistry({
|
||||
config: {
|
||||
plugins: {
|
||||
load: { paths: [plugin.file] },
|
||||
allow: ["memory-export-only"],
|
||||
slots: { memory: "memory-other" },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(registry.cliRegistrars.flatMap((entry) => entry.commands)).not.toContain(
|
||||
"memory-export-only",
|
||||
);
|
||||
const memory = registry.plugins.find((entry) => entry.id === "memory-export-only");
|
||||
expect(memory?.status).toBe("disabled");
|
||||
expect(String(memory?.error ?? "")).toContain('memory slot set to "memory-other"');
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,189 @@
|
|||
import { afterEach, describe, expect, it } from "vitest";
|
||||
import { __testing, clearPluginLoaderCache, resolveRuntimePluginRegistry } from "./loader.js";
|
||||
import { resetPluginLoaderTestStateForTest } from "./loader.test-fixtures.js";
|
||||
import {
|
||||
getMemoryEmbeddingProvider,
|
||||
registerMemoryEmbeddingProvider,
|
||||
} from "./memory-embedding-providers.js";
|
||||
import {
|
||||
buildMemoryPromptSection,
|
||||
getMemoryRuntime,
|
||||
registerMemoryFlushPlanResolver,
|
||||
registerMemoryPromptSection,
|
||||
registerMemoryRuntime,
|
||||
resolveMemoryFlushPlan,
|
||||
} from "./memory-state.js";
|
||||
import { createEmptyPluginRegistry } from "./registry.js";
|
||||
import { setActivePluginRegistry } from "./runtime.js";
|
||||
|
||||
afterEach(() => {
|
||||
resetPluginLoaderTestStateForTest();
|
||||
});
|
||||
|
||||
describe("getCompatibleActivePluginRegistry", () => {
|
||||
it("reuses the active registry only when the load context cache key matches", () => {
|
||||
const registry = createEmptyPluginRegistry();
|
||||
const loadOptions = {
|
||||
config: {
|
||||
plugins: {
|
||||
allow: ["demo"],
|
||||
load: { paths: ["/tmp/demo.js"] },
|
||||
},
|
||||
},
|
||||
workspaceDir: "/tmp/workspace-a",
|
||||
runtimeOptions: {
|
||||
allowGatewaySubagentBinding: true,
|
||||
},
|
||||
};
|
||||
const { cacheKey } = __testing.resolvePluginLoadCacheContext(loadOptions);
|
||||
setActivePluginRegistry(registry, cacheKey);
|
||||
|
||||
expect(__testing.getCompatibleActivePluginRegistry(loadOptions)).toBe(registry);
|
||||
expect(
|
||||
__testing.getCompatibleActivePluginRegistry({
|
||||
...loadOptions,
|
||||
workspaceDir: "/tmp/workspace-b",
|
||||
}),
|
||||
).toBeUndefined();
|
||||
expect(
|
||||
__testing.getCompatibleActivePluginRegistry({
|
||||
...loadOptions,
|
||||
onlyPluginIds: ["demo"],
|
||||
}),
|
||||
).toBeUndefined();
|
||||
expect(
|
||||
__testing.getCompatibleActivePluginRegistry({
|
||||
...loadOptions,
|
||||
runtimeOptions: undefined,
|
||||
}),
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it("does not embed activation secrets in the loader cache key", () => {
|
||||
const { cacheKey } = __testing.resolvePluginLoadCacheContext({
|
||||
config: {
|
||||
plugins: {
|
||||
allow: ["telegram"],
|
||||
},
|
||||
},
|
||||
activationSourceConfig: {
|
||||
plugins: {
|
||||
allow: ["telegram"],
|
||||
},
|
||||
channels: {
|
||||
telegram: {
|
||||
enabled: true,
|
||||
botToken: "secret-token",
|
||||
},
|
||||
},
|
||||
},
|
||||
autoEnabledReasons: {
|
||||
telegram: ["telegram configured"],
|
||||
},
|
||||
});
|
||||
|
||||
expect(cacheKey).not.toContain("secret-token");
|
||||
expect(cacheKey).not.toContain("botToken");
|
||||
expect(cacheKey).not.toContain("telegram configured");
|
||||
});
|
||||
|
||||
it("falls back to the current active runtime when no compatibility-shaping inputs are supplied", () => {
|
||||
const registry = createEmptyPluginRegistry();
|
||||
setActivePluginRegistry(registry, "startup-registry");
|
||||
|
||||
expect(__testing.getCompatibleActivePluginRegistry()).toBe(registry);
|
||||
});
|
||||
|
||||
it("does not reuse the active registry when core gateway method names differ", () => {
|
||||
const registry = createEmptyPluginRegistry();
|
||||
const loadOptions = {
|
||||
config: {
|
||||
plugins: {
|
||||
allow: ["demo"],
|
||||
load: { paths: ["/tmp/demo.js"] },
|
||||
},
|
||||
},
|
||||
workspaceDir: "/tmp/workspace-a",
|
||||
coreGatewayHandlers: {
|
||||
"sessions.get": () => undefined,
|
||||
},
|
||||
};
|
||||
const { cacheKey } = __testing.resolvePluginLoadCacheContext(loadOptions);
|
||||
setActivePluginRegistry(registry, cacheKey);
|
||||
|
||||
expect(__testing.getCompatibleActivePluginRegistry(loadOptions)).toBe(registry);
|
||||
expect(
|
||||
__testing.getCompatibleActivePluginRegistry({
|
||||
...loadOptions,
|
||||
coreGatewayHandlers: {
|
||||
"sessions.get": () => undefined,
|
||||
"sessions.list": () => undefined,
|
||||
},
|
||||
}),
|
||||
).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveRuntimePluginRegistry", () => {
|
||||
it("reuses the compatible active registry before attempting a fresh load", () => {
|
||||
const registry = createEmptyPluginRegistry();
|
||||
const loadOptions = {
|
||||
config: {
|
||||
plugins: {
|
||||
allow: ["demo"],
|
||||
},
|
||||
},
|
||||
workspaceDir: "/tmp/workspace-a",
|
||||
};
|
||||
const { cacheKey } = __testing.resolvePluginLoadCacheContext(loadOptions);
|
||||
setActivePluginRegistry(registry, cacheKey);
|
||||
|
||||
expect(resolveRuntimePluginRegistry(loadOptions)).toBe(registry);
|
||||
});
|
||||
|
||||
it("falls back to the current active runtime when no explicit load context is provided", () => {
|
||||
const registry = createEmptyPluginRegistry();
|
||||
setActivePluginRegistry(registry, "startup-registry");
|
||||
|
||||
expect(resolveRuntimePluginRegistry()).toBe(registry);
|
||||
});
|
||||
});
|
||||
|
||||
describe("clearPluginLoaderCache", () => {
|
||||
it("resets registered memory plugin registries", () => {
|
||||
registerMemoryEmbeddingProvider({
|
||||
id: "stale",
|
||||
create: async () => ({ provider: null }),
|
||||
});
|
||||
registerMemoryPromptSection(() => ["stale memory section"]);
|
||||
registerMemoryFlushPlanResolver(() => ({
|
||||
softThresholdTokens: 1,
|
||||
forceFlushTranscriptBytes: 2,
|
||||
reserveTokensFloor: 3,
|
||||
prompt: "stale",
|
||||
systemPrompt: "stale",
|
||||
relativePath: "memory/stale.md",
|
||||
}));
|
||||
registerMemoryRuntime({
|
||||
async getMemorySearchManager() {
|
||||
return { manager: null };
|
||||
},
|
||||
resolveMemoryBackendConfig() {
|
||||
return { backend: "builtin" as const };
|
||||
},
|
||||
});
|
||||
expect(buildMemoryPromptSection({ availableTools: new Set() })).toEqual([
|
||||
"stale memory section",
|
||||
]);
|
||||
expect(resolveMemoryFlushPlan({})?.relativePath).toBe("memory/stale.md");
|
||||
expect(getMemoryRuntime()).toBeDefined();
|
||||
expect(getMemoryEmbeddingProvider("stale")).toBeDefined();
|
||||
|
||||
clearPluginLoaderCache();
|
||||
|
||||
expect(buildMemoryPromptSection({ availableTools: new Set() })).toEqual([]);
|
||||
expect(resolveMemoryFlushPlan({})).toBeNull();
|
||||
expect(getMemoryRuntime()).toBeUndefined();
|
||||
expect(getMemoryEmbeddingProvider("stale")).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
|
@ -0,0 +1,127 @@
|
|||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { resetDiagnosticEventsForTest } from "../infra/diagnostic-events.js";
|
||||
import { withEnv } from "../test-utils/env.js";
|
||||
import { clearPluginDiscoveryCache } from "./discovery.js";
|
||||
import { clearPluginLoaderCache, loadOpenClawPlugins } from "./loader.js";
|
||||
import { clearPluginManifestRegistryCache } from "./manifest-registry.js";
|
||||
import { resetPluginRuntimeStateForTest } from "./runtime.js";
|
||||
|
||||
export type TempPlugin = { dir: string; file: string; id: string };
|
||||
export type PluginLoadConfig = NonNullable<Parameters<typeof loadOpenClawPlugins>[0]>["config"];
|
||||
export type PluginRegistry = ReturnType<typeof loadOpenClawPlugins>;
|
||||
|
||||
function chmodSafeDir(dir: string) {
|
||||
if (process.platform === "win32") {
|
||||
return;
|
||||
}
|
||||
fs.chmodSync(dir, 0o755);
|
||||
}
|
||||
|
||||
function mkdtempSafe(prefix: string) {
|
||||
const dir = fs.mkdtempSync(prefix);
|
||||
chmodSafeDir(dir);
|
||||
return dir;
|
||||
}
|
||||
|
||||
export function mkdirSafe(dir: string) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
chmodSafeDir(dir);
|
||||
}
|
||||
|
||||
const fixtureRoot = mkdtempSafe(path.join(os.tmpdir(), "openclaw-plugin-"));
|
||||
let tempDirIndex = 0;
|
||||
const prevBundledDir = process.env.OPENCLAW_BUNDLED_PLUGINS_DIR;
|
||||
|
||||
export const EMPTY_PLUGIN_SCHEMA = {
|
||||
type: "object",
|
||||
additionalProperties: false,
|
||||
properties: {},
|
||||
};
|
||||
|
||||
export function makeTempDir() {
|
||||
const dir = path.join(fixtureRoot, `case-${tempDirIndex++}`);
|
||||
mkdirSafe(dir);
|
||||
return dir;
|
||||
}
|
||||
|
||||
export function writePlugin(params: {
|
||||
id: string;
|
||||
body: string;
|
||||
dir?: string;
|
||||
filename?: string;
|
||||
}): TempPlugin {
|
||||
const dir = params.dir ?? makeTempDir();
|
||||
const filename = params.filename ?? `${params.id}.cjs`;
|
||||
mkdirSafe(dir);
|
||||
const file = path.join(dir, filename);
|
||||
fs.writeFileSync(file, params.body, "utf-8");
|
||||
fs.writeFileSync(
|
||||
path.join(dir, "openclaw.plugin.json"),
|
||||
JSON.stringify(
|
||||
{
|
||||
id: params.id,
|
||||
configSchema: EMPTY_PLUGIN_SCHEMA,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf-8",
|
||||
);
|
||||
return { dir, file, id: params.id };
|
||||
}
|
||||
|
||||
export function useNoBundledPlugins() {
|
||||
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = "/nonexistent/bundled/plugins";
|
||||
}
|
||||
|
||||
export function loadBundleFixture(params: {
|
||||
pluginId: string;
|
||||
build: (bundleRoot: string) => void;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
onlyPluginIds?: string[];
|
||||
}) {
|
||||
useNoBundledPlugins();
|
||||
const workspaceDir = makeTempDir();
|
||||
const stateDir = makeTempDir();
|
||||
const bundleRoot = path.join(workspaceDir, ".openclaw", "extensions", params.pluginId);
|
||||
params.build(bundleRoot);
|
||||
return withEnv({ OPENCLAW_STATE_DIR: stateDir, ...params.env }, () =>
|
||||
loadOpenClawPlugins({
|
||||
workspaceDir,
|
||||
onlyPluginIds: params.onlyPluginIds ?? [params.pluginId],
|
||||
config: {
|
||||
plugins: {
|
||||
entries: {
|
||||
[params.pluginId]: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
cache: false,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
export function resetPluginLoaderTestStateForTest() {
|
||||
clearPluginLoaderCache();
|
||||
clearPluginDiscoveryCache();
|
||||
clearPluginManifestRegistryCache();
|
||||
resetPluginRuntimeStateForTest();
|
||||
resetDiagnosticEventsForTest();
|
||||
if (prevBundledDir === undefined) {
|
||||
delete process.env.OPENCLAW_BUNDLED_PLUGINS_DIR;
|
||||
} else {
|
||||
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = prevBundledDir;
|
||||
}
|
||||
}
|
||||
|
||||
export function cleanupPluginLoaderFixturesForTest() {
|
||||
try {
|
||||
fs.rmSync(fixtureRoot, { recursive: true, force: true });
|
||||
} catch {
|
||||
// ignore cleanup failures in tests
|
||||
}
|
||||
}
|
||||
|
|
@ -1,25 +1,27 @@
|
|||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { afterAll, afterEach, describe, expect, it } from "vitest";
|
||||
import { applyPluginAutoEnable } from "../config/plugin-auto-enable.js";
|
||||
import { clearInternalHooks, getRegisteredEventKeys } from "../hooks/internal-hooks.js";
|
||||
import { emitDiagnosticEvent, resetDiagnosticEventsForTest } from "../infra/diagnostic-events.js";
|
||||
import { emitDiagnosticEvent } from "../infra/diagnostic-events.js";
|
||||
import { withEnv } from "../test-utils/env.js";
|
||||
import { clearPluginCommands, getPluginCommandSpecs } from "./command-registry-state.js";
|
||||
import { clearPluginDiscoveryCache } from "./discovery.js";
|
||||
import { getGlobalHookRunner, resetGlobalHookRunner } from "./hook-runner-global.js";
|
||||
import { createHookRunner } from "./hooks.js";
|
||||
import { __testing, clearPluginLoaderCache, loadOpenClawPlugins } from "./loader.js";
|
||||
import {
|
||||
__testing,
|
||||
clearPluginLoaderCache,
|
||||
loadOpenClawPluginCliRegistry,
|
||||
loadOpenClawPlugins,
|
||||
resolveRuntimePluginRegistry,
|
||||
} from "./loader.js";
|
||||
import { clearPluginManifestRegistryCache } from "./manifest-registry.js";
|
||||
cleanupPluginLoaderFixturesForTest,
|
||||
EMPTY_PLUGIN_SCHEMA,
|
||||
makeTempDir,
|
||||
mkdirSafe,
|
||||
type PluginLoadConfig,
|
||||
type PluginRegistry,
|
||||
resetPluginLoaderTestStateForTest,
|
||||
type TempPlugin,
|
||||
useNoBundledPlugins,
|
||||
writePlugin,
|
||||
} from "./loader.test-fixtures.js";
|
||||
import {
|
||||
getMemoryEmbeddingProvider,
|
||||
listMemoryEmbeddingProviders,
|
||||
registerMemoryEmbeddingProvider,
|
||||
} from "./memory-embedding-providers.js";
|
||||
|
|
@ -39,33 +41,6 @@ import {
|
|||
resetPluginRuntimeStateForTest,
|
||||
setActivePluginRegistry,
|
||||
} from "./runtime.js";
|
||||
|
||||
type TempPlugin = { dir: string; file: string; id: string };
|
||||
type PluginLoadConfig = NonNullable<Parameters<typeof loadOpenClawPlugins>[0]>["config"];
|
||||
type PluginRegistry = ReturnType<typeof loadOpenClawPlugins>;
|
||||
|
||||
function chmodSafeDir(dir: string) {
|
||||
if (process.platform === "win32") {
|
||||
return;
|
||||
}
|
||||
fs.chmodSync(dir, 0o755);
|
||||
}
|
||||
|
||||
function mkdtempSafe(prefix: string) {
|
||||
const dir = fs.mkdtempSync(prefix);
|
||||
chmodSafeDir(dir);
|
||||
return dir;
|
||||
}
|
||||
|
||||
function mkdirSafe(dir: string) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
chmodSafeDir(dir);
|
||||
}
|
||||
|
||||
const fixtureRoot = mkdtempSafe(path.join(os.tmpdir(), "openclaw-plugin-"));
|
||||
let tempDirIndex = 0;
|
||||
const prevBundledDir = process.env.OPENCLAW_BUNDLED_PLUGINS_DIR;
|
||||
const EMPTY_PLUGIN_SCHEMA = { type: "object", additionalProperties: false, properties: {} };
|
||||
let cachedBundledTelegramDir = "";
|
||||
let cachedBundledMemoryDir = "";
|
||||
const BUNDLED_TELEGRAM_PLUGIN_BODY = `module.exports = {
|
||||
|
|
@ -92,38 +67,6 @@ const BUNDLED_TELEGRAM_PLUGIN_BODY = `module.exports = {
|
|||
},
|
||||
};`;
|
||||
|
||||
function makeTempDir() {
|
||||
const dir = path.join(fixtureRoot, `case-${tempDirIndex++}`);
|
||||
mkdirSafe(dir);
|
||||
return dir;
|
||||
}
|
||||
|
||||
function writePlugin(params: {
|
||||
id: string;
|
||||
body: string;
|
||||
dir?: string;
|
||||
filename?: string;
|
||||
}): TempPlugin {
|
||||
const dir = params.dir ?? makeTempDir();
|
||||
const filename = params.filename ?? `${params.id}.cjs`;
|
||||
mkdirSafe(dir);
|
||||
const file = path.join(dir, filename);
|
||||
fs.writeFileSync(file, params.body, "utf-8");
|
||||
fs.writeFileSync(
|
||||
path.join(dir, "openclaw.plugin.json"),
|
||||
JSON.stringify(
|
||||
{
|
||||
id: params.id,
|
||||
configSchema: EMPTY_PLUGIN_SCHEMA,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf-8",
|
||||
);
|
||||
return { dir, file, id: params.id };
|
||||
}
|
||||
|
||||
function simplePluginBody(id: string) {
|
||||
return `module.exports = { id: ${JSON.stringify(id)}, register() {} };`;
|
||||
}
|
||||
|
|
@ -261,10 +204,6 @@ function expectTelegramLoaded(registry: ReturnType<typeof loadOpenClawPlugins>)
|
|||
expect(registry.channels.some((entry) => entry.plugin.id === "telegram")).toBe(true);
|
||||
}
|
||||
|
||||
function useNoBundledPlugins() {
|
||||
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = "/nonexistent/bundled/plugins";
|
||||
}
|
||||
|
||||
function loadRegistryFromSinglePlugin(params: {
|
||||
plugin: TempPlugin;
|
||||
pluginConfig?: Record<string, unknown>;
|
||||
|
|
@ -516,48 +455,6 @@ function createEscapingEntryFixture(params: { id: string; sourceBody: string })
|
|||
return { pluginDir, outsideEntry, linkedEntry };
|
||||
}
|
||||
|
||||
function loadBundleFixture(params: {
|
||||
pluginId: string;
|
||||
build: (bundleRoot: string) => void;
|
||||
env?: NodeJS.ProcessEnv;
|
||||
onlyPluginIds?: string[];
|
||||
}) {
|
||||
useNoBundledPlugins();
|
||||
const workspaceDir = makeTempDir();
|
||||
const stateDir = makeTempDir();
|
||||
const bundleRoot = path.join(workspaceDir, ".openclaw", "extensions", params.pluginId);
|
||||
params.build(bundleRoot);
|
||||
return withEnv({ OPENCLAW_STATE_DIR: stateDir, ...params.env }, () =>
|
||||
loadOpenClawPlugins({
|
||||
workspaceDir,
|
||||
onlyPluginIds: params.onlyPluginIds ?? [params.pluginId],
|
||||
config: {
|
||||
plugins: {
|
||||
entries: {
|
||||
[params.pluginId]: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
cache: false,
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
function resolveLoadedPluginSource(
|
||||
registry: ReturnType<typeof loadOpenClawPlugins>,
|
||||
pluginId: string,
|
||||
|
|
@ -770,195 +667,13 @@ function expectEscapingEntryRejected(params: {
|
|||
}
|
||||
|
||||
afterEach(() => {
|
||||
clearPluginLoaderCache();
|
||||
clearPluginDiscoveryCache();
|
||||
clearPluginManifestRegistryCache();
|
||||
resetPluginRuntimeStateForTest();
|
||||
resetDiagnosticEventsForTest();
|
||||
if (prevBundledDir === undefined) {
|
||||
delete process.env.OPENCLAW_BUNDLED_PLUGINS_DIR;
|
||||
} else {
|
||||
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = prevBundledDir;
|
||||
}
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
resetPluginLoaderTestStateForTest();
|
||||
});
|
||||
|
||||
afterAll(() => {
|
||||
try {
|
||||
fs.rmSync(fixtureRoot, { recursive: true, force: true });
|
||||
} catch {
|
||||
// ignore cleanup failures
|
||||
} finally {
|
||||
cachedBundledTelegramDir = "";
|
||||
cachedBundledMemoryDir = "";
|
||||
}
|
||||
cleanupPluginLoaderFixturesForTest();
|
||||
cachedBundledTelegramDir = "";
|
||||
cachedBundledMemoryDir = "";
|
||||
});
|
||||
|
||||
describe("loadOpenClawPlugins", () => {
|
||||
|
|
@ -1593,11 +1308,7 @@ module.exports = { id: "throws-after-import", register() {} };`,
|
|||
});
|
||||
|
||||
it("can scope bundled provider loads to deepseek without hanging", () => {
|
||||
if (prevBundledDir === undefined) {
|
||||
delete process.env.OPENCLAW_BUNDLED_PLUGINS_DIR;
|
||||
} else {
|
||||
process.env.OPENCLAW_BUNDLED_PLUGINS_DIR = prevBundledDir;
|
||||
}
|
||||
resetPluginLoaderTestStateForTest();
|
||||
|
||||
const scoped = loadOpenClawPlugins({
|
||||
cache: false,
|
||||
|
|
@ -2986,401 +2697,6 @@ module.exports = {
|
|||
expect(registry.channels).toHaveLength(expectedChannels);
|
||||
});
|
||||
|
||||
it("passes validated plugin config into non-activating CLI metadata loads", async () => {
|
||||
useNoBundledPlugins();
|
||||
const plugin = writePlugin({
|
||||
id: "config-cli",
|
||||
filename: "config-cli.cjs",
|
||||
body: `module.exports = {
|
||||
id: "config-cli",
|
||||
register(api) {
|
||||
if (!api.pluginConfig || api.pluginConfig.token !== "ok") {
|
||||
throw new Error("missing plugin config");
|
||||
}
|
||||
api.registerCli(() => {}, {
|
||||
descriptors: [
|
||||
{
|
||||
name: "cfg",
|
||||
description: "Config-backed CLI command",
|
||||
hasSubcommands: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
};`,
|
||||
});
|
||||
fs.writeFileSync(
|
||||
path.join(plugin.dir, "openclaw.plugin.json"),
|
||||
JSON.stringify(
|
||||
{
|
||||
id: "config-cli",
|
||||
configSchema: {
|
||||
type: "object",
|
||||
additionalProperties: false,
|
||||
properties: {
|
||||
token: { type: "string" },
|
||||
},
|
||||
required: ["token"],
|
||||
},
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const registry = await loadOpenClawPluginCliRegistry({
|
||||
config: {
|
||||
plugins: {
|
||||
load: { paths: [plugin.file] },
|
||||
allow: ["config-cli"],
|
||||
entries: {
|
||||
"config-cli": {
|
||||
config: {
|
||||
token: "ok",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(registry.cliRegistrars.flatMap((entry) => entry.commands)).toContain("cfg");
|
||||
expect(registry.plugins.find((entry) => entry.id === "config-cli")?.status).toBe("loaded");
|
||||
});
|
||||
|
||||
it("uses the real channel entry in cli-metadata mode for CLI metadata capture", async () => {
|
||||
useNoBundledPlugins();
|
||||
const pluginDir = makeTempDir();
|
||||
const fullMarker = path.join(pluginDir, "full-loaded.txt");
|
||||
const modeMarker = path.join(pluginDir, "registration-mode.txt");
|
||||
const runtimeMarker = path.join(pluginDir, "runtime-set.txt");
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(pluginDir, "package.json"),
|
||||
JSON.stringify(
|
||||
{
|
||||
name: "@openclaw/cli-metadata-channel",
|
||||
openclaw: { extensions: ["./index.cjs"], setupEntry: "./setup-entry.cjs" },
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf-8",
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(pluginDir, "openclaw.plugin.json"),
|
||||
JSON.stringify(
|
||||
{
|
||||
id: "cli-metadata-channel",
|
||||
configSchema: EMPTY_PLUGIN_SCHEMA,
|
||||
channels: ["cli-metadata-channel"],
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf-8",
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(pluginDir, "index.cjs"),
|
||||
`const { defineChannelPluginEntry } = require("openclaw/plugin-sdk/core");
|
||||
require("node:fs").writeFileSync(${JSON.stringify(fullMarker)}, "loaded", "utf-8");
|
||||
module.exports = {
|
||||
...defineChannelPluginEntry({
|
||||
id: "cli-metadata-channel",
|
||||
name: "CLI Metadata Channel",
|
||||
description: "cli metadata channel",
|
||||
setRuntime() {
|
||||
require("node:fs").writeFileSync(${JSON.stringify(runtimeMarker)}, "loaded", "utf-8");
|
||||
},
|
||||
plugin: {
|
||||
id: "cli-metadata-channel",
|
||||
meta: {
|
||||
id: "cli-metadata-channel",
|
||||
label: "CLI Metadata Channel",
|
||||
selectionLabel: "CLI Metadata Channel",
|
||||
docsPath: "/channels/cli-metadata-channel",
|
||||
blurb: "cli metadata channel",
|
||||
},
|
||||
capabilities: { chatTypes: ["direct"] },
|
||||
config: {
|
||||
listAccountIds: () => [],
|
||||
resolveAccount: () => ({ accountId: "default" }),
|
||||
},
|
||||
outbound: { deliveryMode: "direct" },
|
||||
},
|
||||
registerCliMetadata(api) {
|
||||
require("node:fs").writeFileSync(
|
||||
${JSON.stringify(modeMarker)},
|
||||
String(api.registrationMode),
|
||||
"utf-8",
|
||||
);
|
||||
api.registerCli(() => {}, {
|
||||
descriptors: [
|
||||
{
|
||||
name: "cli-metadata-channel",
|
||||
description: "Channel CLI metadata",
|
||||
hasSubcommands: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
registerFull() {
|
||||
throw new Error("full channel entry should not run during CLI metadata capture");
|
||||
},
|
||||
}),
|
||||
};`,
|
||||
"utf-8",
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(pluginDir, "setup-entry.cjs"),
|
||||
`throw new Error("setup entry should not load during CLI metadata capture");`,
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const registry = await loadOpenClawPluginCliRegistry({
|
||||
config: {
|
||||
plugins: {
|
||||
load: { paths: [pluginDir] },
|
||||
allow: ["cli-metadata-channel"],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(fs.existsSync(fullMarker)).toBe(true);
|
||||
expect(fs.existsSync(runtimeMarker)).toBe(false);
|
||||
expect(fs.readFileSync(modeMarker, "utf-8")).toBe("cli-metadata");
|
||||
expect(registry.cliRegistrars.flatMap((entry) => entry.commands)).toContain(
|
||||
"cli-metadata-channel",
|
||||
);
|
||||
});
|
||||
|
||||
it("collects channel CLI metadata during full plugin loads", () => {
|
||||
useNoBundledPlugins();
|
||||
const pluginDir = makeTempDir();
|
||||
const modeMarker = path.join(pluginDir, "registration-mode.txt");
|
||||
const fullMarker = path.join(pluginDir, "full-loaded.txt");
|
||||
|
||||
fs.writeFileSync(
|
||||
path.join(pluginDir, "package.json"),
|
||||
JSON.stringify(
|
||||
{
|
||||
name: "@openclaw/full-cli-metadata-channel",
|
||||
openclaw: { extensions: ["./index.cjs"] },
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf-8",
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(pluginDir, "openclaw.plugin.json"),
|
||||
JSON.stringify(
|
||||
{
|
||||
id: "full-cli-metadata-channel",
|
||||
configSchema: EMPTY_PLUGIN_SCHEMA,
|
||||
channels: ["full-cli-metadata-channel"],
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf-8",
|
||||
);
|
||||
fs.writeFileSync(
|
||||
path.join(pluginDir, "index.cjs"),
|
||||
`const { defineChannelPluginEntry } = require("openclaw/plugin-sdk/core");
|
||||
module.exports = {
|
||||
...defineChannelPluginEntry({
|
||||
id: "full-cli-metadata-channel",
|
||||
name: "Full CLI Metadata Channel",
|
||||
description: "full cli metadata channel",
|
||||
plugin: {
|
||||
id: "full-cli-metadata-channel",
|
||||
meta: {
|
||||
id: "full-cli-metadata-channel",
|
||||
label: "Full CLI Metadata Channel",
|
||||
selectionLabel: "Full CLI Metadata Channel",
|
||||
docsPath: "/channels/full-cli-metadata-channel",
|
||||
blurb: "full cli metadata channel",
|
||||
},
|
||||
capabilities: { chatTypes: ["direct"] },
|
||||
config: {
|
||||
listAccountIds: () => [],
|
||||
resolveAccount: () => ({ accountId: "default" }),
|
||||
},
|
||||
outbound: { deliveryMode: "direct" },
|
||||
},
|
||||
registerCliMetadata(api) {
|
||||
require("node:fs").writeFileSync(
|
||||
${JSON.stringify(modeMarker)},
|
||||
String(api.registrationMode),
|
||||
"utf-8",
|
||||
);
|
||||
api.registerCli(() => {}, {
|
||||
descriptors: [
|
||||
{
|
||||
name: "full-cli-metadata-channel",
|
||||
description: "Full-load channel CLI metadata",
|
||||
hasSubcommands: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
registerFull() {
|
||||
require("node:fs").writeFileSync(${JSON.stringify(fullMarker)}, "loaded", "utf-8");
|
||||
},
|
||||
}),
|
||||
};`,
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const registry = loadOpenClawPlugins({
|
||||
cache: false,
|
||||
config: {
|
||||
plugins: {
|
||||
load: { paths: [pluginDir] },
|
||||
allow: ["full-cli-metadata-channel"],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(fs.readFileSync(modeMarker, "utf-8")).toBe("full");
|
||||
expect(fs.existsSync(fullMarker)).toBe(true);
|
||||
expect(registry.cliRegistrars.flatMap((entry) => entry.commands)).toContain(
|
||||
"full-cli-metadata-channel",
|
||||
);
|
||||
});
|
||||
|
||||
it("awaits async plugin registration when collecting CLI metadata", async () => {
|
||||
useNoBundledPlugins();
|
||||
const plugin = writePlugin({
|
||||
id: "async-cli",
|
||||
filename: "async-cli.cjs",
|
||||
body: `module.exports = {
|
||||
id: "async-cli",
|
||||
async register(api) {
|
||||
await Promise.resolve();
|
||||
api.registerCli(() => {}, {
|
||||
descriptors: [
|
||||
{
|
||||
name: "async-cli",
|
||||
description: "Async CLI metadata",
|
||||
hasSubcommands: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
};`,
|
||||
});
|
||||
|
||||
const registry = await loadOpenClawPluginCliRegistry({
|
||||
config: {
|
||||
plugins: {
|
||||
load: { paths: [plugin.file] },
|
||||
allow: ["async-cli"],
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(registry.cliRegistrars.flatMap((entry) => entry.commands)).toContain("async-cli");
|
||||
expect(
|
||||
registry.diagnostics.some((entry) => entry.message.includes("async registration is ignored")),
|
||||
).toBe(false);
|
||||
});
|
||||
|
||||
it("applies memory slot gating to non-bundled CLI metadata loads", async () => {
|
||||
useNoBundledPlugins();
|
||||
const plugin = writePlugin({
|
||||
id: "memory-external",
|
||||
filename: "memory-external.cjs",
|
||||
body: `module.exports = {
|
||||
id: "memory-external",
|
||||
kind: "memory",
|
||||
register(api) {
|
||||
api.registerCli(() => {}, {
|
||||
descriptors: [
|
||||
{
|
||||
name: "memory-external",
|
||||
description: "External memory CLI metadata",
|
||||
hasSubcommands: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
};`,
|
||||
});
|
||||
fs.writeFileSync(
|
||||
path.join(plugin.dir, "openclaw.plugin.json"),
|
||||
JSON.stringify(
|
||||
{
|
||||
id: "memory-external",
|
||||
kind: "memory",
|
||||
configSchema: EMPTY_PLUGIN_SCHEMA,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
),
|
||||
"utf-8",
|
||||
);
|
||||
|
||||
const registry = await loadOpenClawPluginCliRegistry({
|
||||
config: {
|
||||
plugins: {
|
||||
load: { paths: [plugin.file] },
|
||||
allow: ["memory-external"],
|
||||
slots: { memory: "memory-other" },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(registry.cliRegistrars.flatMap((entry) => entry.commands)).not.toContain(
|
||||
"memory-external",
|
||||
);
|
||||
const memory = registry.plugins.find((entry) => entry.id === "memory-external");
|
||||
expect(memory?.status).toBe("disabled");
|
||||
expect(String(memory?.error ?? "")).toContain('memory slot set to "memory-other"');
|
||||
});
|
||||
|
||||
it("re-evaluates memory slot gating after resolving exported plugin kind", async () => {
|
||||
useNoBundledPlugins();
|
||||
const plugin = writePlugin({
|
||||
id: "memory-export-only",
|
||||
filename: "memory-export-only.cjs",
|
||||
body: `module.exports = {
|
||||
id: "memory-export-only",
|
||||
kind: "memory",
|
||||
register(api) {
|
||||
api.registerCli(() => {}, {
|
||||
descriptors: [
|
||||
{
|
||||
name: "memory-export-only",
|
||||
description: "Export-only memory CLI metadata",
|
||||
hasSubcommands: true,
|
||||
},
|
||||
],
|
||||
});
|
||||
},
|
||||
};`,
|
||||
});
|
||||
|
||||
const registry = await loadOpenClawPluginCliRegistry({
|
||||
config: {
|
||||
plugins: {
|
||||
load: { paths: [plugin.file] },
|
||||
allow: ["memory-export-only"],
|
||||
slots: { memory: "memory-other" },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
expect(registry.cliRegistrars.flatMap((entry) => entry.commands)).not.toContain(
|
||||
"memory-export-only",
|
||||
);
|
||||
const memory = registry.plugins.find((entry) => entry.id === "memory-export-only");
|
||||
expect(memory?.status).toBe("disabled");
|
||||
expect(String(memory?.error ?? "")).toContain('memory slot set to "memory-other"');
|
||||
});
|
||||
|
||||
it("blocks before_prompt_build but preserves legacy model overrides when prompt injection is disabled", async () => {
|
||||
useNoBundledPlugins();
|
||||
const plugin = writePlugin({
|
||||
|
|
@ -4358,171 +3674,3 @@ export const runtimeValue = helperValue;`,
|
|||
expect(record?.status).toBe("loaded");
|
||||
});
|
||||
});
|
||||
|
||||
describe("getCompatibleActivePluginRegistry", () => {
|
||||
it("reuses the active registry only when the load context cache key matches", () => {
|
||||
const registry = createEmptyPluginRegistry();
|
||||
const loadOptions = {
|
||||
config: {
|
||||
plugins: {
|
||||
allow: ["demo"],
|
||||
load: { paths: ["/tmp/demo.js"] },
|
||||
},
|
||||
},
|
||||
workspaceDir: "/tmp/workspace-a",
|
||||
runtimeOptions: {
|
||||
allowGatewaySubagentBinding: true,
|
||||
},
|
||||
};
|
||||
const { cacheKey } = __testing.resolvePluginLoadCacheContext(loadOptions);
|
||||
setActivePluginRegistry(registry, cacheKey);
|
||||
|
||||
expect(__testing.getCompatibleActivePluginRegistry(loadOptions)).toBe(registry);
|
||||
expect(
|
||||
__testing.getCompatibleActivePluginRegistry({
|
||||
...loadOptions,
|
||||
workspaceDir: "/tmp/workspace-b",
|
||||
}),
|
||||
).toBeUndefined();
|
||||
expect(
|
||||
__testing.getCompatibleActivePluginRegistry({
|
||||
...loadOptions,
|
||||
onlyPluginIds: ["demo"],
|
||||
}),
|
||||
).toBeUndefined();
|
||||
expect(
|
||||
__testing.getCompatibleActivePluginRegistry({
|
||||
...loadOptions,
|
||||
runtimeOptions: undefined,
|
||||
}),
|
||||
).toBeUndefined();
|
||||
});
|
||||
|
||||
it("does not embed activation secrets in the loader cache key", () => {
|
||||
const { cacheKey } = __testing.resolvePluginLoadCacheContext({
|
||||
config: {
|
||||
plugins: {
|
||||
allow: ["telegram"],
|
||||
},
|
||||
},
|
||||
activationSourceConfig: {
|
||||
plugins: {
|
||||
allow: ["telegram"],
|
||||
},
|
||||
channels: {
|
||||
telegram: {
|
||||
enabled: true,
|
||||
botToken: "secret-token",
|
||||
},
|
||||
},
|
||||
},
|
||||
autoEnabledReasons: {
|
||||
telegram: ["telegram configured"],
|
||||
},
|
||||
});
|
||||
|
||||
expect(cacheKey).not.toContain("secret-token");
|
||||
expect(cacheKey).not.toContain("botToken");
|
||||
expect(cacheKey).not.toContain("telegram configured");
|
||||
});
|
||||
|
||||
it("falls back to the current active runtime when no compatibility-shaping inputs are supplied", () => {
|
||||
const registry = createEmptyPluginRegistry();
|
||||
setActivePluginRegistry(registry, "startup-registry");
|
||||
|
||||
expect(__testing.getCompatibleActivePluginRegistry()).toBe(registry);
|
||||
});
|
||||
|
||||
it("does not reuse the active registry when core gateway method names differ", () => {
|
||||
const registry = createEmptyPluginRegistry();
|
||||
const loadOptions = {
|
||||
config: {
|
||||
plugins: {
|
||||
allow: ["demo"],
|
||||
load: { paths: ["/tmp/demo.js"] },
|
||||
},
|
||||
},
|
||||
workspaceDir: "/tmp/workspace-a",
|
||||
coreGatewayHandlers: {
|
||||
"sessions.get": () => undefined,
|
||||
},
|
||||
};
|
||||
const { cacheKey } = __testing.resolvePluginLoadCacheContext(loadOptions);
|
||||
setActivePluginRegistry(registry, cacheKey);
|
||||
|
||||
expect(__testing.getCompatibleActivePluginRegistry(loadOptions)).toBe(registry);
|
||||
expect(
|
||||
__testing.getCompatibleActivePluginRegistry({
|
||||
...loadOptions,
|
||||
coreGatewayHandlers: {
|
||||
"sessions.get": () => undefined,
|
||||
"sessions.list": () => undefined,
|
||||
},
|
||||
}),
|
||||
).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
||||
describe("resolveRuntimePluginRegistry", () => {
|
||||
it("reuses the compatible active registry before attempting a fresh load", () => {
|
||||
const registry = createEmptyPluginRegistry();
|
||||
const loadOptions = {
|
||||
config: {
|
||||
plugins: {
|
||||
allow: ["demo"],
|
||||
},
|
||||
},
|
||||
workspaceDir: "/tmp/workspace-a",
|
||||
};
|
||||
const { cacheKey } = __testing.resolvePluginLoadCacheContext(loadOptions);
|
||||
setActivePluginRegistry(registry, cacheKey);
|
||||
|
||||
expect(resolveRuntimePluginRegistry(loadOptions)).toBe(registry);
|
||||
});
|
||||
|
||||
it("falls back to the current active runtime when no explicit load context is provided", () => {
|
||||
const registry = createEmptyPluginRegistry();
|
||||
setActivePluginRegistry(registry, "startup-registry");
|
||||
|
||||
expect(resolveRuntimePluginRegistry()).toBe(registry);
|
||||
});
|
||||
});
|
||||
|
||||
describe("clearPluginLoaderCache", () => {
|
||||
it("resets registered memory plugin registries", () => {
|
||||
registerMemoryEmbeddingProvider({
|
||||
id: "stale",
|
||||
create: async () => ({ provider: null }),
|
||||
});
|
||||
registerMemoryPromptSection(() => ["stale memory section"]);
|
||||
registerMemoryFlushPlanResolver(() => ({
|
||||
softThresholdTokens: 1,
|
||||
forceFlushTranscriptBytes: 2,
|
||||
reserveTokensFloor: 3,
|
||||
prompt: "stale",
|
||||
systemPrompt: "stale",
|
||||
relativePath: "memory/stale.md",
|
||||
}));
|
||||
registerMemoryRuntime({
|
||||
async getMemorySearchManager() {
|
||||
return { manager: null };
|
||||
},
|
||||
resolveMemoryBackendConfig() {
|
||||
return { backend: "builtin" as const };
|
||||
},
|
||||
});
|
||||
expect(buildMemoryPromptSection({ availableTools: new Set() })).toEqual([
|
||||
"stale memory section",
|
||||
]);
|
||||
expect(resolveMemoryFlushPlan({})?.relativePath).toBe("memory/stale.md");
|
||||
expect(getMemoryRuntime()).toBeDefined();
|
||||
expect(getMemoryEmbeddingProvider("stale")).toBeDefined();
|
||||
|
||||
clearPluginLoaderCache();
|
||||
|
||||
expect(buildMemoryPromptSection({ availableTools: new Set() })).toEqual([]);
|
||||
expect(resolveMemoryFlushPlan({})).toBeNull();
|
||||
expect(getMemoryRuntime()).toBeUndefined();
|
||||
expect(getMemoryEmbeddingProvider("stale")).toBeUndefined();
|
||||
});
|
||||
});
|
||||
|
|
|
|||
Loading…
Reference in New Issue