From 74f243a0d0b95591b58e7c3d83df23fc3d5c8a75 Mon Sep 17 00:00:00 2001 From: Vincent Koc Date: Sun, 3 May 2026 00:19:37 -0700 Subject: [PATCH] fix(cli): keep plugin toggles out of channel config (#76525) --- CHANGELOG.md | 1 + src/cli/plugins-cli-test-helpers.ts | 12 ++++++++---- src/cli/plugins-cli.policy.test.ts | 21 +++++++++++++++++++-- src/cli/plugins-cli.ts | 8 ++++++-- 4 files changed, 34 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index de2e81ed0d3..3f99d1f2d49 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -19,6 +19,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- CLI/plugins: keep `plugins enable` and `plugins disable` from creating unconfigured channel config sections, so channel plugins with required setup fields no longer fail validation during lifecycle probes. Thanks @vincentkoc. - Agents/sessions: keep delayed `sessions_send` A2A replies alive after soft wait-window timeouts, while preserving terminal run timeouts and avoiding stale target replies in requester sessions. Fixes #76443. Thanks @ryswork1993 and @vincentkoc. - CLI/sessions: keep intentional empty agent replies silent after tool-delivered channel output, instead of surfacing a misleading "No reply from agent." fallback. Thanks @vincentkoc. - Config/doctor: cap `.clobbered.*` forensic snapshots per config path and serialize snapshot writes so repeated `doctor --fix` recovery loops cannot flood the config directory. Fixes #76454; carries forward #65649. Thanks @JUSTICEESSIELP, @rsnow, and @vincentkoc. diff --git a/src/cli/plugins-cli-test-helpers.ts b/src/cli/plugins-cli-test-helpers.ts index 4de1bbe1389..1eef4d23caf 100644 --- a/src/cli/plugins-cli-test-helpers.ts +++ b/src/cli/plugins-cli-test-helpers.ts @@ -198,11 +198,15 @@ vi.mock("../plugins/marketplace.js", () => ({ })); vi.mock("../plugins/enable.js", () => ({ - enablePluginInConfig: ((cfg: OpenClawConfig, pluginId: string) => - invokeMock<[OpenClawConfig, string], unknown>( + enablePluginInConfig: (( + ...args: Parameters<(typeof import("../plugins/enable.js"))["enablePluginInConfig"]> + ) => + invokeMock< + Parameters<(typeof import("../plugins/enable.js"))["enablePluginInConfig"]>, + unknown + >( enablePluginInConfig, - cfg, - pluginId, + ...args, )) as (typeof import("../plugins/enable.js"))["enablePluginInConfig"], })); diff --git a/src/cli/plugins-cli.policy.test.ts b/src/cli/plugins-cli.policy.test.ts index e5a88721e60..64afd5591a5 100644 --- a/src/cli/plugins-cli.policy.test.ts +++ b/src/cli/plugins-cli.policy.test.ts @@ -32,6 +32,7 @@ describe("plugins cli policy mutations", () => { } it("refreshes the persisted plugin registry after enabling a plugin", async () => { + const sourceConfig = {} as OpenClawConfig; const enabledConfig = { plugins: { entries: { @@ -39,7 +40,7 @@ describe("plugins cli policy mutations", () => { }, }, } as OpenClawConfig; - loadConfig.mockReturnValue({} as OpenClawConfig); + loadConfig.mockReturnValue(sourceConfig); enablePluginInConfig.mockReturnValue({ config: enabledConfig, enabled: true, @@ -49,6 +50,9 @@ describe("plugins cli policy mutations", () => { await runPluginsCommand(["plugins", "enable", "alpha"]); + expect(enablePluginInConfig).toHaveBeenCalledWith(sourceConfig, "alpha", { + updateChannelConfig: false, + }); expect(writeConfigFile).toHaveBeenCalledWith(enabledConfig); expect(refreshPluginRegistry).toHaveBeenCalledWith({ config: enabledConfig, @@ -100,7 +104,9 @@ describe("plugins cli policy mutations", () => { await runPluginsCommand(["plugins", "enable", alias]); - expect(enablePluginInConfig).toHaveBeenCalledWith(sourceConfig, pluginId); + expect(enablePluginInConfig).toHaveBeenCalledWith(sourceConfig, pluginId, { + updateChannelConfig: false, + }); expect(writeConfigFile).toHaveBeenCalledWith(enabledConfig); }, ); @@ -142,4 +148,15 @@ describe("plugins cli policy mutations", () => { 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 = writeConfigFile.mock.calls[0]?.[0] as OpenClawConfig; + expect(nextConfig.plugins?.entries?.twitch?.enabled).toBe(false); + expect(nextConfig.channels?.twitch).toBeUndefined(); + }); }); diff --git a/src/cli/plugins-cli.ts b/src/cli/plugins-cli.ts index 0c11b84f7da..631271189e5 100644 --- a/src/cli/plugins-cli.ts +++ b/src/cli/plugins-cli.ts @@ -126,7 +126,9 @@ export function registerPluginsCli(program: Command) { if (!report.plugins.some((plugin) => matchesPluginId(plugin, id))) { return reportMissingPlugin(id); } - const enableResult = enablePluginInConfig(cfg, id); + const enableResult = enablePluginInConfig(cfg, id, { + updateChannelConfig: false, + }); let next: OpenClawConfig = enableResult.config; const slotResult = applySlotSelectionForPlugin(next, id); next = slotResult.config; @@ -171,7 +173,9 @@ export function registerPluginsCli(program: Command) { if (!report.plugins.some((plugin) => matchesPluginId(plugin, id))) { return reportMissingPlugin(id); } - const next = setPluginEnabledInConfig(cfg, id, false); + const next = setPluginEnabledInConfig(cfg, id, false, { + updateChannelConfig: false, + }); await replaceConfigFile({ nextConfig: next, ...(snapshot.hash !== undefined ? { baseHash: snapshot.hash } : {}),