import { afterEach, 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"; const ORIGINAL_OPENCLAW_NIX_MODE = process.env.OPENCLAW_NIX_MODE; 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(); }); afterEach(() => { if (ORIGINAL_OPENCLAW_NIX_MODE === undefined) { delete process.env.OPENCLAW_NIX_MODE; } else { process.env.OPENCLAW_NIX_MODE = ORIGINAL_OPENCLAW_NIX_MODE; } }); function mockPluginRegistry(ids: string[]) { buildPluginRegistrySnapshotReport.mockReturnValue({ plugins: ids.map((id) => ({ id })), diagnostics: [], registrySource: "derived", registryDiagnostics: [], }); } function requireFirstWrittenConfig(): OpenClawConfig { const [config] = writeConfigFile.mock.calls[0] ?? []; if (!config) { throw new Error("expected writeConfigFile to receive a config"); } return config; } function requirePluginEntries( config: OpenClawConfig, ): NonNullable["entries"]> { if (!config.plugins?.entries) { throw new Error("expected plugin entries in config"); } return config.plugins.entries; } it("refreshes the persisted plugin registry after enabling a plugin", async () => { const sourceConfig = {} as OpenClawConfig; const enabledConfig = { plugins: { entries: { alpha: { enabled: true }, }, }, } as OpenClawConfig; loadConfig.mockReturnValue(sourceConfig); enablePluginInConfig.mockReturnValue({ config: enabledConfig, enabled: true, pluginId: "alpha", }); mockPluginRegistry(["alpha"]); await runPluginsCommand(["plugins", "enable", "alpha"]); expect(enablePluginInConfig).toHaveBeenCalledWith(sourceConfig, "alpha", { updateChannelConfig: false, }); expect(writeConfigFile).toHaveBeenCalledWith(enabledConfig); expect(refreshPluginRegistry).toHaveBeenCalledWith({ config: enabledConfig, installRecords: {}, policyPluginIds: ["alpha"], reason: "policy-changed", }); }); it("refuses plugin enablement in Nix mode before config mutation", async () => { const previous = process.env.OPENCLAW_NIX_MODE; process.env.OPENCLAW_NIX_MODE = "1"; try { await expect(runPluginsCommand(["plugins", "enable", "alpha"])).rejects.toThrow( "OPENCLAW_NIX_MODE=1", ); } finally { if (previous === undefined) { delete process.env.OPENCLAW_NIX_MODE; } else { process.env.OPENCLAW_NIX_MODE = previous; } } expect(enablePluginInConfig).not.toHaveBeenCalled(); expect(writeConfigFile).not.toHaveBeenCalled(); }); it("refreshes the persisted plugin registry after disabling a plugin", async () => { loadConfig.mockReturnValue({ plugins: { entries: { alpha: { enabled: true }, }, }, } as OpenClawConfig); mockPluginRegistry(["alpha"]); await runPluginsCommand(["plugins", "disable", "alpha"]); const nextConfig = requireFirstWrittenConfig(); const entries = requirePluginEntries(nextConfig); expect(entries.alpha).toMatchObject({ enabled: false }); expect(refreshPluginRegistry).toHaveBeenCalledWith({ config: nextConfig, installRecords: {}, policyPluginIds: ["alpha"], 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, { updateChannelConfig: false, }); 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 = requireFirstWrittenConfig(); const entries = requirePluginEntries(nextConfig); expect(entries[pluginId]).toMatchObject({ enabled: false }); expect(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(); }, ); it("does not create a channel config when disabling a channel plugin by policy", async () => { loadConfig.mockReturnValue({} as OpenClawConfig); mockPluginRegistry(["twitch"]); await runPluginsCommand(["plugins", "disable", "twitch"]); const nextConfig = requireFirstWrittenConfig(); const entries = requirePluginEntries(nextConfig); expect(entries.twitch).toMatchObject({ enabled: false }); expect(nextConfig.channels?.twitch).toBeUndefined(); }); });