test: split plugin loader coverage by concern

This commit is contained in:
Shakker 2026-04-02 12:49:27 +01:00 committed by Peter Steinberger
parent b53dcb9380
commit db76dbc546
6 changed files with 955 additions and 870 deletions

View File

@ -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,
)

View File

@ -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);
});
});

View File

@ -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"');
});
});

View File

@ -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();
});
});

View File

@ -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
}
}

View File

@ -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();
});
});