From 0e23107ffbabc5303ee50f656015eccf5723915e Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Fri, 24 Apr 2026 20:52:34 +0100 Subject: [PATCH] feat(google-meet): format setup status by default --- docs/plugins/google-meet.md | 1 + extensions/google-meet/index.test.ts | 74 ++++++++++++++++++++++++++++ extensions/google-meet/src/cli.ts | 21 +++++++- 3 files changed, 94 insertions(+), 2 deletions(-) diff --git a/docs/plugins/google-meet.md b/docs/plugins/google-meet.md index 73110a3ccd1..ea5e64b5732 100644 --- a/docs/plugins/google-meet.md +++ b/docs/plugins/google-meet.md @@ -75,6 +75,7 @@ The setup output is meant to be agent-readable. It reports Chrome profile, audio bridge, node pinning, delayed realtime intro, and, when Twilio delegation is configured, whether the `voice-call` plugin and Twilio credentials are ready. Treat any `ok: false` check as a blocker before asking an agent to join. +Use `openclaw googlemeet setup --json` for scripts or machine-readable output. Join a meeting: diff --git a/extensions/google-meet/index.test.ts b/extensions/google-meet/index.test.ts index 0f4d282a776..7f110adc9a3 100644 --- a/extensions/google-meet/index.test.ts +++ b/extensions/google-meet/index.test.ts @@ -1,10 +1,12 @@ import { EventEmitter } from "node:events"; import { PassThrough, Writable } from "node:stream"; +import { Command } from "commander"; import type { OpenClawPluginApi } from "openclaw/plugin-sdk/plugin-entry"; import type { RealtimeVoiceProviderPlugin } from "openclaw/plugin-sdk/realtime-voice"; import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import { createTestPluginApi } from "../../test/helpers/plugins/plugin-api.ts"; import plugin from "./index.js"; +import { registerGoogleMeetCli } from "./src/cli.js"; import { resolveGoogleMeetConfig, resolveGoogleMeetConfigWithEnv } from "./src/config.js"; import { buildGoogleMeetPreflightReport, @@ -19,6 +21,7 @@ import { import { startNodeRealtimeAudioBridge } from "./src/realtime-node.js"; import { startCommandRealtimeAudioBridge } from "./src/realtime.js"; import { normalizeMeetUrl } from "./src/runtime.js"; +import type { GoogleMeetRuntime } from "./src/runtime.js"; import { buildMeetDtmfSequence, normalizeDialInNumber } from "./src/transports/twilio.js"; const voiceCallMocks = vi.hoisted(() => ({ @@ -57,6 +60,18 @@ const noopLogger = { debug: vi.fn(), }; +function captureStdout() { + let output = ""; + const writeSpy = vi.spyOn(process.stdout, "write").mockImplementation(((chunk: unknown) => { + output += String(chunk); + return true; + }) as typeof process.stdout.write); + return { + output: () => output, + restore: () => writeSpy.mockRestore(), + }; +} + type TestBridgeProcess = { stdin?: { write(chunk: unknown): unknown } | null; stdout?: { on(event: "data", listener: (chunk: unknown) => void): unknown } | null; @@ -627,6 +642,65 @@ describe("google-meet plugin", () => { ); }); + it("CLI setup prints human-readable checks by default", async () => { + const program = new Command(); + const stdout = captureStdout(); + registerGoogleMeetCli({ + program, + config: resolveGoogleMeetConfig({}), + ensureRuntime: async () => + ({ + setupStatus: () => ({ + ok: true, + checks: [ + { + id: "audio-bridge", + ok: true, + message: "Chrome command-pair realtime audio bridge configured", + }, + ], + }), + }) as GoogleMeetRuntime, + }); + + try { + await program.parseAsync(["googlemeet", "setup"], { from: "user" }); + expect(stdout.output()).toContain("Google Meet setup: OK"); + expect(stdout.output()).toContain( + "[ok] audio-bridge: Chrome command-pair realtime audio bridge configured", + ); + expect(stdout.output()).not.toContain('"checks"'); + } finally { + stdout.restore(); + } + }); + + it("CLI setup preserves JSON output with --json", async () => { + const program = new Command(); + const stdout = captureStdout(); + registerGoogleMeetCli({ + program, + config: resolveGoogleMeetConfig({}), + ensureRuntime: async () => + ({ + setupStatus: () => ({ + ok: false, + checks: [{ id: "twilio-voice-call-plugin", ok: false, message: "missing" }], + }), + }) as GoogleMeetRuntime, + }); + + try { + await program.parseAsync(["googlemeet", "setup", "--json"], { from: "user" }); + expect(JSON.parse(stdout.output())).toMatchObject({ + ok: false, + checks: [{ id: "twilio-voice-call-plugin", ok: false }], + }); + } finally { + stdout.restore(); + } + }); + it("launches Chrome after the BlackHole check", async () => { const originalPlatform = process.platform; Object.defineProperty(process, "platform", { value: "darwin" }); diff --git a/extensions/google-meet/src/cli.ts b/extensions/google-meet/src/cli.ts index 8dff6026db7..426efb03de9 100644 --- a/extensions/google-meet/src/cli.ts +++ b/extensions/google-meet/src/cli.ts @@ -40,6 +40,10 @@ type ResolveSpaceOptions = { json?: boolean; }; +type SetupOptions = { + json?: boolean; +}; + function writeStdoutJson(value: unknown): void { process.stdout.write(`${JSON.stringify(value, null, 2)}\n`); } @@ -71,6 +75,13 @@ function parseOptionalNumber(value: string | undefined): number | undefined { return parsed; } +function writeSetupStatus(status: ReturnType): void { + writeStdoutLine("Google Meet setup: %s", status.ok ? "OK" : "needs attention"); + for (const check of status.checks) { + writeStdoutLine("[%s] %s: %s", check.ok ? "ok" : "fail", check.id, check.message); + } +} + function resolveMeetingInput(config: GoogleMeetConfig, value?: string): string { const meeting = value?.trim() || config.defaults.meeting; if (!meeting) { @@ -319,9 +330,15 @@ export function registerGoogleMeetCli(params: { root .command("setup") .description("Show Google Meet transport setup status") - .action(async () => { + .option("--json", "Print JSON output", false) + .action(async (options: SetupOptions) => { const rt = await params.ensureRuntime(); - writeStdoutJson(rt.setupStatus()); + const status = rt.setupStatus(); + if (options.json) { + writeStdoutJson(status); + return; + } + writeSetupStatus(status); }); root