@kodelyth/googlechat 2026.5.42 → 2026.6.1

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 +16 -4
  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
@@ -1,543 +0,0 @@
1
- import fs from "node:fs/promises";
2
- import os from "node:os";
3
- import path from "node:path";
4
- import { afterAll, afterEach, beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
5
-
6
- const mocks = vi.hoisted(() => ({
7
- buildHostnameAllowlistPolicyFromSuffixAllowlist: vi.fn((hosts: string[]) => ({
8
- hostnameAllowlist: hosts,
9
- })),
10
- fetchWithSsrFGuard: vi.fn(),
11
- gaxiosCtor: vi.fn(
12
- function MockGaxios(
13
- this: {
14
- defaults: Record<string, unknown>;
15
- interceptors: {
16
- request: { add: ReturnType<typeof vi.fn> };
17
- response: { add: ReturnType<typeof vi.fn> };
18
- };
19
- },
20
- defaults,
21
- ) {
22
- this.defaults = defaults as Record<string, unknown>;
23
- this.interceptors = {
24
- request: { add: vi.fn() },
25
- response: { add: vi.fn() },
26
- };
27
- },
28
- ),
29
- }));
30
-
31
- vi.mock("klaw/plugin-sdk/ssrf-runtime", () => ({
32
- buildHostnameAllowlistPolicyFromSuffixAllowlist:
33
- mocks.buildHostnameAllowlistPolicyFromSuffixAllowlist,
34
- fetchWithSsrFGuard: mocks.fetchWithSsrFGuard,
35
- }));
36
-
37
- vi.mock("gaxios", () => ({
38
- Gaxios: mocks.gaxiosCtor,
39
- }));
40
-
41
- let testing: typeof import("./google-auth.runtime.js").testing;
42
- let createGoogleAuthFetch: typeof import("./google-auth.runtime.js").createGoogleAuthFetch;
43
- let getGoogleAuthTransport: typeof import("./google-auth.runtime.js").getGoogleAuthTransport;
44
- let resolveValidatedGoogleChatCredentials: typeof import("./google-auth.runtime.js").resolveValidatedGoogleChatCredentials;
45
-
46
- beforeAll(async () => {
47
- ({
48
- testing,
49
- createGoogleAuthFetch,
50
- getGoogleAuthTransport,
51
- resolveValidatedGoogleChatCredentials,
52
- } = await import("./google-auth.runtime.js"));
53
- });
54
-
55
- beforeEach(() => {
56
- testing.resetGoogleAuthRuntimeForTests();
57
- mocks.buildHostnameAllowlistPolicyFromSuffixAllowlist.mockClear();
58
- mocks.fetchWithSsrFGuard.mockReset();
59
- mocks.gaxiosCtor.mockClear();
60
- });
61
-
62
- afterEach(() => {
63
- vi.restoreAllMocks();
64
- vi.unstubAllGlobals();
65
- vi.unstubAllEnvs();
66
- });
67
-
68
- afterAll(() => {
69
- vi.doUnmock("klaw/plugin-sdk/ssrf-runtime");
70
- vi.doUnmock("gaxios");
71
- vi.resetModules();
72
- });
73
-
74
- function mockCallArg(mock: ReturnType<typeof vi.fn>, callIndex = 0, argIndex = 0): unknown {
75
- const call = mock.mock.calls[callIndex];
76
- if (!call) {
77
- throw new Error(`Expected mock call ${callIndex}`);
78
- }
79
- return call[argIndex];
80
- }
81
-
82
- describe("googlechat google auth runtime", () => {
83
- it("routes Google auth fetches through the SSRF guard and preserves explicit proxy mTLS", async () => {
84
- const release = vi.fn();
85
- const injectedFetch = vi.fn(globalThis.fetch);
86
- mocks.fetchWithSsrFGuard.mockResolvedValueOnce({
87
- response: new Response("ok", { status: 200 }),
88
- release,
89
- });
90
-
91
- const guardedFetch = createGoogleAuthFetch(injectedFetch);
92
- const response = await guardedFetch("https://oauth2.googleapis.com/token", {
93
- agent: { proxy: new URL("http://proxy.example:8080") },
94
- cert: "CLIENT_CERT",
95
- headers: { "content-type": "application/json" },
96
- key: "CLIENT_KEY",
97
- method: "POST",
98
- proxy: "http://proxy.example:8080",
99
- } as RequestInit);
100
-
101
- expect(mocks.fetchWithSsrFGuard).toHaveBeenCalledWith({
102
- auditContext: "googlechat.auth.google-auth",
103
- dispatcherPolicy: {
104
- allowPrivateProxy: true,
105
- mode: "explicit-proxy",
106
- proxyTls: {
107
- cert: "CLIENT_CERT",
108
- key: "CLIENT_KEY",
109
- },
110
- proxyUrl: "http://proxy.example:8080",
111
- },
112
- fetchImpl: injectedFetch,
113
- init: {
114
- headers: { "content-type": "application/json" },
115
- method: "POST",
116
- },
117
- policy: {
118
- hostnameAllowlist: ["accounts.google.com", "googleapis.com"],
119
- },
120
- url: "https://oauth2.googleapis.com/token",
121
- });
122
- await expect(response.text()).resolves.toBe("ok");
123
- expect(release).toHaveBeenCalledOnce();
124
- });
125
-
126
- it("lets the guard resolve the ambient runtime fetch when no override is injected", async () => {
127
- const release = vi.fn();
128
- mocks.fetchWithSsrFGuard.mockResolvedValueOnce({
129
- response: new Response("ok", { status: 200 }),
130
- release,
131
- });
132
-
133
- const guardedFetch = createGoogleAuthFetch();
134
- await guardedFetch("https://oauth2.googleapis.com/token", {
135
- method: "POST",
136
- } as RequestInit);
137
-
138
- expect(mockCallArg(mocks.fetchWithSsrFGuard)).not.toHaveProperty("fetchImpl");
139
- expect(release).toHaveBeenCalledOnce();
140
- });
141
-
142
- it("keeps using the guard-selected runtime fetch even if global fetch changes later", async () => {
143
- const release = vi.fn();
144
- const originalFetch = globalThis.fetch;
145
- mocks.fetchWithSsrFGuard.mockResolvedValueOnce({
146
- response: new Response("ok", { status: 200 }),
147
- release,
148
- });
149
-
150
- const guardedFetch = createGoogleAuthFetch();
151
- (globalThis as Record<string, unknown>).fetch = vi.fn(async () => new Response("patched"));
152
-
153
- try {
154
- await guardedFetch("https://oauth2.googleapis.com/token", {
155
- method: "POST",
156
- } as RequestInit);
157
- } finally {
158
- (globalThis as Record<string, unknown>).fetch = originalFetch;
159
- }
160
-
161
- expect(mockCallArg(mocks.fetchWithSsrFGuard)).not.toHaveProperty("fetchImpl");
162
- expect(release).toHaveBeenCalledOnce();
163
- });
164
-
165
- it("bypasses explicit proxy when noProxy excludes the Google auth host", async () => {
166
- const release = vi.fn();
167
- mocks.fetchWithSsrFGuard.mockResolvedValueOnce({
168
- response: new Response("ok", { status: 200 }),
169
- release,
170
- });
171
-
172
- const guardedFetch = createGoogleAuthFetch();
173
- const response = await guardedFetch("https://oauth2.googleapis.com/token", {
174
- cert: "CLIENT_CERT",
175
- key: "CLIENT_KEY",
176
- method: "POST",
177
- noProxy: ["oauth2.googleapis.com"],
178
- proxy: "http://proxy.example:8080",
179
- } as RequestInit);
180
-
181
- expect(mocks.fetchWithSsrFGuard).toHaveBeenCalledWith({
182
- auditContext: "googlechat.auth.google-auth",
183
- dispatcherPolicy: {
184
- connect: {
185
- cert: "CLIENT_CERT",
186
- key: "CLIENT_KEY",
187
- },
188
- mode: "direct",
189
- },
190
- init: {
191
- method: "POST",
192
- },
193
- policy: {
194
- hostnameAllowlist: ["accounts.google.com", "googleapis.com"],
195
- },
196
- url: "https://oauth2.googleapis.com/token",
197
- });
198
- await expect(response.text()).resolves.toBe("ok");
199
- expect(release).toHaveBeenCalledOnce();
200
- });
201
-
202
- it("preserves env-proxy transport when HTTPS proxy is configured", async () => {
203
- const release = vi.fn();
204
- mocks.fetchWithSsrFGuard.mockResolvedValueOnce({
205
- response: new Response("ok", { status: 200 }),
206
- release,
207
- });
208
- vi.stubEnv("HTTPS_PROXY", "http://env-proxy.example:8080");
209
- vi.stubEnv("https_proxy", "http://lower-proxy.example:8080");
210
-
211
- const guardedFetch = createGoogleAuthFetch();
212
- const response = await guardedFetch("https://oauth2.googleapis.com/token", {
213
- cert: "CLIENT_CERT",
214
- key: "CLIENT_KEY",
215
- method: "POST",
216
- } as RequestInit);
217
-
218
- expect(mocks.fetchWithSsrFGuard).toHaveBeenCalledWith({
219
- auditContext: "googlechat.auth.google-auth",
220
- dispatcherPolicy: {
221
- mode: "env-proxy",
222
- proxyTls: {
223
- cert: "CLIENT_CERT",
224
- key: "CLIENT_KEY",
225
- },
226
- },
227
- init: {
228
- method: "POST",
229
- },
230
- policy: {
231
- hostnameAllowlist: ["accounts.google.com", "googleapis.com"],
232
- },
233
- url: "https://oauth2.googleapis.com/token",
234
- });
235
- await expect(response.text()).resolves.toBe("ok");
236
- expect(release).toHaveBeenCalledOnce();
237
- });
238
-
239
- it("matches gaxios proxy env precedence for Google auth requests", () => {
240
- vi.stubEnv("HTTP_PROXY", "http://upper-http-proxy.example:8080");
241
- vi.stubEnv("http_proxy", "http://lower-http-proxy.example:8080");
242
- vi.stubEnv("HTTPS_PROXY", "http://upper-https-proxy.example:8080");
243
- vi.stubEnv("https_proxy", "http://lower-https-proxy.example:8080");
244
-
245
- expect(testing.resolveGoogleAuthEnvProxyUrl("https")).toBe(
246
- "http://upper-https-proxy.example:8080",
247
- );
248
- expect(testing.resolveGoogleAuthEnvProxyUrl("http")).toBe(
249
- "http://upper-http-proxy.example:8080",
250
- );
251
- });
252
-
253
- it("releases guarded auth fetch resources even when callers do not consume the body", async () => {
254
- const release = vi.fn();
255
- mocks.fetchWithSsrFGuard.mockResolvedValueOnce({
256
- response: new Response("ok", { status: 200 }),
257
- release,
258
- });
259
-
260
- const guardedFetch = createGoogleAuthFetch();
261
- const response = await guardedFetch("https://oauth2.googleapis.com/token", {
262
- method: "POST",
263
- } as RequestInit);
264
-
265
- expect(release).toHaveBeenCalledOnce();
266
- await expect(response.text()).resolves.toBe("ok");
267
- });
268
-
269
- it("rejects oversized guarded auth responses before buffering them into memory", async () => {
270
- const release = vi.fn();
271
- let chunkIndex = 0;
272
- const chunks = [new Uint8Array(700 * 1024), new Uint8Array(400 * 1024)];
273
- const body = new ReadableStream<Uint8Array>({
274
- pull(controller) {
275
- if (chunkIndex < chunks.length) {
276
- controller.enqueue(chunks[chunkIndex++]);
277
- return;
278
- }
279
- controller.close();
280
- },
281
- });
282
- mocks.fetchWithSsrFGuard.mockResolvedValueOnce({
283
- response: new Response(body, { status: 200 }),
284
- release,
285
- });
286
-
287
- const guardedFetch = createGoogleAuthFetch();
288
-
289
- await expect(
290
- guardedFetch("https://oauth2.googleapis.com/token", {
291
- method: "POST",
292
- } as RequestInit),
293
- ).rejects.toThrow("Google auth response exceeds 1048576 bytes.");
294
- expect(release).toHaveBeenCalledOnce();
295
- });
296
-
297
- it("rejects non-stream guarded auth responses instead of buffering them unbounded", async () => {
298
- const release = vi.fn();
299
- const arrayBuffer = vi.fn(async () => new ArrayBuffer(16));
300
- mocks.fetchWithSsrFGuard.mockResolvedValueOnce({
301
- response: {
302
- arrayBuffer,
303
- body: null,
304
- headers: new Headers(),
305
- status: 200,
306
- statusText: "OK",
307
- } as unknown as Response,
308
- release,
309
- });
310
-
311
- const guardedFetch = createGoogleAuthFetch();
312
-
313
- await expect(
314
- guardedFetch("https://oauth2.googleapis.com/token", {
315
- method: "POST",
316
- } as RequestInit),
317
- ).rejects.toThrow(
318
- "Google auth response body stream unavailable; refusing to buffer unbounded response.",
319
- );
320
- expect(arrayBuffer).not.toHaveBeenCalled();
321
- expect(release).toHaveBeenCalledOnce();
322
- });
323
-
324
- it("rejects oversized auth responses from content-length before reading the body", async () => {
325
- const release = vi.fn();
326
- const arrayBuffer = vi.fn(async () => new ArrayBuffer(16));
327
- mocks.fetchWithSsrFGuard.mockResolvedValueOnce({
328
- response: {
329
- arrayBuffer,
330
- body: null,
331
- headers: new Headers({
332
- "content-length": String(2 * 1024 * 1024),
333
- }),
334
- status: 200,
335
- statusText: "OK",
336
- } as unknown as Response,
337
- release,
338
- });
339
-
340
- const guardedFetch = createGoogleAuthFetch();
341
-
342
- await expect(
343
- guardedFetch("https://oauth2.googleapis.com/token", {
344
- method: "POST",
345
- } as RequestInit),
346
- ).rejects.toThrow("Google auth response exceeds 1048576 bytes.");
347
- expect(arrayBuffer).not.toHaveBeenCalled();
348
- expect(release).toHaveBeenCalledOnce();
349
- });
350
-
351
- it("builds a scoped Gaxios transport without mutating global window", async () => {
352
- const originalWindowDescriptor = Object.getOwnPropertyDescriptor(globalThis, "window");
353
- Reflect.deleteProperty(globalThis as object, "window");
354
- try {
355
- const transport = await getGoogleAuthTransport();
356
- const transportDefaults = transport.defaults as { fetchImplementation?: unknown };
357
- const requestInterceptorAdd = transport.interceptors.request.add as unknown as ReturnType<
358
- typeof vi.fn
359
- >;
360
- const responseInterceptorAdd = transport.interceptors.response.add as unknown as ReturnType<
361
- typeof vi.fn
362
- >;
363
- const requestInterceptor = mockCallArg(requestInterceptorAdd) as
364
- | { resolved?: unknown }
365
- | undefined;
366
- const responseInterceptor = mockCallArg(responseInterceptorAdd) as
367
- | { resolved?: unknown }
368
- | undefined;
369
-
370
- expect(mocks.gaxiosCtor).toHaveBeenCalledOnce();
371
- expect(typeof transportDefaults.fetchImplementation).toBe("function");
372
- expect(requestInterceptorAdd).toHaveBeenCalledOnce();
373
- expect(typeof requestInterceptor?.resolved).toBe("function");
374
- expect(responseInterceptorAdd).toHaveBeenCalledOnce();
375
- expect(typeof responseInterceptor?.resolved).toBe("function");
376
- expect("window" in globalThis).toBe(false);
377
- } finally {
378
- if (originalWindowDescriptor) {
379
- Object.defineProperty(globalThis, "window", originalWindowDescriptor);
380
- }
381
- }
382
- });
383
-
384
- it("keeps auth transports isolated from google-auth interceptor mutations", async () => {
385
- const first = await getGoogleAuthTransport();
386
- const second = await getGoogleAuthTransport();
387
-
388
- expect(first).not.toBe(second);
389
- expect(mocks.gaxiosCtor).toHaveBeenCalledTimes(2);
390
- expect(first.interceptors.request.add).toHaveBeenCalledOnce();
391
- expect(first.interceptors.response.add).toHaveBeenCalledOnce();
392
- expect(second.interceptors.request.add).toHaveBeenCalledOnce();
393
- expect(second.interceptors.response.add).toHaveBeenCalledOnce();
394
- });
395
-
396
- it("normalizes Google auth request headers before upstream interceptors run", () => {
397
- const config = {
398
- headers: { "x-test": "1" },
399
- url: new URL("https://www.googleapis.com/oauth2/v1/certs"),
400
- };
401
-
402
- const normalized = testing.normalizeGoogleAuthPreparedRequestHeaders(config);
403
-
404
- expect(normalized.headers).toBeInstanceOf(Headers);
405
- expect(normalized.headers.has("x-test")).toBe(true);
406
- expect(normalized.headers.get("x-test")).toBe("1");
407
- });
408
-
409
- it("normalizes Google auth response headers before upstream cache-control reads", () => {
410
- const response = {
411
- data: {},
412
- headers: {
413
- "cache-control": "public, max-age=3600",
414
- },
415
- };
416
-
417
- const normalized = testing.normalizeGoogleAuthResponseHeaders(response);
418
-
419
- expect(normalized.headers).toBeInstanceOf(Headers);
420
- expect(normalized.headers.get("cache-control")).toBe("public, max-age=3600");
421
- });
422
-
423
- it("rejects service-account credentials that override Google auth endpoints", async () => {
424
- await expect(
425
- resolveValidatedGoogleChatCredentials({
426
- accountId: "default",
427
- config: {},
428
- credentialSource: "inline",
429
- credentials: {
430
- client_email: "bot@example.iam.gserviceaccount.com",
431
- private_key: "key",
432
- token_uri: "https://evil.example/token",
433
- type: "service_account",
434
- },
435
- enabled: true,
436
- }),
437
- ).rejects.toThrow(/token_uri/);
438
- });
439
-
440
- it("reads and validates service-account files before passing them to google-auth", async () => {
441
- const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "googlechat-auth-"));
442
- try {
443
- const credentialsPath = path.join(tempDir, "service-account.json");
444
- await fs.writeFile(
445
- credentialsPath,
446
- JSON.stringify({
447
- auth_provider_x509_cert_url: "https://www.googleapis.com/oauth2/v1/certs",
448
- auth_uri: "https://accounts.google.com/o/oauth2/auth",
449
- client_email: "bot@example.iam.gserviceaccount.com",
450
- private_key: "key",
451
- token_uri: "https://oauth2.googleapis.com/token",
452
- type: "service_account",
453
- universe_domain: "googleapis.com",
454
- }),
455
- "utf8",
456
- );
457
-
458
- const credentials = await resolveValidatedGoogleChatCredentials({
459
- accountId: "default",
460
- config: {},
461
- credentialSource: "file",
462
- credentialsFile: credentialsPath,
463
- enabled: true,
464
- });
465
- if (!credentials) {
466
- throw new Error("expected validated credentials");
467
- }
468
- expect(credentials.client_email).toBe("bot@example.iam.gserviceaccount.com");
469
- expect(credentials.token_uri).toBe("https://oauth2.googleapis.com/token");
470
- expect(credentials.type).toBe("service_account");
471
- } finally {
472
- await fs.rm(tempDir, { force: true, recursive: true });
473
- }
474
- });
475
-
476
- it("accepts symlinked service-account files used by secret mounts", async () => {
477
- const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "googlechat-auth-link-"));
478
- try {
479
- const credentialsPath = path.join(tempDir, "service-account.json");
480
- const symlinkPath = path.join(tempDir, "service-account-link.json");
481
- await fs.writeFile(
482
- credentialsPath,
483
- JSON.stringify({
484
- auth_provider_x509_cert_url: "https://www.googleapis.com/oauth2/v1/certs",
485
- auth_uri: "https://accounts.google.com/o/oauth2/auth",
486
- client_email: "bot@example.iam.gserviceaccount.com",
487
- private_key: "key",
488
- token_uri: "https://oauth2.googleapis.com/token",
489
- type: "service_account",
490
- universe_domain: "googleapis.com",
491
- }),
492
- "utf8",
493
- );
494
- try {
495
- await fs.symlink(credentialsPath, symlinkPath);
496
- } catch (error) {
497
- if ((error as NodeJS.ErrnoException).code === "EPERM") {
498
- return;
499
- }
500
- throw error;
501
- }
502
-
503
- const credentials = await resolveValidatedGoogleChatCredentials({
504
- accountId: "default",
505
- config: {},
506
- credentialSource: "file",
507
- credentialsFile: symlinkPath,
508
- enabled: true,
509
- });
510
- if (!credentials) {
511
- throw new Error("expected validated credentials");
512
- }
513
- expect(credentials.client_email).toBe("bot@example.iam.gserviceaccount.com");
514
- expect(credentials.token_uri).toBe("https://oauth2.googleapis.com/token");
515
- expect(credentials.type).toBe("service_account");
516
- } finally {
517
- await fs.rm(tempDir, { force: true, recursive: true });
518
- }
519
- });
520
-
521
- it("does not disclose raw credential paths or OS errors when file reads fail", async () => {
522
- const missingPath = path.join(os.tmpdir(), "googlechat-auth-missing", "service-account.json");
523
-
524
- let thrown: unknown;
525
- try {
526
- await resolveValidatedGoogleChatCredentials({
527
- accountId: "default",
528
- config: {},
529
- credentialSource: "file",
530
- credentialsFile: missingPath,
531
- enabled: true,
532
- });
533
- } catch (error) {
534
- thrown = error;
535
- }
536
-
537
- expect(thrown).toBeInstanceOf(Error);
538
- expect((thrown as Error).message).toBe("Failed to load Google Chat service account file.");
539
- expect((thrown as Error).message).not.toMatch(
540
- /ENOENT|service-account\.json|googlechat-auth-missing/,
541
- );
542
- });
543
- });