@kodelyth/googlechat 2026.5.42 → 2026.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (61) hide show
  1. package/klaw.plugin.json +967 -2
  2. package/package.json +18 -6
  3. package/api.ts +0 -3
  4. package/channel-config-api.ts +0 -1
  5. package/channel-plugin-api.ts +0 -1
  6. package/config-api.ts +0 -2
  7. package/contract-api.ts +0 -5
  8. package/doctor-contract-api.ts +0 -1
  9. package/index.ts +0 -20
  10. package/runtime-api.ts +0 -55
  11. package/secret-contract-api.ts +0 -5
  12. package/setup-entry.ts +0 -13
  13. package/setup-plugin-api.ts +0 -3
  14. package/src/accounts.ts +0 -181
  15. package/src/actions.test.ts +0 -289
  16. package/src/actions.ts +0 -227
  17. package/src/api.ts +0 -316
  18. package/src/approval-auth.test.ts +0 -24
  19. package/src/approval-auth.ts +0 -32
  20. package/src/auth.ts +0 -218
  21. package/src/channel-config.test.ts +0 -39
  22. package/src/channel.adapters.ts +0 -340
  23. package/src/channel.deps.runtime.ts +0 -29
  24. package/src/channel.runtime.ts +0 -17
  25. package/src/channel.setup.ts +0 -98
  26. package/src/channel.test.ts +0 -784
  27. package/src/channel.ts +0 -277
  28. package/src/config-schema.test.ts +0 -31
  29. package/src/config-schema.ts +0 -3
  30. package/src/doctor-contract.test.ts +0 -75
  31. package/src/doctor-contract.ts +0 -182
  32. package/src/doctor.ts +0 -57
  33. package/src/gateway.ts +0 -63
  34. package/src/google-auth.runtime.test.ts +0 -543
  35. package/src/google-auth.runtime.ts +0 -568
  36. package/src/group-policy.ts +0 -17
  37. package/src/monitor-access.test.ts +0 -491
  38. package/src/monitor-access.ts +0 -465
  39. package/src/monitor-durable.test.ts +0 -39
  40. package/src/monitor-durable.ts +0 -23
  41. package/src/monitor-reply-delivery.ts +0 -156
  42. package/src/monitor-routing.ts +0 -65
  43. package/src/monitor-types.ts +0 -33
  44. package/src/monitor-webhook.test.ts +0 -587
  45. package/src/monitor-webhook.ts +0 -303
  46. package/src/monitor.reply-delivery.test.ts +0 -144
  47. package/src/monitor.test.ts +0 -159
  48. package/src/monitor.ts +0 -527
  49. package/src/monitor.webhook-routing.test.ts +0 -257
  50. package/src/runtime.ts +0 -9
  51. package/src/secret-contract.test.ts +0 -60
  52. package/src/secret-contract.ts +0 -161
  53. package/src/setup-core.ts +0 -40
  54. package/src/setup-surface.ts +0 -243
  55. package/src/setup.test.ts +0 -619
  56. package/src/targets.test.ts +0 -453
  57. package/src/targets.ts +0 -66
  58. package/src/types.config.ts +0 -3
  59. package/src/types.ts +0 -73
  60. package/test-api.ts +0 -2
  61. package/tsconfig.json +0 -16
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kodelyth/googlechat",
3
- "version": "2026.5.42",
3
+ "version": "2026.6.2",
4
4
  "description": "Klaw Google Chat channel plugin",
5
5
  "repository": {
6
6
  "type": "git",
@@ -13,15 +13,19 @@
13
13
  "zod": "4.4.3"
14
14
  },
15
15
  "devDependencies": {
16
- "@kodelyth/plugin-sdk": "1.0.1",
17
- "@kodelyth/klaw": "2026.5.42"
16
+ "@kodelyth/plugin-sdk": "workspace:*",
17
+ "@kodelyth/klaw": "workspace:*"
18
18
  },
