test: collapse googlechat helper suites

This commit is contained in:
Peter Steinberger
2026-03-25 05:31:51 +00:00
parent 149c4683a3
commit c22f3c514b
4 changed files with 344 additions and 368 deletions

View File

@@ -1,131 +0,0 @@
import { describe, expect, it } from "vitest";
import type { OpenClawConfig } from "../runtime-api.js";
import { resolveGoogleChatAccount } from "./accounts.js";
describe("resolveGoogleChatAccount", () => {
it("inherits shared defaults from accounts.default for named accounts", () => {
const cfg: OpenClawConfig = {
channels: {
googlechat: {
accounts: {
default: {
audienceType: "app-url",
audience: "https://example.com/googlechat",
webhookPath: "/googlechat",
},
andy: {
serviceAccountFile: "/tmp/andy-sa.json",
},
},
},
},
};
const resolved = resolveGoogleChatAccount({ cfg, accountId: "andy" });
expect(resolved.config.audienceType).toBe("app-url");
expect(resolved.config.audience).toBe("https://example.com/googlechat");
expect(resolved.config.webhookPath).toBe("/googlechat");
expect(resolved.config.serviceAccountFile).toBe("/tmp/andy-sa.json");
});
it("prefers top-level and account overrides over accounts.default", () => {
const cfg: OpenClawConfig = {
channels: {
googlechat: {
audienceType: "project-number",
audience: "1234567890",
accounts: {
default: {
audienceType: "app-url",
audience: "https://default.example.com/googlechat",
webhookPath: "/googlechat-default",
},
april: {
webhookPath: "/googlechat-april",
},
},
},
},
};
const resolved = resolveGoogleChatAccount({ cfg, accountId: "april" });
expect(resolved.config.audienceType).toBe("project-number");
expect(resolved.config.audience).toBe("1234567890");
expect(resolved.config.webhookPath).toBe("/googlechat-april");
});
it("does not inherit disabled state from accounts.default for named accounts", () => {
const cfg: OpenClawConfig = {
channels: {
googlechat: {
accounts: {
default: {
enabled: false,
audienceType: "app-url",
audience: "https://example.com/googlechat",
},
andy: {
serviceAccountFile: "/tmp/andy-sa.json",
},
},
},
},
};
const resolved = resolveGoogleChatAccount({ cfg, accountId: "andy" });
expect(resolved.enabled).toBe(true);
expect(resolved.config.enabled).toBeUndefined();
expect(resolved.config.audienceType).toBe("app-url");
});
it("does not inherit default-account credentials into named accounts", () => {
const cfg: OpenClawConfig = {
channels: {
googlechat: {
accounts: {
default: {
serviceAccountRef: {
source: "env",
provider: "test",
id: "default-sa",
},
audienceType: "app-url",
audience: "https://example.com/googlechat",
},
andy: {
serviceAccountFile: "/tmp/andy-sa.json",
},
},
},
},
};
const resolved = resolveGoogleChatAccount({ cfg, accountId: "andy" });
expect(resolved.credentialSource).toBe("file");
expect(resolved.credentialsFile).toBe("/tmp/andy-sa.json");
expect(resolved.config.audienceType).toBe("app-url");
});
it("does not inherit dangerous name matching from accounts.default", () => {
const cfg: OpenClawConfig = {
channels: {
googlechat: {
accounts: {
default: {
dangerouslyAllowNameMatching: true,
audienceType: "app-url",
audience: "https://example.com/googlechat",
},
andy: {
serviceAccountFile: "/tmp/andy-sa.json",
},
},
},
},
};
const resolved = resolveGoogleChatAccount({ cfg, accountId: "andy" });
expect(resolved.config.dangerouslyAllowNameMatching).toBeUndefined();
expect(resolved.config.audienceType).toBe("app-url");
});
});

View File

