From 91618438bc9c2abf8db6a160a38ec20587eb11b1 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 3 Apr 2026 14:02:42 +0100 Subject: [PATCH] test: narrow googlechat channel test deps --- .../googlechat/src/channel.deps.runtime.ts | 29 +++ extensions/googlechat/src/channel.test.ts | 198 +++++++++++++----- extensions/googlechat/src/channel.ts | 30 ++- 3 files changed, 190 insertions(+), 67 deletions(-) create mode 100644 extensions/googlechat/src/channel.deps.runtime.ts diff --git a/extensions/googlechat/src/channel.deps.runtime.ts b/extensions/googlechat/src/channel.deps.runtime.ts new file mode 100644 index 00000000000..e8c4adf0880 --- /dev/null +++ b/extensions/googlechat/src/channel.deps.runtime.ts @@ -0,0 +1,29 @@ +export { + buildChannelConfigSchema, + chunkTextForOutbound, + createAccountStatusSink, + DEFAULT_ACCOUNT_ID, + fetchRemoteMedia, + getChatChannelMeta, + GoogleChatConfigSchema, + loadOutboundMediaFromUrl, + missingTargetError, + PAIRING_APPROVED_MESSAGE, + resolveChannelMediaMaxBytes, + runPassiveAccountLifecycle, + type ChannelMessageActionAdapter, + type ChannelStatusIssue, + type OpenClawConfig, +} from "../runtime-api.js"; +export { + listGoogleChatAccountIds, + resolveDefaultGoogleChatAccountId, + resolveGoogleChatAccount, + type ResolvedGoogleChatAccount, +} from "./accounts.js"; +export { + isGoogleChatSpaceTarget, + isGoogleChatUserTarget, + normalizeGoogleChatTarget, + resolveGoogleChatOutboundSpace, +} from "./targets.js"; diff --git a/extensions/googlechat/src/channel.test.ts b/extensions/googlechat/src/channel.test.ts index 40a535d7e64..54d0cb96cd0 100644 --- a/extensions/googlechat/src/channel.test.ts +++ b/extensions/googlechat/src/channel.test.ts @@ -9,66 +9,170 @@ const uploadGoogleChatAttachmentMock = vi.hoisted(() => vi.fn()); const sendGoogleChatMessageMock = vi.hoisted(() => vi.fn()); const resolveGoogleChatAccountMock = vi.hoisted(() => vi.fn()); const resolveGoogleChatOutboundSpaceMock = vi.hoisted(() => vi.fn()); -const loadWebMediaMock = vi.hoisted(() => vi.fn()); const fetchRemoteMediaMock = vi.hoisted(() => vi.fn()); const loadOutboundMediaFromUrlMock = vi.hoisted(() => vi.fn()); +const probeGoogleChatMock = vi.hoisted(() => vi.fn()); +const startGoogleChatMonitorMock = vi.hoisted(() => vi.fn()); -vi.mock("./api.js", async (importOriginal) => { - const actual = await importOriginal(); +const DEFAULT_ACCOUNT_ID = "default"; + +function normalizeGoogleChatTarget(raw?: string | null): string | undefined { + const trimmed = raw?.trim(); + if (!trimmed) { + return undefined; + } + const withoutPrefix = trimmed.replace(/^(googlechat|google-chat|gchat):/i, ""); + const normalized = withoutPrefix + .replace(/^user:(users\/)?/i, "users/") + .replace(/^space:(spaces\/)?/i, "spaces/"); + if (normalized.toLowerCase().startsWith("users/")) { + const suffix = normalized.slice("users/".length); + return suffix.includes("@") ? `users/${suffix.toLowerCase()}` : normalized; + } + if (normalized.toLowerCase().startsWith("spaces/")) { + return normalized; + } + if (normalized.includes("@")) { + return `users/${normalized.toLowerCase()}`; + } + return normalized; +} + +function resolveGoogleChatAccountImpl(params: { cfg: OpenClawConfig; accountId?: string | null }) { + const accountId = params.accountId?.trim() || DEFAULT_ACCOUNT_ID; + const channelConfig = (params.cfg.channels?.googlechat ?? {}) as Record; + const accounts = + (channelConfig.accounts as Record> | undefined) ?? {}; + const scoped = accountId === DEFAULT_ACCOUNT_ID ? {} : (accounts[accountId] ?? {}); + const config = { ...channelConfig, ...scoped } as Record; + const serviceAccount = config.serviceAccount; return { - ...actual, - sendGoogleChatMessage: sendGoogleChatMessageMock, - uploadGoogleChatAttachment: uploadGoogleChatAttachmentMock, + accountId, + name: typeof config.name === "string" ? config.name : undefined, + enabled: channelConfig.enabled !== false && scoped.enabled !== false, + config, + credentialSource: serviceAccount ? "inline" : "none", + }; +} + +vi.mock("./channel.runtime.js", () => { + return { + googleChatChannelRuntime: { + probeGoogleChat: (...args: unknown[]) => probeGoogleChatMock(...args), + resolveGoogleChatWebhookPath: () => "/googlechat/webhook", + sendGoogleChatMessage: (...args: unknown[]) => sendGoogleChatMessageMock(...args), + startGoogleChatMonitor: (...args: unknown[]) => startGoogleChatMonitorMock(...args), + uploadGoogleChatAttachment: (...args: unknown[]) => uploadGoogleChatAttachmentMock(...args), + }, }; }); -vi.mock("./accounts.js", async (importOriginal) => { - const actual = await importOriginal(); +vi.mock("./channel.deps.runtime.js", () => { return { - ...actual, - resolveGoogleChatAccount: resolveGoogleChatAccountMock, + DEFAULT_ACCOUNT_ID: "default", + GoogleChatConfigSchema: {}, + buildChannelConfigSchema: () => ({}), + chunkTextForOutbound: (text: string, maxChars: number) => { + const words = text.split(/\s+/).filter(Boolean); + const chunks: string[] = []; + let current = ""; + for (const word of words) { + const next = current ? `${current} ${word}` : word; + if (current && next.length > maxChars) { + chunks.push(current); + current = word; + continue; + } + current = next; + } + if (current) { + chunks.push(current); + } + return chunks; + }, + createAccountStatusSink: () => () => {}, + fetchRemoteMedia: (...args: unknown[]) => fetchRemoteMediaMock(...args), + getChatChannelMeta: (id: string) => ({ id, name: id }), + isGoogleChatSpaceTarget: (value: string) => value.toLowerCase().startsWith("spaces/"), + isGoogleChatUserTarget: (value: string) => value.toLowerCase().startsWith("users/"), + listGoogleChatAccountIds: (cfg: OpenClawConfig) => { + const ids = Object.keys(cfg.channels?.googlechat?.accounts ?? {}); + return ids.length > 0 ? ids : ["default"]; + }, + loadOutboundMediaFromUrl: (...args: unknown[]) => loadOutboundMediaFromUrlMock(...args), + missingTargetError: (channel: string, hint: string) => + new Error(`${channel} target is required (${hint})`), + normalizeGoogleChatTarget, + PAIRING_APPROVED_MESSAGE: "approved", + resolveChannelMediaMaxBytes: (params: { + cfg: OpenClawConfig; + resolveChannelLimitMb: (args: { + cfg: OpenClawConfig; + accountId?: string; + }) => number | undefined; + accountId?: string; + }) => { + const limitMb = params.resolveChannelLimitMb({ + cfg: params.cfg, + accountId: params.accountId, + }); + return typeof limitMb === "number" ? limitMb * 1024 * 1024 : undefined; + }, + resolveDefaultGoogleChatAccountId: () => "default", + resolveGoogleChatAccount: (...args: Parameters) => + resolveGoogleChatAccountMock(...args), + resolveGoogleChatOutboundSpace: (...args: unknown[]) => + resolveGoogleChatOutboundSpaceMock(...args), + runPassiveAccountLifecycle: async (params: { start: () => Promise }) => + await params.start(), }; }); -vi.mock("./targets.js", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - resolveGoogleChatOutboundSpace: resolveGoogleChatOutboundSpaceMock, - }; +resolveGoogleChatAccountMock.mockImplementation(resolveGoogleChatAccountImpl); +resolveGoogleChatOutboundSpaceMock.mockImplementation(async ({ target }: { target: string }) => { + const normalized = normalizeGoogleChatTarget(target); + if (!normalized) { + throw new Error("Missing Google Chat target."); + } + return normalized.toLowerCase().startsWith("users/") + ? `spaces/DM-${normalized.slice("users/".length)}` + : normalized.replace(/\/messages\/.+$/, ""); }); - -vi.mock("../runtime-api.js", async (importOriginal) => { - const actual = await importOriginal(); - return { - ...actual, - loadOutboundMediaFromUrl: (...args: Parameters) => - loadOutboundMediaFromUrlMock(...args), - loadWebMedia: (...args: Parameters) => loadWebMediaMock(...args), - fetchRemoteMedia: (...args: Parameters) => - fetchRemoteMediaMock(...args), - }; -}); - -const accountsActual = await vi.importActual("./accounts.js"); -const targetsActual = await vi.importActual("./targets.js"); -const runtimeApiActual = - await vi.importActual("../runtime-api.js"); - -resolveGoogleChatAccountMock.mockImplementation(accountsActual.resolveGoogleChatAccount); -resolveGoogleChatOutboundSpaceMock.mockImplementation(targetsActual.resolveGoogleChatOutboundSpace); +loadOutboundMediaFromUrlMock.mockImplementation(async (mediaUrl: string) => ({ + buffer: Buffer.from("default-bytes"), + fileName: mediaUrl.split("/").pop() || "attachment", + contentType: "application/octet-stream", +})); +fetchRemoteMediaMock.mockImplementation(async () => ({ + buffer: Buffer.from("remote-bytes"), + fileName: "remote.png", + contentType: "image/png", +})); import { googlechatPlugin } from "./channel.js"; afterEach(() => { vi.clearAllMocks(); - resolveGoogleChatAccountMock.mockImplementation(accountsActual.resolveGoogleChatAccount); - resolveGoogleChatOutboundSpaceMock.mockImplementation( - targetsActual.resolveGoogleChatOutboundSpace, - ); - loadOutboundMediaFromUrlMock.mockImplementation(runtimeApiActual.loadOutboundMediaFromUrl); - loadWebMediaMock.mockImplementation(runtimeApiActual.loadWebMedia); - fetchRemoteMediaMock.mockImplementation(runtimeApiActual.fetchRemoteMedia); + resolveGoogleChatAccountMock.mockImplementation(resolveGoogleChatAccountImpl); + resolveGoogleChatOutboundSpaceMock.mockImplementation(async ({ target }: { target: string }) => { + const normalized = normalizeGoogleChatTarget(target); + if (!normalized) { + throw new Error("Missing Google Chat target."); + } + return normalized.toLowerCase().startsWith("users/") + ? `spaces/DM-${normalized.slice("users/".length)}` + : normalized.replace(/\/messages\/.+$/, ""); + }); + loadOutboundMediaFromUrlMock.mockImplementation(async (mediaUrl: string) => ({ + buffer: Buffer.from("default-bytes"), + fileName: mediaUrl.split("/").pop() || "attachment", + contentType: "application/octet-stream", + })); + fetchRemoteMediaMock.mockImplementation(async () => ({ + buffer: Buffer.from("remote-bytes"), + fileName: "remote.png", + contentType: "image/png", + })); }); function createGoogleChatCfg(): OpenClawConfig { @@ -93,11 +197,6 @@ function setupRuntimeMediaMocks(params: { loadFileName: string; loadBytes: strin fileName: params.loadFileName, contentType: "image/png", })); - const loadWebMedia = vi.fn(async () => ({ - buffer: Buffer.from(params.loadBytes), - fileName: params.loadFileName, - contentType: "image/png", - })); const fetchRemoteMedia = vi.fn(async () => ({ buffer: Buffer.from("remote-bytes"), fileName: "remote.png", @@ -105,10 +204,9 @@ function setupRuntimeMediaMocks(params: { loadFileName: string; loadBytes: strin })); loadOutboundMediaFromUrlMock.mockImplementation(loadOutboundMediaFromUrl); - loadWebMediaMock.mockImplementation(loadWebMedia); fetchRemoteMediaMock.mockImplementation(fetchRemoteMedia); - return { loadOutboundMediaFromUrl, loadWebMedia, fetchRemoteMedia }; + return { loadOutboundMediaFromUrl, fetchRemoteMedia }; } describe("googlechatPlugin outbound sendMedia", () => { diff --git a/extensions/googlechat/src/channel.ts b/extensions/googlechat/src/channel.ts index 54476d4fc5b..0c42f8abeb0 100644 --- a/extensions/googlechat/src/channel.ts +++ b/extensions/googlechat/src/channel.ts @@ -22,41 +22,37 @@ import { createComputedAccountStatusAdapter, createDefaultChannelRuntimeState, } from "openclaw/plugin-sdk/status-helpers"; +import { googlechatMessageActions } from "./actions.js"; +import { googleChatApprovalAuth } from "./approval-auth.js"; import { buildChannelConfigSchema, chunkTextForOutbound, - DEFAULT_ACCOUNT_ID, createAccountStatusSink, + DEFAULT_ACCOUNT_ID, + fetchRemoteMedia, getChatChannelMeta, + GoogleChatConfigSchema, + listGoogleChatAccountIds, loadOutboundMediaFromUrl, missingTargetError, PAIRING_APPROVED_MESSAGE, - fetchRemoteMedia, resolveChannelMediaMaxBytes, + resolveDefaultGoogleChatAccountId, + resolveGoogleChatAccount, + resolveGoogleChatOutboundSpace, runPassiveAccountLifecycle, + isGoogleChatSpaceTarget, + isGoogleChatUserTarget, + normalizeGoogleChatTarget, type ChannelMessageActionAdapter, type ChannelStatusIssue, type OpenClawConfig, -} from "../runtime-api.js"; -import { GoogleChatConfigSchema } from "../runtime-api.js"; -import { - listGoogleChatAccountIds, - resolveDefaultGoogleChatAccountId, - resolveGoogleChatAccount, type ResolvedGoogleChatAccount, -} from "./accounts.js"; -import { googlechatMessageActions } from "./actions.js"; -import { googleChatApprovalAuth } from "./approval-auth.js"; +} from "./channel.deps.runtime.js"; import { resolveGoogleChatGroupRequireMention } from "./group-policy.js"; import { getGoogleChatRuntime } from "./runtime.js"; import { googlechatSetupAdapter } from "./setup-core.js"; import { googlechatSetupWizard } from "./setup-surface.js"; -import { - isGoogleChatSpaceTarget, - isGoogleChatUserTarget, - normalizeGoogleChatTarget, - resolveGoogleChatOutboundSpace, -} from "./targets.js"; const meta = getChatChannelMeta("googlechat");