mirror of
https://github.com/openclaw/openclaw.git
synced 2026-05-06 18:10:45 +00:00
fix(cli): reject missing plugin ids before config writes
This commit is contained in:
@@ -1,19 +1,36 @@
|
||||
import { beforeEach, describe, expect, it } from "vitest";
|
||||
import type { OpenClawConfig } from "../config/config.js";
|
||||
import {
|
||||
buildPluginRegistrySnapshotReport,
|
||||
enablePluginInConfig,
|
||||
loadConfig,
|
||||
refreshPluginRegistry,
|
||||
resetPluginsCliTestState,
|
||||
runtimeErrors,
|
||||
runPluginsCommand,
|
||||
writeConfigFile,
|
||||
} from "./plugins-cli-test-helpers.js";
|
||||
|
||||
describe("plugins cli policy mutations", () => {
|
||||
const compatibilityPluginIds = [
|
||||
{ alias: "openai-codex", pluginId: "openai" },
|
||||
{ alias: "google-gemini-cli", pluginId: "google" },
|
||||
{ alias: "minimax-portal-auth", pluginId: "minimax" },
|
||||
] as const;
|
||||
|
||||
beforeEach(() => {
|
||||
resetPluginsCliTestState();
|
||||
});
|
||||
|
||||
function mockPluginRegistry(ids: string[]) {
|
||||
buildPluginRegistrySnapshotReport.mockReturnValue({
|
||||
plugins: ids.map((id) => ({ id })),
|
||||
diagnostics: [],
|
||||
registrySource: "derived",
|
||||
registryDiagnostics: [],
|
||||
});
|
||||
}
|
||||
|
||||
it("refreshes the persisted plugin registry after enabling a plugin", async () => {
|
||||
const enabledConfig = {
|
||||
plugins: {
|
||||
@@ -28,6 +45,7 @@ describe("plugins cli policy mutations", () => {
|
||||
enabled: true,
|
||||
pluginId: "alpha",
|
||||
});
|
||||
mockPluginRegistry(["alpha"]);
|
||||
|
||||
await runPluginsCommand(["plugins", "enable", "alpha"]);
|
||||
|
||||
@@ -48,6 +66,7 @@ describe("plugins cli policy mutations", () => {
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig);
|
||||
mockPluginRegistry(["alpha"]);
|
||||
|
||||
await runPluginsCommand(["plugins", "disable", "alpha"]);
|
||||
|
||||
@@ -60,4 +79,67 @@ describe("plugins cli policy mutations", () => {
|
||||
reason: "policy-changed",
|
||||
});
|
||||
});
|
||||
|
||||
it.each(compatibilityPluginIds)(
|
||||
"enables compatibility id $alias through canonical plugin $pluginId",
|
||||
async ({ alias, pluginId }) => {
|
||||
const sourceConfig = {} as OpenClawConfig;
|
||||
const enabledConfig = {
|
||||
plugins: {
|
||||
entries: {
|
||||
[pluginId]: { enabled: true },
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig;
|
||||
loadConfig.mockReturnValue(sourceConfig);
|
||||
enablePluginInConfig.mockReturnValue({
|
||||
config: enabledConfig,
|
||||
enabled: true,
|
||||
});
|
||||
mockPluginRegistry([pluginId]);
|
||||
|
||||
await runPluginsCommand(["plugins", "enable", alias]);
|
||||
|
||||
expect(enablePluginInConfig).toHaveBeenCalledWith(sourceConfig, pluginId);
|
||||
expect(writeConfigFile).toHaveBeenCalledWith(enabledConfig);
|
||||
},
|
||||
);
|
||||
|
||||
it.each(compatibilityPluginIds)(
|
||||
"disables compatibility id $alias through canonical plugin $pluginId",
|
||||
async ({ alias, pluginId }) => {
|
||||
loadConfig.mockReturnValue({
|
||||
plugins: {
|
||||
entries: {
|
||||
[pluginId]: { enabled: true },
|
||||
},
|
||||
},
|
||||
} as OpenClawConfig);
|
||||
mockPluginRegistry([pluginId]);
|
||||
|
||||
await runPluginsCommand(["plugins", "disable", alias]);
|
||||
|
||||
const nextConfig = writeConfigFile.mock.calls[0]?.[0] as OpenClawConfig;
|
||||
expect(nextConfig.plugins?.entries?.[pluginId]?.enabled).toBe(false);
|
||||
expect(nextConfig.plugins?.entries?.[alias]).toBeUndefined();
|
||||
},
|
||||
);
|
||||
|
||||
it.each(["enable", "disable"] as const)(
|
||||
"rejects %s for a plugin that is not discovered",
|
||||
async (command) => {
|
||||
mockPluginRegistry(["alpha"]);
|
||||
|
||||
await expect(runPluginsCommand(["plugins", command, "missing-plugin"])).rejects.toThrow(
|
||||
"__exit__:1",
|
||||
);
|
||||
|
||||
expect(runtimeErrors).toContain(
|
||||
"Plugin not found: missing-plugin. Run `openclaw plugins list` to see installed plugins.",
|
||||
);
|
||||
expect(enablePluginInConfig).not.toHaveBeenCalled();
|
||||
expect(writeConfigFile).not.toHaveBeenCalled();
|
||||
expect(refreshPluginRegistry).not.toHaveBeenCalled();
|
||||
},
|
||||
);
|
||||
});
|
||||
|
||||
@@ -51,6 +51,17 @@ function formatRegistryState(state: "missing" | "fresh" | "stale"): string {
|
||||
return theme.warn(state);
|
||||
}
|
||||
|
||||
function reportMissingPlugin(id: string) {
|
||||
defaultRuntime.error(
|
||||
`Plugin not found: ${id}. Run \`openclaw plugins list\` to see installed plugins.`,
|
||||
);
|
||||
return defaultRuntime.exit(1);
|
||||
}
|
||||
|
||||
function matchesPluginId(plugin: { id: string }, id: string) {
|
||||
return plugin.id === id;
|
||||
}
|
||||
|
||||
export function registerPluginsCli(program: Command) {
|
||||
const plugins = program
|
||||
.command("plugins")
|
||||
@@ -102,12 +113,19 @@ export function registerPluginsCli(program: Command) {
|
||||
.argument("<id>", "Plugin id")
|
||||
.action(async (id: string) => {
|
||||
const { enablePluginInConfig } = await import("../plugins/enable.js");
|
||||
const { normalizePluginId } = await import("../plugins/config-state.js");
|
||||
const { buildPluginRegistrySnapshotReport } = await import("../plugins/status.js");
|
||||
const { applySlotSelectionForPlugin, logSlotWarnings } =
|
||||
await import("./plugins-command-helpers.js");
|
||||
const { refreshPluginRegistryAfterConfigMutation } =
|
||||
await import("./plugins-registry-refresh.js");
|
||||
const snapshot = await readConfigFileSnapshot();
|
||||
const cfg = (snapshot.sourceConfig ?? snapshot.config) as OpenClawConfig;
|
||||
const report = buildPluginRegistrySnapshotReport({ config: cfg });
|
||||
id = normalizePluginId(id);
|
||||
if (!report.plugins.some((plugin) => matchesPluginId(plugin, id))) {
|
||||
return reportMissingPlugin(id);
|
||||
}
|
||||
const enableResult = enablePluginInConfig(cfg, id);
|
||||
let next: OpenClawConfig = enableResult.config;
|
||||
const slotResult = applySlotSelectionForPlugin(next, id);
|
||||
@@ -141,11 +159,18 @@ export function registerPluginsCli(program: Command) {
|
||||
.description("Disable a plugin in config")
|
||||
.argument("<id>", "Plugin id")
|
||||
.action(async (id: string) => {
|
||||
const { normalizePluginId } = await import("../plugins/config-state.js");
|
||||
const { buildPluginRegistrySnapshotReport } = await import("../plugins/status.js");
|
||||
const { setPluginEnabledInConfig } = await import("./plugins-config.js");
|
||||
const { refreshPluginRegistryAfterConfigMutation } =
|
||||
await import("./plugins-registry-refresh.js");
|
||||
const snapshot = await readConfigFileSnapshot();
|
||||
const cfg = (snapshot.sourceConfig ?? snapshot.config) as OpenClawConfig;
|
||||
const report = buildPluginRegistrySnapshotReport({ config: cfg });
|
||||
id = normalizePluginId(id);
|
||||
if (!report.plugins.some((plugin) => matchesPluginId(plugin, id))) {
|
||||
return reportMissingPlugin(id);
|
||||
}
|
||||
const next = setPluginEnabledInConfig(cfg, id, false);
|
||||
await replaceConfigFile({
|
||||
nextConfig: next,
|
||||
|
||||
Reference in New Issue
Block a user