@@ -1,4 +1,4 @@
import { describe, expect, it, vi } from "vitest";
import { afterEach, describe, expect, it, vi } from "vitest";
import {
createDirectoryTestRuntime,
expectDirectorySurface,
@@ -7,6 +7,8 @@ import type { OpenClawConfig, PluginRuntime } from "../runtime-api.js";
const uploadGoogleChatAttachmentMock = vi.hoisted(() => vi.fn());
const sendGoogleChatMessageMock = vi.hoisted(() => vi.fn());
const resolveGoogleChatAccountMock = vi.hoisted(() => vi.fn());
const resolveGoogleChatOutboundSpaceMock = vi.hoisted(() => vi.fn());
vi.mock("./api.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("./api.js")>();
@@ -17,9 +19,39 @@ vi.mock("./api.js", async (importOriginal) => {
};
});
vi.mock("./accounts.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("./accounts.js")>();
return {
...actual,
resolveGoogleChatAccount: resolveGoogleChatAccountMock,
};
});
vi.mock("./targets.js", async (importOriginal) => {
const actual = await importOriginal<typeof import("./targets.js")>();
return {
...actual,
resolveGoogleChatOutboundSpace: resolveGoogleChatOutboundSpaceMock,
};
});
const accountsActual = await vi.importActual<typeof import("./accounts.js")>("./accounts.js");
const targetsActual = await vi.importActual<typeof import("./targets.js")>("./targets.js");
resolveGoogleChatAccountMock.mockImplementation(accountsActual.resolveGoogleChatAccount);
resolveGoogleChatOutboundSpaceMock.mockImplementation(targetsActual.resolveGoogleChatOutboundSpace);
import { googlechatPlugin } from "./channel.js";
import { setGoogleChatRuntime } from "./runtime.js";
afterEach(() => {
vi.clearAllMocks();
resolveGoogleChatAccountMock.mockImplementation(accountsActual.resolveGoogleChatAccount);
resolveGoogleChatOutboundSpaceMock.mockImplementation(
targetsActual.resolveGoogleChatOutboundSpace,
);
});
function createGoogleChatCfg(): OpenClawConfig {
return {
channels: {
@@ -162,6 +194,188 @@ describe("googlechatPlugin outbound sendMedia", () => {
});
});
const resolveTarget = googlechatPlugin.outbound?.resolveTarget;
describe("googlechatPlugin outbound resolveTarget", () => {
it("resolves valid chat targets", () => {
if (!resolveTarget) {
throw new Error("Expected googlechatPlugin.outbound.resolveTarget to be defined");
}
const result = resolveTarget({
to: "spaces/AAA",
mode: "explicit",
allowFrom: [],
});
expect(result.ok).toBe(true);
if (!result.ok) {
throw result.error;
}
expect(result.to).toBe("spaces/AAA");
});
it("resolves email targets", () => {
if (!resolveTarget) {
throw new Error("Expected googlechatPlugin.outbound.resolveTarget to be defined");
}
const result = resolveTarget({
to: "user@example.com",
mode: "explicit",
allowFrom: [],
});
expect(result.ok).toBe(true);
if (!result.ok) {
throw result.error;
}
expect(result.to).toBe("users/user@example.com");
});
it("errors on invalid targets", () => {
if (!resolveTarget) {
throw new Error("Expected googlechatPlugin.outbound.resolveTarget to be defined");
}
const result = resolveTarget({
to: " ",
mode: "explicit",
allowFrom: [],
});
expect(result.ok).toBe(false);
if (result.ok) {
throw new Error("Expected invalid target to fail");
}
expect(result.error).toBeDefined();
});
it("errors when no target is provided", () => {
if (!resolveTarget) {
throw new Error("Expected googlechatPlugin.outbound.resolveTarget to be defined");
}
const result = resolveTarget({
to: undefined,
mode: "explicit",
allowFrom: [],
});
expect(result.ok).toBe(false);
if (result.ok) {
throw new Error("Expected missing target to fail");
}
expect(result.error).toBeDefined();
});
});
describe("googlechatPlugin outbound cfg threading", () => {
it("threads resolved cfg into sendText account resolution", async () => {
const cfg = {
channels: {
googlechat: {
serviceAccount: {
type: "service_account",
},
},
},
};
const account = {
accountId: "default",
config: {},
credentialSource: "inline",
};
resolveGoogleChatAccountMock.mockReturnValue(account);
resolveGoogleChatOutboundSpaceMock.mockResolvedValue("spaces/AAA");
sendGoogleChatMessageMock.mockResolvedValue({
messageName: "spaces/AAA/messages/msg-1",
});
await googlechatPlugin.outbound?.sendText?.({
cfg: cfg as never,
to: "users/123",
text: "hello",
accountId: "default",
});
expect(resolveGoogleChatAccountMock).toHaveBeenCalledWith({
cfg,
accountId: "default",
});
expect(sendGoogleChatMessageMock).toHaveBeenCalledWith(
expect.objectContaining({
account,
space: "spaces/AAA",
text: "hello",
}),
);
});
it("threads resolved cfg into sendMedia account and media loading path", async () => {
const cfg = {
channels: {
googlechat: {
serviceAccount: {
type: "service_account",
},
mediaMaxMb: 8,
},
},
};
const account = {
accountId: "default",
config: { mediaMaxMb: 20 },
credentialSource: "inline",
};
const { fetchRemoteMedia } = setupRuntimeMediaMocks({
loadFileName: "unused.png",
loadBytes: "should-not-be-used",
});
resolveGoogleChatAccountMock.mockReturnValue(account);
resolveGoogleChatOutboundSpaceMock.mockResolvedValue("spaces/AAA");
uploadGoogleChatAttachmentMock.mockResolvedValue({
attachmentUploadToken: "token-1",
});
sendGoogleChatMessageMock.mockResolvedValue({
messageName: "spaces/AAA/messages/msg-2",
});
await googlechatPlugin.outbound?.sendMedia?.({
cfg: cfg as never,
to: "users/123",
text: "photo",
mediaUrl: "https://example.com/file.png",
accountId: "default",
});
expect(resolveGoogleChatAccountMock).toHaveBeenCalledWith({
cfg,
accountId: "default",
});
expect(fetchRemoteMedia).toHaveBeenCalledWith(
expect.objectContaining({
url: "https://example.com/file.png",
maxBytes: 8 * 1024 * 1024,
}),
);
expect(uploadGoogleChatAttachmentMock).toHaveBeenCalledWith(
expect.objectContaining({
account,
space: "spaces/AAA",
filename: "remote.png",
}),
);
expect(sendGoogleChatMessageMock).toHaveBeenCalledWith(
expect.objectContaining({
account,
attachments: [{ attachmentUploadToken: "token-1", contentName: "remote.png" }],
}),
);
});
});
describe("googlechat directory", () => {
const runtimeEnv = createDirectoryTestRuntime() as never;

View File

@@ -1,235 +0,0 @@
import { installCommonResolveTargetErrorCases } from "openclaw/plugin-sdk/testing";
import { beforeEach, describe, expect, it, vi } from "vitest";
const runtimeMocks = vi.hoisted(() => ({
chunkMarkdownText: vi.fn((text: string) => [text]),
fetchRemoteMedia: vi.fn(),
}));
vi.mock("../runtime-api.js", () => ({
getChatChannelMeta: () => ({ id: "googlechat", label: "Google Chat" }),
missingTargetError: (provider: string, hint: string) =>
new Error(`Delivering to ${provider} requires target ${hint}`),
GoogleChatConfigSchema: {},
DEFAULT_ACCOUNT_ID: "default",
PAIRING_APPROVED_MESSAGE: "Approved",
applyAccountNameToChannelSection: vi.fn(),
buildChannelConfigSchema: vi.fn(),
deleteAccountFromConfigSection: vi.fn(),
formatPairingApproveHint: vi.fn(),
migrateBaseNameToDefaultAccount: vi.fn(),
normalizeAccountId: vi.fn(),
resolveChannelMediaMaxBytes: vi.fn(),
resolveGoogleChatGroupRequireMention: vi.fn(),
setAccountEnabledInConfigSection: vi.fn(),
}));
vi.mock("./accounts.js", () => ({
listGoogleChatAccountIds: vi.fn(),
resolveDefaultGoogleChatAccountId: vi.fn(),
resolveGoogleChatAccount: vi.fn(),
}));
vi.mock("./actions.js", () => ({
googlechatMessageActions: [],
}));
vi.mock("./api.js", () => ({
sendGoogleChatMessage: vi.fn(),
uploadGoogleChatAttachment: vi.fn(),
probeGoogleChat: vi.fn(),
}));
vi.mock("./monitor.js", () => ({
resolveGoogleChatWebhookPath: vi.fn(),
startGoogleChatMonitor: vi.fn(),
}));
vi.mock("./setup-core.js", () => ({
googlechatSetupAdapter: {},
}));
vi.mock("./setup-surface.js", () => ({
googlechatSetupWizard: {},
}));
vi.mock("./runtime.js", () => ({
getGoogleChatRuntime: vi.fn(() => ({
channel: {
text: { chunkMarkdownText: runtimeMocks.chunkMarkdownText },
media: { fetchRemoteMedia: runtimeMocks.fetchRemoteMedia },
},
})),
}));
vi.mock("./targets.js", () => ({
normalizeGoogleChatTarget: (raw?: string | null) => {
if (!raw?.trim()) return undefined;
if (raw === "invalid-target") return undefined;
const trimmed = raw.trim().replace(/^(googlechat|google-chat|gchat):/i, "");
if (trimmed.startsWith("spaces/")) return trimmed;
if (trimmed.includes("@")) return `users/${trimmed.toLowerCase()}`;
return `users/${trimmed}`;
},
isGoogleChatUserTarget: (value: string) => value.startsWith("users/"),
isGoogleChatSpaceTarget: (value: string) => value.startsWith("spaces/"),
resolveGoogleChatOutboundSpace: vi.fn(),
}));
import { resolveChannelMediaMaxBytes } from "../runtime-api.js";
import { resolveGoogleChatAccount } from "./accounts.js";
import { sendGoogleChatMessage, uploadGoogleChatAttachment } from "./api.js";
import { googlechatPlugin } from "./channel.js";
import { resolveGoogleChatOutboundSpace } from "./targets.js";
const resolveTarget = googlechatPlugin.outbound!.resolveTarget!;
describe("googlechat resolveTarget", () => {
it("should resolve valid target", () => {
const result = resolveTarget({
to: "spaces/AAA",
mode: "explicit",
allowFrom: [],
});
expect(result.ok).toBe(true);
if (!result.ok) {
throw result.error;
}
expect(result.to).toBe("spaces/AAA");
});
it("should resolve email target", () => {
const result = resolveTarget({
to: "user@example.com",
mode: "explicit",
allowFrom: [],
});
expect(result.ok).toBe(true);
if (!result.ok) {
throw result.error;
}
expect(result.to).toBe("users/user@example.com");
});
installCommonResolveTargetErrorCases({
resolveTarget,
implicitAllowFrom: ["spaces/BBB"],
});
});
describe("googlechat outbound cfg threading", () => {
beforeEach(() => {
runtimeMocks.fetchRemoteMedia.mockReset();
runtimeMocks.chunkMarkdownText.mockClear();
vi.mocked(resolveGoogleChatAccount).mockReset();
vi.mocked(resolveGoogleChatOutboundSpace).mockReset();
vi.mocked(resolveChannelMediaMaxBytes).mockReset();
vi.mocked(uploadGoogleChatAttachment).mockReset();
vi.mocked(sendGoogleChatMessage).mockReset();
});
it("threads resolved cfg into sendText account resolution", async () => {
const cfg = {
channels: {
googlechat: {
serviceAccount: {
type: "service_account",
},
},
},
};
const account = {
accountId: "default",
config: {},
credentialSource: "inline",
};
vi.mocked(resolveGoogleChatAccount).mockReturnValue(account as any);
vi.mocked(resolveGoogleChatOutboundSpace).mockResolvedValue("spaces/AAA");
vi.mocked(sendGoogleChatMessage).mockResolvedValue({
messageName: "spaces/AAA/messages/msg-1",
} as any);
await googlechatPlugin.outbound!.sendText!({
cfg: cfg as any,
to: "users/123",
text: "hello",
accountId: "default",
});
expect(resolveGoogleChatAccount).toHaveBeenCalledWith({
cfg,
accountId: "default",
});
expect(sendGoogleChatMessage).toHaveBeenCalledWith(
expect.objectContaining({
account,
space: "spaces/AAA",
text: "hello",
}),
);
});
it("threads resolved cfg into sendMedia account and media loading path", async () => {
const cfg = {
channels: {
googlechat: {
serviceAccount: {
type: "service_account",
},
mediaMaxMb: 8,
},
},
};
const account = {
accountId: "default",
config: { mediaMaxMb: 20 },
credentialSource: "inline",
};
vi.mocked(resolveGoogleChatAccount).mockReturnValue(account as any);
vi.mocked(resolveGoogleChatOutboundSpace).mockResolvedValue("spaces/AAA");
vi.mocked(resolveChannelMediaMaxBytes).mockReturnValue(1024);
runtimeMocks.fetchRemoteMedia.mockResolvedValueOnce({
buffer: Buffer.from("file"),
fileName: "file.png",
contentType: "image/png",
});
vi.mocked(uploadGoogleChatAttachment).mockResolvedValue({
attachmentUploadToken: "token-1",
} as any);
vi.mocked(sendGoogleChatMessage).mockResolvedValue({
messageName: "spaces/AAA/messages/msg-2",
} as any);
await googlechatPlugin.outbound!.sendMedia!({
cfg: cfg as any,
to: "users/123",
text: "photo",
mediaUrl: "https://example.com/file.png",
accountId: "default",
});
expect(resolveGoogleChatAccount).toHaveBeenCalledWith({
cfg,
accountId: "default",
});
expect(runtimeMocks.fetchRemoteMedia).toHaveBeenCalledWith({
url: "https://example.com/file.png",
maxBytes: 1024,
});
expect(uploadGoogleChatAttachment).toHaveBeenCalledWith(
expect.objectContaining({
account,
space: "spaces/AAA",
filename: "file.png",
}),
);
expect(sendGoogleChatMessage).toHaveBeenCalledWith(
expect.objectContaining({
account,
attachments: [{ attachmentUploadToken: "token-1", contentName: "file.png" }],
}),
);
});
});

View File

@@ -13,7 +13,7 @@ import {
waitForStartedMocks,
} from "../../../test/helpers/extensions/start-account-lifecycle.js";
import type { OpenClawConfig } from "../runtime-api.js";
import type { ResolvedGoogleChatAccount } from "./accounts.js";
import { resolveGoogleChatAccount, type ResolvedGoogleChatAccount } from "./accounts.js";
import { googlechatPlugin } from "./channel.js";
import { googlechatSetupAdapter } from "./setup-core.js";
@@ -184,3 +184,131 @@ describe("googlechat setup", () => {
expectLifecyclePatch(patches, { running: false });
});
});
describe("resolveGoogleChatAccount", () => {
it("inherits shared defaults from accounts.default for named accounts", () => {
const cfg: OpenClawConfig = {
channels: {
googlechat: {
accounts: {
default: {
audienceType: "app-url",
audience: "https://example.com/googlechat",
webhookPath: "/googlechat",
},
andy: {
serviceAccountFile: "/tmp/andy-sa.json",
},
},
},
},
};
const resolved = resolveGoogleChatAccount({ cfg, accountId: "andy" });
expect(resolved.config.audienceType).toBe("app-url");
expect(resolved.config.audience).toBe("https://example.com/googlechat");
expect(resolved.config.webhookPath).toBe("/googlechat");
expect(resolved.config.serviceAccountFile).toBe("/tmp/andy-sa.json");
});
it("prefers top-level and account overrides over accounts.default", () => {
const cfg: OpenClawConfig = {
channels: {
googlechat: {
audienceType: "project-number",
audience: "1234567890",
accounts: {
default: {
audienceType: "app-url",
audience: "https://default.example.com/googlechat",
webhookPath: "/googlechat-default",
},
april: {
webhookPath: "/googlechat-april",
},
},
},
},
};
const resolved = resolveGoogleChatAccount({ cfg, accountId: "april" });
expect(resolved.config.audienceType).toBe("project-number");
expect(resolved.config.audience).toBe("1234567890");
expect(resolved.config.webhookPath).toBe("/googlechat-april");
});
it("does not inherit disabled state from accounts.default for named accounts", () => {
const cfg: OpenClawConfig = {
channels: {
googlechat: {
accounts: {
default: {
enabled: false,
audienceType: "app-url",
audience: "https://example.com/googlechat",
},
andy: {
serviceAccountFile: "/tmp/andy-sa.json",
},
},
},
},
};
const resolved = resolveGoogleChatAccount({ cfg, accountId: "andy" });
expect(resolved.enabled).toBe(true);
expect(resolved.config.enabled).toBeUndefined();
expect(resolved.config.audienceType).toBe("app-url");
});
it("does not inherit default-account credentials into named accounts", () => {
const cfg: OpenClawConfig = {
channels: {
googlechat: {
accounts: {
default: {
serviceAccountRef: {
source: "env",
provider: "test",
id: "default-sa",
},
audienceType: "app-url",
audience: "https://example.com/googlechat",
},
andy: {
serviceAccountFile: "/tmp/andy-sa.json",
},
},
},
},
};
const resolved = resolveGoogleChatAccount({ cfg, accountId: "andy" });
expect(resolved.credentialSource).toBe("file");
expect(resolved.credentialsFile).toBe("/tmp/andy-sa.json");
expect(resolved.config.audienceType).toBe("app-url");
});
it("does not inherit dangerous name matching from accounts.default", () => {
const cfg: OpenClawConfig = {
channels: {
googlechat: {
accounts: {
default: {
dangerouslyAllowNameMatching: true,
audienceType: "app-url",
audience: "https://example.com/googlechat",
},
andy: {
serviceAccountFile: "/tmp/andy-sa.json",
},
},
},
},
};
const resolved = resolveGoogleChatAccount({ cfg, accountId: "andy" });
expect(resolved.config.dangerouslyAllowNameMatching).toBeUndefined();
expect(resolved.config.audienceType).toBe("app-url");
});
});