@@ -5,8 +5,13 @@ import { afterEach, describe, expect, it, vi } from "vitest";
55import type { ConfigWriteNotification } from "../config/config.js" ;
66import type { OpenClawConfig } from "../config/types.openclaw.js" ;
77import { consumeGatewaySigusr1RestartIntent } from "../infra/restart.js" ;
8+ import {
9+ pinActivePluginChannelRegistry ,
10+ releasePinnedPluginChannelRegistry ,
11+ } from "../plugins/runtime.js" ;
812import { createEmptyRuntimeWebToolsMetadata } from "../secrets/runtime-fast-path.js" ;
913import { activateSecretsRuntimeSnapshot , clearSecretsRuntimeSnapshot } from "../secrets/runtime.js" ;
14+ import { createChannelTestPluginBase , createTestRegistry } from "../test-utils/channel-plugins.js" ;
1015import { diffConfigPaths } from "./config-diff.js" ;
1116import {
1217 buildGatewayReloadPlan ,
@@ -141,7 +146,13 @@ vi.mock("../agents/agent-bundle-mcp-tools.js", () => ({
141146 disposeAllSessionMcpRuntimes : hoisted . disposeAllSessionMcpRuntimes ,
142147} ) ) ;
143148
144- function createReloadHandlersForTest ( logReload = { info : vi . fn ( ) , warn : vi . fn ( ) } ) {
149+ function createReloadHandlersForTest (
150+ logReload = { info : vi . fn ( ) , warn : vi . fn ( ) } ,
151+ channels ?: {
152+ start : ( channel : ChannelKind ) => Promise < void > ;
153+ stop : ( channel : ChannelKind ) => Promise < void > ;
154+ } ,
155+ ) {
145156 const cron = { start : vi . fn ( async ( ) => { } ) , stop : vi . fn ( ) } ;
146157 const heartbeatRunner = {
147158 stop : vi . fn ( ) ,
@@ -158,8 +169,8 @@ function createReloadHandlersForTest(logReload = { info: vi.fn(), warn: vi.fn()
158169 channelHealthMonitor : null ,
159170 } ) ,
160171 setState : vi . fn ( ) ,
161- startChannel : vi . fn ( async ( ) => { } ) ,
162- stopChannel : vi . fn ( async ( ) => { } ) ,
172+ startChannel : channels ?. start ?? vi . fn ( async ( ) => { } ) ,
173+ stopChannel : channels ?. stop ?? vi . fn ( async ( ) => { } ) ,
163174 stopPostReadySidecars : vi . fn ( ) ,
164175 reloadPlugins : vi . fn (
165176 async ( ) : Promise < GatewayPluginReloadResult > => ( {
@@ -889,6 +900,42 @@ describe("gateway channel hot reload handlers", () => {
889900 }
890901 }
891902
903+ it ( "restarts WhatsApp when the planner receives a selfChatMode change" , async ( ) => {
904+ const whatsappPlugin = {
905+ ...createChannelTestPluginBase ( { id : "whatsapp" } ) ,
906+ reload : {
907+ configPrefixes : [ "web" , "channels.whatsapp.accounts" , "channels.whatsapp.selfChatMode" ] ,
908+ noopPrefixes : [ "channels.whatsapp" ] ,
909+ } ,
910+ } ;
911+ const registry = createTestRegistry ( [
912+ { pluginId : "whatsapp" , plugin : whatsappPlugin , source : "test" } ,
913+ ] ) ;
914+ const events : string [ ] = [ ] ;
915+ const channels = {
916+ stop : vi . fn ( async ( channel : ChannelKind ) => {
917+ events . push ( `stop:${ channel } ` ) ;
918+ } ) ,
919+ start : vi . fn ( async ( channel : ChannelKind ) => {
920+ events . push ( `start:${ channel } ` ) ;
921+ } ) ,
922+ } ;
923+
924+ pinActivePluginChannelRegistry ( registry ) ;
925+ try {
926+ const plan = buildGatewayReloadPlan ( [ "channels.whatsapp.selfChatMode" ] ) ;
927+ const { applyHotReload } = createReloadHandlersForTest ( undefined , channels ) ;
928+
929+ expect ( plan . restartGateway ) . toBe ( false ) ;
930+ expect ( plan . restartChannels ) . toEqual ( new Set ( [ "whatsapp" ] ) ) ;
931+ await withChannelReloadsEnabled ( ( ) => applyHotReload ( plan , { } ) ) ;
932+
933+ expect ( events ) . toEqual ( [ "stop:whatsapp" , "start:whatsapp" ] ) ;
934+ } finally {
935+ releasePinnedPluginChannelRegistry ( registry ) ;
936+ }
937+ } ) ;
938+
892939 it ( "continues restarting later channels after a hot-reload stop failure" , async ( ) => {
893940 const events : string [ ] = [ ] ;
894941 const setState = vi . fn ( ) ;
0 commit comments