mirror of
https://github.com/moltbot/moltbot.git
synced 2026-04-20 13:13:06 +00:00
Plugins: align CLI metadata loader behavior
This commit is contained in:
@@ -65,6 +65,13 @@ function createCliRegistry(params?: {
|
||||
};
|
||||
}
|
||||
|
||||
function createEmptyCliRegistry(params?: { diagnostics?: Array<{ message: string }> }) {
|
||||
return {
|
||||
cliRegistrars: [],
|
||||
diagnostics: params?.diagnostics ?? [],
|
||||
};
|
||||
}
|
||||
|
||||
function expectPluginLoaderConfig(config: OpenClawConfig) {
|
||||
expect(mocks.loadOpenClawPlugins).toHaveBeenCalledWith(
|
||||
expect.objectContaining({
|
||||
@@ -115,7 +122,10 @@ describe("registerPluginCliCommands", () => {
|
||||
mocks.loadOpenClawPluginCliRegistry.mockReset();
|
||||
mocks.loadOpenClawPluginCliRegistry.mockResolvedValue(createCliRegistry());
|
||||
mocks.loadOpenClawPlugins.mockReset();
|
||||
mocks.loadOpenClawPlugins.mockReturnValue(createCliRegistry());
|
||||
mocks.loadOpenClawPlugins.mockReturnValue({
|
||||
...createCliRegistry(),
|
||||
diagnostics: [],
|
||||
});
|
||||
mocks.applyPluginAutoEnable.mockReset();
|
||||
mocks.applyPluginAutoEnable.mockImplementation(({ config }) => ({ config, changes: [] }));
|
||||
});
|
||||
@@ -231,6 +241,51 @@ describe("registerPluginCliCommands", () => {
|
||||
expect(mocks.loadOpenClawPluginCliRegistry).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("falls back to awaited CLI metadata collection when runtime loading ignored async registration", async () => {
|
||||
const asyncRegistrar = vi.fn(async ({ program }: { program: Command }) => {
|
||||
const asyncCommand = program.command("async-cli").description("Async CLI");
|
||||
asyncCommand.command("run").action(mocks.memoryListAction);
|
||||
});
|
||||
mocks.loadOpenClawPlugins.mockReturnValue(
|
||||
createEmptyCliRegistry({
|
||||
diagnostics: [
|
||||
{
|
||||
message: "plugin register returned a promise; async registration is ignored",
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
mocks.loadOpenClawPluginCliRegistry.mockResolvedValue({
|
||||
cliRegistrars: [
|
||||
{
|
||||
pluginId: "async-plugin",
|
||||
register: asyncRegistrar,
|
||||
commands: ["async-cli"],
|
||||
descriptors: [
|
||||
{
|
||||
name: "async-cli",
|
||||
description: "Async CLI",
|
||||
hasSubcommands: true,
|
||||
},
|
||||
],
|
||||
source: "bundled",
|
||||
},
|
||||
],
|
||||
diagnostics: [],
|
||||
});
|
||||
const program = createProgram();
|
||||
program.exitOverride();
|
||||
|
||||
await registerPluginCliCommands(program, {} as OpenClawConfig, undefined, undefined, {
|
||||
mode: "lazy",
|
||||
});
|
||||
|
||||
expect(mocks.loadOpenClawPluginCliRegistry).toHaveBeenCalledTimes(1);
|
||||
await program.parseAsync(["async-cli", "run"], { from: "user" });
|
||||
expect(asyncRegistrar).toHaveBeenCalledTimes(1);
|
||||
expect(mocks.memoryListAction).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it("lazy-registers descriptor-backed plugin commands on first invocation", async () => {
|
||||
const program = createProgram();
|
||||
program.exitOverride();
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
loadOpenClawPlugins,
|
||||
type PluginLoadOptions,
|
||||
} from "./loader.js";
|
||||
import type { PluginRegistry } from "./registry.js";
|
||||
import type { OpenClawPluginCliCommandDescriptor } from "./types.js";
|
||||
import type { PluginLogger } from "./types.js";
|
||||
|
||||
@@ -34,6 +35,28 @@ function canRegisterPluginCliLazily(entry: {
|
||||
return entry.commands.every((command) => descriptorNames.has(command));
|
||||
}
|
||||
|
||||
function hasIgnoredAsyncPluginRegistration(registry: PluginRegistry): boolean {
|
||||
return (registry.diagnostics ?? []).some(
|
||||
(entry) =>
|
||||
entry.message === "plugin register returned a promise; async registration is ignored",
|
||||
);
|
||||
}
|
||||
|
||||
function mergeCliRegistrars(params: {
|
||||
runtimeRegistry: PluginRegistry;
|
||||
metadataRegistry: PluginRegistry;
|
||||
}) {
|
||||
const metadataCommands = new Set(
|
||||
params.metadataRegistry.cliRegistrars.flatMap((entry) => entry.commands),
|
||||
);
|
||||
return [
|
||||
...params.metadataRegistry.cliRegistrars,
|
||||
...params.runtimeRegistry.cliRegistrars.filter(
|
||||
(entry) => !entry.commands.some((command) => metadataCommands.has(command)),
|
||||
),
|
||||
];
|
||||
}
|
||||
|
||||
function resolvePluginCliLoadContext(cfg?: OpenClawConfig, env?: NodeJS.ProcessEnv) {
|
||||
const config = cfg ?? loadConfig();
|
||||
const resolvedConfig = applyPluginAutoEnable({ config, env: env ?? process.env }).config;
|
||||
@@ -72,22 +95,52 @@ async function loadPluginCliMetadataRegistry(
|
||||
};
|
||||
}
|
||||
|
||||
function loadPluginCliCommandRegistry(
|
||||
async function loadPluginCliCommandRegistry(
|
||||
cfg?: OpenClawConfig,
|
||||
env?: NodeJS.ProcessEnv,
|
||||
loaderOptions?: Pick<PluginLoadOptions, "pluginSdkResolution">,
|
||||
) {
|
||||
const context = resolvePluginCliLoadContext(cfg, env);
|
||||
return {
|
||||
...context,
|
||||
registry: loadOpenClawPlugins({
|
||||
const runtimeRegistry = loadOpenClawPlugins({
|
||||
config: context.config,
|
||||
workspaceDir: context.workspaceDir,
|
||||
env,
|
||||
logger: context.logger,
|
||||
...loaderOptions,
|
||||
});
|
||||
|
||||
if (!hasIgnoredAsyncPluginRegistration(runtimeRegistry)) {
|
||||
return {
|
||||
...context,
|
||||
registry: runtimeRegistry,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const metadataRegistry = await loadOpenClawPluginCliRegistry({
|
||||
config: context.config,
|
||||
workspaceDir: context.workspaceDir,
|
||||
env,
|
||||
logger: context.logger,
|
||||
...loaderOptions,
|
||||
}),
|
||||
};
|
||||
});
|
||||
return {
|
||||
...context,
|
||||
registry: {
|
||||
...runtimeRegistry,
|
||||
cliRegistrars: mergeCliRegistrars({
|
||||
runtimeRegistry,
|
||||
metadataRegistry,
|
||||
}),
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
log.warn(`plugin CLI metadata fallback failed: ${String(error)}`);
|
||||
return {
|
||||
...context,
|
||||
registry: runtimeRegistry,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export async function getPluginCliCommandDescriptors(
|
||||
@@ -120,7 +173,7 @@ export async function registerPluginCliCommands(
|
||||
loaderOptions?: Pick<PluginLoadOptions, "pluginSdkResolution">,
|
||||
options?: RegisterPluginCliOptions,
|
||||
) {
|
||||
const { config, workspaceDir, logger, registry } = loadPluginCliCommandRegistry(
|
||||
const { config, workspaceDir, logger, registry } = await loadPluginCliCommandRegistry(
|
||||
cfg,
|
||||
env,
|
||||
loaderOptions,
|
||||
|
||||
@@ -2953,6 +2953,46 @@ module.exports = {
|
||||
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({
|
||||
|
||||
@@ -1563,26 +1563,6 @@ export async function loadOpenClawPluginCliRegistry(
|
||||
continue;
|
||||
}
|
||||
|
||||
if (manifestRecord.kind === "memory") {
|
||||
const memoryDecision = resolveMemorySlotDecision({
|
||||
id: record.id,
|
||||
kind: "memory",
|
||||
slot: memorySlot,
|
||||
selectedId: selectedMemoryPluginId,
|
||||
});
|
||||
if (!memoryDecision.enabled) {
|
||||
record.enabled = false;
|
||||
record.status = "disabled";
|
||||
record.error = memoryDecision.reason;
|
||||
registry.plugins.push(record);
|
||||
seenIds.set(pluginId, candidate.origin);
|
||||
continue;
|
||||
}
|
||||
if (memoryDecision.selected) {
|
||||
selectedMemoryPluginId = record.id;
|
||||
}
|
||||
}
|
||||
|
||||
if (!manifestRecord.configSchema) {
|
||||
pushPluginLoadError("missing config schema");
|
||||
continue;
|
||||
@@ -1658,6 +1638,24 @@ export async function loadOpenClawPluginCliRegistry(
|
||||
}
|
||||
record.kind = definition?.kind ?? record.kind;
|
||||
|
||||
const memoryDecision = resolveMemorySlotDecision({
|
||||
id: record.id,
|
||||
kind: record.kind,
|
||||
slot: memorySlot,
|
||||
selectedId: selectedMemoryPluginId,
|
||||
});
|
||||
if (!memoryDecision.enabled) {
|
||||
record.enabled = false;
|
||||
record.status = "disabled";
|
||||
record.error = memoryDecision.reason;
|
||||
registry.plugins.push(record);
|
||||
seenIds.set(pluginId, candidate.origin);
|
||||
continue;
|
||||
}
|
||||
if (memoryDecision.selected && record.kind === "memory") {
|
||||
selectedMemoryPluginId = record.id;
|
||||
}
|
||||
|
||||
if (typeof register !== "function") {
|
||||
logger.error(`[plugins] ${record.id} missing register/activate export`);
|
||||
pushPluginLoadError("plugin export missing register/activate");
|
||||
|
||||
Reference in New Issue
Block a user