From 5c8761976ceb9fc2ff4dc262429ae05cb17bd922 Mon Sep 17 00:00:00 2001 From: Peter Lee Date: Fri, 19 Jun 2026 07:41:26 -0500 Subject: [PATCH] fix(whatsapp): restart listener on selfChatMode config change (#93873) Merged via squash. Prepared head SHA: d85f604f01111d2105b8d716a011ad536766a294 Co-authored-by: xialonglee <22994703+xialonglee@users.noreply.github.com> Co-authored-by: steipete <58493+steipete@users.noreply.github.com> Reviewed-by: @steipete --- extensions/whatsapp/index.test.ts | 2 +- extensions/whatsapp/src/shared.ts | 2 +- src/gateway/config-reload.test.ts | 10 +++- src/gateway/server-reload-handlers.test.ts | 53 ++++++++++++++++++++-- 4 files changed, 61 insertions(+), 6 deletions(-) diff --git a/extensions/whatsapp/index.test.ts b/extensions/whatsapp/index.test.ts index 81e697d8eaa1..b56b22e3b17f 100644 --- a/extensions/whatsapp/index.test.ts +++ b/extensions/whatsapp/index.test.ts @@ -15,7 +15,7 @@ describe("whatsapp bundled entries", () => { it("declares account config as channel-restart reload metadata", () => { expect(whatsappPlugin.reload).toEqual({ - configPrefixes: ["web", "channels.whatsapp.accounts"], + configPrefixes: ["web", "channels.whatsapp.accounts", "channels.whatsapp.selfChatMode"], noopPrefixes: ["channels.whatsapp"], }); }); diff --git a/extensions/whatsapp/src/shared.ts b/extensions/whatsapp/src/shared.ts index c0b8591be2de..441adcac3394 100644 --- a/extensions/whatsapp/src/shared.ts +++ b/extensions/whatsapp/src/shared.ts @@ -181,7 +181,7 @@ export function createWhatsAppPluginBase(params: { // the broad `channels.whatsapp` noop prefix below otherwise swallows it as a // hot no-op and leaves the account connected until a full restart. reload: { - configPrefixes: ["web", "channels.whatsapp.accounts"], + configPrefixes: ["web", "channels.whatsapp.accounts", "channels.whatsapp.selfChatMode"], noopPrefixes: ["channels.whatsapp"], }, gatewayMethodDescriptors: [{ name: "web.login.start" }, { name: "web.login.wait" }], diff --git a/src/gateway/config-reload.test.ts b/src/gateway/config-reload.test.ts index aba0ccaff74e..048a1d8efbb0 100644 --- a/src/gateway/config-reload.test.ts +++ b/src/gateway/config-reload.test.ts @@ -160,7 +160,7 @@ describe("buildGatewayReloadPlan", () => { resolveAccount: () => ({}), }, reload: { - configPrefixes: ["web", "channels.whatsapp.accounts"], + configPrefixes: ["web", "channels.whatsapp.accounts", "channels.whatsapp.selfChatMode"], noopPrefixes: ["channels.whatsapp"], }, }; @@ -235,6 +235,14 @@ describe("buildGatewayReloadPlan", () => { expect(plan.noopPaths).toStrictEqual([]); }); + it("restarts the WhatsApp channel when selfChatMode changes (configPrefix wins over broad noop prefix)", () => { + const plan = buildGatewayReloadPlan(["channels.whatsapp.selfChatMode"]); + expect(plan.restartGateway).toBe(false); + expect(plan.restartChannels).toEqual(new Set(["whatsapp"])); + expect(plan.hotReasons).toContain("channels.whatsapp.selfChatMode"); + expect(plan.noopPaths).toStrictEqual([]); + }); + it("keeps other channels.whatsapp.* changes as hot no-ops", () => { const plan = buildGatewayReloadPlan(["channels.whatsapp.replyToMode"]); expect(plan.restartGateway).toBe(false); diff --git a/src/gateway/server-reload-handlers.test.ts b/src/gateway/server-reload-handlers.test.ts index 8232fb86ebaf..c92b0eb50005 100644 --- a/src/gateway/server-reload-handlers.test.ts +++ b/src/gateway/server-reload-handlers.test.ts @@ -5,8 +5,13 @@ import { afterEach, describe, expect, it, vi } from "vitest"; import type { ConfigWriteNotification } from "../config/config.js"; import type { OpenClawConfig } from "../config/types.openclaw.js"; import { consumeGatewaySigusr1RestartIntent } from "../infra/restart.js"; +import { + pinActivePluginChannelRegistry, + releasePinnedPluginChannelRegistry, +} from "../plugins/runtime.js"; import { createEmptyRuntimeWebToolsMetadata } from "../secrets/runtime-fast-path.js"; import { activateSecretsRuntimeSnapshot, clearSecretsRuntimeSnapshot } from "../secrets/runtime.js"; +import { createChannelTestPluginBase, createTestRegistry } from "../test-utils/channel-plugins.js"; import { diffConfigPaths } from "./config-diff.js"; import { buildGatewayReloadPlan, @@ -141,7 +146,13 @@ vi.mock("../agents/agent-bundle-mcp-tools.js", () => ({ disposeAllSessionMcpRuntimes: hoisted.disposeAllSessionMcpRuntimes, })); -function createReloadHandlersForTest(logReload = { info: vi.fn(), warn: vi.fn() }) { +function createReloadHandlersForTest( + logReload = { info: vi.fn(), warn: vi.fn() }, + channels?: { + start: (channel: ChannelKind) => Promise; + stop: (channel: ChannelKind) => Promise; + }, +) { const cron = { start: vi.fn(async () => {}), stop: vi.fn() }; const heartbeatRunner = { stop: vi.fn(), @@ -158,8 +169,8 @@ function createReloadHandlersForTest(logReload = { info: vi.fn(), warn: vi.fn() channelHealthMonitor: null, }), setState: vi.fn(), - startChannel: vi.fn(async () => {}), - stopChannel: vi.fn(async () => {}), + startChannel: channels?.start ?? vi.fn(async () => {}), + stopChannel: channels?.stop ?? vi.fn(async () => {}), stopPostReadySidecars: vi.fn(), reloadPlugins: vi.fn( async (): Promise => ({ @@ -889,6 +900,42 @@ describe("gateway channel hot reload handlers", () => { } } + it("restarts WhatsApp when the planner receives a selfChatMode change", async () => { + const whatsappPlugin = { + ...createChannelTestPluginBase({ id: "whatsapp" }), + reload: { + configPrefixes: ["web", "channels.whatsapp.accounts", "channels.whatsapp.selfChatMode"], + noopPrefixes: ["channels.whatsapp"], + }, + }; + const registry = createTestRegistry([ + { pluginId: "whatsapp", plugin: whatsappPlugin, source: "test" }, + ]); + const events: string[] = []; + const channels = { + stop: vi.fn(async (channel: ChannelKind) => { + events.push(`stop:${channel}`); + }), + start: vi.fn(async (channel: ChannelKind) => { + events.push(`start:${channel}`); + }), + }; + + pinActivePluginChannelRegistry(registry); + try { + const plan = buildGatewayReloadPlan(["channels.whatsapp.selfChatMode"]); + const { applyHotReload } = createReloadHandlersForTest(undefined, channels); + + expect(plan.restartGateway).toBe(false); + expect(plan.restartChannels).toEqual(new Set(["whatsapp"])); + await withChannelReloadsEnabled(() => applyHotReload(plan, {})); + + expect(events).toEqual(["stop:whatsapp", "start:whatsapp"]); + } finally { + releasePinnedPluginChannelRegistry(registry); + } + }); + it("continues restarting later channels after a hot-reload stop failure", async () => { const events: string[] = []; const setState = vi.fn();