From 0bcf07690137e6052f006ad9206e9bee0a736fd6 Mon Sep 17 00:00:00 2001 From: Tak Hoffman <781889+Takhoffman@users.noreply.github.com> Date: Fri, 27 Mar 2026 23:56:18 -0500 Subject: [PATCH] fix(regression): auto-enable channel status state --- .../server-methods/channels.status.test.ts | 117 ++++++++++++++++++ src/gateway/server-methods/channels.ts | 6 +- 2 files changed, 122 insertions(+), 1 deletion(-) create mode 100644 src/gateway/server-methods/channels.status.test.ts diff --git a/src/gateway/server-methods/channels.status.test.ts b/src/gateway/server-methods/channels.status.test.ts new file mode 100644 index 00000000000..81a566aa1f0 --- /dev/null +++ b/src/gateway/server-methods/channels.status.test.ts @@ -0,0 +1,117 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const mocks = vi.hoisted(() => ({ + loadConfig: vi.fn(), + applyPluginAutoEnable: vi.fn(), + listChannelPlugins: vi.fn(), + buildChannelUiCatalog: vi.fn(), + buildChannelAccountSnapshot: vi.fn(), + getChannelActivity: vi.fn(), +})); + +vi.mock("../../config/config.js", async () => { + const actual = + await vi.importActual("../../config/config.js"); + return { + ...actual, + loadConfig: mocks.loadConfig, + }; +}); + +vi.mock("../../config/plugin-auto-enable.js", () => ({ + applyPluginAutoEnable: mocks.applyPluginAutoEnable, +})); + +vi.mock("../../channels/plugins/index.js", () => ({ + listChannelPlugins: mocks.listChannelPlugins, + getChannelPlugin: vi.fn(), + normalizeChannelId: (value: string) => value, +})); + +vi.mock("../../channels/plugins/catalog.js", () => ({ + buildChannelUiCatalog: mocks.buildChannelUiCatalog, +})); + +vi.mock("../../channels/plugins/status.js", () => ({ + buildChannelAccountSnapshot: mocks.buildChannelAccountSnapshot, +})); + +vi.mock("../../infra/channel-activity.js", () => ({ + getChannelActivity: mocks.getChannelActivity, +})); + +import { channelsHandlers } from "./channels.js"; + +describe("channelsHandlers channels.status", () => { + beforeEach(() => { + vi.clearAllMocks(); + mocks.loadConfig.mockReturnValue({}); + mocks.applyPluginAutoEnable.mockImplementation(({ config }) => ({ config, changes: [] })); + mocks.buildChannelUiCatalog.mockReturnValue({ + order: ["whatsapp"], + labels: { whatsapp: "WhatsApp" }, + detailLabels: { whatsapp: "WhatsApp" }, + systemImages: { whatsapp: undefined }, + entries: { whatsapp: { id: "whatsapp" } }, + }); + mocks.buildChannelAccountSnapshot.mockResolvedValue({ + accountId: "default", + configured: true, + }); + mocks.getChannelActivity.mockReturnValue({ + inboundAt: null, + outboundAt: null, + }); + mocks.listChannelPlugins.mockReturnValue([ + { + id: "whatsapp", + config: { + listAccountIds: () => ["default"], + resolveAccount: () => ({}), + isEnabled: () => true, + isConfigured: async (_account: unknown, cfg: { autoEnabled?: boolean }) => + Boolean(cfg.autoEnabled), + }, + }, + ]); + }); + + it("uses the auto-enabled config snapshot for channel account state", async () => { + const autoEnabledConfig = { autoEnabled: true }; + mocks.applyPluginAutoEnable.mockReturnValue({ config: autoEnabledConfig, changes: [] }); + const respond = vi.fn(); + + await channelsHandlers["channels.status"]({ + params: { probe: false, timeoutMs: 2000 } as never, + respond, + context: { + getRuntimeSnapshot: () => ({ + channels: {}, + channelAccounts: {}, + }), + } as never, + }); + + expect(mocks.applyPluginAutoEnable).toHaveBeenCalledWith({ + config: {}, + env: process.env, + }); + expect(mocks.buildChannelAccountSnapshot).toHaveBeenCalledWith( + expect.objectContaining({ + cfg: autoEnabledConfig, + accountId: "default", + }), + ); + expect(respond).toHaveBeenCalledWith( + true, + expect.objectContaining({ + channels: { + whatsapp: expect.objectContaining({ + configured: true, + }), + }, + }), + undefined, + ); + }); +}); diff --git a/src/gateway/server-methods/channels.ts b/src/gateway/server-methods/channels.ts index d00d8726532..ef86a162c89 100644 --- a/src/gateway/server-methods/channels.ts +++ b/src/gateway/server-methods/channels.ts @@ -10,6 +10,7 @@ import { buildChannelAccountSnapshot } from "../../channels/plugins/status.js"; import type { ChannelAccountSnapshot, ChannelPlugin } from "../../channels/plugins/types.js"; import type { OpenClawConfig } from "../../config/config.js"; import { loadConfig, readConfigFileSnapshot } from "../../config/config.js"; +import { applyPluginAutoEnable } from "../../config/plugin-auto-enable.js"; import { getChannelActivity } from "../../infra/channel-activity.js"; import { DEFAULT_ACCOUNT_ID } from "../../routing/session-key.js"; import { defaultRuntime } from "../../runtime.js"; @@ -82,7 +83,10 @@ export const channelsHandlers: GatewayRequestHandlers = { const probe = (params as { probe?: boolean }).probe === true; const timeoutMsRaw = (params as { timeoutMs?: unknown }).timeoutMs; const timeoutMs = typeof timeoutMsRaw === "number" ? Math.max(1000, timeoutMsRaw) : 10_000; - const cfg = loadConfig(); + const cfg = applyPluginAutoEnable({ + config: loadConfig(), + env: process.env, + }).config; const runtime = context.getRuntimeSnapshot(); const plugins = listChannelPlugins(); const pluginMap = new Map(