19
19
  "peerDependencies": {
20
- "@kodelyth/klaw": ">=2026.5.19"
20
+ "@kodelyth/klaw": ">=2026.5.19",
21
+ "klaw": ">=2026.5.39"
21
22
  },
22
23
  "peerDependenciesMeta": {
23
24
  "@kodelyth/klaw": {
24
25
  "optional": true
26
+ },
27
+ "klaw": {
28
+ "optional": true
25
29
  }
26
30
  },
27
31
  "klaw": {
@@ -83,6 +87,14 @@
83
87
  "release": {
84
88
  "publishToClawHub": true,
85
89
  "publishToNpm": true
86
- }
87
- }
90
+ },
91
+ "runtimeExtensions": [
92
+ "./dist/index.js"
93
+ ],
94
+ "runtimeSetupEntry": "./dist/setup-entry.js"
95
+ },
96
+ "files": [
97
+ "dist/**",
98
+ "klaw.plugin.json"
99
+ ]
88
100
  }
package/api.ts DELETED
@@ -1,3 +0,0 @@
1
- export { googlechatPlugin } from "./src/channel.js";
2
- export { googlechatSetupAdapter } from "./src/setup-core.js";
3
- export { googlechatSetupWizard } from "./src/setup-surface.js";
@@ -1 +0,0 @@
1
- export { GoogleChatChannelConfigSchema } from "./src/config-schema.js";
@@ -1 +0,0 @@
1
- export { googlechatPlugin } from "./src/channel.js";
package/config-api.ts DELETED
@@ -1,2 +0,0 @@
1
- export { GoogleChatConfigSchema } from "klaw/plugin-sdk/bundled-channel-config-schema";
2
- export { buildChannelConfigSchema } from "klaw/plugin-sdk/channel-config-primitives";
package/contract-api.ts DELETED
@@ -1,5 +0,0 @@
1
- export {
2
- collectRuntimeConfigAssignments,
3
- secretTargetRegistryEntries,
4
- } from "./src/secret-contract.js";
5
- export { normalizeCompatibilityConfig, legacyConfigRules } from "./src/doctor-contract.js";
@@ -1 +0,0 @@
1
- export { normalizeCompatibilityConfig, legacyConfigRules } from "./src/doctor-contract.js";
package/index.ts DELETED
@@ -1,20 +0,0 @@
1
- import { defineBundledChannelEntry } from "klaw/plugin-sdk/channel-entry-contract";
2
-
3
- export default defineBundledChannelEntry({
4
- id: "googlechat",
5
- name: "Google Chat",
6
- description: "Klaw Google Chat channel plugin",
7
- importMetaUrl: import.meta.url,
8
- plugin: {
9
- specifier: "./channel-plugin-api.js",
10
- exportName: "googlechatPlugin",
11
- },
12
- secrets: {
13
- specifier: "./secret-contract-api.js",
14
- exportName: "channelSecrets",
15
- },
16
- runtime: {
17
- specifier: "./runtime-api.js",
18
- exportName: "setGoogleChatRuntime",
19
- },
20
- });
package/runtime-api.ts DELETED
@@ -1,55 +0,0 @@
1
- // Private runtime barrel for the bundled Google Chat extension.
2
- // Keep this barrel thin and avoid broad plugin-sdk surfaces during bootstrap.
3
-
4
- export { DEFAULT_ACCOUNT_ID } from "klaw/plugin-sdk/account-id";
5
- export {
6
- createActionGate,
7
- jsonResult,
8
- readNumberParam,
9
- readReactionParams,
10
- readStringParam,
11
- } from "klaw/plugin-sdk/channel-actions";
12
- export { buildChannelConfigSchema } from "klaw/plugin-sdk/channel-config-primitives";
13
- export type {
14
- ChannelMessageActionAdapter,
15
- ChannelMessageActionName,
16
- ChannelStatusIssue,
17
- } from "klaw/plugin-sdk/channel-contract";
18
- export { missingTargetError } from "klaw/plugin-sdk/channel-feedback";
19
- export {
20
- createAccountStatusSink,
21
- runPassiveAccountLifecycle,
22
- } from "klaw/plugin-sdk/channel-lifecycle";
23
- export { createChannelPairingController } from "klaw/plugin-sdk/channel-pairing";
24
- export { createChannelMessageReplyPipeline } from "klaw/plugin-sdk/channel-message";
25
- export { PAIRING_APPROVED_MESSAGE } from "klaw/plugin-sdk/channel-status";
26
- export { chunkTextForOutbound } from "klaw/plugin-sdk/text-chunking";
27
- export type { KlawConfig } from "klaw/plugin-sdk/config-contracts";
28
- export { GoogleChatConfigSchema } from "klaw/plugin-sdk/bundled-channel-config-schema";
29
- export {
30
- GROUP_POLICY_BLOCKED_LABEL,
31
- resolveAllowlistProviderRuntimeGroupPolicy,
32
- resolveDefaultGroupPolicy,
33
- warnMissingProviderGroupPolicyFallbackOnce,
34
- } from "klaw/plugin-sdk/runtime-group-policy";
35
- export { isDangerousNameMatchingEnabled } from "klaw/plugin-sdk/dangerous-name-runtime";
36
- export { readRemoteMediaBuffer, resolveChannelMediaMaxBytes } from "klaw/plugin-sdk/media-runtime";
37
- export { loadOutboundMediaFromUrl } from "klaw/plugin-sdk/outbound-media";
38
- export type { PluginRuntime } from "klaw/plugin-sdk/runtime-store";
39
- export { fetchWithSsrFGuard } from "klaw/plugin-sdk/ssrf-runtime";
40
- export type { GoogleChatAccountConfig, GoogleChatConfig } from "klaw/plugin-sdk/config-contracts";
41
- export { extractToolSend } from "klaw/plugin-sdk/tool-send";
42
- export { resolveInboundMentionDecision } from "klaw/plugin-sdk/channel-inbound";
43
- export { resolveInboundRouteEnvelopeBuilderWithRuntime } from "klaw/plugin-sdk/inbound-envelope";
44
- export { resolveWebhookPath } from "klaw/plugin-sdk/webhook-ingress";
45
- export {
46
- registerWebhookTargetWithPluginRoute,
47
- resolveWebhookTargetWithAuthOrReject,
48
- withResolvedWebhookRequestPipeline,
49
- } from "klaw/plugin-sdk/webhook-targets";
50
- export {
51
- createWebhookInFlightLimiter,
52
- readJsonWebhookBodyOrReject,
53
- type WebhookInFlightLimiter,
54
- } from "klaw/plugin-sdk/webhook-request-guards";
55
- export { setGoogleChatRuntime } from "./src/runtime.js";
@@ -1,5 +0,0 @@
1
- export {
2
- channelSecrets,
3
- collectRuntimeConfigAssignments,
4
- secretTargetRegistryEntries,
5
- } from "./src/secret-contract.js";
package/setup-entry.ts DELETED
@@ -1,13 +0,0 @@
1
- import { defineBundledChannelSetupEntry } from "klaw/plugin-sdk/channel-entry-contract";
2
-
3
- export default defineBundledChannelSetupEntry({
4
- importMetaUrl: import.meta.url,
5
- plugin: {
6
- specifier: "./setup-plugin-api.js",
7
- exportName: "googlechatSetupPlugin",
8
- },
9
- secrets: {
10
- specifier: "./secret-contract-api.js",
11
- exportName: "channelSecrets",
12
- },
13
- });
@@ -1,3 +0,0 @@
1
- // Keep bundled setup entry imports narrow so setup loads do not pull the
2
- // broader Google Chat runtime plugin surface.
3
- export { googlechatSetupPlugin } from "./src/channel.setup.js";
package/src/accounts.ts DELETED
@@ -1,181 +0,0 @@
1
- import {
2
- createAccountListHelpers,
3
- DEFAULT_ACCOUNT_ID,
4
- normalizeAccountId,
5
- type KlawConfig,
6
- resolveAccountEntry,
7
- resolveMergedAccountConfig,
8
- } from "klaw/plugin-sdk/account-resolution";
9
- import { safeParseJsonWithSchema, safeParseWithSchema } from "klaw/plugin-sdk/extension-shared";
10
- import { mergePairLoopGuardConfig } from "klaw/plugin-sdk/pair-loop-guard-runtime";
11
- import { isSecretRef } from "klaw/plugin-sdk/secret-input";
12
- import { normalizeOptionalString } from "klaw/plugin-sdk/string-coerce-runtime";
13
- import { z } from "zod";
14
- import type { GoogleChatAccountConfig } from "./types.config.js";
15
-
16
- type GoogleChatCredentialSource = "file" | "inline" | "env" | "none";
17
-
18
- export type ResolvedGoogleChatAccount = {
19
- accountId: string;
20
- name?: string;
21
- enabled: boolean;
22
- config: GoogleChatAccountConfig;
23
- credentialSource: GoogleChatCredentialSource;
24
- credentials?: Record<string, unknown>;
25
- credentialsFile?: string;
26
- };
27
-
28
- export type GoogleChatConfigAccessorAccount = {
29
- config: GoogleChatAccountConfig;
30
- };
31
-
32
- const ENV_SERVICE_ACCOUNT = "GOOGLE_CHAT_SERVICE_ACCOUNT";
33
- const ENV_SERVICE_ACCOUNT_FILE = "GOOGLE_CHAT_SERVICE_ACCOUNT_FILE";
34
- const JsonRecordSchema = z.record(z.string(), z.unknown());
35
-
36
- const {
37
- listAccountIds: listGoogleChatAccountIds,
38
- resolveDefaultAccountId: resolveDefaultGoogleChatAccountId,
39
- } = createAccountListHelpers("googlechat", {
40
- implicitDefaultAccount: {
41
- channelKeys: ["serviceAccount", "serviceAccountRef", "serviceAccountFile"],
42
- envVars: [ENV_SERVICE_ACCOUNT, ENV_SERVICE_ACCOUNT_FILE],
43
- },
44
- });
45
- export { listGoogleChatAccountIds, resolveDefaultGoogleChatAccountId };
46
-
47
- function mergeGoogleChatAccountConfig(cfg: KlawConfig, accountId: string): GoogleChatAccountConfig {
48
- const raw = cfg.channels?.["googlechat"] ?? {};
49
- const base = resolveMergedAccountConfig<GoogleChatAccountConfig>({
50
- channelConfig: raw as GoogleChatAccountConfig,
51
- accounts: raw.accounts as Record<string, Partial<GoogleChatAccountConfig>> | undefined,
52
- accountId,
53
- omitKeys: ["defaultAccount"],
54
- nestedObjectKeys: ["botLoopProtection"],
55
- });
56
- const defaultAccountConfig = resolveAccountEntry(raw.accounts, DEFAULT_ACCOUNT_ID) ?? {};
57
- if (accountId === DEFAULT_ACCOUNT_ID) {
58
- return base;
59
- }
60
- const {
61
- enabled: _ignoredEnabled,
62
- dangerouslyAllowNameMatching: _ignoredDangerouslyAllowNameMatching,
63
- serviceAccount: _ignoredServiceAccount,
64
- serviceAccountRef: _ignoredServiceAccountRef,
65
- serviceAccountFile: _ignoredServiceAccountFile,
66
- ...defaultAccountShared
67
- } = defaultAccountConfig;
68
- // In multi-account setups, allow accounts.default to provide shared defaults
69
- // (for example webhook/audience fields) while preserving top-level and account overrides.
70
- const botLoopProtection = mergePairLoopGuardConfig(
71
- defaultAccountShared.botLoopProtection,
72
- base.botLoopProtection,
73
- );
74
- return {
75
- ...defaultAccountShared,
76
- ...base,
77
- ...(botLoopProtection ? { botLoopProtection } : {}),
78
- } as GoogleChatAccountConfig;
79
- }
80
-
81
- export function resolveGoogleChatConfigAccessorAccount(params: {
82
- cfg: KlawConfig;
83
- accountId?: string | null;
84
- }): GoogleChatConfigAccessorAccount {
85
- const accountId = normalizeAccountId(
86
- params.accountId ?? params.cfg.channels?.googlechat?.defaultAccount,
87
- );
88
- return { config: mergeGoogleChatAccountConfig(params.cfg, accountId) };
89
- }
90
-
91
- function parseServiceAccount(value: unknown): Record<string, unknown> | null {
92
- if (isSecretRef(value)) {
93
- return null;
94
- }
95
-
96
- if (typeof value === "string") {
97
- const trimmed = value.trim();
98
- if (!trimmed) {
99
- return null;
100
- }
101
- return safeParseJsonWithSchema(JsonRecordSchema, trimmed);
102
- }
103
-
104
- return safeParseWithSchema(JsonRecordSchema, value);
105
- }
106
-
107
- function resolveCredentialsFromConfig(params: {
108
- accountId: string;
109
- account: GoogleChatAccountConfig;
110
- }): {
111
- credentials?: Record<string, unknown>;
112
- credentialsFile?: string;
113
- source: GoogleChatCredentialSource;
114
- } {
115
- const { account, accountId } = params;
116
- const inline = parseServiceAccount(account.serviceAccount);
117
- if (inline) {
118
- return { credentials: inline, source: "inline" };
119
- }
120
-
121
- if (isSecretRef(account.serviceAccount)) {
122
- throw new Error(
123
- `channels.googlechat.accounts.${accountId}.serviceAccount: unresolved SecretRef "${account.serviceAccount.source}:${account.serviceAccount.provider}:${account.serviceAccount.id}". Resolve this command against an active gateway runtime snapshot before reading it.`,
124
- );
125
- }
126
-
127
- if (isSecretRef(account.serviceAccountRef)) {
128
- throw new Error(
129
- `channels.googlechat.accounts.${accountId}.serviceAccount: unresolved SecretRef "${account.serviceAccountRef.source}:${account.serviceAccountRef.provider}:${account.serviceAccountRef.id}". Resolve this command against an active gateway runtime snapshot before reading it.`,
130
- );
131
- }
132
-
133
- const file = normalizeOptionalString(account.serviceAccountFile);
134
- if (file) {
135
- return { credentialsFile: file, source: "file" };
136
- }
137
-
138
- if (accountId === DEFAULT_ACCOUNT_ID) {
139
- const envJson = process.env[ENV_SERVICE_ACCOUNT];
140
- const envInline = parseServiceAccount(envJson);
141
- if (envInline) {
142
- return { credentials: envInline, source: "env" };
143
- }
144
- const envFile = normalizeOptionalString(process.env[ENV_SERVICE_ACCOUNT_FILE]);
145
- if (envFile) {
146
- return { credentialsFile: envFile, source: "env" };
147
- }
148
- }
149
-
150
- return { source: "none" };
151
- }
152
-
153
- export function resolveGoogleChatAccount(params: {
154
- cfg: KlawConfig;
155
- accountId?: string | null;
156
- }): ResolvedGoogleChatAccount {
157
- const accountId = normalizeAccountId(
158
- params.accountId ?? params.cfg.channels?.["googlechat"]?.defaultAccount,
159
- );
160
- const baseEnabled = params.cfg.channels?.["googlechat"]?.enabled !== false;
161
- const merged = mergeGoogleChatAccountConfig(params.cfg, accountId);
162
- const accountEnabled = merged.enabled !== false;
163
- const enabled = baseEnabled && accountEnabled;
164
- const credentials = resolveCredentialsFromConfig({ accountId, account: merged });
165
-
166
- return {
167
- accountId,
168
- name: normalizeOptionalString(merged.name),
169
- enabled,
170
- config: merged,
171
- credentialSource: credentials.source,
172
- credentials: credentials.credentials,
173
- credentialsFile: credentials.credentialsFile,
174
- };
175
- }
176
-
177
- export function listEnabledGoogleChatAccounts(cfg: KlawConfig): ResolvedGoogleChatAccount[] {
178
- return listGoogleChatAccountIds(cfg)
179
- .map((accountId) => resolveGoogleChatAccount({ cfg, accountId }))
180
- .filter((account) => account.enabled);
181
- }
@@ -1,289 +0,0 @@
1
- import path from "node:path";
2
- import { afterAll, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
3
-
4
- const listEnabledGoogleChatAccounts = vi.hoisted(() => vi.fn());
5
- const resolveGoogleChatAccount = vi.hoisted(() => vi.fn());
6
- const createGoogleChatReaction = vi.hoisted(() => vi.fn());
7
- const deleteGoogleChatReaction = vi.hoisted(() => vi.fn());
8
- const listGoogleChatReactions = vi.hoisted(() => vi.fn());
9
- const sendGoogleChatMessage = vi.hoisted(() => vi.fn());
10
- const uploadGoogleChatAttachment = vi.hoisted(() => vi.fn());
11
- const resolveGoogleChatOutboundSpace = vi.hoisted(() => vi.fn());
12
- const getGoogleChatRuntime = vi.hoisted(() => vi.fn());
13
-
14
- vi.mock("./accounts.js", () => ({
15
- listEnabledGoogleChatAccounts,
16
- resolveGoogleChatAccount,
17
- }));
18
-
19
- vi.mock("./api.js", () => ({
20
- createGoogleChatReaction,
21
- deleteGoogleChatReaction,
22
- listGoogleChatReactions,
23
- sendGoogleChatMessage,
24
- uploadGoogleChatAttachment,
25
- }));
26
-
27
- vi.mock("./runtime.js", () => ({
28
- getGoogleChatRuntime,
29
- }));
30
-
31
- vi.mock("./targets.js", () => ({
32
- resolveGoogleChatOutboundSpace,
33
- }));
34
-
35
- let googlechatMessageActions: typeof import("./actions.js").googlechatMessageActions;
36
-
37
- describe("googlechat message actions", () => {
38
- beforeAll(async () => {
39
- ({ googlechatMessageActions } = await import("./actions.js"));
40
- });
41
-
42
- beforeEach(() => {
43
- vi.clearAllMocks();
44
- });
45
-
46
- afterAll(() => {
47
- vi.doUnmock("./accounts.js");
48
- vi.doUnmock("./api.js");
49
- vi.doUnmock("./runtime.js");
50
- vi.doUnmock("./targets.js");
51
- vi.resetModules();
52
- });
53
-
54
- function buildAccount(overrides: Record<string, unknown> = {}) {
55
- return {
56
- accountId: "default",
57
- enabled: true,
58
- credentialSource: "service-account",
59
- config: {},
60
- ...overrides,
61
- };
62
- }
63
-
64
- function expectJsonResult(result: unknown, details: Record<string, unknown>) {
65
- expect(result).toEqual({
66
- content: [
67
- {
68
- type: "text",
69
- text: JSON.stringify(details, null, 2),
70
- },
71
- ],
72
- details,
73
- });
74
- }
75
-
76
- it("describes send and reaction actions only when enabled accounts exist", () => {
77
- listEnabledGoogleChatAccounts.mockReturnValueOnce([]);
78
- expect(googlechatMessageActions.describeMessageTool?.({ cfg: {} as never })).toBeNull();
79
-
80
- listEnabledGoogleChatAccounts.mockReturnValueOnce([
81
- {
82
- enabled: true,
83
- credentialSource: "service-account",
84
- config: { actions: { reactions: true } },
85
- },
86
- ]);
87
-
88
- expect(googlechatMessageActions.describeMessageTool?.({ cfg: {} as never })).toEqual({
89
- actions: ["send", "upload-file", "react", "reactions"],
90
- });
91
- });
92
-
93
- it("honors account-scoped reaction gates during discovery", () => {
94
- resolveGoogleChatAccount.mockImplementation(({ accountId }: { accountId?: string | null }) => ({
95
- enabled: true,
96
- credentialSource: "service-account",
97
- config: {
98
- actions: { reactions: accountId === "work" },
99
- },
100
- }));
101
-
102
- expect(
103
- googlechatMessageActions.describeMessageTool?.({ cfg: {} as never, accountId: "default" }),
104
- ).toEqual({
105
- actions: ["send", "upload-file"],
106
- });
107
- expect(
108
- googlechatMessageActions.describeMessageTool?.({ cfg: {} as never, accountId: "work" }),
109
- ).toEqual({
110
- actions: ["send", "upload-file", "react", "reactions"],
111
- });
112
- });
113
-
114
- it("sends messages with uploaded media through the resolved space", async () => {
115
- const account = buildAccount({
116
- config: { mediaMaxMb: 5 },
117
- });
118
- resolveGoogleChatAccount.mockReturnValue(account);
119
- resolveGoogleChatOutboundSpace.mockResolvedValue("spaces/AAA");
120
- const readRemoteMediaBuffer = vi.fn(async () => ({
121
- buffer: Buffer.from("remote-bytes"),
122
- fileName: "remote.png",
123
- contentType: "image/png",
124
- }));
125
- getGoogleChatRuntime.mockReturnValue({
126
- channel: {
127
- media: {
128
- readRemoteMediaBuffer,
129
- },
130
- },
131
- });
132
- uploadGoogleChatAttachment.mockResolvedValue({
133
- attachmentUploadToken: "token-1",
134
- });
135
- sendGoogleChatMessage.mockResolvedValue({
136
- messageName: "spaces/AAA/messages/msg-1",
137
- });
138
-
139
- if (!googlechatMessageActions.handleAction) {
140
- throw new Error("Expected googlechatMessageActions.handleAction to be defined");
141
- }
142
- const result = await googlechatMessageActions.handleAction({
143
- action: "send",
144
- params: {
145
- to: "spaces/AAA",
146
- message: "caption",
147
- media: "https://example.com/file.png",
148
- threadId: "thread-1",
149
- },
150
- cfg: {},
151
- accountId: "default",
152
- } as never);
153
-
154
- expect(resolveGoogleChatOutboundSpace).toHaveBeenCalledWith({
155
- account,
156
- target: "spaces/AAA",
157
- });
158
- expect(readRemoteMediaBuffer).toHaveBeenCalledWith({
159
- url: "https://example.com/file.png",
160
- maxBytes: 5 * 1024 * 1024,
161
- });
162
- expect(uploadGoogleChatAttachment).toHaveBeenCalledWith({
163
- account,
164
- space: "spaces/AAA",
165
- filename: "remote.png",
166
- buffer: Buffer.from("remote-bytes"),
167
- contentType: "image/png",
168
- });
169
- expect(sendGoogleChatMessage).toHaveBeenCalledWith({
170
- account,
171
- space: "spaces/AAA",
172
- text: "caption",
173
- thread: "thread-1",
174
- attachments: [{ attachmentUploadToken: "token-1", contentName: "remote.png" }],
175
- });
176
- expectJsonResult(result, { ok: true, to: "spaces/AAA" });
177
- });
178
-
179
- it("routes upload-file through the same attachment upload path with filename override", async () => {
180
- const account = buildAccount({
181
- config: { mediaMaxMb: 5 },
182
- });
183
- resolveGoogleChatAccount.mockReturnValue(account);
184
- resolveGoogleChatOutboundSpace.mockResolvedValue("spaces/BBB");
185
- const localRoot = "/tmp/googlechat-action-test";
186
- const localPath = path.join(localRoot, "local.md");
187
- const readFile = vi.fn(async () => Buffer.from("local-bytes"));
188
- getGoogleChatRuntime.mockReturnValue({
189
- channel: {
190
- media: {
191
- readRemoteMediaBuffer: vi.fn(),
192
- },
193
- },
194
- });
195
- uploadGoogleChatAttachment.mockResolvedValue({
196
- attachmentUploadToken: "token-2",
197
- });
198
- sendGoogleChatMessage.mockResolvedValue({
199
- messageName: "spaces/BBB/messages/msg-2",
200
- });
201
-
202
- if (!googlechatMessageActions.handleAction) {
203
- throw new Error("Expected googlechatMessageActions.handleAction to be defined");
204
- }
205
- const result = await googlechatMessageActions.handleAction({
206
- action: "upload-file",
207
- params: {
208
- to: "spaces/BBB",
209
- path: localPath,
210
- message: "notes",
211
- filename: "renamed.txt",
212
- },
213
- cfg: {},
214
- accountId: "default",
215
- mediaLocalRoots: [localRoot],
216
- mediaReadFile: readFile,
217
- } as never);
218
-
219
- expect(readFile).toHaveBeenCalledWith(localPath);
220
- expect(uploadGoogleChatAttachment).toHaveBeenCalledWith({
221
- account,
222
- space: "spaces/BBB",
223
- filename: "renamed.txt",
224
- buffer: Buffer.from("local-bytes"),
225
- contentType: "text/markdown",
226
- });
227
- expect(sendGoogleChatMessage).toHaveBeenCalledWith({
228
- account,
229
- space: "spaces/BBB",
230
- text: "notes",
231
- thread: undefined,
232
- attachments: [{ attachmentUploadToken: "token-2", contentName: "renamed.txt" }],
233
- });
234
- expectJsonResult(result, { ok: true, to: "spaces/BBB" });
235
- });
236
-
237
- it("removes only matching app reactions on react remove", async () => {
238
- const account = buildAccount({
239
- config: { botUser: "users/app-bot" },
240
- });
241
- resolveGoogleChatAccount.mockReturnValue(account);
242
- listGoogleChatReactions.mockResolvedValue([
243
- {
244
- name: "reactions/1",
245
- emoji: { unicode: "👍" },
246
- user: { name: "users/app" },
247
- },
248
- {
249
- name: "reactions/2",
250
- emoji: { unicode: "👍" },
251
- user: { name: "users/app-bot" },
252
- },
253
- {
254
- name: "reactions/3",
255
- emoji: { unicode: "👍" },
256
- user: { name: "users/other" },
257
- },
258
- ]);
259
-
260
- if (!googlechatMessageActions.handleAction) {
261
- throw new Error("Expected googlechatMessageActions.handleAction to be defined");
262
- }
263
- const result = await googlechatMessageActions.handleAction({
264
- action: "react",
265
- params: {
266
- messageId: "spaces/AAA/messages/msg-1",
267
- emoji: "👍",
268
- remove: true,
269
- },
270
- cfg: {},
271
- accountId: "default",
272
- } as never);
273
-
274
- expect(listGoogleChatReactions).toHaveBeenCalledWith({
275
- account,
276
- messageName: "spaces/AAA/messages/msg-1",
277
- });
278
- expect(deleteGoogleChatReaction).toHaveBeenCalledTimes(2);
279
- expect(deleteGoogleChatReaction).toHaveBeenNthCalledWith(1, {
280
- account,
281
- reactionName: "reactions/1",
282
- });
283
- expect(deleteGoogleChatReaction).toHaveBeenNthCalledWith(2, {
284
- account,
285
- reactionName: "reactions/2",
286
- });
287
- expectJsonResult(result, { ok: true, removed: 2 });
288
- });
289
- });