@openclaw/google-meet 2026.5.2 → 2026.5.3-beta.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.
package/src/cli.test.ts DELETED
@@ -1,1013 +0,0 @@
1
- import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
2
- import { tmpdir } from "node:os";
3
- import path from "node:path";
4
- import { Command } from "commander";
5
- import { afterEach, describe, expect, it, vi } from "vitest";
6
- import { registerGoogleMeetCli } from "./cli.js";
7
- import { resolveGoogleMeetConfig } from "./config.js";
8
- import type { GoogleMeetRuntime } from "./runtime.js";
9
-
10
- const fetchGuardMocks = vi.hoisted(() => ({
11
- fetchWithSsrFGuard: vi.fn(
12
- async (params: {
13
- url: string;
14
- init?: RequestInit;
15
- }): Promise<{
16
- response: Response;
17
- release: () => Promise<void>;
18
- }> => ({
19
- response: await fetch(params.url, params.init),
20
- release: vi.fn(async () => {}),
21
- }),
22
- ),
23
- }));
24
-
25
- vi.mock("openclaw/plugin-sdk/ssrf-runtime", async (importOriginal) => {
26
- const actual = await importOriginal<typeof import("openclaw/plugin-sdk/ssrf-runtime")>();
27
- return {
28
- ...actual,
29
- fetchWithSsrFGuard: fetchGuardMocks.fetchWithSsrFGuard,
30
- };
31
- });
32
-
33
- function captureStdout() {
34
- let output = "";
35
- const writeSpy = vi.spyOn(process.stdout, "write").mockImplementation(((chunk: unknown) => {
36
- output += String(chunk);
37
- return true;
38
- }) as typeof process.stdout.write);
39
- return {
40
- output: () => output,
41
- restore: () => writeSpy.mockRestore(),
42
- };
43
- }
44
-
45
- function jsonResponse(value: unknown): Response {
46
- return new Response(JSON.stringify(value), {
47
- status: 200,
48
- headers: { "Content-Type": "application/json" },
49
- });
50
- }
51
-
52
- function requestUrl(input: RequestInfo | URL): URL {
53
- if (typeof input === "string") {
54
- return new URL(input);
55
- }
56
- if (input instanceof URL) {
57
- return input;
58
- }
59
- return new URL(input.url);
60
- }
61
-
62
- function stubMeetArtifactsApi(options: { failSmartNoteDocumentBody?: boolean } = {}) {
63
- vi.stubGlobal(
64
- "fetch",
65
- vi.fn(async (input: RequestInfo | URL) => {
66
- const url = requestUrl(input);
67
- if (url.pathname === "/v2/spaces/abc-defg-hij") {
68
- return jsonResponse({
69
- name: "spaces/abc-defg-hij",
70
- meetingCode: "abc-defg-hij",
71
- meetingUri: "https://meet.google.com/abc-defg-hij",
72
- });
73
- }
74
- if (url.pathname === "/calendar/v3/calendars/primary/events") {
75
- return jsonResponse({
76
- items: [
77
- {
78
- id: "event-1",
79
- summary: "Project sync",
80
- hangoutLink: "https://meet.google.com/abc-defg-hij",
81
- start: { dateTime: "2026-04-25T10:00:00Z" },
82
- end: { dateTime: "2026-04-25T10:30:00Z" },
83
- },
84
- ],
85
- });
86
- }
87
- if (url.pathname === "/v2/conferenceRecords") {
88
- return jsonResponse({
89
- conferenceRecords: [
90
- {
91
- name: "conferenceRecords/rec-1",
92
- space: "spaces/abc-defg-hij",
93
- startTime: "2026-04-25T10:00:00Z",
94
- endTime: "2026-04-25T10:30:00Z",
95
- },
96
- ],
97
- });
98
- }
99
- if (url.pathname === "/v2/conferenceRecords/rec-1") {
100
- return jsonResponse({
101
- name: "conferenceRecords/rec-1",
102
- space: "spaces/abc-defg-hij",
103
- startTime: "2026-04-25T10:00:00Z",
104
- endTime: "2026-04-25T10:30:00Z",
105
- });
106
- }
107
- if (url.pathname === "/v2/conferenceRecords/rec-1/participants") {
108
- return jsonResponse({
109
- participants: [
110
- {
111
- name: "conferenceRecords/rec-1/participants/p1",
112
- signedinUser: { user: "users/alice", displayName: "Alice" },
113
- },
114
- ],
115
- });
116
- }
117
- if (url.pathname === "/v2/conferenceRecords/rec-1/participants/p1/participantSessions") {
118
- return jsonResponse({
119
- participantSessions: [
120
- {
121
- name: "conferenceRecords/rec-1/participants/p1/participantSessions/s1",
122
- startTime: "2026-04-25T10:00:00Z",
123
- endTime: "2026-04-25T10:10:00Z",
124
- },
125
- ],
126
- });
127
- }
128
- if (url.pathname === "/v2/conferenceRecords/rec-1/recordings") {
129
- return jsonResponse({
130
- recordings: [
131
- {
132
- name: "conferenceRecords/rec-1/recordings/r1",
133
- state: "FILE_GENERATED",
134
- driveDestination: { file: "drive-file-1" },
135
- },
136
- ],
137
- });
138
- }
139
- if (url.pathname === "/v2/conferenceRecords/rec-1/transcripts") {
140
- return jsonResponse({
141
- transcripts: [
142
- {
143
- name: "conferenceRecords/rec-1/transcripts/t1",
144
- state: "FILE_GENERATED",
145
- docsDestination: { document: "doc-1" },
146
- },
147
- ],
148
- });
149
- }
150
- if (url.pathname === "/v2/conferenceRecords/rec-1/transcripts/t1/entries") {
151
- return jsonResponse({
152
- transcriptEntries: [
153
- {
154
- name: "conferenceRecords/rec-1/transcripts/t1/entries/e1",
155
- text: "Hello from the transcript.",
156
- startTime: "2026-04-25T10:01:00Z",
157
- participant: "conferenceRecords/rec-1/participants/p1",
158
- },
159
- ],
160
- });
161
- }
162
- if (url.pathname === "/v2/conferenceRecords/rec-1/smartNotes") {
163
- return jsonResponse({
164
- smartNotes: [
165
- {
166
- name: "conferenceRecords/rec-1/smartNotes/sn1",
167
- state: "FILE_GENERATED",
168
- docsDestination: { document: "notes-1" },
169
- },
170
- ],
171
- });
172
- }
173
- if (url.pathname === "/drive/v3/files/doc-1/export") {
174
- return new Response("Transcript document body.", {
175
- status: 200,
176
- headers: { "Content-Type": "text/plain" },
177
- });
178
- }
179
- if (url.pathname === "/drive/v3/files/notes-1/export") {
180
- if (options.failSmartNoteDocumentBody) {
181
- return new Response("insufficientPermissions", { status: 403 });
182
- }
183
- return new Response("Smart note document body.", {
184
- status: 200,
185
- headers: { "Content-Type": "text/plain" },
186
- });
187
- }
188
- return new Response("not found", { status: 404 });
189
- }),
190
- );
191
- }
192
-
193
- function setupCli(params: {
194
- config?: Parameters<typeof resolveGoogleMeetConfig>[0];
195
- runtime?: Partial<GoogleMeetRuntime>;
196
- ensureRuntime?: () => Promise<GoogleMeetRuntime>;
197
- }) {
198
- const program = new Command();
199
- registerGoogleMeetCli({
200
- program,
201
- config: resolveGoogleMeetConfig(params.config ?? {}),
202
- ensureRuntime:
203
- params.ensureRuntime ?? (async () => (params.runtime ?? {}) as unknown as GoogleMeetRuntime),
204
- });
205
- return program;
206
- }
207
-
208
- describe("google-meet CLI", () => {
209
- afterEach(() => {
210
- vi.unstubAllGlobals();
211
- });
212
-
213
- it("prints setup checks as text and JSON", async () => {
214
- {
215
- const stdout = captureStdout();
216
- try {
217
- await setupCli({
218
- runtime: {
219
- setupStatus: async () => ({
220
- ok: true,
221
- checks: [
222
- {
223
- id: "audio-bridge",
224
- ok: true,
225
- message: "Chrome command-pair realtime audio bridge configured (pcm16-24khz)",
226
- },
227
- ],
228
- }),
229
- },
230
- }).parseAsync(["googlemeet", "setup"], { from: "user" });
231
- expect(stdout.output()).toContain("Google Meet setup: OK");
232
- expect(stdout.output()).toContain(
233
- "[ok] audio-bridge: Chrome command-pair realtime audio bridge configured (pcm16-24khz)",
234
- );
235
- expect(stdout.output()).not.toContain('"checks"');
236
- } finally {
237
- stdout.restore();
238
- }
239
- }
240
-
241
- {
242
- const stdout = captureStdout();
243
- try {
244
- await setupCli({
245
- runtime: {
246
- setupStatus: async () => ({
247
- ok: false,
248
- checks: [{ id: "twilio-voice-call-plugin", ok: false, message: "missing" }],
249
- }),
250
- },
251
- }).parseAsync(["googlemeet", "setup", "--json"], { from: "user" });
252
- expect(JSON.parse(stdout.output())).toMatchObject({
253
- ok: false,
254
- checks: [{ id: "twilio-voice-call-plugin", ok: false }],
255
- });
256
- } finally {
257
- stdout.restore();
258
- }
259
- }
260
- });
261
-
262
- it("prints artifacts and attendance output", async () => {
263
- stubMeetArtifactsApi();
264
-
265
- const artifactsStdout = captureStdout();
266
- try {
267
- await setupCli({}).parseAsync(
268
- [
269
- "googlemeet",
270
- "artifacts",
271
- "--access-token",
272
- "token",
273
- "--expires-at",
274
- String(Date.now() + 120_000),
275
- "--conference-record",
276
- "rec-1",
277
- "--json",
278
- ],
279
- { from: "user" },
280
- );
281
- expect(JSON.parse(artifactsStdout.output())).toMatchObject({
282
- conferenceRecords: [{ name: "conferenceRecords/rec-1" }],
283
- artifacts: [
284
- {
285
- recordings: [{ name: "conferenceRecords/rec-1/recordings/r1" }],
286
- transcripts: [{ name: "conferenceRecords/rec-1/transcripts/t1" }],
287
- transcriptEntries: [
288
- {
289
- transcript: "conferenceRecords/rec-1/transcripts/t1",
290
- entries: [{ text: "Hello from the transcript." }],
291
- },
292
- ],
293
- smartNotes: [{ name: "conferenceRecords/rec-1/smartNotes/sn1" }],
294
- },
295
- ],
296
- tokenSource: "cached-access-token",
297
- });
298
- } finally {
299
- artifactsStdout.restore();
300
- }
301
-
302
- const attendanceStdout = captureStdout();
303
- try {
304
- await setupCli({}).parseAsync(
305
- [
306
- "googlemeet",
307
- "attendance",
308
- "--access-token",
309
- "token",
310
- "--expires-at",
311
- String(Date.now() + 120_000),
312
- "--conference-record",
313
- "rec-1",
314
- ],
315
- { from: "user" },
316
- );
317
- expect(attendanceStdout.output()).toContain("attendance rows: 1");
318
- expect(attendanceStdout.output()).toContain("participant: Alice");
319
- expect(attendanceStdout.output()).toContain(
320
- "conferenceRecords/rec-1/participants/p1/participantSessions/s1",
321
- );
322
- } finally {
323
- attendanceStdout.restore();
324
- }
325
- });
326
-
327
- it("ends an active conference for a Meet space", async () => {
328
- const fetchMock = vi.fn(async (input: RequestInfo | URL, _init?: RequestInit) => {
329
- const url = requestUrl(input);
330
- if (url.pathname === "/v2/spaces/abc-defg-hij") {
331
- return jsonResponse({
332
- name: "spaces/space-resource-123",
333
- meetingCode: "abc-defg-hij",
334
- meetingUri: "https://meet.google.com/abc-defg-hij",
335
- });
336
- }
337
- if (url.pathname === "/v2/spaces/space-resource-123:endActiveConference") {
338
- return jsonResponse({});
339
- }
340
- return new Response("not found", { status: 404 });
341
- });
342
- vi.stubGlobal("fetch", fetchMock);
343
-
344
- const stdout = captureStdout();
345
- try {
346
- await setupCli({}).parseAsync(
347
- [
348
- "googlemeet",
349
- "end-active-conference",
350
- "https://meet.google.com/abc-defg-hij",
351
- "--access-token",
352
- "token",
353
- "--expires-at",
354
- String(Date.now() + 120_000),
355
- "--json",
356
- ],
357
- { from: "user" },
358
- );
359
- expect(JSON.parse(stdout.output())).toMatchObject({
360
- space: "spaces/space-resource-123",
361
- ended: true,
362
- tokenSource: "cached-access-token",
363
- });
364
- expect(fetchMock).toHaveBeenCalledWith(
365
- "https://meet.googleapis.com/v2/spaces/space-resource-123:endActiveConference",
366
- expect.objectContaining({ method: "POST", body: "{}" }),
367
- );
368
- } finally {
369
- stdout.restore();
370
- }
371
- });
372
-
373
- it("rejects access policy flags when create would use browser fallback", async () => {
374
- await expect(
375
- setupCli({
376
- runtime: {
377
- createViaBrowser: vi.fn(async () => {
378
- throw new Error("browser fallback should not run");
379
- }),
380
- },
381
- }).parseAsync(["googlemeet", "create", "--access-type", "OPEN"], { from: "user" }),
382
- ).rejects.toThrow("access policy options require OAuth/API room creation");
383
- });
384
-
385
- it("prints the latest conference record", async () => {
386
- stubMeetArtifactsApi();
387
- const stdout = captureStdout();
388
-
389
- try {
390
- await setupCli({}).parseAsync(
391
- [
392
- "googlemeet",
393
- "latest",
394
- "--access-token",
395
- "token",
396
- "--expires-at",
397
- String(Date.now() + 120_000),
398
- "--meeting",
399
- "abc-defg-hij",
400
- ],
401
- { from: "user" },
402
- );
403
- expect(stdout.output()).toContain("space: spaces/abc-defg-hij");
404
- expect(stdout.output()).toContain("conference record: conferenceRecords/rec-1");
405
- } finally {
406
- stdout.restore();
407
- }
408
- });
409
-
410
- it("prints the latest conference record from today's calendar", async () => {
411
- stubMeetArtifactsApi();
412
- const stdout = captureStdout();
413
-
414
- try {
415
- await setupCli({}).parseAsync(
416
- [
417
- "googlemeet",
418
- "latest",
419
- "--access-token",
420
- "token",
421
- "--expires-at",
422
- String(Date.now() + 120_000),
423
- "--today",
424
- ],
425
- { from: "user" },
426
- );
427
- expect(stdout.output()).toContain("calendar event: Project sync");
428
- expect(stdout.output()).toContain("conference record: conferenceRecords/rec-1");
429
- } finally {
430
- stdout.restore();
431
- }
432
- });
433
-
434
- it("prints calendar event previews", async () => {
435
- stubMeetArtifactsApi();
436
- const stdout = captureStdout();
437
-
438
- try {
439
- await setupCli({}).parseAsync(
440
- [
441
- "googlemeet",
442
- "calendar-events",
443
- "--access-token",
444
- "token",
445
- "--expires-at",
446
- String(Date.now() + 120_000),
447
- "--today",
448
- ],
449
- { from: "user" },
450
- );
451
- expect(stdout.output()).toContain("meet events: 1");
452
- expect(stdout.output()).toContain("* Project sync");
453
- expect(stdout.output()).toContain("https://meet.google.com/abc-defg-hij");
454
- } finally {
455
- stdout.restore();
456
- }
457
- });
458
-
459
- it("prints markdown artifact and attendance output", async () => {
460
- stubMeetArtifactsApi();
461
- const tempDir = mkdtempSync(path.join(tmpdir(), "openclaw-google-meet-artifacts-"));
462
- const outputPath = path.join(tempDir, "artifacts.md");
463
- const artifactsStdout = captureStdout();
464
-
465
- try {
466
- await setupCli({}).parseAsync(
467
- [
468
- "googlemeet",
469
- "artifacts",
470
- "--access-token",
471
- "token",
472
- "--expires-at",
473
- String(Date.now() + 120_000),
474
- "--conference-record",
475
- "rec-1",
476
- "--format",
477
- "markdown",
478
- "--output",
479
- outputPath,
480
- ],
481
- { from: "user" },
482
- );
483
- const markdown = readFileSync(outputPath, "utf8");
484
- expect(artifactsStdout.output()).toContain(`wrote: ${outputPath}`);
485
- expect(markdown).toContain("# Google Meet Artifacts");
486
- expect(markdown).toContain("## conferenceRecords/rec-1");
487
- expect(markdown).toContain("### Transcript Entries: conferenceRecords/rec-1/transcripts/t1");
488
- expect(markdown).toContain("Hello from the transcript.");
489
- } finally {
490
- artifactsStdout.restore();
491
- rmSync(tempDir, { recursive: true, force: true });
492
- }
493
-
494
- const attendanceStdout = captureStdout();
495
- try {
496
- await setupCli({}).parseAsync(
497
- [
498
- "googlemeet",
499
- "attendance",
500
- "--access-token",
501
- "token",
502
- "--expires-at",
503
- String(Date.now() + 120_000),
504
- "--conference-record",
505
- "rec-1",
506
- "--format",
507
- "markdown",
508
- ],
509
- { from: "user" },
510
- );
511
- expect(attendanceStdout.output()).toContain("# Google Meet Attendance");
512
- expect(attendanceStdout.output()).toContain("## Alice");
513
- expect(attendanceStdout.output()).toContain(
514
- "conferenceRecords/rec-1/participants/p1/participantSessions/s1",
515
- );
516
- } finally {
517
- attendanceStdout.restore();
518
- }
519
- });
520
-
521
- it("prints CSV attendance output", async () => {
522
- stubMeetArtifactsApi();
523
- const stdout = captureStdout();
524
-
525
- try {
526
- await setupCli({}).parseAsync(
527
- [
528
- "googlemeet",
529
- "attendance",
530
- "--access-token",
531
- "token",
532
- "--expires-at",
533
- String(Date.now() + 120_000),
534
- "--conference-record",
535
- "rec-1",
536
- "--format",
537
- "csv",
538
- ],
539
- { from: "user" },
540
- );
541
- expect(stdout.output()).toContain("conferenceRecord,displayName,user");
542
- expect(stdout.output()).toContain("conferenceRecords/rec-1,Alice,users/alice");
543
- } finally {
544
- stdout.restore();
545
- }
546
- });
547
-
548
- it("writes an export bundle", async () => {
549
- stubMeetArtifactsApi();
550
- const stdout = captureStdout();
551
- const tempDir = mkdtempSync(path.join(tmpdir(), "openclaw-google-meet-export-"));
552
-
553
- try {
554
- await setupCli({}).parseAsync(
555
- [
556
- "googlemeet",
557
- "export",
558
- "--access-token",
559
- "token",
560
- "--expires-at",
561
- String(Date.now() + 120_000),
562
- "--conference-record",
563
- "rec-1",
564
- "--include-doc-bodies",
565
- "--zip",
566
- "--output",
567
- tempDir,
568
- ],
569
- { from: "user" },
570
- );
571
- expect(stdout.output()).toContain(`export: ${tempDir}`);
572
- expect(readFileSync(path.join(tempDir, "summary.md"), "utf8")).toContain(
573
- "# Google Meet Artifacts",
574
- );
575
- expect(readFileSync(path.join(tempDir, "attendance.csv"), "utf8")).toContain(
576
- "conferenceRecords/rec-1,Alice,users/alice",
577
- );
578
- expect(readFileSync(path.join(tempDir, "transcript.md"), "utf8")).toContain(
579
- "Hello from the transcript.",
580
- );
581
- expect(readFileSync(path.join(tempDir, "transcript.md"), "utf8")).toContain(
582
- "Transcript document body.",
583
- );
584
- const manifest = JSON.parse(readFileSync(path.join(tempDir, "manifest.json"), "utf8"));
585
- expect(manifest).toMatchObject({
586
- request: {
587
- conferenceRecord: "rec-1",
588
- includeDocumentBodies: true,
589
- },
590
- tokenSource: "cached-access-token",
591
- counts: {
592
- attendanceRows: 1,
593
- warnings: 0,
594
- },
595
- files: expect.arrayContaining([
596
- "summary.md",
597
- "attendance.csv",
598
- "transcript.md",
599
- "artifacts.json",
600
- "attendance.json",
601
- "manifest.json",
602
- ]),
603
- });
604
- expect(JSON.parse(readFileSync(path.join(tempDir, "artifacts.json"), "utf8"))).toMatchObject({
605
- conferenceRecords: [{ name: "conferenceRecords/rec-1" }],
606
- artifacts: [{ transcripts: [{ documentText: "Transcript document body." }] }],
607
- });
608
- expect(readFileSync(`${tempDir}.zip`).subarray(0, 4).toString("hex")).toBe("504b0304");
609
- } finally {
610
- stdout.restore();
611
- rmSync(tempDir, { recursive: true, force: true });
612
- rmSync(`${tempDir}.zip`, { force: true });
613
- }
614
- });
615
-
616
- it("includes artifact warnings in export summaries and manifests", async () => {
617
- stubMeetArtifactsApi({ failSmartNoteDocumentBody: true });
618
- const stdout = captureStdout();
619
- const tempDir = mkdtempSync(path.join(tmpdir(), "openclaw-google-meet-export-warning-"));
620
-
621
- try {
622
- await setupCli({}).parseAsync(
623
- [
624
- "googlemeet",
625
- "export",
626
- "--access-token",
627
- "token",
628
- "--expires-at",
629
- String(Date.now() + 120_000),
630
- "--conference-record",
631
- "rec-1",
632
- "--include-doc-bodies",
633
- "--output",
634
- tempDir,
635
- "--json",
636
- ],
637
- { from: "user" },
638
- );
639
- const summary = readFileSync(path.join(tempDir, "summary.md"), "utf8");
640
- expect(summary).toContain("### Warnings");
641
- expect(summary).toContain("Document body warning");
642
- const manifest = JSON.parse(readFileSync(path.join(tempDir, "manifest.json"), "utf8"));
643
- expect(manifest).toMatchObject({
644
- counts: { warnings: 1 },
645
- warnings: [
646
- {
647
- type: "smart_note_document_body",
648
- conferenceRecord: "conferenceRecords/rec-1",
649
- resource: "conferenceRecords/rec-1/smartNotes/sn1",
650
- },
651
- ],
652
- });
653
- } finally {
654
- stdout.restore();
655
- rmSync(tempDir, { recursive: true, force: true });
656
- }
657
- });
658
-
659
- it("accepts --json on session status", async () => {
660
- const stdout = captureStdout();
661
- try {
662
- await setupCli({
663
- runtime: {
664
- status: async () => ({
665
- found: true,
666
- sessions: [
667
- {
668
- id: "meet_1",
669
- url: "https://meet.google.com/abc-defg-hij",
670
- state: "active",
671
- transport: "twilio",
672
- mode: "realtime",
673
- participantIdentity: "Twilio PSTN participant",
674
- createdAt: "2026-04-25T00:00:00.000Z",
675
- updatedAt: "2026-04-25T00:00:01.000Z",
676
- realtime: { enabled: true, provider: "openai", toolPolicy: "safe-read-only" },
677
- notes: [],
678
- },
679
- ],
680
- }),
681
- },
682
- }).parseAsync(["googlemeet", "status", "--json"], { from: "user" });
683
- expect(JSON.parse(stdout.output())).toMatchObject({
684
- found: true,
685
- sessions: [{ id: "meet_1", transport: "twilio" }],
686
- });
687
- } finally {
688
- stdout.restore();
689
- }
690
- });
691
-
692
- it("runs a listen-first health probe", async () => {
693
- const testListen = vi.fn(async () => ({
694
- createdSession: true,
695
- listenVerified: true,
696
- listenTimedOut: false,
697
- transcriptLines: 1,
698
- session: {
699
- id: "meet_1",
700
- url: "https://meet.google.com/abc-defg-hij",
701
- state: "active" as const,
702
- transport: "chrome-node" as const,
703
- mode: "transcribe" as const,
704
- participantIdentity: "signed-in Google Chrome profile on a paired node",
705
- createdAt: "2026-04-25T00:00:00.000Z",
706
- updatedAt: "2026-04-25T00:00:01.000Z",
707
- realtime: { enabled: false, provider: "openai", toolPolicy: "safe-read-only" },
708
- notes: [],
709
- },
710
- }));
711
- const stdout = captureStdout();
712
- try {
713
- await setupCli({
714
- runtime: { testListen },
715
- }).parseAsync(
716
- [
717
- "googlemeet",
718
- "test-listen",
719
- "https://meet.google.com/abc-defg-hij",
720
- "--transport",
721
- "chrome-node",
722
- "--timeout-ms",
723
- "30000",
724
- ],
725
- { from: "user" },
726
- );
727
- expect(testListen).toHaveBeenCalledWith({
728
- url: "https://meet.google.com/abc-defg-hij",
729
- transport: "chrome-node",
730
- timeoutMs: 30000,
731
- });
732
- expect(JSON.parse(stdout.output())).toMatchObject({
733
- listenVerified: true,
734
- transcriptLines: 1,
735
- });
736
- } finally {
737
- stdout.restore();
738
- }
739
- });
740
-
741
- it("prints a dry-run export manifest without writing files", async () => {
742
- stubMeetArtifactsApi();
743
- const stdout = captureStdout();
744
- const parentDir = mkdtempSync(path.join(tmpdir(), "openclaw-google-meet-export-dry-run-"));
745
- const outputDir = path.join(parentDir, "bundle");
746
-
747
- try {
748
- await setupCli({}).parseAsync(
749
- [
750
- "googlemeet",
751
- "export",
752
- "--access-token",
753
- "token",
754
- "--expires-at",
755
- String(Date.now() + 120_000),
756
- "--conference-record",
757
- "rec-1",
758
- "--include-doc-bodies",
759
- "--output",
760
- outputDir,
761
- "--dry-run",
762
- ],
763
- { from: "user" },
764
- );
765
- const payload = JSON.parse(stdout.output());
766
- expect(payload).toMatchObject({
767
- dryRun: true,
768
- manifest: {
769
- request: {
770
- conferenceRecord: "rec-1",
771
- includeDocumentBodies: true,
772
- },
773
- counts: {
774
- attendanceRows: 1,
775
- transcriptEntries: 1,
776
- warnings: 0,
777
- },
778
- files: expect.arrayContaining(["summary.md", "manifest.json"]),
779
- },
780
- tokenSource: "cached-access-token",
781
- });
782
- expect(existsSync(outputDir)).toBe(false);
783
- } finally {
784
- stdout.restore();
785
- rmSync(parentDir, { recursive: true, force: true });
786
- }
787
- });
788
-
789
- it("prints human-readable session doctor output", async () => {
790
- const stdout = captureStdout();
791
- try {
792
- await setupCli({
793
- runtime: {
794
- status: async () => ({
795
- found: true,
796
- session: {
797
- id: "meet_1",
798
- url: "https://meet.google.com/abc-defg-hij",
799
- state: "active",
800
- transport: "chrome-node",
801
- mode: "realtime",
802
- participantIdentity: "signed-in Google Chrome profile on a paired node",
803
- createdAt: "2026-04-25T00:00:00.000Z",
804
- updatedAt: "2026-04-25T00:00:01.000Z",
805
- realtime: { enabled: true, provider: "openai", toolPolicy: "safe-read-only" },
806
- chrome: {
807
- audioBackend: "blackhole-2ch",
808
- launched: true,
809
- nodeId: "node-1",
810
- audioBridge: { type: "node-command-pair", provider: "openai" },
811
- health: {
812
- inCall: true,
813
- captioning: true,
814
- transcriptLines: 2,
815
- lastCaptionAt: "2026-04-25T00:00:03.000Z",
816
- lastCaptionSpeaker: "Alice",
817
- lastCaptionText: "Can everyone hear OpenClaw?",
818
- providerConnected: true,
819
- realtimeReady: true,
820
- audioInputActive: true,
821
- audioOutputActive: false,
822
- lastInputAt: "2026-04-25T00:00:02.000Z",
823
- lastInputBytes: 160,
824
- lastOutputBytes: 0,
825
- },
826
- },
827
- notes: [],
828
- },
829
- }),
830
- },
831
- }).parseAsync(["googlemeet", "doctor", "meet_1"], { from: "user" });
832
- expect(stdout.output()).toContain("session: meet_1");
833
- expect(stdout.output()).toContain("node: node-1");
834
- expect(stdout.output()).toContain("provider connected: yes");
835
- expect(stdout.output()).toContain("captioning: yes");
836
- expect(stdout.output()).toContain("transcript lines: 2");
837
- expect(stdout.output()).toContain("last caption text: Alice: Can everyone hear OpenClaw?");
838
- expect(stdout.output()).toContain("audio input active: yes");
839
- expect(stdout.output()).toContain("audio output active: no");
840
- } finally {
841
- stdout.restore();
842
- }
843
- });
844
-
845
- it("prints Twilio session doctor output", async () => {
846
- const stdout = captureStdout();
847
- try {
848
- await setupCli({
849
- runtime: {
850
- status: async () => ({
851
- found: true,
852
- session: {
853
- id: "meet_1",
854
- url: "https://meet.google.com/abc-defg-hij",
855
- state: "active",
856
- transport: "twilio",
857
- mode: "realtime",
858
- participantIdentity: "Twilio phone participant",
859
- createdAt: "2026-04-25T00:00:00.000Z",
860
- updatedAt: "2026-04-25T00:00:01.000Z",
861
- realtime: { enabled: true, provider: "openai", toolPolicy: "safe-read-only" },
862
- twilio: {
863
- dialInNumber: "+15551234567",
864
- pinProvided: true,
865
- dtmfSequence: "ww123456#",
866
- voiceCallId: "call-1",
867
- dtmfSent: true,
868
- introSent: true,
869
- },
870
- notes: [],
871
- },
872
- }),
873
- },
874
- }).parseAsync(["googlemeet", "doctor", "meet_1"], { from: "user" });
875
- expect(stdout.output()).toContain("session: meet_1");
876
- expect(stdout.output()).toContain("transport: twilio");
877
- expect(stdout.output()).toContain("twilio dial-in: +15551234567");
878
- expect(stdout.output()).toContain("voice call id: call-1");
879
- expect(stdout.output()).toContain("dtmf sent: yes");
880
- expect(stdout.output()).toContain("intro sent: yes");
881
- expect(stdout.output()).not.toContain("audio input active:");
882
- } finally {
883
- stdout.restore();
884
- }
885
- });
886
-
887
- it("verifies OAuth refresh without printing secrets", async () => {
888
- const fetchMock = vi.fn(async (_input: RequestInfo | URL, _init?: RequestInit) =>
889
- jsonResponse({
890
- access_token: "new-access-token",
891
- expires_in: 3600,
892
- token_type: "Bearer",
893
- }),
894
- );
895
- vi.stubGlobal("fetch", fetchMock);
896
- const ensureRuntime = vi.fn(async () => {
897
- throw new Error("runtime should not be loaded for OAuth doctor");
898
- });
899
- const stdout = captureStdout();
900
-
901
- try {
902
- await setupCli({
903
- config: {
904
- oauth: {
905
- clientId: "client-id",
906
- clientSecret: "client-secret",
907
- refreshToken: "rt-secret",
908
- },
909
- },
910
- ensureRuntime: ensureRuntime as unknown as () => Promise<GoogleMeetRuntime>,
911
- }).parseAsync(["googlemeet", "doctor", "--oauth", "--json"], { from: "user" });
912
- const output = stdout.output();
913
- expect(output).not.toContain("new-access-token");
914
- expect(output).not.toContain("rt-secret");
915
- expect(output).not.toContain("client-secret");
916
- expect(JSON.parse(output)).toMatchObject({
917
- ok: true,
918
- configured: true,
919
- tokenSource: "refresh-token",
920
- checks: [
921
- { id: "oauth-config", ok: true },
922
- { id: "oauth-token", ok: true },
923
- ],
924
- });
925
- expect(ensureRuntime).not.toHaveBeenCalled();
926
- const body = fetchMock.mock.calls[0]?.[1]?.body as URLSearchParams;
927
- expect(body.get("grant_type")).toBe("refresh_token");
928
- } finally {
929
- stdout.restore();
930
- }
931
- });
932
-
933
- it("can prove Google Meet API create access", async () => {
934
- vi.stubGlobal(
935
- "fetch",
936
- vi.fn(async (input: RequestInfo | URL) => {
937
- const url = requestUrl(input).href;
938
- if (url === "https://oauth2.googleapis.com/token") {
939
- return jsonResponse({
940
- access_token: "new-access-token",
941
- expires_in: 3600,
942
- token_type: "Bearer",
943
- });
944
- }
945
- if (url === "https://meet.googleapis.com/v2/spaces") {
946
- return jsonResponse({
947
- name: "spaces/new-space",
948
- meetingUri: "https://meet.google.com/new-abcd-xyz",
949
- });
950
- }
951
- return new Response("not found", { status: 404 });
952
- }),
953
- );
954
- const stdout = captureStdout();
955
-
956
- try {
957
- await setupCli({
958
- config: {
959
- oauth: {
960
- clientId: "client-id",
961
- refreshToken: "refresh-token",
962
- },
963
- },
964
- }).parseAsync(["googlemeet", "doctor", "--oauth", "--create-space", "--json"], {
965
- from: "user",
966
- });
967
- expect(JSON.parse(stdout.output())).toMatchObject({
968
- ok: true,
969
- tokenSource: "refresh-token",
970
- createdSpace: "spaces/new-space",
971
- meetingUri: "https://meet.google.com/new-abcd-xyz",
972
- checks: [
973
- { id: "oauth-config", ok: true },
974
- { id: "oauth-token", ok: true },
975
- { id: "meet-spaces-create", ok: true },
976
- ],
977
- });
978
- } finally {
979
- stdout.restore();
980
- }
981
- });
982
-
983
- it("recovers and summarizes an existing Meet tab", async () => {
984
- const stdout = captureStdout();
985
- try {
986
- await setupCli({
987
- config: { defaultTransport: "chrome-node" },
988
- runtime: {
989
- recoverCurrentTab: async () => ({
990
- transport: "chrome-node",
991
- nodeId: "node-1",
992
- found: true,
993
- targetId: "tab-1",
994
- tab: { targetId: "tab-1", url: "https://meet.google.com/abc-defg-hij" },
995
- browser: {
996
- inCall: false,
997
- manualActionRequired: true,
998
- manualActionReason: "meet-admission-required",
999
- manualActionMessage: "Admit the OpenClaw browser participant in Google Meet.",
1000
- browserUrl: "https://meet.google.com/abc-defg-hij",
1001
- },
1002
- message: "Admit the OpenClaw browser participant in Google Meet.",
1003
- }),
1004
- },
1005
- }).parseAsync(["googlemeet", "recover-tab"], { from: "user" });
1006
- expect(stdout.output()).toContain("Google Meet current tab: found");
1007
- expect(stdout.output()).toContain("target: tab-1");
1008
- expect(stdout.output()).toContain("manual reason: meet-admission-required");
1009
- } finally {
1010
- stdout.restore();
1011
- }
1012
- });
1013
- });