fix(whatsapp): restart listener on selfChatMode config change (#93873)

Merged via squash.

Prepared head SHA: d85f604f01
Co-authored-by: xialonglee <22994703+xialonglee@users.noreply.github.com>
Co-authored-by: steipete <58493+steipete@users.noreply.github.com>
Reviewed-by: @steipete
This commit is contained in:
Peter Lee
2026-06-19 07:41:26 -05:00
committed by GitHub
parent 7fafad8c49
commit 5c8761976c
4 changed files with 61 additions and 6 deletions

View File

@@ -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"],
});
});

View File

@@ -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" }],

View File

@@ -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);

View File

@@ -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<void>;
stop: (channel: ChannelKind) => Promise<void>;
},
) {
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<GatewayPluginReloadResult> => ({
@@ -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();