diff --git a/extensions/bluebubbles/src/client.ts b/extensions/bluebubbles/src/client.ts index 3a65779ebc8..ff8052f2d10 100644 --- a/extensions/bluebubbles/src/client.ts +++ b/extensions/bluebubbles/src/client.ts @@ -11,13 +11,13 @@ // - #60715 BB health check fails on LAN/private serverUrl // - #66869 move `?password=` → header auth (future-proofed via AuthStrategy) +import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/account-id"; import { formatErrorMessage } from "openclaw/plugin-sdk/error-runtime"; import { isBlockedHostnameOrIp, type SsrFPolicy } from "openclaw/plugin-sdk/ssrf-runtime"; import { resolveBlueBubblesServerAccount } from "./account-resolve.js"; import { extractAttachments } from "./monitor-normalize.js"; import { postMultipartFormData } from "./multipart.js"; import { resolveRequestUrl } from "./request-url.js"; -import { DEFAULT_ACCOUNT_ID } from "./runtime-api.js"; import type { OpenClawConfig } from "./runtime-api.js"; import { getBlueBubblesRuntime } from "./runtime.js"; import { diff --git a/extensions/whatsapp/src/account-ids.ts b/extensions/whatsapp/src/account-ids.ts new file mode 100644 index 00000000000..5945e86551d --- /dev/null +++ b/extensions/whatsapp/src/account-ids.ts @@ -0,0 +1,13 @@ +import { createAccountListHelpers } from "openclaw/plugin-sdk/account-core"; + +const { + listConfiguredAccountIds, + listAccountIds, + resolveDefaultAccountId: resolveDefaultWhatsAppAccountId, +} = createAccountListHelpers("whatsapp"); + +export { + listConfiguredAccountIds, + listAccountIds as listWhatsAppAccountIds, + resolveDefaultWhatsAppAccountId, +}; diff --git a/extensions/whatsapp/src/accounts.ts b/extensions/whatsapp/src/accounts.ts index 97df8c8487b..58401ae3c42 100644 --- a/extensions/whatsapp/src/accounts.ts +++ b/extensions/whatsapp/src/accounts.ts @@ -1,7 +1,6 @@ import fs from "node:fs"; import path from "node:path"; import { - createAccountListHelpers, DEFAULT_ACCOUNT_ID, normalizeAccountId, resolveUserPath, @@ -11,9 +10,16 @@ import type { DmPolicy, GroupPolicy, ReplyToMode } from "openclaw/plugin-sdk/con import { resolveOAuthDir } from "openclaw/plugin-sdk/state-paths"; import { normalizeOptionalString } from "openclaw/plugin-sdk/text-runtime"; import { resolveMergedWhatsAppAccountConfig } from "./account-config.js"; +import { + listConfiguredAccountIds, + listWhatsAppAccountIds, + resolveDefaultWhatsAppAccountId, +} from "./account-ids.js"; import type { WhatsAppAccountConfig } from "./account-types.js"; import { hasWebCredsSync } from "./creds-files.js"; +export { listWhatsAppAccountIds, resolveDefaultWhatsAppAccountId } from "./account-ids.js"; + export type ResolvedWhatsAppAccount = { accountId: string; name?: string; @@ -43,11 +49,6 @@ export type ResolvedWhatsAppAccount = { export const DEFAULT_WHATSAPP_MEDIA_MAX_MB = 50; -const { listConfiguredAccountIds, listAccountIds, resolveDefaultAccountId } = - createAccountListHelpers("whatsapp"); -export const listWhatsAppAccountIds = listAccountIds; -export const resolveDefaultWhatsAppAccountId = resolveDefaultAccountId; - export function listWhatsAppAuthDirs(cfg: OpenClawConfig): string[] { const oauthDir = resolveOAuthDir(); const whatsappDir = path.join(oauthDir, "whatsapp"); diff --git a/extensions/whatsapp/src/active-listener.test.ts b/extensions/whatsapp/src/active-listener.test.ts index 185d0f97bed..860ad1055e7 100644 --- a/extensions/whatsapp/src/active-listener.test.ts +++ b/extensions/whatsapp/src/active-listener.test.ts @@ -1,4 +1,5 @@ -import { afterEach, describe, expect, it, vi } from "vitest"; +import { beforeEach, describe, expect, it, vi } from "vitest"; +import { getActiveWebListener, resolveWebAccountId } from "./active-listener.js"; vi.mock("openclaw/plugin-sdk/config-runtime", () => ({ loadConfig: () => ({ @@ -6,18 +7,19 @@ vi.mock("openclaw/plugin-sdk/config-runtime", () => ({ }), })); +const registryMocks = vi.hoisted(() => ({ + getRegisteredWhatsAppConnectionController: vi.fn(), +})); + +vi.mock("./connection-controller-registry.js", () => ({ + getRegisteredWhatsAppConnectionController: + registryMocks.getRegisteredWhatsAppConnectionController, +})); + const WHATSAPP_ACTIVE_LISTENER_TEST_CFG = { channels: { whatsapp: { accounts: { work: { enabled: true } }, defaultAccount: "work" } }, }; -type ActiveListenerModule = typeof import("./active-listener.js"); - -const activeListenerModuleUrl = new URL("./active-listener.ts", import.meta.url).href; - -async function importActiveListenerModule(cacheBust: string): Promise { - return (await import(`${activeListenerModuleUrl}?t=${cacheBust}`)) as ActiveListenerModule; -} - function makeListener() { return { sendMessage: vi.fn(async () => ({ messageId: "msg-1" })), @@ -27,53 +29,43 @@ function makeListener() { }; } -afterEach(() => { - vi.doUnmock("./connection-controller-registry.js"); +beforeEach(() => { + registryMocks.getRegisteredWhatsAppConnectionController.mockReset(); }); describe("active WhatsApp listener view", () => { - it("reads controller-backed state across duplicate module instances", async () => { + it("reads controller-backed state", () => { const listener = makeListener(); - vi.doMock("./connection-controller-registry.js", () => ({ - getRegisteredWhatsAppConnectionController: (accountId: string) => + registryMocks.getRegisteredWhatsAppConnectionController.mockImplementation( + (accountId: string) => accountId === "work" ? { getActiveListener: () => listener, } : null, - })); + ); - const first = await importActiveListenerModule(`first-${Date.now()}`); - const second = await importActiveListenerModule(`second-${Date.now()}`); - - expect(first.getActiveWebListener("work")).toBe(listener); - expect(second.getActiveWebListener("work")).toBe(listener); + expect(getActiveWebListener("work")).toBe(listener); }); - it("resolves the configured default account when accountId is omitted", async () => { + it("resolves the configured default account when accountId is omitted", () => { const listener = makeListener(); - vi.doMock("./connection-controller-registry.js", () => ({ - getRegisteredWhatsAppConnectionController: (accountId: string) => + registryMocks.getRegisteredWhatsAppConnectionController.mockImplementation( + (accountId: string) => accountId === "work" ? { getActiveListener: () => listener, } : null, - })); + ); - const mod = await importActiveListenerModule(`default-${Date.now()}`); - - expect(mod.resolveWebAccountId({ cfg: WHATSAPP_ACTIVE_LISTENER_TEST_CFG })).toBe("work"); - expect(mod.getActiveWebListener("work")).toBe(listener); + expect(resolveWebAccountId({ cfg: WHATSAPP_ACTIVE_LISTENER_TEST_CFG })).toBe("work"); + expect(getActiveWebListener("work")).toBe(listener); }); - it("returns null when the controller has no active listener for the account", async () => { - vi.doMock("./connection-controller-registry.js", () => ({ - getRegisteredWhatsAppConnectionController: () => null, - })); + it("returns null when the controller has no active listener for the account", () => { + registryMocks.getRegisteredWhatsAppConnectionController.mockReturnValue(null); - const mod = await importActiveListenerModule(`missing-${Date.now()}`); - - expect(mod.getActiveWebListener("work")).toBeNull(); + expect(getActiveWebListener("work")).toBeNull(); }); }); diff --git a/extensions/whatsapp/src/active-listener.ts b/extensions/whatsapp/src/active-listener.ts index 10afa52480d..26336c17c79 100644 --- a/extensions/whatsapp/src/active-listener.ts +++ b/extensions/whatsapp/src/active-listener.ts @@ -1,5 +1,5 @@ import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime"; -import { resolveDefaultWhatsAppAccountId } from "./accounts.js"; +import { resolveDefaultWhatsAppAccountId } from "./account-ids.js"; import { getRegisteredWhatsAppConnectionController } from "./connection-controller-registry.js"; import type { ActiveWebListener, ActiveWebSendOptions } from "./inbound/types.js"; diff --git a/extensions/whatsapp/src/channel.setup.test.ts b/extensions/whatsapp/src/channel.setup.test.ts index a7bcb9ee18e..b9533af4535 100644 --- a/extensions/whatsapp/src/channel.setup.test.ts +++ b/extensions/whatsapp/src/channel.setup.test.ts @@ -2,9 +2,8 @@ import { DEFAULT_ACCOUNT_ID } from "openclaw/plugin-sdk/routing"; import type { RuntimeEnv } from "openclaw/plugin-sdk/runtime-env"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { createQueuedWizardPrompter } from "../../../test/helpers/plugins/setup-wizard.js"; -import { whatsappApprovalAuth } from "./approval-auth.js"; import { WHATSAPP_AUTH_UNSTABLE_CODE } from "./auth-store.js"; -import { whatsappPlugin } from "./channel.js"; +import { whatsappSetupPlugin } from "./channel.setup.js"; import { checkWhatsAppHeartbeatReady } from "./heartbeat.js"; import type { OpenClawConfig } from "./runtime-api.js"; import { finalizeWhatsAppSetup } from "./setup-finalize.js"; @@ -160,13 +159,6 @@ describe("whatsapp setup wizard", () => { hoisted.resolveWhatsAppAuthDir.mockReturnValue({ authDir: "/tmp/openclaw-whatsapp-test" }); }); - it("exposes approval auth through approvalCapability only", () => { - expect(whatsappPlugin.approvalCapability).toBe(whatsappApprovalAuth); - expect(typeof whatsappPlugin.auth?.login).toBe("function"); - expect("authorizeActorAction" in (whatsappPlugin.auth ?? {})).toBe(false); - expect("getActionAvailabilityState" in (whatsappPlugin.auth ?? {})).toBe(false); - }); - it("applies owner allowlist when forceAllowFrom is enabled", async () => { const harness = createWhatsAppOwnerAllowlistHarness(createQueuedWizardPrompter); @@ -245,7 +237,7 @@ describe("whatsapp setup wizard", () => { }); it("surfaces accounts.default group warning paths for named accounts", () => { - const warnings = whatsappPlugin.security?.collectWarnings?.({ + const warnings = whatsappSetupPlugin.security?.collectWarnings?.({ cfg: { channels: { whatsapp: { @@ -277,7 +269,7 @@ describe("whatsapp setup wizard", () => { }); it("surfaces mixed-case default-account group warning paths for named accounts", () => { - const warnings = whatsappPlugin.security?.collectWarnings?.({ + const warnings = whatsappSetupPlugin.security?.collectWarnings?.({ cfg: { channels: { whatsapp: { @@ -458,7 +450,7 @@ describe("whatsapp setup wizard", () => { hoisted.readWebAuthState.mockResolvedValueOnce("unstable"); await expect( - whatsappPlugin.config.isConfigured?.( + whatsappSetupPlugin.config.isConfigured?.( { authDir: "/tmp/work", } as never, diff --git a/extensions/whatsapp/src/connection-controller-registry.test.ts b/extensions/whatsapp/src/connection-controller-registry.test.ts new file mode 100644 index 00000000000..1ae5778bb6b --- /dev/null +++ b/extensions/whatsapp/src/connection-controller-registry.test.ts @@ -0,0 +1,27 @@ +import { describe, expect, it, vi } from "vitest"; + +type RegistryModule = typeof import("./connection-controller-registry.js"); + +const registryModuleUrl = new URL("./connection-controller-registry.ts", import.meta.url).href; + +async function importRegistryModule(cacheBust: string): Promise { + return (await import(`${registryModuleUrl}?t=${cacheBust}`)) as RegistryModule; +} + +describe("WhatsApp connection controller registry", () => { + it("shares registered controllers across duplicate module instances", async () => { + const first = await importRegistryModule(`first-${Date.now()}`); + const second = await importRegistryModule(`second-${Date.now()}`); + const controller = { + getActiveListener: vi.fn(() => null), + }; + + first.registerWhatsAppConnectionController("work", controller); + + try { + expect(second.getRegisteredWhatsAppConnectionController("work")).toBe(controller); + } finally { + first.unregisterWhatsAppConnectionController("work", controller); + } + }); +});