// Google Meet tests cover index plugin behavior. import { EventEmitter } from "node:events"; import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs"; import { tmpdir } from "node:os"; import path from "node:path"; import { PassThrough, Writable } from "node:stream"; import { createContext, Script } from "node:vm"; import { validateJsonSchemaValue, type JsonSchemaObject, } from "openclaw/plugin-sdk/json-schema-runtime"; import type { RealtimeTranscriptionProviderPlugin } from "openclaw/plugin-sdk/realtime-transcription"; import type { RealtimeVoiceProviderPlugin } from "openclaw/plugin-sdk/realtime-voice"; import { afterAll, afterEach, beforeEach, describe, expect, it, vi } from "vitest"; import plugin, { testing as googleMeetPluginTesting } from "./index.js"; import { extractGoogleMeetUriFromCalendarEvent, findGoogleMeetCalendarEvent, listGoogleMeetCalendarEvents, } from "./src/calendar.js"; import { resolveGoogleMeetConfig, resolveGoogleMeetConfigWithEnv } from "./src/config.js"; import { buildGoogleMeetPreflightReport, createGoogleMeetSpace, fetchGoogleMeetArtifacts, fetchGoogleMeetAttendance, fetchLatestGoogleMeetConferenceRecord, fetchGoogleMeetSpace, normalizeGoogleMeetSpaceName, } from "./src/meet.js"; import { handleGoogleMeetNodeHostCommand } from "./src/node-host.js"; import { startNodeRealtimeAudioBridge } from "./src/realtime-node.js"; import { convertGoogleMeetTtsAudioForBridge, extendGoogleMeetOutputEchoSuppression, isGoogleMeetLikelyAssistantEchoTranscript, GOOGLE_MEET_AGENT_TRANSCRIPT_DEBOUNCE_MS, resolveGoogleMeetRealtimeProvider, resolveGoogleMeetRealtimeTranscriptionProvider, startCommandAgentAudioBridge, startCommandRealtimeAudioBridge, } from "./src/realtime.js"; import { GoogleMeetRuntime, normalizeMeetUrl } from "./src/runtime.js"; import { invokeGoogleMeetGatewayMethodForTest, noopLogger, setupGoogleMeetPlugin, } from "./src/test-support/plugin-harness.js"; import { testing as chromeTransportTesting } from "./src/transports/chrome.js"; import { buildMeetDtmfSequence, normalizeDialInNumber, prefixDtmfWait, } from "./src/transports/twilio.js"; import type { GoogleMeetSession } from "./src/transports/types.js"; type GoogleMeetManifestConfigSchema = JsonSchemaObject & { properties?: Record }>; }; const voiceCallMocks = vi.hoisted(() => ({ joinMeetViaVoiceCallGateway: vi.fn(async () => ({ callId: "call-1", dtmfSent: true, introSent: true, })), endMeetVoiceCallGatewayCall: vi.fn(async () => {}), getMeetVoiceCallGatewayCall: vi.fn( async (): Promise<{ found: boolean; call?: { callId: string } }> => ({ found: true, call: { callId: "call-1" }, }), ), isVoiceCallMissingError: vi.fn((error: unknown) => String(error).includes("Call not found")), speakMeetViaVoiceCallGateway: vi.fn(async () => {}), })); const fetchGuardMocks = vi.hoisted(() => ({ fetchWithSsrFGuard: vi.fn( async (params: { url: string; init?: RequestInit; }): Promise<{ response: Response; release: () => Promise; }> => ({ response: await fetch(params.url, params.init), release: vi.fn(async () => {}), }), ), })); vi.mock("openclaw/plugin-sdk/ssrf-runtime", async (importOriginal) => { const actual = await importOriginal(); return { ...actual, fetchWithSsrFGuard: fetchGuardMocks.fetchWithSsrFGuard, }; }); vi.mock("./src/voice-call-gateway.js", () => ({ joinMeetViaVoiceCallGateway: voiceCallMocks.joinMeetViaVoiceCallGateway, endMeetVoiceCallGatewayCall: voiceCallMocks.endMeetVoiceCallGatewayCall, getMeetVoiceCallGatewayCall: voiceCallMocks.getMeetVoiceCallGatewayCall, isVoiceCallMissingError: voiceCallMocks.isVoiceCallMissingError, speakMeetViaVoiceCallGateway: voiceCallMocks.speakMeetViaVoiceCallGateway, })); function setup( config?: Parameters[1], options?: Parameters[2], ) { const harness = setupGoogleMeetPlugin(plugin, config, options); googleMeetPluginTesting.setCallGatewayFromCliForTests( async (method, _opts, params) => (await invokeGoogleMeetGatewayMethodForTest(harness.methods, method, params)) as Record< string, unknown >, ); googleMeetPluginTesting.setPlatformForTests(() => options?.registerPlatform ?? "darwin"); return harness; } function jsonResponse(value: unknown): Response { return new Response(JSON.stringify(value), { status: 200, headers: { "Content-Type": "application/json" }, }); } function requireGoogleMeetManifestConfigSchema(manifest: { configSchema?: GoogleMeetManifestConfigSchema; }): GoogleMeetManifestConfigSchema { if (!manifest.configSchema) { throw new Error("Google Meet manifest did not include a config schema"); } return manifest.configSchema; } function requireConfigProperty( properties: Record | undefined, key: string, ): Record { const value = properties?.[key]; if (!value || typeof value !== "object" || Array.isArray(value)) { throw new Error(`Expected Google Meet config schema property ${key}`); } return value as Record; } function isRecord(value: unknown): value is Record { return Boolean(value) && typeof value === "object" && !Array.isArray(value); } type MockSessionEntry = { sessionId?: string; updatedAt?: number; [key: string]: unknown; }; function createMockSessionRuntime(sessionStore: Record) { return { resolveStorePath: vi.fn(() => "/tmp/sessions.json"), loadSessionStore: vi.fn(() => sessionStore), saveSessionStore: vi.fn(async () => {}), updateSessionStore: vi.fn(async (_storePath, mutator: (store: never) => unknown) => mutator(sessionStore as never), ), getSessionEntry: vi.fn( ({ sessionKey }: { sessionKey: string }) => sessionStore[sessionKey] as MockSessionEntry, ), patchSessionEntry: vi.fn( async ({ sessionKey, fallbackEntry, update, }: { sessionKey: string; fallbackEntry: MockSessionEntry; update: (entry: MockSessionEntry) => Promise | MockSessionEntry; }) => { const current = (sessionStore[sessionKey] as MockSessionEntry | undefined) ?? fallbackEntry; const patch = await update(current); const next = { ...current, ...patch }; sessionStore[sessionKey] = next; return next; }, ), resolveSessionFilePath: vi.fn(() => "/tmp/session.json"), }; } function requireRecord(value: unknown, label: string): Record { if (!isRecord(value)) { throw new Error(`Expected ${label} to be an object`); } return value; } function mockCall(mock: { mock: { calls: unknown[][] } }, callIndex = 0): unknown[] { const call = mock.mock.calls.at(callIndex); if (!call) { throw new Error(`Expected mock call at index ${callIndex}`); } return call; } function mockCallArg(mock: { mock: { calls: unknown[][] } }, argIndex: number, callIndex = 0) { return mockCall(mock, callIndex)[argIndex]; } function expectRespondedOk(respond: { mock: { calls: unknown[][] } }): void { expect(mockCallArg(respond, 0)).toBe(true); } function requireRespondPayload( respond: { mock: { calls: unknown[][] } }, label: string, ): Record { return requireRecord(mockCallArg(respond, 1), label); } function requireSetupCheck(checks: unknown[] | undefined, id: string): Record { const check = checks ?.map((item) => requireRecord(item, "setup check")) .find((item) => item.id === id); if (!check) { throw new Error(`Expected setup check ${id}`); } return check; } function requireFetchGuardCall(auditContext: string): Record { const call = ( fetchGuardMocks.fetchWithSsrFGuard.mock.calls as Array<[Record]> ).find(([params]) => params.auditContext === auditContext); if (!call) { throw new Error(`Expected fetchWithSsrFGuard call for ${auditContext}`); } return call[0]; } function requestUrl(input: RequestInfo | URL): URL { if (typeof input === "string") { return new URL(input); } if (input instanceof URL) { return input; } return new URL(input.url); } function mockLocalMeetBrowserRequest( browserActResult: Record | (() => Record) = { inCall: true, micMuted: false, title: "Meet call", url: "https://meet.google.com/abc-defg-hij", }, ) { const callGatewayFromCli = vi.fn( async ( _method: string, _opts: unknown, params?: unknown, _extra?: unknown, ): Promise> => { const request = params as { path?: string; body?: { fn?: string; targetId?: string; url?: string }; }; if (request.path === "/tabs") { return { tabs: [] }; } if (request.path === "/tabs/open") { return { targetId: "local-meet-tab", title: "Meet", url: request.body?.url ?? "https://meet.google.com/abc-defg-hij", }; } if (request.path === "/tabs/focus") { return { ok: true }; } if (request.path === "/permissions/grant") { return { ok: true, origin: "https://meet.google.com", grantedPermissions: ["audioCapture", "videoCapture", "speakerSelection"], unsupportedPermissions: [], }; } if (request.path === "/act") { return { result: JSON.stringify( typeof browserActResult === "function" ? browserActResult() : browserActResult, ), }; } throw new Error(`unexpected browser request path ${request.path}`); }, ); chromeTransportTesting.setDepsForTest({ callGatewayFromCli }); return callGatewayFromCli; } function stubMeetArtifactsApi() { const fetchMock = vi.fn(async (input: RequestInfo | URL) => { const url = requestUrl(input); if (url.pathname === "/v2/spaces/abc-defg-hij") { return jsonResponse({ name: "spaces/abc-defg-hij", meetingCode: "abc-defg-hij", meetingUri: "https://meet.google.com/abc-defg-hij", }); } if (url.pathname === "/calendar/v3/calendars/primary/events") { return jsonResponse({ items: [ { id: "event-1", summary: "Project sync", hangoutLink: "https://meet.google.com/abc-defg-hij", start: { dateTime: "2026-04-25T10:00:00Z" }, end: { dateTime: "2026-04-25T10:30:00Z" }, }, ], }); } if (url.pathname === "/v2/conferenceRecords") { return jsonResponse({ conferenceRecords: [ { name: "conferenceRecords/rec-1", space: "spaces/abc-defg-hij", startTime: "2026-04-25T10:00:00Z", endTime: "2026-04-25T10:30:00Z", }, ], }); } if (url.pathname === "/v2/conferenceRecords/rec-1") { return jsonResponse({ name: "conferenceRecords/rec-1", space: "spaces/abc-defg-hij", startTime: "2026-04-25T10:00:00Z", endTime: "2026-04-25T10:30:00Z", }); } if (url.pathname === "/v2/conferenceRecords/rec-1/participants") { return jsonResponse({ participants: [ { name: "conferenceRecords/rec-1/participants/p1", earliestStartTime: "2026-04-25T10:00:00Z", latestEndTime: "2026-04-25T10:30:00Z", signedinUser: { user: "users/alice", displayName: "Alice" }, }, ], }); } if (url.pathname === "/v2/conferenceRecords/rec-1/participants/p1/participantSessions") { return jsonResponse({ participantSessions: [ { name: "conferenceRecords/rec-1/participants/p1/participantSessions/s1", startTime: "2026-04-25T10:00:00Z", endTime: "2026-04-25T10:30:00Z", }, ], }); } if (url.pathname === "/v2/conferenceRecords/rec-1/recordings") { return jsonResponse({ recordings: [ { name: "conferenceRecords/rec-1/recordings/r1", driveDestination: { file: "drive/file-1" }, }, ], }); } if (url.pathname === "/v2/conferenceRecords/rec-1/transcripts") { return jsonResponse({ transcripts: [ { name: "conferenceRecords/rec-1/transcripts/t1", docsDestination: { document: "docs/doc-1" }, }, ], }); } if (url.pathname === "/v2/conferenceRecords/rec-1/transcripts/t1/entries") { return jsonResponse({ transcriptEntries: [ { name: "conferenceRecords/rec-1/transcripts/t1/entries/e1", participant: "conferenceRecords/rec-1/participants/p1", text: "Hello from the transcript.", languageCode: "en-US", startTime: "2026-04-25T10:01:00Z", endTime: "2026-04-25T10:01:05Z", }, ], }); } if (url.pathname === "/v2/conferenceRecords/rec-1/smartNotes") { return jsonResponse({ smartNotes: [ { name: "conferenceRecords/rec-1/smartNotes/sn1", docsDestination: { document: "docs/doc-2" }, }, ], }); } if (url.pathname === "/drive/v3/files/doc-1/export") { return new Response("Transcript document body.", { status: 200, headers: { "Content-Type": "text/plain" }, }); } if (url.pathname === "/drive/v3/files/doc-2/export") { return new Response("Smart note document body.", { status: 200, headers: { "Content-Type": "text/plain" }, }); } return new Response(`unexpected ${url.pathname}`, { status: 404 }); }); vi.stubGlobal("fetch", fetchMock); return fetchMock; } type TestBridgeProcess = { stdin?: { write(chunk: unknown): unknown } | null; stdout?: { on(event: "data", listener: (chunk: unknown) => void): unknown } | null; stderr: PassThrough; killed: boolean; kill: ReturnType; on: EventEmitter["on"]; emit: EventEmitter["emit"]; }; describe("google-meet plugin", () => { beforeEach(() => { vi.clearAllMocks(); voiceCallMocks.joinMeetViaVoiceCallGateway.mockResolvedValue({ callId: "call-1", dtmfSent: true, introSent: true, }); voiceCallMocks.endMeetVoiceCallGatewayCall.mockResolvedValue(undefined); voiceCallMocks.getMeetVoiceCallGatewayCall.mockResolvedValue({ found: true, call: { callId: "call-1" }, }); voiceCallMocks.isVoiceCallMissingError.mockImplementation((error: unknown) => String(error).includes("Call not found"), ); voiceCallMocks.speakMeetViaVoiceCallGateway.mockResolvedValue(undefined); }); afterEach(() => { vi.useRealTimers(); vi.unstubAllGlobals(); vi.unstubAllEnvs(); chromeTransportTesting.setDepsForTest(null); googleMeetPluginTesting.setCallGatewayFromCliForTests(); googleMeetPluginTesting.setPlatformForTests(); }); afterAll(() => { vi.doUnmock("openclaw/plugin-sdk/ssrf-runtime"); vi.doUnmock("./src/voice-call-gateway.js"); vi.resetModules(); }); it("defaults to chrome agent mode with safe read-only tools", () => { const config = resolveGoogleMeetConfig({}); expect(config.enabled).toBe(true); expect(config.defaults).toEqual({}); expect(config.preview).toEqual({ enrollmentAcknowledged: false }); expect(config.defaultTransport).toBe("chrome"); expect(config.defaultMode).toBe("agent"); expect(config.chrome).toEqual({ audioBackend: "blackhole-2ch", launch: true, guestName: "OpenClaw Agent", reuseExistingTab: true, autoJoin: true, joinTimeoutMs: 30000, waitForInCallMs: 20000, audioFormat: "pcm16-24khz", audioBufferBytes: 4096, audioInputCommand: [ "sox", "-q", "--buffer", "4096", "-t", "coreaudio", "BlackHole 2ch", "-t", "raw", "-r", "24000", "-c", "1", "-e", "signed-integer", "-b", "16", "-L", "-", ], audioOutputCommand: [ "sox", "-q", "--buffer", "4096", "-t", "raw", "-r", "24000", "-c", "1", "-e", "signed-integer", "-b", "16", "-L", "-", "-t", "coreaudio", "BlackHole 2ch", ], bargeInRmsThreshold: 650, bargeInPeakThreshold: 2500, bargeInCooldownMs: 900, }); expect(config.chromeNode).toEqual({}); expect(config.twilio).toEqual({}); expect(config.voiceCall).toEqual({ enabled: true, requestTimeoutMs: 30000, dtmfDelayMs: 12000, postDtmfSpeechDelayMs: 5000, }); expect(config.realtime.strategy).toBe("agent"); expect(config.realtime.provider).toBe("openai"); expect(config.realtime.transcriptionProvider).toBe("openai"); expect(config.realtime.introMessage).toBe("Say exactly: I'm here and listening."); expect(config.realtime.toolPolicy).toBe("safe-read-only"); expect(config.realtime.providers).toEqual({}); expect(config.realtime.instructions).toContain("openclaw_agent_consult"); expect(config.oauth).toEqual({}); expect(config.auth).toEqual({ provider: "google-oauth" }); expect(resolveGoogleMeetConfig({ defaultMode: "realtime" }).defaultMode).toBe("agent"); }); it("resolves separate realtime providers for agent transcription and bidi voice", () => { const realtime = resolveGoogleMeetConfig({ realtime: { provider: "openai", transcriptionProvider: "openai", voiceProvider: "google", model: "gemini-2.5-flash-native-audio-preview-12-2025", }, }); expect(realtime.realtime.provider).toBe("openai"); expect(realtime.realtime.transcriptionProvider).toBe("openai"); expect(realtime.realtime.voiceProvider).toBe("google"); expect(realtime.realtime.model).toBe("gemini-2.5-flash-native-audio-preview-12-2025"); }); it("keeps realtime.provider as the transcription compatibility fallback", () => { const custom = resolveGoogleMeetConfig({ realtime: { provider: "custom-stt", }, }); expect(custom.realtime.provider).toBe("custom-stt"); expect(custom.realtime.transcriptionProvider).toBe("custom-stt"); const google = resolveGoogleMeetConfig({ realtime: { provider: "google", }, }); expect(google.realtime.provider).toBe("google"); expect(google.realtime.transcriptionProvider).toBe("openai"); }); it("uses voiceProvider for bidi and transcriptionProvider for agent mode resolution", () => { const voiceProviders: RealtimeVoiceProviderPlugin[] = [ { id: "openai", label: "OpenAI", autoSelectOrder: 1, isConfigured: () => true, createBridge: () => { throw new Error("unused"); }, }, { id: "google", label: "Google", autoSelectOrder: 2, resolveConfig: ({ rawConfig }) => rawConfig, isConfigured: () => true, createBridge: () => { throw new Error("unused"); }, }, ]; const transcriptionProviders: RealtimeTranscriptionProviderPlugin[] = [ { id: "openai", label: "OpenAI", autoSelectOrder: 1, isConfigured: () => true, createSession: () => { throw new Error("unused"); }, }, ]; const config = resolveGoogleMeetConfig({ realtime: { provider: "openai", transcriptionProvider: "openai", voiceProvider: "google", model: "gemini-2.5-flash-native-audio-preview-12-2025", }, }); const voiceProvider = resolveGoogleMeetRealtimeProvider({ config, fullConfig: {} as never, providers: voiceProviders, }); expect(voiceProvider.provider.id).toBe("google"); expect(voiceProvider.providerConfig).toEqual({ model: "gemini-2.5-flash-native-audio-preview-12-2025", }); const transcriptionProvider = resolveGoogleMeetRealtimeTranscriptionProvider({ config, fullConfig: {} as never, providers: transcriptionProviders, }); expect(transcriptionProvider.provider.id).toBe("openai"); }); it("declares advanced config metadata in the plugin entry and manifest", () => { const manifest = JSON.parse( readFileSync(new URL("./openclaw.plugin.json", import.meta.url), "utf8"), ) as { uiHints?: Record; configSchema?: GoogleMeetManifestConfigSchema; }; const configSchema = requireGoogleMeetManifestConfigSchema(manifest); const entry = plugin as unknown as { configSchema: { uiHints?: Record; }; }; for (const key of [ "chrome.audioBufferBytes", "chrome.bargeInInputCommand", "chrome.bargeInRmsThreshold", "chrome.bargeInPeakThreshold", "chrome.bargeInCooldownMs", "voiceCall.postDtmfSpeechDelayMs", ]) { expect(entry.configSchema.uiHints?.[key]).toHaveProperty("advanced", true); expect(manifest.uiHints?.[key]).toHaveProperty("advanced", true); } const chromeProperties = configSchema.properties?.chrome?.properties; expect(requireConfigProperty(chromeProperties, "audioBufferBytes")).toEqual({ type: "number", default: 4096, }); expect(requireConfigProperty(chromeProperties, "bargeInInputCommand")).toEqual({ type: "array", items: { type: "string" }, }); expect(requireConfigProperty(chromeProperties, "bargeInRmsThreshold")).toEqual({ type: "number", default: 650, }); expect(requireConfigProperty(chromeProperties, "bargeInPeakThreshold")).toEqual({ type: "number", default: 2500, }); expect(requireConfigProperty(chromeProperties, "bargeInCooldownMs")).toEqual({ type: "number", default: 900, }); expect( requireConfigProperty( configSchema.properties?.voiceCall?.properties, "postDtmfSpeechDelayMs", ), ).toEqual({ type: "number", default: 5000, }); const result = validateJsonSchemaValue({ schema: configSchema, cacheKey: "google-meet.manifest.voice-call-post-dtmf-speech-delay", value: { voiceCall: { postDtmfSpeechDelayMs: 750, }, }, }); expect(result.ok).toBe(true); }); it("resolves the realtime consult agent id", () => { expect( resolveGoogleMeetConfig({ realtime: { agentId: " jay ", }, }).realtime.agentId, ).toBe("jay"); }); it("preserves an empty realtime intro message for silent joins", () => { expect( resolveGoogleMeetConfig({ realtime: { introMessage: "", }, }).realtime.introMessage, ).toBe(""); }); it("keeps legacy command-pair audio format when custom commands omit a format", () => { const config = resolveGoogleMeetConfig({ chrome: { audioInputCommand: ["capture-legacy"], audioOutputCommand: ["play-legacy"], }, }); expect(config.chrome.audioFormat).toBe("g711-ulaw-8khz"); expect(config.chrome.audioInputCommand).toEqual(["capture-legacy"]); expect(config.chrome.audioOutputCommand).toEqual(["play-legacy"]); }); it("lets generated Chrome audio commands use a configured SoX buffer", () => { const config = resolveGoogleMeetConfig({ chrome: { audioBufferBytes: 2048 } }); expect(config.chrome.audioBufferBytes).toBe(2048); expect(config.chrome.audioInputCommand).toEqual([ "sox", "-q", "--buffer", "2048", "-t", "coreaudio", "BlackHole 2ch", "-t", "raw", "-r", "24000", "-c", "1", "-e", "signed-integer", "-b", "16", "-L", "-", ]); expect(config.chrome.audioOutputCommand?.slice(0, 4)).toEqual([ "sox", "-q", "--buffer", "2048", ]); }); it("clamps configured Chrome audio buffers above SoX's minimum", () => { const config = resolveGoogleMeetConfig({ chrome: { audioBufferBytes: 1 } }); expect(config.chrome.audioBufferBytes).toBe(17); expect(config.chrome.audioInputCommand?.slice(0, 4)).toEqual(["sox", "-q", "--buffer", "17"]); expect(config.chrome.audioOutputCommand?.slice(0, 4)).toEqual(["sox", "-q", "--buffer", "17"]); }); it("uses env fallbacks for OAuth, preview, and default meeting values", () => { const config = resolveGoogleMeetConfigWithEnv( {}, { OPENCLAW_GOOGLE_MEET_CLIENT_ID: "client-id", GOOGLE_MEET_CLIENT_SECRET: "client-secret", OPENCLAW_GOOGLE_MEET_REFRESH_TOKEN: "refresh-token", GOOGLE_MEET_ACCESS_TOKEN: "access-token", OPENCLAW_GOOGLE_MEET_ACCESS_TOKEN_EXPIRES_AT: "123456", GOOGLE_MEET_DEFAULT_MEETING: "https://meet.google.com/abc-defg-hij", OPENCLAW_GOOGLE_MEET_PREVIEW_ACK: "true", }, ); expect(config.defaults).toEqual({ meeting: "https://meet.google.com/abc-defg-hij" }); expect(config.preview).toEqual({ enrollmentAcknowledged: true }); expect(config.oauth).toEqual({ clientId: "client-id", clientSecret: "client-secret", refreshToken: "refresh-token", accessToken: "access-token", expiresAt: 123456, }); }); it.each(["0x10", "1e3"])("ignores non-decimal env numeric fallbacks: %s", (expiresAt) => { const config = resolveGoogleMeetConfigWithEnv( {}, { OPENCLAW_GOOGLE_MEET_ACCESS_TOKEN: "access-token", OPENCLAW_GOOGLE_MEET_ACCESS_TOKEN_EXPIRES_AT: expiresAt, }, ); expect(config.oauth).toEqual({ accessToken: "access-token" }); }); it("requires explicit Meet URLs", () => { expect(normalizeMeetUrl("https://meet.google.com/abc-defg-hij")).toBe( "https://meet.google.com/abc-defg-hij", ); expect(() => normalizeMeetUrl("https://example.com/abc-defg-hij")).toThrow("meet.google.com"); }); it("advertises only the googlemeet CLI descriptor", () => { const { cliRegistrations } = setup(); expect(cliRegistrations).toEqual([ { commands: ["googlemeet"], descriptors: [ { name: "googlemeet", description: "Join and manage Google Meet calls", hasSubcommands: true, }, ], }, ]); }); it("registers the node-host command used by chrome-node transport", () => { const { nodeHostCommands, nodeInvokePolicies } = setup(); const command = nodeHostCommands.find( (entry): entry is Record => isRecord(entry) && entry.command === "googlemeet.chrome", ); if (!command) { throw new Error("expected googlemeet.chrome node host command"); } expect(command.cap).toBe("google-meet"); expect(command.dangerous).toBe(true); expect(typeof command.handle).toBe("function"); expect(nodeInvokePolicies).toHaveLength(1); expect(nodeInvokePolicies[0]).toMatchObject({ commands: ["googlemeet.chrome"], dangerous: true, }); }); it("keeps the agent tool visible on non-macOS hosts but blocks local Chrome talk-back joins", async () => { const { cliRegistrations, methods, tools } = setup(undefined, { registerPlatform: "linux" }); const tool = tools[0] as { execute: (id: string, params: unknown) => Promise<{ isError?: boolean; content: unknown }>; }; expect(tools).toHaveLength(1); expect(cliRegistrations).toHaveLength(1); expect(methods.has("googlemeet.setup")).toBe(true); expect( googleMeetPluginTesting.isGoogleMeetAgentToolActionUnsupportedOnHost({ config: resolveGoogleMeetConfig({}), raw: { action: "join" }, platform: "linux", }), ).toBe(true); const blocked = await tool.execute("id", { action: "join" }); expect(JSON.stringify(blocked)).toContain("local Chrome talk-back audio is macOS-only"); expect( googleMeetPluginTesting.isGoogleMeetAgentToolActionUnsupportedOnHost({ config: resolveGoogleMeetConfig({}), raw: { action: "join", mode: "transcribe" }, platform: "linux", }), ).toBe(false); expect( googleMeetPluginTesting.isGoogleMeetAgentToolActionUnsupportedOnHost({ config: resolveGoogleMeetConfig({}), raw: { action: "join", transport: "chrome-node" }, platform: "linux", }), ).toBe(false); }); it("returns structured gateway errors for missing session ids", async () => { const { methods } = setup(); for (const method of ["googlemeet.leave", "googlemeet.speak"]) { const handler = methods.get(method) as | ((ctx: { params: Record; respond: ReturnType; }) => Promise) | undefined; const respond = vi.fn(); await handler?.({ params: {}, respond }); expect(respond).toHaveBeenCalledWith( false, { error: "sessionId required" }, { code: "INVALID_REQUEST", message: "sessionId required", details: { error: "sessionId required" }, }, ); } }); it("uses a provider-safe flat tool parameter schema", () => { const { tools } = setup(); const tool = tools[0] as { description?: string; parameters: unknown }; expect(tool.description).toContain("recover_current_tab"); expect(JSON.stringify(tool.parameters)).not.toContain("anyOf"); const parameters = requireRecord(tool.parameters, "Google Meet tool parameters"); expect(parameters.type).toBe("object"); const properties = requireRecord( parameters.properties, "Google Meet tool parameter properties", ); const action = requireRecord(properties.action, "Google Meet action parameter"); expect(action.type).toBe("string"); expect(action.enum).toEqual([ "join", "create", "status", "setup_status", "resolve_space", "preflight", "latest", "calendar_events", "artifacts", "attendance", "export", "recover_current_tab", "leave", "end_active_conference", "speak", "test_speech", "test_listen", ]); expect(action.description).toContain("recover_current_tab"); expect(properties.transport).toEqual({ type: "string", enum: ["chrome", "chrome-node", "twilio"], description: "Join transport", }); expect(properties.mode).toEqual({ type: "string", enum: ["agent", "bidi", "transcribe"], description: "Join mode. agent uses realtime transcription, the configured OpenClaw agent, and regular TTS. bidi uses the realtime voice model directly. transcribe joins observe-only.", }); }); it("normalizes Meet URLs, codes, and space names for the Meet API", () => { expect(normalizeGoogleMeetSpaceName("spaces/abc-defg-hij")).toBe("spaces/abc-defg-hij"); expect(normalizeGoogleMeetSpaceName("abc-defg-hij")).toBe("spaces/abc-defg-hij"); expect(normalizeGoogleMeetSpaceName("https://meet.google.com/abc-defg-hij")).toBe( "spaces/abc-defg-hij", ); expect(() => normalizeGoogleMeetSpaceName("https://example.com/abc-defg-hij")).toThrow( "meet.google.com", ); }); it("finds Google Meet links from Calendar events", async () => { const fetchMock = stubMeetArtifactsApi(); expect( extractGoogleMeetUriFromCalendarEvent({ conferenceData: { entryPoints: [ { entryPointType: "video", uri: "https://meet.google.com/abc-defg-hij", }, ], }, }), ).toBe("https://meet.google.com/abc-defg-hij"); const event = await findGoogleMeetCalendarEvent({ accessToken: "token", now: new Date("2026-04-25T09:50:00Z"), timeMin: "2026-04-25T00:00:00Z", timeMax: "2026-04-26T00:00:00Z", }); expect(event.calendarId).toBe("primary"); expect(event.meetingUri).toBe("https://meet.google.com/abc-defg-hij"); expect(event.event.summary).toBe("Project sync"); const calendarEvents = await listGoogleMeetCalendarEvents({ accessToken: "token", now: new Date("2026-04-25T09:50:00Z"), timeMin: "2026-04-25T00:00:00Z", timeMax: "2026-04-26T00:00:00Z", }); expect(calendarEvents.calendarId).toBe("primary"); expect(calendarEvents.events).toHaveLength(1); expect(calendarEvents.events[0]?.meetingUri).toBe("https://meet.google.com/abc-defg-hij"); expect(calendarEvents.events[0]?.selected).toBe(true); expect(calendarEvents.events[0]?.event.summary).toBe("Project sync"); const calendarCall = fetchMock.mock.calls.find(([input]) => { const url = requestUrl(input); return url.pathname === "/calendar/v3/calendars/primary/events"; }); if (!calendarCall) { throw new Error("Expected Calendar events.list fetch call"); } const url = requestUrl(calendarCall[0]); expect(url.searchParams.get("singleEvents")).toBe("true"); expect(url.searchParams.get("orderBy")).toBe("startTime"); const guardCall = requireFetchGuardCall("google-meet.calendar.events.list"); expect(guardCall.policy).toEqual({ allowedHostnames: ["www.googleapis.com"] }); }); it("adds a reauth hint for missing Calendar scopes", async () => { vi.stubGlobal( "fetch", vi.fn(async () => new Response("insufficientPermissions", { status: 403 })), ); await expect( findGoogleMeetCalendarEvent({ accessToken: "token", timeMin: "2026-04-25T00:00:00Z", timeMax: "2026-04-26T00:00:00Z", }), ).rejects.toThrow("calendar.events.readonly"); await expect( findGoogleMeetCalendarEvent({ accessToken: "token", timeMin: "2026-04-25T00:00:00Z", timeMax: "2026-04-26T00:00:00Z", }), ).rejects.toThrow("googlemeet auth login"); }); it("fetches Meet spaces without percent-encoding the spaces path separator", async () => { const fetchMock = vi.fn(async () => { return new Response( JSON.stringify({ name: "spaces/abc-defg-hij", meetingCode: "abc-defg-hij", meetingUri: "https://meet.google.com/abc-defg-hij", }), { status: 200, headers: { "Content-Type": "application/json" } }, ); }); vi.stubGlobal("fetch", fetchMock); const space = await fetchGoogleMeetSpace({ accessToken: "token", meeting: "spaces/abc-defg-hij", }); expect(space.name).toBe("spaces/abc-defg-hij"); expect(space.meetingCode).toBe("abc-defg-hij"); expect(space.meetingUri).toBe("https://meet.google.com/abc-defg-hij"); const guardCall = requireFetchGuardCall("google-meet.spaces.get"); expect(guardCall.url).toBe("https://meet.googleapis.com/v2/spaces/abc-defg-hij"); expect(requireRecord(guardCall.init, "spaces.get init").headers).toEqual({ Authorization: "Bearer token", Accept: "application/json", }); expect(guardCall.policy).toEqual({ allowedHostnames: ["meet.googleapis.com"] }); expect(fetchMock).toHaveBeenCalledWith("https://meet.googleapis.com/v2/spaces/abc-defg-hij", { headers: { Authorization: "Bearer token", Accept: "application/json", }, }); }); it("creates Meet spaces and returns the meeting URL", async () => { const fetchMock = vi.fn(async () => { return new Response( JSON.stringify({ name: "spaces/new-space", meetingCode: "new-abcd-xyz", meetingUri: "https://meet.google.com/new-abcd-xyz", }), { status: 200, headers: { "Content-Type": "application/json" } }, ); }); vi.stubGlobal("fetch", fetchMock); const result = await createGoogleMeetSpace({ accessToken: "token" }); expect(result.meetingUri).toBe("https://meet.google.com/new-abcd-xyz"); expect(result.space.name).toBe("spaces/new-space"); expect(result.space.meetingCode).toBe("new-abcd-xyz"); expect(result.space.meetingUri).toBe("https://meet.google.com/new-abcd-xyz"); const guardCall = requireFetchGuardCall("google-meet.spaces.create"); expect(guardCall.url).toBe("https://meet.googleapis.com/v2/spaces"); expect(guardCall.init).toEqual({ method: "POST", headers: { Authorization: "Bearer token", Accept: "application/json", "Content-Type": "application/json", }, body: "{}", }); expect(guardCall.policy).toEqual({ allowedHostnames: ["meet.googleapis.com"] }); }); it("lists Meet artifact metadata for the latest conference record by default", async () => { const fetchMock = stubMeetArtifactsApi(); const result = await fetchGoogleMeetArtifacts({ accessToken: "token", meeting: "abc-defg-hij", pageSize: 2, }); expect(result.input).toBe("abc-defg-hij"); expect(result.space?.name).toBe("spaces/abc-defg-hij"); expect(result.conferenceRecords.map((record) => record.name)).toEqual([ "conferenceRecords/rec-1", ]); expect(result.artifacts).toHaveLength(1); const artifact = result.artifacts[0]; expect(artifact?.conferenceRecord.name).toBe("conferenceRecords/rec-1"); expect(artifact?.participants.map((participant) => participant.name)).toEqual([ "conferenceRecords/rec-1/participants/p1", ]); expect(artifact?.recordings.map((recording) => recording.name)).toEqual([ "conferenceRecords/rec-1/recordings/r1", ]); expect(artifact?.transcripts.map((transcript) => transcript.name)).toEqual([ "conferenceRecords/rec-1/transcripts/t1", ]); expect(artifact?.transcriptEntries).toHaveLength(1); expect(artifact?.transcriptEntries[0]?.transcript).toBe( "conferenceRecords/rec-1/transcripts/t1", ); expect(artifact?.transcriptEntries[0]?.entries).toEqual([ { name: "conferenceRecords/rec-1/transcripts/t1/entries/e1", participant: "conferenceRecords/rec-1/participants/p1", text: "Hello from the transcript.", languageCode: "en-US", startTime: "2026-04-25T10:01:00Z", endTime: "2026-04-25T10:01:05Z", }, ]); expect(artifact?.smartNotes.map((smartNote) => smartNote.name)).toEqual([ "conferenceRecords/rec-1/smartNotes/sn1", ]); const listCall = fetchMock.mock.calls.find(([input]) => { const url = requestUrl(input); return url.pathname === "/v2/conferenceRecords"; }); if (!listCall) { throw new Error("Expected conferenceRecords.list fetch call"); } const listUrl = requestUrl(listCall[0]); expect(listUrl.searchParams.get("filter")).toBe('space.name = "spaces/abc-defg-hij"'); expect(listUrl.searchParams.get("pageSize")).toBe("1"); expect(requireFetchGuardCall("google-meet.conferenceRecords.smartNotes.list").url).toBe( "https://meet.googleapis.com/v2/conferenceRecords/rec-1/smartNotes?pageSize=2", ); expect( requireFetchGuardCall("google-meet.conferenceRecords.transcripts.entries.list").url, ).toBe( "https://meet.googleapis.com/v2/conferenceRecords/rec-1/transcripts/t1/entries?pageSize=2", ); }); it("keeps all conference records available when requested", async () => { const fetchMock = stubMeetArtifactsApi(); await fetchGoogleMeetArtifacts({ accessToken: "token", meeting: "abc-defg-hij", pageSize: 2, allConferenceRecords: true, }); const listCall = fetchMock.mock.calls.find(([input]) => { const url = requestUrl(input); return url.pathname === "/v2/conferenceRecords"; }); if (!listCall) { throw new Error("Expected conferenceRecords.list fetch call"); } const listUrl = requestUrl(listCall[0]); expect(listUrl.searchParams.get("pageSize")).toBe("2"); expect(listUrl.searchParams.get("filter")).toBe('space.name = "spaces/abc-defg-hij"'); }); it("exports linked Google Docs bodies when requested", async () => { const fetchMock = stubMeetArtifactsApi(); const result = await fetchGoogleMeetArtifacts({ accessToken: "token", conferenceRecord: "rec-1", includeDocumentBodies: true, }); expect(result.artifacts).toHaveLength(1); expect(result.artifacts[0]?.transcripts.map((transcript) => transcript.documentText)).toEqual([ "Transcript document body.", ]); expect(result.artifacts[0]?.smartNotes.map((smartNote) => smartNote.documentText)).toEqual([ "Smart note document body.", ]); const driveCalls = fetchMock.mock.calls .map(([input]) => requestUrl(input)) .filter((url) => url.pathname.startsWith("/drive/v3/files/")); expect(driveCalls.map((url) => url.pathname)).toEqual([ "/drive/v3/files/doc-1/export", "/drive/v3/files/doc-2/export", ]); expect(driveCalls.map((url) => url.searchParams.get("mimeType"))).toEqual([ "text/plain", "text/plain", ]); }); it("fetches only the latest Meet conference record for a meeting", async () => { const fetchMock = stubMeetArtifactsApi(); const result = await fetchLatestGoogleMeetConferenceRecord({ accessToken: "token", meeting: "abc-defg-hij", }); expect(result.input).toBe("abc-defg-hij"); expect(result.space.name).toBe("spaces/abc-defg-hij"); expect(result.conferenceRecord?.name).toBe("conferenceRecords/rec-1"); const listCall = fetchMock.mock.calls.find(([input]) => { const url = requestUrl(input); return url.pathname === "/v2/conferenceRecords"; }); if (!listCall) { throw new Error("Expected conferenceRecords.list fetch call"); } const listUrl = requestUrl(listCall[0]); expect(listUrl.searchParams.get("pageSize")).toBe("1"); expect(listUrl.searchParams.get("filter")).toBe('space.name = "spaces/abc-defg-hij"'); }); it("lists Meet attendance rows with participant sessions", async () => { const fetchMock = stubMeetArtifactsApi(); const result = await fetchGoogleMeetAttendance({ accessToken: "token", conferenceRecord: "rec-1", pageSize: 3, }); expect(result.input).toBe("rec-1"); expect(result.conferenceRecords.map((record) => record.name)).toEqual([ "conferenceRecords/rec-1", ]); expect(result.attendance).toHaveLength(1); expect(result.attendance[0]?.conferenceRecord).toBe("conferenceRecords/rec-1"); expect(result.attendance[0]?.participant).toBe("conferenceRecords/rec-1/participants/p1"); expect(result.attendance[0]?.displayName).toBe("Alice"); expect(result.attendance[0]?.user).toBe("users/alice"); expect(result.attendance[0]?.sessions.map((session) => session.name)).toEqual([ "conferenceRecords/rec-1/participants/p1/participantSessions/s1", ]); expect(fetchMock).toHaveBeenCalledWith( "https://meet.googleapis.com/v2/conferenceRecords/rec-1", { headers: { Authorization: "Bearer token", Accept: "application/json", }, }, ); }); it("merges duplicate attendance participants and annotates timing", async () => { const fetchMock = vi.fn(async (input: RequestInfo | URL) => { const url = requestUrl(input); if (url.pathname === "/v2/conferenceRecords/rec-1") { return jsonResponse({ name: "conferenceRecords/rec-1", startTime: "2026-04-25T10:00:00Z", endTime: "2026-04-25T11:00:00Z", }); } if (url.pathname === "/v2/conferenceRecords/rec-1/participants") { return jsonResponse({ participants: [ { name: "conferenceRecords/rec-1/participants/p1", signedinUser: { user: "users/alice", displayName: "Alice" }, }, { name: "conferenceRecords/rec-1/participants/p2", signedinUser: { user: "users/alice", displayName: "Alice" }, }, ], }); } if (url.pathname === "/v2/conferenceRecords/rec-1/participants/p1/participantSessions") { return jsonResponse({ participantSessions: [ { name: "conferenceRecords/rec-1/participants/p1/participantSessions/s1", startTime: "2026-04-25T10:10:00Z", endTime: "2026-04-25T10:30:00Z", }, ], }); } if (url.pathname === "/v2/conferenceRecords/rec-1/participants/p2/participantSessions") { return jsonResponse({ participantSessions: [ { name: "conferenceRecords/rec-1/participants/p2/participantSessions/s1", startTime: "2026-04-25T10:40:00Z", endTime: "2026-04-25T10:50:00Z", }, ], }); } return new Response(`unexpected ${url.pathname}`, { status: 404 }); }); vi.stubGlobal("fetch", fetchMock); const result = await fetchGoogleMeetAttendance({ accessToken: "token", conferenceRecord: "rec-1", }); expect(result.attendance).toHaveLength(1); const row = result.attendance[0]; expect(row?.displayName).toBe("Alice"); expect(row?.participants).toEqual([ "conferenceRecords/rec-1/participants/p1", "conferenceRecords/rec-1/participants/p2", ]); expect(row?.firstJoinTime).toBe("2026-04-25T10:10:00.000Z"); expect(row?.lastLeaveTime).toBe("2026-04-25T10:50:00.000Z"); expect(row?.durationMs).toBe(1_800_000); expect(row?.late).toBe(true); expect(row?.earlyLeave).toBe(true); expect(row?.sessions.map((session) => session.name)).toEqual([ "conferenceRecords/rec-1/participants/p1/participantSessions/s1", "conferenceRecords/rec-1/participants/p2/participantSessions/s1", ]); }); it("surfaces Developer Preview acknowledgment blockers in preflight reports", () => { const report = buildGoogleMeetPreflightReport({ input: "abc-defg-hij", space: { name: "spaces/abc-defg-hij" }, previewAcknowledged: false, tokenSource: "cached-access-token", }); expect(report.resolvedSpaceName).toBe("spaces/abc-defg-hij"); expect(report.previewAcknowledged).toBe(false); expect(report.blockers).toHaveLength(1); expect(report.blockers[0]).toContain("Developer Preview Program"); }); it("builds Twilio dial plans from a PIN", () => { expect(normalizeDialInNumber("+1 (555) 123-4567")).toBe("+15551234567"); expect(buildMeetDtmfSequence({ pin: "123 456" })).toBe("123456#"); expect(buildMeetDtmfSequence({ dtmfSequence: "ww123#" })).toBe("ww123#"); expect(prefixDtmfWait("123456#", 12000)).toBe("wwwwwwwwwwwwwwwwwwwwwwww123456#"); }); it("joins a Twilio session through the tool without page parsing", async () => { const { tools } = setup({ defaultTransport: "twilio" }); const tool = tools[0] as { execute: (id: string, params: unknown) => Promise<{ details: { session: unknown } }>; }; const result = await tool.execute("id", { action: "join", url: "https://meet.google.com/abc-defg-hij", dialInNumber: "+15551234567", pin: "123456", }); const session = requireRecord(result.details.session, "Twilio session"); expect(session.transport).toBe("twilio"); expect(session.mode).toBe("agent"); expect(session.twilio).toEqual({ dialInNumber: "+15551234567", pinProvided: true, dtmfSequence: "wwwwwwwwwwwwwwwwwwwwwwww123456#", voiceCallId: "call-1", dtmfSent: true, introSent: true, }); const [voiceCallParams] = voiceCallMocks.joinMeetViaVoiceCallGateway.mock .calls[0] as unknown as [Record]; expect(requireRecord(voiceCallParams.config, "voice-call config").defaultTransport).toBe( "twilio", ); expect(voiceCallParams.dialInNumber).toBe("+15551234567"); expect(voiceCallParams.dtmfSequence).toBe("wwwwwwwwwwwwwwwwwwwwwwww123456#"); expect(typeof requireRecord(voiceCallParams.logger, "voice-call logger").info).toBe("function"); expect(voiceCallParams.message).toBe("Say exactly: I'm here and listening."); expect(String(voiceCallParams.sessionKey)).toMatch(/^voice:google-meet:meet_/); }); it("passes the caller session key through tool joins for agent context forking", async () => { const { tools } = setup( {}, { toolContext: { sessionKey: "agent:main:discord:channel:general" } }, ); const gatewayParams: unknown[] = []; googleMeetPluginTesting.setCallGatewayFromCliForTests(async (_method, _opts, params) => { gatewayParams.push(params); return { ok: true }; }); const tool = tools[0] as { execute: (id: string, params: unknown) => Promise; }; await tool.execute("id", { action: "join", url: "https://meet.google.com/abc-defg-hij", requesterSessionKey: "agent:main:wrong", }); const gatewayJoinParams = requireRecord(gatewayParams[0], "gateway join params"); expect(gatewayJoinParams.url).toBe("https://meet.google.com/abc-defg-hij"); expect(gatewayJoinParams.requesterSessionKey).toBe("agent:main:discord:channel:general"); }); it("explains that Twilio joins need dial-in details", async () => { const { tools } = setup({ defaultTransport: "twilio" }); const tool = tools[0] as { execute: (id: string, params: unknown) => Promise<{ details: { error?: string } }>; }; const result = await tool.execute("id", { action: "join", url: "https://meet.google.com/abc-defg-hij", }); expect(result.details.error).toContain("Twilio transport requires a Meet dial-in phone number"); expect(result.details.error).toContain("Google Meet URLs do not include dial-in details"); }); it("hangs up delegated Twilio calls on leave", async () => { const { tools } = setup({ defaultTransport: "twilio" }); const tool = tools[0] as { execute: (id: string, params: unknown) => Promise<{ details: { session: { id: string } } }>; }; const joined = await tool.execute("id", { action: "join", url: "https://meet.google.com/abc-defg-hij", dialInNumber: "+15551234567", pin: "123456", }); await tool.execute("id", { action: "leave", sessionId: joined.details.session.id }); const [endParams] = mockCall(voiceCallMocks.endMeetVoiceCallGatewayCall) as [ Record, ]; expect(requireRecord(endParams.config, "voice-call end config").defaultTransport).toBe( "twilio", ); expect(endParams.callId).toBe("call-1"); expect(voiceCallMocks.endMeetVoiceCallGatewayCall).toHaveBeenCalledWith({ config: endParams.config, callId: "call-1", }); }); it("does not reuse Twilio Meet sessions whose delegated call is no longer active", async () => { voiceCallMocks.getMeetVoiceCallGatewayCall.mockResolvedValueOnce({ found: false }); const { tools } = setup({ defaultTransport: "twilio" }); const tool = tools[0] as { execute: ( id: string, params: unknown, ) => Promise<{ details: { session: { id: string; state: string; notes: string[] } } }>; }; const first = await tool.execute("id", { action: "join", url: "https://meet.google.com/abc-defg-hij", dialInNumber: "+15551234567", pin: "123456", }); const second = await tool.execute("id", { action: "join", url: "https://meet.google.com/abc-defg-hij", dialInNumber: "+15551234567", pin: "123456", }); expect(first.details.session.state).toBe("ended"); expect(first.details.session.notes).toContain("Voice Call is no longer active."); expect(second.details.session.id).not.toBe(first.details.session.id); expect(voiceCallMocks.joinMeetViaVoiceCallGateway).toHaveBeenCalledTimes(2); }); it("delegates Twilio session speech through voice-call", async () => { const { tools } = setup({ defaultTransport: "twilio" }); const tool = tools[0] as { execute: (id: string, params: unknown) => Promise<{ details: { session: { id: string } } }>; }; const joined = await tool.execute("id", { action: "join", url: "https://meet.google.com/abc-defg-hij", dialInNumber: "+15551234567", pin: "123456", }); const spoken = await tool.execute("id", { action: "speak", sessionId: joined.details.session.id, message: "Say exactly: hello after joining.", }); expect(requireRecord(spoken.details, "spoken details").spoken).toBe(true); const [speakParams] = voiceCallMocks.speakMeetViaVoiceCallGateway.mock.calls.at( 0, ) as unknown as [Record]; expect(requireRecord(speakParams.config, "voice-call speak config").defaultTransport).toBe( "twilio", ); expect(speakParams.callId).toBe("call-1"); expect(speakParams.message).toBe("Say exactly: hello after joining."); expect(voiceCallMocks.speakMeetViaVoiceCallGateway).toHaveBeenCalledWith({ config: speakParams.config, callId: "call-1", message: "Say exactly: hello after joining.", }); }); it("reports setup status through the tool", async () => { const originalPlatform = process.platform; Object.defineProperty(process, "platform", { value: "darwin" }); try { const { tools } = setup({ chrome: { audioInputCommand: ["openclaw-audio-bridge", "capture"], audioOutputCommand: ["openclaw-audio-bridge", "play"], }, }); const tool = tools[0] as { execute: (id: string, params: unknown) => Promise<{ details: { ok?: boolean } }>; }; const result = await tool.execute("id", { action: "setup_status" }); expect(result.details.ok).toBe(true); } finally { Object.defineProperty(process, "platform", { value: originalPlatform }); } }); it("rejects agent-mode external audio bridges in setup status", async () => { const originalPlatform = process.platform; Object.defineProperty(process, "platform", { value: "darwin" }); try { const { tools } = setup( { defaultMode: "agent", defaultTransport: "chrome", chrome: { audioBridgeCommand: ["bridge", "start"], audioInputCommand: ["capture-meet"], audioOutputCommand: ["play-meet"], }, }, { runCommandWithTimeoutHandler: async (argv) => { if (argv[0] === "/usr/sbin/system_profiler") { return { code: 0, stdout: "BlackHole 2ch", stderr: "" }; } return { code: 0, stdout: "", stderr: "" }; }, }, ); const tool = tools[0] as { execute: ( id: string, params: unknown, ) => Promise<{ details: { ok?: boolean; checks?: unknown[] } }>; }; const result = await tool.execute("id", { action: "setup_status" }); expect(result.details.ok).toBe(false); const audioBridgeCheck = result.details.checks ?.map((check) => requireRecord(check, "setup check")) .find((check) => check.id === "audio-bridge"); if (!audioBridgeCheck) { throw new Error("Expected audio-bridge setup check"); } expect(audioBridgeCheck.ok).toBe(false); expect(String(audioBridgeCheck.message)).toContain("chrome.audioBridgeCommand is bidi-only"); } finally { Object.defineProperty(process, "platform", { value: originalPlatform }); } }); it("reports attendance through the tool", async () => { stubMeetArtifactsApi(); const { tools } = setup(); const tool = tools[0] as { execute: ( id: string, params: unknown, ) => Promise<{ details: { attendance?: Array<{ displayName?: string }> } }>; }; const result = await tool.execute("id", { action: "attendance", accessToken: "token", expiresAt: Date.now() + 120_000, conferenceRecord: "rec-1", pageSize: "3", }); expect(result.details.attendance).toHaveLength(1); expect(result.details.attendance?.[0]?.displayName).toBe("Alice"); }); it("rejects fractional attendance page sizes", async () => { stubMeetArtifactsApi(); const { tools } = setup(); const tool = tools[0] as { execute: (id: string, params: unknown) => Promise<{ details: { error?: string } }>; }; const result = await tool.execute("id", { action: "attendance", accessToken: "token", expiresAt: Date.now() + 120_000, conferenceRecord: "rec-1", pageSize: "3.5", }); expect(result.details.error).toBe("pageSize must be a positive integer"); }); it("writes export bundles through the tool", async () => { stubMeetArtifactsApi(); const tempDir = mkdtempSync(path.join(tmpdir(), "openclaw-google-meet-tool-export-")); const { tools } = setup(); const tool = tools[0] as { execute: ( id: string, params: unknown, ) => Promise<{ details: { files?: string[]; zipFile?: string } }>; }; try { const result = await tool.execute("id", { action: "export", accessToken: "token", expiresAt: Date.now() + 120_000, conferenceRecord: "rec-1", includeDocumentBodies: true, outputDir: tempDir, zip: true, }); expect(result.details.files).toContain(path.join(tempDir, "manifest.json")); expect(result.details.zipFile).toBe(`${tempDir}.zip`); const manifest = requireRecord( JSON.parse(readFileSync(path.join(tempDir, "manifest.json"), "utf8")), "export manifest", ); expect(manifest.request).toEqual({ conferenceRecord: "rec-1", includeDocumentBodies: true, includeTranscriptEntries: true, allConferenceRecords: false, mergeDuplicateParticipants: true, }); expect(manifest.counts).toEqual({ conferenceRecords: 1, artifacts: 1, recordings: 1, transcripts: 1, transcriptEntries: 1, smartNotes: 1, attendanceRows: 1, warnings: 0, }); expect(manifest.files).toEqual([ "summary.md", "attendance.csv", "transcript.md", "artifacts.json", "attendance.json", "manifest.json", ]); } finally { rmSync(tempDir, { recursive: true, force: true }); rmSync(`${tempDir}.zip`, { force: true }); } }); it("dry-runs export bundles through the tool", async () => { stubMeetArtifactsApi(); const parentDir = mkdtempSync(path.join(tmpdir(), "openclaw-google-meet-tool-dry-run-")); const outputDir = path.join(parentDir, "bundle"); const { tools } = setup(); const tool = tools[0] as { execute: ( id: string, params: unknown, ) => Promise<{ details: { dryRun?: boolean; manifest?: { files?: string[] } } }>; }; try { const result = await tool.execute("id", { action: "export", accessToken: "token", expiresAt: Date.now() + 120_000, conferenceRecord: "rec-1", outputDir, dryRun: true, }); expect(result.details.dryRun).toBe(true); expect(result.details.manifest?.files).toEqual([ "summary.md", "attendance.csv", "transcript.md", "artifacts.json", "attendance.json", "manifest.json", ]); expect(existsSync(outputDir)).toBe(false); } finally { rmSync(parentDir, { recursive: true, force: true }); } }); it("reports the latest conference record through the tool", async () => { stubMeetArtifactsApi(); const { tools } = setup(); const tool = tools[0] as { execute: ( id: string, params: unknown, ) => Promise<{ details: { conferenceRecord?: { name?: string } } }>; }; const result = await tool.execute("id", { action: "latest", accessToken: "token", expiresAt: Date.now() + 120_000, meeting: "abc-defg-hij", }); expect(result.details.conferenceRecord?.name).toBe("conferenceRecords/rec-1"); }); it("reports the latest conference record from today's calendar through the tool", async () => { stubMeetArtifactsApi(); const { tools } = setup(); const tool = tools[0] as { execute: ( id: string, params: unknown, ) => Promise<{ details: { calendarEvent?: { meetingUri?: string } } }>; }; const result = await tool.execute("id", { action: "latest", accessToken: "token", expiresAt: Date.now() + 120_000, today: true, }); expect(result.details.calendarEvent?.meetingUri).toBe("https://meet.google.com/abc-defg-hij"); }); it("reports calendar event previews through the tool", async () => { stubMeetArtifactsApi(); const { tools } = setup(); const tool = tools[0] as { execute: ( id: string, params: unknown, ) => Promise<{ details: { events?: Array<{ selected?: boolean; meetingUri?: string }> } }>; }; const result = await tool.execute("id", { action: "calendar_events", accessToken: "token", expiresAt: Date.now() + 120_000, today: true, }); expect(result.details.events).toHaveLength(1); expect(result.details.events?.[0]?.selected).toBe(true); expect(result.details.events?.[0]?.meetingUri).toBe("https://meet.google.com/abc-defg-hij"); }); it("fails setup status when the configured Chrome node is not connected", async () => { const { tools } = setup( { defaultTransport: "chrome-node", chromeNode: { node: "parallels-macos" }, }, { nodesListResult: { nodes: [ { nodeId: "node-1", displayName: "parallels-macos", connected: false, caps: [], commands: [], remoteIp: "192.168.0.25", }, ], }, }, ); const tool = tools[0] as { execute: ( id: string, params: unknown, ) => Promise<{ details: { ok?: boolean; checks?: unknown[] } }>; }; const result = await tool.execute("id", { action: "setup_status" }); expect(result.details.ok).toBe(false); const check = requireSetupCheck(result.details.checks, "chrome-node-connected"); expect(check.ok).toBe(false); expect(check.message).toContain("parallels-macos"); expect(check.message).toContain("offline"); expect(check.message).toContain("missing googlemeet.chrome"); expect(check.message).toContain("missing browser.proxy/browser capability"); }); it("reports missing local Chrome audio prerequisites in setup status", async () => { const originalPlatform = process.platform; Object.defineProperty(process, "platform", { value: "darwin" }); try { const { tools } = setup( { defaultTransport: "chrome" }, { runCommandWithTimeoutHandler: async (argv) => { if (argv[0] === "/usr/sbin/system_profiler") { return { code: 0, stdout: "Built-in Output", stderr: "" }; } return { code: 0, stdout: "", stderr: "" }; }, }, ); const tool = tools[0] as { execute: ( id: string, params: unknown, ) => Promise<{ details: { ok?: boolean; checks?: unknown[] } }>; }; const result = await tool.execute("id", { action: "setup_status", transport: "chrome" }); expect(result.details.ok).toBe(false); const check = requireSetupCheck(result.details.checks, "chrome-local-audio-device"); expect(check.ok).toBe(false); expect(check.message).toContain("BlackHole 2ch audio device not found"); } finally { Object.defineProperty(process, "platform", { value: originalPlatform }); } }); it("reports missing local Chrome audio commands in setup status", async () => { const originalPlatform = process.platform; Object.defineProperty(process, "platform", { value: "darwin" }); try { const { tools } = setup( { defaultTransport: "chrome" }, { runCommandWithTimeoutHandler: async (argv) => { if (argv[0] === "/usr/sbin/system_profiler") { return { code: 0, stdout: "BlackHole 2ch", stderr: "" }; } if (argv[0] === "/bin/sh" && argv.at(-1) === "sox") { return { code: 1, stdout: "", stderr: "" }; } return { code: 0, stdout: "", stderr: "" }; }, }, ); const tool = tools[0] as { execute: ( id: string, params: unknown, ) => Promise<{ details: { ok?: boolean; checks?: unknown[] } }>; }; const result = await tool.execute("id", { action: "setup_status", transport: "chrome" }); expect(result.details.ok).toBe(false); const check = requireSetupCheck(result.details.checks, "chrome-local-audio-commands"); expect(check.ok).toBe(false); expect(check.message).toBe("Chrome audio command missing: sox"); } finally { Object.defineProperty(process, "platform", { value: originalPlatform }); } }); it("checks a configured local barge-in command in setup status", async () => { const originalPlatform = process.platform; Object.defineProperty(process, "platform", { value: "darwin" }); try { const { tools } = setup( { defaultTransport: "chrome", chrome: { bargeInInputCommand: ["missing-barge-capture"], }, }, { runCommandWithTimeoutHandler: async (argv) => { if (argv[0] === "/usr/sbin/system_profiler") { return { code: 0, stdout: "BlackHole 2ch", stderr: "" }; } if (argv[0] === "/bin/sh" && argv.at(-1) === "missing-barge-capture") { return { code: 1, stdout: "", stderr: "" }; } return { code: 0, stdout: "", stderr: "" }; }, }, ); const tool = tools[0] as { execute: ( id: string, params: unknown, ) => Promise<{ details: { ok?: boolean; checks?: unknown[] } }>; }; const result = await tool.execute("id", { action: "setup_status", transport: "chrome" }); expect(result.details.ok).toBe(false); const check = requireSetupCheck(result.details.checks, "chrome-local-audio-commands"); expect(check.ok).toBe(false); expect(check.message).toBe("Chrome audio command missing: missing-barge-capture"); } finally { Object.defineProperty(process, "platform", { value: originalPlatform }); } }); it("skips local Chrome audio prerequisites for observe-only setup status", async () => { const originalPlatform = process.platform; Object.defineProperty(process, "platform", { value: "darwin" }); try { const { tools, runCommandWithTimeout } = setup( { defaultMode: "transcribe", defaultTransport: "chrome" }, { runCommandWithTimeoutHandler: async () => ({ code: 1, stdout: "Built-in Output", stderr: "", }), }, ); const tool = tools[0] as { execute: ( id: string, params: unknown, ) => Promise<{ details: { ok?: boolean; checks?: Array<{ id?: string; ok?: boolean }> } }>; }; const result = await tool.execute("id", { action: "setup_status", transport: "chrome", mode: "transcribe", }); expect(result.details.ok).toBe(true); const check = requireSetupCheck(result.details.checks, "audio-bridge"); expect(check.ok).toBe(true); expect(check.message).toBe( "Chrome observe-only mode does not require a realtime audio bridge", ); expect( result.details.checks?.filter( (checkLocal) => checkLocal.id === "chrome-local-audio-device", ), ).toStrictEqual([]); expect(runCommandWithTimeout).not.toHaveBeenCalled(); } finally { Object.defineProperty(process, "platform", { value: originalPlatform }); } }); it("reports Twilio delegation readiness when voice-call is enabled", async () => { vi.stubEnv("TWILIO_ACCOUNT_SID", "AC123"); vi.stubEnv("TWILIO_AUTH_TOKEN", "secret"); vi.stubEnv("TWILIO_FROM_NUMBER", "+15550001234"); const { tools } = setup( { defaultTransport: "chrome-node", chromeNode: { node: "parallels-macos" }, }, { fullConfig: { plugins: { allow: ["google-meet", "voice-call"], entries: { "voice-call": { enabled: true, config: { provider: "twilio", publicUrl: "https://voice.example.com/voice/webhook", }, }, }, }, }, }, ); const tool = tools[0] as { execute: ( id: string, params: unknown, ) => Promise<{ details: { ok?: boolean; checks?: unknown[] } }>; }; const result = await tool.execute("id", { action: "setup_status" }); expect(result.details.ok).toBe(true); expect(requireSetupCheck(result.details.checks, "twilio-voice-call-plugin").ok).toBe(true); expect(requireSetupCheck(result.details.checks, "twilio-voice-call-credentials").ok).toBe(true); expect(requireSetupCheck(result.details.checks, "twilio-voice-call-webhook").ok).toBe(true); }); it("reports missing voice-call wiring for explicit Twilio transport", async () => { vi.stubEnv("TWILIO_ACCOUNT_SID", ""); vi.stubEnv("TWILIO_AUTH_TOKEN", ""); vi.stubEnv("TWILIO_FROM_NUMBER", ""); const { tools } = setup( { defaultTransport: "chrome" }, { fullConfig: { plugins: { allow: ["google-meet"], entries: { "voice-call": { enabled: false }, }, }, }, }, ); const tool = tools[0] as { execute: ( id: string, params: unknown, ) => Promise<{ details: { ok?: boolean; checks?: unknown[] } }>; }; const result = await tool.execute("id", { action: "setup_status", transport: "twilio" }); expect(result.details.ok).toBe(false); expect(requireSetupCheck(result.details.checks, "twilio-voice-call-plugin").ok).toBe(false); expect(requireSetupCheck(result.details.checks, "twilio-voice-call-credentials").ok).toBe( false, ); }); it("reports missing voice-call plugin entry for explicit Twilio transport", async () => { vi.stubEnv("TWILIO_ACCOUNT_SID", "AC123"); vi.stubEnv("TWILIO_AUTH_TOKEN", "secret"); vi.stubEnv("TWILIO_FROM_NUMBER", "+15550001234"); const { tools } = setup( { defaultTransport: "chrome" }, { fullConfig: { plugins: { allow: ["google-meet", "voice-call"], entries: {}, }, }, }, ); const tool = tools[0] as { execute: ( id: string, params: unknown, ) => Promise<{ details: { ok?: boolean; checks?: unknown[] } }>; }; const result = await tool.execute("id", { action: "setup_status", transport: "twilio" }); expect(result.details.ok).toBe(false); expect(requireSetupCheck(result.details.checks, "twilio-voice-call-plugin").ok).toBe(false); }); it("reports missing Twilio dial plan for explicit Twilio setup", async () => { vi.stubEnv("TWILIO_ACCOUNT_SID", "AC123"); vi.stubEnv("TWILIO_AUTH_TOKEN", "secret"); vi.stubEnv("TWILIO_FROM_NUMBER", "+15550001234"); const { tools } = setup( { defaultTransport: "chrome" }, { fullConfig: { plugins: { allow: ["google-meet", "voice-call"], entries: { "voice-call": { enabled: true, config: { provider: "twilio", publicUrl: "https://voice.example.com/voice/webhook", }, }, }, }, }, }, ); const tool = tools[0] as { execute: ( id: string, params: unknown, ) => Promise<{ details: { ok?: boolean; checks?: unknown[] } }>; }; const result = await tool.execute("id", { action: "setup_status", transport: "twilio" }); expect(result.details.ok).toBe(false); const check = requireSetupCheck(result.details.checks, "twilio-dial-plan"); expect(check.ok).toBe(false); expect(check.message).toContain("dial-in phone number"); }); it("accepts request-provided Twilio dial-in details during setup", async () => { vi.stubEnv("TWILIO_ACCOUNT_SID", "AC123"); vi.stubEnv("TWILIO_AUTH_TOKEN", "secret"); vi.stubEnv("TWILIO_FROM_NUMBER", "+15550001234"); const { tools } = setup( { defaultTransport: "chrome" }, { fullConfig: { plugins: { allow: ["google-meet", "voice-call"], entries: { "voice-call": { enabled: true, config: { provider: "twilio", publicUrl: "https://voice.example.com/voice/webhook", }, }, }, }, }, }, ); const tool = tools[0] as { execute: ( id: string, params: unknown, ) => Promise<{ details: { ok?: boolean; checks?: unknown[] } }>; }; const result = await tool.execute("id", { action: "setup_status", transport: "twilio", dialInNumber: "+15551234567", }); expect(result.details.ok).toBe(true); const check = requireSetupCheck(result.details.checks, "twilio-dial-plan"); expect(check.ok).toBe(true); expect(check.message).toContain("request includes"); }); it.each([ "http://127.0.0.1:3334/voice/webhook", "http://[::1]:3334/voice/webhook", "http://[fd00::1]/voice/webhook", ])( "reports local voice-call publicUrl %s as unusable for Twilio transport", async (publicUrl) => { vi.stubEnv("TWILIO_ACCOUNT_SID", "AC123"); vi.stubEnv("TWILIO_AUTH_TOKEN", "secret"); vi.stubEnv("TWILIO_FROM_NUMBER", "+15550001234"); const { tools } = setup( { defaultTransport: "twilio" }, { fullConfig: { plugins: { allow: ["google-meet", "voice-call"], entries: { "voice-call": { enabled: true, config: { provider: "twilio", publicUrl, }, }, }, }, }, }, ); const tool = tools[0] as { execute: ( id: string, params: unknown, ) => Promise<{ details: { ok?: boolean; checks?: unknown[] } }>; }; const result = await tool.execute("id", { action: "setup_status" }); expect(result.details.ok).toBe(false); expect(requireSetupCheck(result.details.checks, "twilio-voice-call-webhook").ok).toBe(false); }, ); it("opens local Chrome Meet in observe-only mode without BlackHole checks", async () => { const originalPlatform = process.platform; Object.defineProperty(process, "platform", { value: "darwin" }); try { const { methods, runCommandWithTimeout } = setup({ defaultMode: "transcribe", chrome: { browserProfile: "meet-devtools", }, }); const callGatewayFromCli = mockLocalMeetBrowserRequest({ inCall: true, micMuted: true, captioning: true, captionsEnabledAttempted: true, transcriptLines: 1, lastCaptionAt: "2026-04-27T10:00:00.000Z", lastCaptionSpeaker: "Alice", lastCaptionText: "Can everyone hear the agent?", recentTranscript: [ { at: "2026-04-27T10:00:00.000Z", speaker: "Alice", text: "Can everyone hear the agent?", }, ], title: "Meet call", url: "https://meet.google.com/abc-defg-hij", }); const handler = methods.get("googlemeet.join") as | ((ctx: { params: Record; respond: ReturnType; }) => Promise) | undefined; const respond = vi.fn(); await handler?.({ params: { url: "https://meet.google.com/abc-defg-hij" }, respond, }); expectRespondedOk(respond); expect(runCommandWithTimeout).not.toHaveBeenCalled(); const openCall = callGatewayFromCli.mock.calls.find( (call) => requireRecord(call[2], "browser request").path === "/tabs/open", ); if (!openCall) { throw new Error("Expected browser /tabs/open request"); } expect(openCall[0]).toBe("browser.request"); expect(openCall[2]).toEqual({ method: "POST", path: "/tabs/open", timeoutMs: 30000, body: { url: "https://meet.google.com/abc-defg-hij?hl=en" }, }); expect(openCall[3]).toEqual({ progress: false }); expect( callGatewayFromCli.mock.calls.some( (call) => (call[2] as { path?: string }).path === "/permissions/grant", ), ).toBe(false); const payload = requireRespondPayload(respond, "join response payload"); const session = requireRecord(payload.session, "join session"); const chrome = requireRecord(session.chrome, "join chrome session"); const health = requireRecord(chrome.health, "join chrome health"); expect(health.captioning).toBe(true); expect(health.captionsEnabledAttempted).toBe(true); expect(health.transcriptLines).toBe(1); expect(health.lastCaptionSpeaker).toBe("Alice"); expect(health.lastCaptionText).toBe("Can everyone hear the agent?"); const recentTranscript = health.recentTranscript as unknown[]; expect(recentTranscript).toHaveLength(1); const transcriptLine = requireRecord(recentTranscript[0], "recent transcript line"); expect(transcriptLine.speaker).toBe("Alice"); expect(transcriptLine.text).toBe("Can everyone hear the agent?"); const actCall = callGatewayFromCli.mock.calls.find( (call) => (call[2] as { path?: string }).path === "/act", ); expect(String((actCall?.[2] as { body?: { fn?: string } } | undefined)?.body?.fn)).toContain( "const allowMicrophone = false", ); expect(String((actCall?.[2] as { body?: { fn?: string } } | undefined)?.body?.fn)).toContain( "const captureCaptions = true", ); } finally { Object.defineProperty(process, "platform", { value: originalPlatform }); } }); it("grants local Chrome Meet media permissions against the opened tab", async () => { const originalPlatform = process.platform; Object.defineProperty(process, "platform", { value: "darwin" }); try { const callGatewayFromCli = mockLocalMeetBrowserRequest({ inCall: true, micMuted: false, title: "Meet call", url: "https://meet.google.com/abc-defg-hij", }); const { methods } = setup({ defaultMode: "bidi", defaultTransport: "chrome", chrome: { audioBridgeCommand: ["bridge", "start"], }, realtime: { introMessage: "" }, }); const handler = methods.get("googlemeet.join") as | ((ctx: { params: Record; respond: ReturnType; }) => Promise) | undefined; const respond = vi.fn(); await handler?.({ params: { url: "https://meet.google.com/abc-defg-hij" }, respond, }); expectRespondedOk(respond); const grantCall = callGatewayFromCli.mock.calls.find( (call) => requireRecord(call[2], "browser request").path === "/permissions/grant", ); if (!grantCall) { throw new Error("Expected browser /permissions/grant request"); } expect(grantCall[0]).toBe("browser.request"); const request = requireRecord(grantCall[2], "permissions request"); expect(request.method).toBe("POST"); expect(request.path).toBe("/permissions/grant"); const body = requireRecord(request.body, "permissions request body"); expect(body.origin).toBe("https://meet.google.com"); expect(body.permissions).toEqual(["audioCapture", "videoCapture"]); expect(body.targetId).toBe("local-meet-tab"); expect(grantCall[3]).toEqual({ progress: false }); } finally { Object.defineProperty(process, "platform", { value: originalPlatform }); } }); it("starts the local realtime audio bridge after Meet is inspected", async () => { const originalPlatform = process.platform; Object.defineProperty(process, "platform", { value: "darwin" }); const events: string[] = []; try { const callGatewayFromCli = vi.fn( async ( _method: string, _opts: unknown, params?: unknown, _extra?: unknown, ): Promise> => { const request = params as { path?: string; body?: { fn?: string; targetId?: string; url?: string }; }; events.push(`browser:${request.path}`); if (request.path === "/tabs") { return { tabs: [] }; } if (request.path === "/tabs/open") { return { targetId: "local-meet-tab", title: "Meet", url: request.body?.url ?? "https://meet.google.com/abc-defg-hij", }; } if (request.path === "/tabs/focus" || request.path === "/permissions/grant") { return { ok: true }; } if (request.path === "/act") { return { result: JSON.stringify({ inCall: true, micMuted: false, title: "Meet call", url: "https://meet.google.com/abc-defg-hij", }), }; } throw new Error(`unexpected browser request path ${request.path}`); }, ); chromeTransportTesting.setDepsForTest({ callGatewayFromCli }); const { methods } = setup( { defaultMode: "bidi", defaultTransport: "chrome", chrome: { audioBridgeCommand: ["bridge", "start"], }, realtime: { introMessage: "" }, }, { runCommandWithTimeoutHandler: async (argv) => { events.push(`command:${argv.join(" ")}`); return argv[0] === "/usr/sbin/system_profiler" ? { code: 0, stdout: "BlackHole 2ch", stderr: "" } : { code: 0, stdout: "", stderr: "" }; }, }, ); const handler = methods.get("googlemeet.join") as | ((ctx: { params: Record; respond: ReturnType; }) => Promise) | undefined; const respond = vi.fn(); await handler?.({ params: { url: "https://meet.google.com/abc-defg-hij" }, respond, }); expectRespondedOk(respond); expect(events.indexOf("browser:/act")).toBeGreaterThan(-1); expect(events.indexOf("command:bridge start")).toBeGreaterThan( events.indexOf("browser:/act"), ); } finally { Object.defineProperty(process, "platform", { value: originalPlatform }); } }); it("does not start the local realtime audio bridge while Meet admission is pending", async () => { const originalPlatform = process.platform; Object.defineProperty(process, "platform", { value: "darwin" }); const events: string[] = []; try { const callGatewayFromCli = vi.fn( async ( _method: string, _opts: unknown, params?: unknown, _extra?: unknown, ): Promise> => { const request = params as { path?: string; body?: { targetId?: string; url?: string } }; events.push(`browser:${request.path}`); if (request.path === "/tabs") { return { tabs: [] }; } if (request.path === "/tabs/open") { return { targetId: "local-meet-tab", title: "Meet", url: request.body?.url ?? "https://meet.google.com/abc-defg-hij", }; } if (request.path === "/tabs/focus" || request.path === "/permissions/grant") { return { ok: true }; } if (request.path === "/act") { return { result: JSON.stringify({ inCall: false, lobbyWaiting: true, manualActionRequired: true, manualActionReason: "meet-admission-required", manualActionMessage: "Admit the OpenClaw browser participant in Google Meet.", title: "Meet", url: "https://meet.google.com/abc-defg-hij", }), }; } throw new Error(`unexpected browser request path ${request.path}`); }, ); chromeTransportTesting.setDepsForTest({ callGatewayFromCli }); const { methods } = setup( { defaultMode: "bidi", defaultTransport: "chrome", chrome: { audioBridgeCommand: ["bridge", "start"], waitForInCallMs: 1, }, realtime: { introMessage: "" }, }, { runCommandWithTimeoutHandler: async (argv) => { events.push(`command:${argv.join(" ")}`); return argv[0] === "/usr/sbin/system_profiler" ? { code: 0, stdout: "BlackHole 2ch", stderr: "" } : { code: 0, stdout: "", stderr: "" }; }, }, ); const handler = methods.get("googlemeet.join") as | ((ctx: { params: Record; respond: ReturnType; }) => Promise) | undefined; const respond = vi.fn(); await handler?.({ params: { url: "https://meet.google.com/abc-defg-hij" }, respond, }); expectRespondedOk(respond); expect(events).toContain("browser:/act"); expect(events).not.toContain("command:bridge start"); } finally { Object.defineProperty(process, "platform", { value: originalPlatform }); } }); it("refreshes observe-only caption health when status is requested", async () => { let openedTab = false; let actCount = 0; const callGatewayFromCli = vi.fn( async ( _method: string, _opts: unknown, params?: unknown, _extra?: unknown, ): Promise> => { const request = params as { path?: string; body?: { targetId?: string; url?: string }; }; if (request.path === "/tabs") { return openedTab ? { tabs: [ { targetId: "local-meet-tab", title: "Meet", url: "https://meet.google.com/abc-defg-hij", }, ], } : { tabs: [] }; } if (request.path === "/tabs/open") { openedTab = true; return { targetId: "local-meet-tab", title: "Meet", url: request.body?.url ?? "https://meet.google.com/abc-defg-hij", }; } if (request.path === "/tabs/focus") { return { ok: true }; } if (request.path === "/act") { actCount += 1; return { result: JSON.stringify( actCount === 1 ? { inCall: true, captioning: false, captionsEnabledAttempted: true, transcriptLines: 0, title: "Meet call", url: "https://meet.google.com/abc-defg-hij", } : { inCall: true, captioning: true, captionsEnabledAttempted: true, transcriptLines: 1, lastCaptionAt: "2026-04-27T10:00:00.000Z", lastCaptionSpeaker: "Alice", lastCaptionText: "Please capture this.", recentTranscript: [ { at: "2026-04-27T10:00:00.000Z", speaker: "Alice", text: "Please capture this.", }, ], title: "Meet call", url: "https://meet.google.com/abc-defg-hij", }, ), }; } throw new Error(`unexpected browser request path ${request.path}`); }, ); chromeTransportTesting.setDepsForTest({ callGatewayFromCli }); const { methods } = setup({ defaultMode: "transcribe", defaultTransport: "chrome", }); const join = (await invokeGoogleMeetGatewayMethodForTest(methods, "googlemeet.join", { url: "https://meet.google.com/abc-defg-hij", })) as { session: { id: string; chrome?: { health?: { transcriptLines?: number } } } }; expect(join.session.chrome?.health?.transcriptLines).toBe(0); const status = (await invokeGoogleMeetGatewayMethodForTest(methods, "googlemeet.status", { sessionId: join.session.id, })) as { session?: { chrome?: { health?: { captioning?: boolean; transcriptLines?: number; lastCaptionText?: string; }; }; }; }; expect(status.session?.chrome?.health?.captioning).toBe(true); expect(status.session?.chrome?.health?.transcriptLines).toBe(1); expect(status.session?.chrome?.health?.lastCaptionText).toBe("Please capture this."); const focusCall = callGatewayFromCli.mock.calls.find( (call) => requireRecord(call[2], "browser request").path === "/tabs/focus", ); if (!focusCall) { throw new Error("Expected browser /tabs/focus request"); } expect(focusCall[0]).toBe("browser.request"); expect(focusCall[2]).toEqual({ method: "POST", path: "/tabs/focus", timeoutMs: 5000, body: { targetId: "local-meet-tab" }, }); expect(focusCall[3]).toEqual({ progress: false }); }); it("refreshes blocked realtime browser health read-only when status is requested", async () => { let openedTab = false; const { methods, nodesInvoke } = setup( { defaultMode: "agent", defaultTransport: "chrome-node", }, { nodesInvokeHandler: async ({ command, params }) => { const raw = params as { path?: string; body?: { url?: string; targetId?: string } }; if (command === "browser.proxy") { if (raw.path === "/tabs") { return { payload: { result: { running: true, tabs: openedTab ? [ { targetId: "tab-1", title: "Meet", url: "https://meet.google.com/abc-defg-hij", }, ] : [], }, }, }; } if (raw.path === "/tabs/open") { openedTab = true; return { payload: { result: { targetId: "tab-1", title: "Meet", url: raw.body?.url ?? "https://meet.google.com/abc-defg-hij", }, }, }; } if (raw.path === "/tabs/focus" || raw.path === "/permissions/grant") { return { payload: { result: { ok: true } } }; } if (raw.path === "/act") { return { payload: { result: { ok: true, targetId: raw.body?.targetId ?? "tab-1", result: JSON.stringify({ inCall: false, manualActionRequired: true, manualActionReason: "meet-audio-choice-required", manualActionMessage: "Choose the Meet microphone path manually.", title: "Meet", url: "https://meet.google.com/abc-defg-hij", }), }, }, }; } } if (command === "googlemeet.chrome") { return { payload: { launched: openedTab } }; } throw new Error(`unexpected invoke ${command}`); }, }, ); const join = (await invokeGoogleMeetGatewayMethodForTest(methods, "googlemeet.join", { url: "https://meet.google.com/abc-defg-hij", })) as { session: { id: string } }; openedTab = true; nodesInvoke.mockClear(); const status = (await invokeGoogleMeetGatewayMethodForTest(methods, "googlemeet.status", { sessionId: join.session.id, })) as { session?: { chrome?: { health?: { manualActionRequired?: boolean } } } }; expect(status.session?.chrome?.health?.manualActionRequired).toBe(true); const actCall = nodesInvoke.mock.calls.find(([rawCall]) => { const call = requireRecord(rawCall, "node invoke"); const params = requireRecord(call.params, "node invoke params"); return call.command === "browser.proxy" && params.path === "/act"; }); if (!actCall) { throw new Error("Expected browser.proxy /act node invoke"); } const actParams = requireRecord( requireRecord(actCall[0], "act node invoke").params, "act params", ); expect(requireRecord(actParams.body, "act body").targetId).toBe("tab-1"); expect( nodesInvoke.mock.calls.some(([rawCall]) => { const call = requireRecord(rawCall, "node invoke"); const params = requireRecord(call.params, "node invoke params"); return call.command === "browser.proxy" && params.path === "/permissions/grant"; }), ).toBe(false); }); it("retries caption enable until the captions button is available", async () => { const makeButton = (label: string) => ({ disabled: false, innerText: "", textContent: "", click: vi.fn(), getAttribute: vi.fn((name: string) => (name === "aria-label" ? label : null)), }); const leaveButton = makeButton("Leave call"); const captionButton = makeButton("Turn on captions"); const page = { buttons: [leaveButton], }; const windowState: Record = {}; const document = { body: { innerText: "", textContent: "" }, title: "Meet", querySelector: vi.fn(() => null), querySelectorAll: vi.fn((selector: string) => { if (selector === "button") { return page.buttons; } if (selector === "input") { return []; } return []; }), }; const context = createContext({ Date, JSON, String, document, location: { href: "https://meet.google.com/abc-defg-hij", hostname: "meet.google.com", }, MutationObserver: class { observe = vi.fn(); }, window: windowState, }); const inspect = new Script( `(${chromeTransportTesting.meetStatusScriptForTest({ allowMicrophone: false, autoJoin: false, captureCaptions: true, guestName: "OpenClaw Agent", })})`, ).runInContext(context) as () => string | Promise; const first = JSON.parse(await inspect()) as { captionsEnabledAttempted?: boolean }; const captionsStateKey = "__openclawMeetCaptions"; const stateAfterFirst = windowState[captionsStateKey] as { enabledAttempted?: boolean; }; expect(first.captionsEnabledAttempted).toBe(false); expect(stateAfterFirst.enabledAttempted).toBe(false); expect(captionButton.click).not.toHaveBeenCalled(); page.buttons = [leaveButton, captionButton]; const second = JSON.parse(await inspect()) as { captionsEnabledAttempted?: boolean }; const stateAfterSecond = windowState[captionsStateKey] as { enabledAttempted?: boolean; }; expect(second.captionsEnabledAttempted).toBe(true); expect(stateAfterSecond.enabledAttempted).toBe(true); expect(captionButton.click).toHaveBeenCalledTimes(1); }); it("reports in-call Meet audio permission problems from button labels", async () => { const makeButton = (label: string) => ({ disabled: false, innerText: "", textContent: "", click: vi.fn(), getAttribute: vi.fn((name: string) => (name === "aria-label" ? label : null)), }); const document = { body: { innerText: "", textContent: "" }, title: "Meet", querySelector: vi.fn(() => null), querySelectorAll: vi.fn((selector: string) => { if (selector === "button") { return [ makeButton("Leave call"), makeButton("Microphone problem. Show more info"), makeButton("Microphone: Permission needed"), makeButton("Speaker: Permission needed"), ]; } if (selector === "input") { return []; } return []; }), }; const context = createContext({ JSON, document, location: { href: "https://meet.google.com/abc-defg-hij", hostname: "meet.google.com", }, window: {}, }); const inspect = new Script( `(${chromeTransportTesting.meetStatusScriptForTest({ allowMicrophone: true, autoJoin: false, captureCaptions: false, guestName: "OpenClaw Agent", })})`, ).runInContext(context) as () => string | Promise; const result = JSON.parse(await inspect()) as { inCall?: boolean; manualActionRequired?: boolean; manualActionReason?: string; manualActionMessage?: string; }; expect(result.inCall).toBe(true); expect(result.manualActionRequired).toBe(true); expect(result.manualActionReason).toBe("meet-permission-required"); expect(result.manualActionMessage).toContain("Allow microphone/camera/speaker permissions"); }); it("uses the local Meet microphone control instead of remote participant mute buttons", async () => { const makeButton = (label: string, disabled = false) => ({ disabled, innerText: "", textContent: "", click: vi.fn(), getAttribute: vi.fn((name: string) => (name === "aria-label" ? label : null)), }); const remoteMute = makeButton("You can't remotely mute Peter Steinberger's microphone", true); const localMic = makeButton("Turn on microphone"); const document = { body: { innerText: "", textContent: "" }, title: "Meet", querySelector: vi.fn(() => null), querySelectorAll: vi.fn((selector: string) => { if (selector === "button") { return [makeButton("Leave call"), remoteMute, localMic]; } if (selector === "input") { return []; } return []; }), }; const context = createContext({ JSON, document, location: { href: "https://meet.google.com/abc-defg-hij", hostname: "meet.google.com", }, window: {}, }); const inspect = new Script( `(${chromeTransportTesting.meetStatusScriptForTest({ allowMicrophone: true, autoJoin: false, captureCaptions: false, guestName: "OpenClaw Agent", })})`, ).runInContext(context) as () => string | Promise; const result = JSON.parse(await inspect()) as { micMuted?: boolean; notes?: string[] }; expect(result.micMuted).toBe(true); expect(localMic.click).toHaveBeenCalledTimes(1); expect(remoteMute.click).not.toHaveBeenCalled(); expect(result.notes).toContain("Attempted to turn on the Meet microphone for talk-back mode."); }); it("blocks realtime speech while the Meet microphone remains muted", async () => { const originalPlatform = process.platform; Object.defineProperty(process, "platform", { value: "darwin" }); try { mockLocalMeetBrowserRequest({ inCall: true, micMuted: true, title: "Meet call", url: "https://meet.google.com/abc-defg-hij", }); const { methods } = setup({ realtime: { introMessage: "" }, chrome: { audioBridgeCommand: ["bridge", "start"], waitForInCallMs: 1, }, }); const handler = methods.get("googlemeet.join") as | ((ctx: { params: Record; respond: ReturnType; }) => Promise) | undefined; const respond = vi.fn(); await handler?.({ params: { url: "https://meet.google.com/abc-defg-hij" }, respond, }); const payload = requireRespondPayload(respond, "join response payload"); expect(payload.spoken).toBe(false); const session = requireRecord(payload.session, "join session"); const chrome = requireRecord(session.chrome, "join chrome session"); const health = requireRecord(chrome.health, "join chrome health"); expect(health.micMuted).toBe(true); expect(health.speechReady).toBe(false); expect(health.speechBlockedReason).toBe("meet-microphone-muted"); } finally { Object.defineProperty(process, "platform", { value: originalPlatform }); } }); it("keeps waiting while the Meet microphone is muted during intro readiness", async () => { const originalPlatform = process.platform; Object.defineProperty(process, "platform", { value: "darwin" }); try { let inspectCount = 0; mockLocalMeetBrowserRequest(() => { inspectCount += 1; return { inCall: true, micMuted: true, title: "Meet call", url: "https://meet.google.com/abc-defg-hij", }; }); const { methods } = setup({ chrome: { audioBridgeCommand: ["bridge", "start"], waitForInCallMs: 1000, }, }); const handler = methods.get("googlemeet.join") as | ((ctx: { params: Record; respond: ReturnType; }) => Promise) | undefined; const respond = vi.fn(); vi.useFakeTimers(); const run = handler?.({ params: { url: "https://meet.google.com/abc-defg-hij" }, respond, }); await vi.advanceTimersByTimeAsync(2_000); await run; const payload = requireRespondPayload(respond, "join response payload"); expect(payload.spoken).toBe(false); const session = requireRecord(payload.session, "join session"); const chrome = requireRecord(session.chrome, "join chrome session"); const health = requireRecord(chrome.health, "join chrome health"); expect(health.micMuted).toBe(true); expect(health.speechReady).toBe(false); expect(health.speechBlockedReason).toBe("meet-microphone-muted"); expect(inspectCount).toBeGreaterThanOrEqual(2); } finally { vi.useRealTimers(); Object.defineProperty(process, "platform", { value: originalPlatform }); } }); it("joins Chrome on a paired node without local Chrome or BlackHole", async () => { const { methods, nodesList, nodesInvoke } = setup( { defaultTransport: "chrome-node", defaultMode: "transcribe", chromeNode: { node: "parallels-macos" }, }, { nodesInvokeResult: { payload: { launched: true } }, }, ); const handler = methods.get("googlemeet.join") as | ((ctx: { params: Record; respond: ReturnType; }) => Promise) | undefined; const respond = vi.fn(); await handler?.({ params: { url: "https://meet.google.com/abc-defg-hij" }, respond, }); expectRespondedOk(respond); expect(mockCall(nodesList)).toStrictEqual([]); const stopCall = nodesInvoke.mock.calls.find(([rawCall]) => { const call = requireRecord(rawCall, "node invoke"); const params = requireRecord(call.params, "node invoke params"); return call.command === "googlemeet.chrome" && params.action === "stopByUrl"; }); if (!stopCall) { throw new Error("Expected googlemeet.chrome stopByUrl node invoke"); } expect(requireRecord(stopCall[0], "stop node invoke").nodeId).toBe("node-1"); expect(requireRecord(stopCall[0], "stop node invoke").command).toBe("googlemeet.chrome"); expect( requireRecord(requireRecord(stopCall[0], "stop node invoke").params, "stop params"), ).toEqual({ action: "stopByUrl", url: "https://meet.google.com/abc-defg-hij", mode: "transcribe", }); const openCall = nodesInvoke.mock.calls.find(([rawCall]) => { const call = requireRecord(rawCall, "node invoke"); const params = requireRecord(call.params, "node invoke params"); return call.command === "browser.proxy" && params.path === "/tabs/open"; }); if (!openCall) { throw new Error("Expected browser.proxy /tabs/open node invoke"); } expect(requireRecord(openCall[0], "open node invoke").nodeId).toBe("node-1"); expect(requireRecord(openCall[0], "open node invoke").command).toBe("browser.proxy"); expect( requireRecord(requireRecord(openCall[0], "open node invoke").params, "open params"), ).toEqual({ method: "POST", path: "/tabs/open", timeoutMs: 30000, body: { url: "https://meet.google.com/abc-defg-hij?hl=en" }, }); const startCall = nodesInvoke.mock.calls.find(([rawCall]) => { const call = requireRecord(rawCall, "node invoke"); const params = requireRecord(call.params, "node invoke params"); return call.command === "googlemeet.chrome" && params.action === "start"; }); if (!startCall) { throw new Error("Expected googlemeet.chrome start node invoke"); } expect(requireRecord(startCall[0], "start node invoke").nodeId).toBe("node-1"); expect(requireRecord(startCall[0], "start node invoke").command).toBe("googlemeet.chrome"); const startParams = requireRecord( requireRecord(startCall[0], "start node invoke").params, "start params", ); expect(startParams.action).toBe("start"); expect(startParams.url).toBe("https://meet.google.com/abc-defg-hij"); expect(startParams.mode).toBe("transcribe"); expect(startParams.launch).toBe(false); expect(startParams.joinTimeoutMs).toBe(30000); const payload = requireRespondPayload(respond, "join response payload"); const session = requireRecord(payload.session, "join session"); expect(session.transport).toBe("chrome-node"); const chrome = requireRecord(session.chrome, "join chrome session"); expect(chrome.nodeId).toBe("node-1"); expect(chrome.launched).toBe(true); }); it("reuses an active Meet session for the same URL and transport", async () => { const { methods, nodesInvoke } = setup( { defaultTransport: "chrome-node", defaultMode: "transcribe", }, { nodesInvokeResult: { payload: { launched: true, browser: { inCall: true, micMuted: false }, }, }, }, ); const handler = methods.get("googlemeet.join") as | ((ctx: { params: Record; respond: ReturnType; }) => Promise) | undefined; const first = vi.fn(); const second = vi.fn(); await handler?.({ params: { url: "https://meet.google.com/abc-defg-hij" }, respond: first, }); await handler?.({ params: { url: "https://meet.google.com/abc-defg-hij" }, respond: second, }); expect( nodesInvoke.mock.calls.filter(([call]) => call.command === "googlemeet.chrome"), ).toHaveLength(2); const payload = requireRespondPayload(second, "second join response payload"); const session = requireRecord(payload.session, "second join session"); const chrome = requireRecord(session.chrome, "second join chrome session"); const health = requireRecord(chrome.health, "second join chrome health"); expect(health.inCall).toBe(true); expect(health.micMuted).toBe(false); expect(session.notes).toContain("Reused existing active Meet session."); }); it("reuses active Meet sessions across URL query differences", async () => { const { methods, nodesInvoke } = setup( { defaultTransport: "chrome-node", defaultMode: "transcribe", }, { nodesInvokeResult: { payload: { launched: true, browser: { inCall: true, micMuted: false }, }, }, }, ); const handler = methods.get("googlemeet.join") as | ((ctx: { params: Record; respond: ReturnType; }) => Promise) | undefined; const first = vi.fn(); const second = vi.fn(); await handler?.({ params: { url: "https://meet.google.com/abc-defg-hij?authuser=me@example.com" }, respond: first, }); await handler?.({ params: { url: "https://meet.google.com/abc-defg-hij" }, respond: second, }); expect( nodesInvoke.mock.calls.filter(([call]) => call.command === "googlemeet.chrome"), ).toHaveLength(2); const payload = requireRespondPayload(second, "second join response payload"); const session = requireRecord(payload.session, "second join session"); expect(session.notes).toContain("Reused existing active Meet session."); }); it("reuses existing Meet browser tabs across URL query differences", async () => { const { methods, nodesInvoke } = setup( { defaultTransport: "chrome-node", defaultMode: "transcribe", }, { nodesInvokeHandler: async (params) => { if (params.command !== "browser.proxy") { return { payload: { launched: true } }; } const proxy = params.params as { path?: string; body?: { targetId?: string; url?: string }; }; if (proxy.path === "/tabs") { return { payload: { result: { running: true, tabs: [ { targetId: "existing-meet-tab", title: "Meet", url: "https://meet.google.com/abc-defg-hij?authuser=me@example.com", }, ], }, }, }; } if (proxy.path === "/tabs/focus") { return { payload: { result: { ok: true } } }; } if (proxy.path === "/act") { return { payload: { result: { result: JSON.stringify({ inCall: true, title: "Meet", url: "https://meet.google.com/abc-defg-hij?authuser=me@example.com", }), }, }, }; } throw new Error(`unexpected browser proxy path ${proxy.path}`); }, }, ); const handler = methods.get("googlemeet.join") as | ((ctx: { params: Record; respond: ReturnType; }) => Promise) | undefined; const respond = vi.fn(); await handler?.({ params: { url: "https://meet.google.com/abc-defg-hij" }, respond, }); const focusCall = nodesInvoke.mock.calls.find(([rawCall]) => { const call = requireRecord(rawCall, "node invoke"); const params = requireRecord(call.params, "node invoke params"); return params.path === "/tabs/focus"; }); if (!focusCall) { throw new Error("Expected browser.proxy /tabs/focus node invoke"); } expect( requireRecord(requireRecord(focusCall[0], "focus node invoke").params, "focus params"), ).toEqual({ method: "POST", path: "/tabs/focus", timeoutMs: 5000, body: { targetId: "existing-meet-tab" }, }); expect( nodesInvoke.mock.calls.some(([rawCall]) => { const call = requireRecord(rawCall, "node invoke"); return requireRecord(call.params, "node invoke params").path === "/tabs/open"; }), ).toBe(false); }); it("recovers and inspects an existing Meet tab without opening a new one", async () => { const { tools, nodesInvoke } = setup( { defaultTransport: "chrome-node", }, { nodesInvokeHandler: async (params) => { if (params.command !== "browser.proxy") { throw new Error(`unexpected command ${params.command}`); } const proxy = params.params as { path?: string; body?: { targetId?: string } }; if (proxy.path === "/tabs") { return { payload: { result: { tabs: [ { targetId: "existing-meet-tab", title: "Meet", url: "https://meet.google.com/abc-defg-hij?authuser=me@example.com", }, ], }, }, }; } if (proxy.path === "/tabs/focus") { return { payload: { result: { ok: true } } }; } if (proxy.path === "/act") { return { payload: { result: { result: JSON.stringify({ inCall: false, manualActionRequired: true, manualActionReason: "meet-admission-required", manualActionMessage: "Admit the OpenClaw browser participant in Google Meet.", title: "Meet", url: "https://meet.google.com/abc-defg-hij?authuser=me@example.com", }), }, }, }; } throw new Error(`unexpected browser proxy path ${proxy.path}`); }, }, ); const tool = tools[0] as { execute: ( id: string, params: unknown, ) => Promise<{ details: { found?: boolean; targetId?: string; browser?: unknown } }>; }; const result = await tool.execute("id", { action: "recover_current_tab", url: "https://meet.google.com/abc-defg-hij", }); expect(result.details.found).toBe(true); expect(result.details.targetId).toBe("existing-meet-tab"); const browser = requireRecord(result.details.browser, "recovered browser state"); expect(browser.manualActionRequired).toBe(true); expect(browser.manualActionReason).toBe("meet-admission-required"); const focusCall = nodesInvoke.mock.calls.find(([rawCall]) => { const call = requireRecord(rawCall, "node invoke"); const params = requireRecord(call.params, "node invoke params"); return params.path === "/tabs/focus"; }); if (!focusCall) { throw new Error("Expected browser.proxy /tabs/focus node invoke"); } expect( requireRecord(requireRecord(focusCall[0], "focus node invoke").params, "focus params"), ).toEqual({ method: "POST", path: "/tabs/focus", timeoutMs: 5000, body: { targetId: "existing-meet-tab" }, }); expect( nodesInvoke.mock.calls.some(([rawCall]) => { const call = requireRecord(rawCall, "node invoke"); return requireRecord(call.params, "node invoke params").path === "/tabs/open"; }), ).toBe(false); }); it("recovers and inspects an existing local Chrome Meet tab", async () => { const callGatewayFromCli = vi.fn( async ( _method: string, _opts: unknown, params?: unknown, _extra?: unknown, ): Promise> => { const request = params as { path?: string; body?: { targetId?: string } }; if (request.path === "/tabs") { return { tabs: [ { targetId: "local-meet-tab", title: "Meet", url: "https://meet.google.com/abc-defg-hij?authuser=me@example.com", }, ], }; } if (request.path === "/tabs/focus") { return { ok: true }; } if (request.path === "/act") { return { result: JSON.stringify({ inCall: false, manualActionRequired: true, manualActionReason: "meet-admission-required", manualActionMessage: "Admit the OpenClaw browser participant in Google Meet.", title: "Meet", url: "https://meet.google.com/abc-defg-hij?authuser=me@example.com", }), }; } throw new Error(`unexpected browser request path ${request.path}`); }, ); chromeTransportTesting.setDepsForTest({ callGatewayFromCli }); const { tools, nodesInvoke } = setup({ defaultTransport: "chrome", chrome: { browserProfile: "meet-devtools", }, }); const tool = tools[0] as { execute: ( id: string, params: unknown, ) => Promise<{ details: { transport?: string; found?: boolean; targetId?: string; browser?: unknown }; }>; }; const result = await tool.execute("id", { action: "recover_current_tab", url: "https://meet.google.com/abc-defg-hij", }); expect(result.details.transport).toBe("chrome"); expect(result.details.found).toBe(true); expect(result.details.targetId).toBe("local-meet-tab"); const browser = requireRecord(result.details.browser, "recovered browser state"); expect(browser.manualActionRequired).toBe(true); expect(browser.manualActionReason).toBe("meet-admission-required"); const focusCall = callGatewayFromCli.mock.calls.find( (call) => requireRecord(call[2], "browser request").path === "/tabs/focus", ); if (!focusCall) { throw new Error("Expected browser /tabs/focus request"); } expect(focusCall[0]).toBe("browser.request"); expect(requireRecord(focusCall[2], "focus request").method).toBe("POST"); expect(requireRecord(focusCall[2], "focus request").path).toBe("/tabs/focus"); expect(requireRecord(focusCall[2], "focus request").query).toBeUndefined(); expect(focusCall[3]).toEqual({ progress: false }); expect(nodesInvoke).not.toHaveBeenCalled(); }); it("exposes a test-speech action that joins the requested meeting", async () => { const { tools, nodesInvoke } = setup( { defaultTransport: "chrome-node", }, { nodesInvokeResult: { payload: { launched: true, browser: { inCall: true }, }, }, }, ); const tool = tools[0] as { execute: (id: string, params: unknown) => Promise<{ details: { createdSession?: boolean } }>; }; const result = await tool.execute("id", { action: "test_speech", url: "https://meet.google.com/abc-defg-hij", message: "Say exactly: hello.", }); const startCall = nodesInvoke.mock.calls.find(([rawCall]) => { const call = requireRecord(rawCall, "node invoke"); const params = requireRecord(call.params, "node invoke params"); return call.command === "googlemeet.chrome" && params.action === "start"; }); if (!startCall) { throw new Error("Expected googlemeet.chrome start node invoke"); } expect(result.details.createdSession).toBe(true); }); it("refreshes realtime browser state in status after a delayed Meet join", async () => { const originalPlatform = process.platform; Object.defineProperty(process, "platform", { value: "darwin" }); try { let browserState: Record = { inCall: false, title: "Meet", url: "https://meet.google.com/abc-defg-hij", }; let opened = false; const callGatewayFromCli = vi.fn( async ( _method: string, _opts: unknown, params?: unknown, _extra?: unknown, ): Promise> => { const request = params as { path?: string; body?: { targetId?: string; url?: string }; }; if (request.path === "/tabs") { return { tabs: opened ? [ { targetId: "local-meet-tab", title: "Meet", url: "https://meet.google.com/abc-defg-hij", }, ] : [], }; } if (request.path === "/tabs/open") { opened = true; return { targetId: "local-meet-tab", title: "Meet", url: request.body?.url ?? "https://meet.google.com/abc-defg-hij", }; } if (request.path === "/tabs/focus" || request.path === "/permissions/grant") { return { ok: true }; } if (request.path === "/act") { return { result: JSON.stringify(browserState) }; } throw new Error(`unexpected browser request path ${request.path}`); }, ); chromeTransportTesting.setDepsForTest({ callGatewayFromCli }); const { methods } = setup({ chrome: { audioBridgeCommand: ["bridge", "start"], waitForInCallMs: 1, }, realtime: { introMessage: "" }, }); const join = methods.get("googlemeet.join") as | ((ctx: { params: Record; respond: ReturnType; }) => Promise) | undefined; const status = methods.get("googlemeet.status") as | ((ctx: { params: Record; respond: ReturnType; }) => Promise) | undefined; const joinRespond = vi.fn(); const statusRespond = vi.fn(); await join?.({ params: { url: "https://meet.google.com/abc-defg-hij" }, respond: joinRespond, }); const joinPayload = requireRespondPayload(joinRespond, "join response payload"); const joinSession = requireRecord(joinPayload.session, "join session"); const joinChrome = requireRecord(joinSession.chrome, "join chrome session"); expect(requireRecord(joinChrome.health, "join chrome health").inCall).toBe(false); browserState = { inCall: true, micMuted: false, title: "Meet", url: "https://meet.google.com/abc-defg-hij", }; await status?.({ params: {}, respond: statusRespond }); const statusPayload = requireRespondPayload(statusRespond, "status response payload"); const sessions = statusPayload.sessions as unknown[]; expect(sessions).toHaveLength(1); const statusSession = requireRecord(sessions[0], "status session"); const statusChrome = requireRecord(statusSession.chrome, "status chrome session"); const statusHealth = requireRecord(statusChrome.health, "status chrome health"); expect(statusHealth.inCall).toBe(true); expect(statusHealth.speechReady).toBe(false); expect(statusHealth.speechBlockedReason).toBe("audio-bridge-unavailable"); } finally { Object.defineProperty(process, "platform", { value: originalPlatform }); } }); it("exposes a test-listen action that proves transcript movement", async () => { const { tools, nodesInvoke } = setup( { defaultTransport: "chrome-node", }, { browserActResult: { inCall: true, captioning: true, transcriptLines: 1, lastCaptionText: "hello from the meeting", title: "Meet call", url: "https://meet.google.com/abc-defg-hij", }, nodesInvokeResult: { payload: { launched: true, }, }, }, ); const tool = tools[0] as { execute: ( id: string, params: unknown, ) => Promise<{ details: { listenVerified?: boolean; transcriptLines?: number } }>; }; const result = await tool.execute("id", { action: "test_listen", url: "https://meet.google.com/abc-defg-hij", timeoutMs: "100", }); const startCall = nodesInvoke.mock.calls.find(([rawCall]) => { const call = requireRecord(rawCall, "node invoke"); const params = requireRecord(call.params, "node invoke params"); return call.command === "googlemeet.chrome" && params.action === "start"; }); if (!startCall) { throw new Error("Expected googlemeet.chrome start node invoke"); } const startParams = requireRecord( requireRecord(startCall[0], "start node invoke").params, "start params", ); expect(startParams.mode).toBe("transcribe"); expect(result.details.listenVerified).toBe(true); expect(result.details.transcriptLines).toBe(1); }); it("rejects fractional test-listen gateway timeouts", async () => { const { methods } = setup({ defaultTransport: "chrome-node" }); await expect( invokeGoogleMeetGatewayMethodForTest(methods, "googlemeet.testListen", { url: "https://meet.google.com/abc-defg-hij", timeoutMs: "100.5", }), ).rejects.toThrow("timeoutMs must be a positive integer"); }); it("does not start a second realtime response for test speech", async () => { const runtime = new GoogleMeetRuntime({ config: resolveGoogleMeetConfig({}), fullConfig: {} as never, runtime: {} as never, logger: noopLogger, }); const session: GoogleMeetSession = { id: "meet_1", url: "https://meet.google.com/abc-defg-hij", transport: "chrome", mode: "agent", state: "active", createdAt: "2026-04-27T00:00:00.000Z", updatedAt: "2026-04-27T00:00:00.000Z", participantIdentity: "signed-in Google Chrome profile", realtime: { enabled: true, strategy: "agent", transcriptionProvider: "openai", toolPolicy: "safe-read-only", }, chrome: { audioBackend: "blackhole-2ch", launched: true, health: { audioOutputActive: true, lastOutputBytes: 10 }, }, notes: [], }; vi.spyOn(runtime, "list").mockReturnValue([session]); const join = vi.spyOn(runtime, "join").mockResolvedValue({ session, spoken: true }); const speak = vi.spyOn(runtime, "speak"); const result = await runtime.testSpeech({ url: "https://meet.google.com/abc-defg-hij", message: "Say exactly: hello.", }); expect(join).toHaveBeenCalledTimes(1); const joinArgs = requireRecord(mockCallArg(join, 0), "test speech join args"); expect(joinArgs.message).toBe("Say exactly: hello."); expect(joinArgs.mode).toBe("agent"); expect(speak).not.toHaveBeenCalled(); expect(result.spoken).toBe(true); expect(result.speechOutputVerified).toBe(false); expect(result.speechOutputTimedOut).toBe(false); }); it("uses the requested bidirectional realtime mode for test speech", async () => { const runtime = new GoogleMeetRuntime({ config: resolveGoogleMeetConfig({ defaultMode: "agent" }), fullConfig: {} as never, runtime: {} as never, logger: noopLogger, }); const session: GoogleMeetSession = { id: "meet_1", url: "https://meet.google.com/abc-defg-hij", transport: "chrome", mode: "bidi", state: "active", createdAt: "2026-04-27T00:00:00.000Z", updatedAt: "2026-04-27T00:00:00.000Z", participantIdentity: "signed-in Google Chrome profile", realtime: { enabled: true, strategy: "bidi", provider: "openai", toolPolicy: "safe-read-only", }, chrome: { audioBackend: "blackhole-2ch", launched: true, health: { audioOutputActive: true, lastOutputBytes: 10 }, }, notes: [], }; vi.spyOn(runtime, "list").mockReturnValue([]); const join = vi.spyOn(runtime, "join").mockResolvedValue({ session, spoken: true }); await runtime.testSpeech({ url: "https://meet.google.com/abc-defg-hij", mode: "bidi", message: "Say exactly: hello.", }); expect(join).toHaveBeenCalledTimes(1); const joinArgs = requireRecord(mockCallArg(join, 0), "test speech join args"); expect(joinArgs.message).toBe("Say exactly: hello."); expect(joinArgs.mode).toBe("bidi"); }); it("rejects observe-only mode for test speech", async () => { const runtime = new GoogleMeetRuntime({ config: resolveGoogleMeetConfig({}), fullConfig: {} as never, runtime: {} as never, logger: noopLogger, }); await expect( runtime.testSpeech({ url: "https://meet.google.com/abc-defg-hij", mode: "transcribe", }), ).rejects.toThrow("test_speech requires mode: agent or bidi"); }); it("rejects realtime and Twilio modes for test listen", async () => { const runtime = new GoogleMeetRuntime({ config: resolveGoogleMeetConfig({}), fullConfig: {} as never, runtime: {} as never, logger: noopLogger, }); await expect( runtime.testListen({ url: "https://meet.google.com/abc-defg-hij", mode: "agent", }), ).rejects.toThrow("test_listen requires mode: transcribe"); await expect( runtime.testListen({ url: "https://meet.google.com/abc-defg-hij", transport: "twilio", }), ).rejects.toThrow("test_listen supports chrome or chrome-node"); }); it("reports manual action when the browser profile needs Google login", async () => { const { tools } = setup( { defaultTransport: "chrome-node", }, { browserActResult: { inCall: false, manualActionRequired: true, manualActionReason: "google-login-required", manualActionMessage: "Sign in to Google in the OpenClaw browser profile, then retry the Meet join.", title: "Sign in - Google Accounts", url: "https://accounts.google.com/signin", }, nodesInvokeResult: { payload: { launched: true, }, }, }, ); const tool = tools[0] as { execute: ( id: string, params: unknown, ) => Promise<{ details: { manualActionRequired?: boolean; manualActionReason?: string; speechReady?: boolean; speechBlockedReason?: string; spoken?: boolean; session?: { chrome?: { health?: { manualActionRequired?: boolean } } }; }; }>; }; const result = await tool.execute("id", { action: "test_speech", url: "https://meet.google.com/abc-defg-hij", message: "Say exactly: hello.", }); expect(result.details.manualActionRequired).toBe(true); expect(result.details.manualActionReason).toBe("google-login-required"); expect(result.details.spoken).toBe(false); expect(result.details.speechReady).toBe(false); expect(result.details.speechBlockedReason).toBe("google-login-required"); const session = requireRecord(result.details.session, "manual action session"); const chrome = requireRecord(session.chrome, "manual action session chrome"); const health = requireRecord(chrome.health, "manual action chrome health"); expect(health.manualActionRequired).toBe(true); expect(health.manualActionReason).toBe("google-login-required"); expect(health.speechReady).toBe(false); expect(health.speechBlockedReason).toBe("google-login-required"); }); it("refreshes browser health before blocking an explicit speech retry", async () => { let openedTab = false; let browserReady = false; const { methods, nodesInvoke } = setup( { defaultTransport: "chrome-node", defaultMode: "agent", }, { nodesInvokeHandler: async ({ command, params }) => { const raw = params as { path?: string; body?: { url?: string; targetId?: string } }; if (command === "browser.proxy") { if (raw.path === "/tabs") { return { payload: { result: { running: true, tabs: openedTab ? [ { targetId: "tab-1", title: "Meet", url: "https://meet.google.com/abc-defg-hij", }, ] : [], }, }, }; } if (raw.path === "/tabs/open") { openedTab = true; return { payload: { result: { targetId: "tab-1", title: "Meet", url: raw.body?.url ?? "https://meet.google.com/abc-defg-hij", }, }, }; } if (raw.path === "/tabs/focus" || raw.path === "/permissions/grant") { return { payload: { result: { ok: true } } }; } if (raw.path === "/act") { return { payload: { result: { ok: true, targetId: raw.body?.targetId ?? "tab-1", result: JSON.stringify( browserReady ? { inCall: true, micMuted: false, manualActionRequired: false, title: "Meet call", url: "https://meet.google.com/abc-defg-hij", } : { inCall: false, manualActionRequired: true, manualActionReason: "google-login-required", manualActionMessage: "Sign in to Google in the OpenClaw browser profile, then retry the Meet join.", title: "Sign in - Google Accounts", url: "https://accounts.google.com/signin", }, ), }, }, }; } } if (command === "googlemeet.chrome") { return { payload: { launched: true } }; } throw new Error(`unexpected invoke ${command}`); }, }, ); const join = (await invokeGoogleMeetGatewayMethodForTest(methods, "googlemeet.join", { url: "https://meet.google.com/abc-defg-hij", message: "Say exactly: hello.", })) as { session: { id: string; chrome?: { health?: { speechBlockedReason?: string } } }; spoken: boolean; }; expect(join.spoken).toBe(false); expect(join.session.chrome?.health?.speechBlockedReason).toBe("google-login-required"); browserReady = true; const retry = (await invokeGoogleMeetGatewayMethodForTest(methods, "googlemeet.speak", { sessionId: join.session.id, message: "Say exactly: hello again.", })) as { found: boolean; spoken: boolean; session?: { chrome?: { health?: { inCall?: boolean; manualActionRequired?: boolean; speechBlockedReason?: string; }; }; }; }; expect(retry.found).toBe(true); expect(retry.spoken).toBe(false); const retrySession = requireRecord(retry.session, "retry session"); const retryChrome = requireRecord(retrySession.chrome, "retry session chrome"); const retryHealth = requireRecord(retryChrome.health, "retry chrome health"); expect(retryHealth.inCall).toBe(true); expect(retryHealth.manualActionRequired).toBe(false); expect(retryHealth.speechBlockedReason).toBe("audio-bridge-unavailable"); const focusCalls = nodesInvoke.mock.calls .map(([call]) => call) .filter( (call): call is { command: string; params: Record } => call.command === "browser.proxy" && isRecord(call.params) && call.params.path === "/tabs/focus", ); expect(focusCalls.length).toBeGreaterThan(0); expect(focusCalls.at(-1)?.params.body).toStrictEqual({ targetId: "tab-1" }); }); it("explains when chrome-node has no capable paired node", async () => { const { tools } = setup( { defaultTransport: "chrome-node", defaultMode: "transcribe", }, { nodesListResult: { nodes: [] }, }, ); const tool = tools[0] as { execute: (id: string, params: unknown) => Promise<{ details: { error?: string } }>; }; const result = await tool.execute("id", { action: "join", url: "https://meet.google.com/abc-defg-hij", }); expect(result.details.error).toContain("No connected Google Meet-capable node"); expect(result.details.error).toContain("openclaw node run"); }); it("requires chromeNode.node when multiple capable nodes are connected", async () => { const { tools } = setup( { defaultTransport: "chrome-node", defaultMode: "transcribe", }, { nodesListResult: { nodes: [ { nodeId: "node-1", displayName: "parallels-macos", connected: true, caps: ["browser"], commands: ["browser.proxy", "googlemeet.chrome"], }, { nodeId: "node-2", displayName: "mac-studio-vm", connected: true, caps: ["browser"], commands: ["browser.proxy", "googlemeet.chrome"], }, ], }, }, ); const tool = tools[0] as { execute: (id: string, params: unknown) => Promise<{ details: { error?: string } }>; }; const result = await tool.execute("id", { action: "join", url: "https://meet.google.com/abc-defg-hij", }); expect(result.details.error).toContain("Multiple Google Meet-capable nodes connected"); expect(result.details.error).toContain("chromeNode.node"); }); it("runs configured Chrome audio bridge commands before launch", async () => { const originalPlatform = process.platform; Object.defineProperty(process, "platform", { value: "darwin" }); try { const { methods, runCommandWithTimeout } = setup({ defaultMode: "bidi", chrome: { waitForInCallMs: 1, audioBridgeHealthCommand: ["bridge", "status"], audioBridgeCommand: ["bridge", "start"], }, }); const callGatewayFromCli = mockLocalMeetBrowserRequest(); const handler = methods.get("googlemeet.join") as | ((ctx: { params: Record; respond: ReturnType; }) => Promise) | undefined; const respond = vi.fn(); await handler?.({ params: { url: "https://meet.google.com/abc-defg-hij" }, respond, }); expectRespondedOk(respond); expect(runCommandWithTimeout).toHaveBeenNthCalledWith(2, ["bridge", "status"], { timeoutMs: 30000, }); expect(runCommandWithTimeout).toHaveBeenNthCalledWith(3, ["bridge", "start"], { timeoutMs: 30000, }); const openRequests = callGatewayFromCli.mock.calls.filter((call) => { const params = call[2]; return call[0] === "browser.request" && isRecord(params) && params.path === "/tabs/open"; }); expect(openRequests).toHaveLength(1); const [method, opts, params, extra] = openRequests[0] ?? []; expect(method).toBe("browser.request"); expect(isRecord(opts)).toBe(true); const request = requireRecord(params, "local browser open request"); expect(request.method).toBe("POST"); expect(request.path).toBe("/tabs/open"); expect(request.body).toStrictEqual({ url: "https://meet.google.com/abc-defg-hij?hl=en" }); expect(extra).toStrictEqual({ progress: false }); } finally { Object.defineProperty(process, "platform", { value: originalPlatform }); } }); it("uses realtime transcription plus regular TTS in Chrome agent mode", async () => { vi.useFakeTimers(); let callbacks: Parameters[0] | undefined; const sendAudio = vi.fn(); const sttSession = { connect: vi.fn(async () => {}), sendAudio, close: vi.fn(), isConnected: vi.fn(() => true), }; const provider: RealtimeTranscriptionProviderPlugin = { id: "openai", label: "OpenAI", defaultModel: "gpt-4o-transcribe", autoSelectOrder: 1, resolveConfig: ({ rawConfig }) => rawConfig, isConfigured: () => true, createSession: (req) => { callbacks = req; return sttSession; }, }; const inputStdout = new PassThrough(); const outputStdinWrites: Buffer[] = []; const makeProcess = (stdio: { stdin?: { write(chunk: unknown): unknown } | null; stdout?: { on(event: "data", listener: (chunk: unknown) => void): unknown } | null; }): TestBridgeProcess => { const proc = new EventEmitter() as unknown as TestBridgeProcess; proc.stdin = stdio.stdin; proc.stdout = stdio.stdout; proc.stderr = new PassThrough(); proc.killed = false; proc.kill = vi.fn(() => { proc.killed = true; return true; }); return proc; }; const outputStdin = new Writable({ write(chunk, _encoding, done) { outputStdinWrites.push(Buffer.from(chunk)); done(); }, }); const inputProcess = makeProcess({ stdout: inputStdout, stdin: null }); const outputProcess = makeProcess({ stdin: outputStdin, stdout: null }); const spawnMock = vi.fn().mockReturnValueOnce(outputProcess).mockReturnValueOnce(inputProcess); const sessionStore: Record = {}; const runtime = { tts: { textToSpeechTelephony: vi.fn(async () => ({ success: true, audioBuffer: Buffer.from([1, 0, 2, 0]), sampleRate: 24_000, provider: "elevenlabs", providerModel: "eleven_multilingual_v2", providerVoice: "pMsXgVXv3BLzUgSXRplE", outputFormat: "pcm16", })), }, agent: { resolveAgentDir: vi.fn(() => "/tmp/agent"), resolveAgentWorkspaceDir: vi.fn(() => "/tmp/workspace"), ensureAgentWorkspace: vi.fn(async () => {}), session: createMockSessionRuntime(sessionStore), runEmbeddedAgent: vi.fn(async () => ({ payloads: [{ text: "Use the Portugal launch data." }], meta: {}, })), resolveAgentTimeoutMs: vi.fn(() => 1000), }, }; const handle = await startCommandAgentAudioBridge({ config: resolveGoogleMeetConfig({ realtime: { provider: "openai", agentId: "jay", introMessage: "" }, }), fullConfig: {} as never, runtime: runtime as never, meetingSessionId: "meet-1", inputCommand: ["capture-meet"], outputCommand: ["play-meet"], logger: noopLogger, providers: [provider], spawn: spawnMock, }); expect(noopLogger.info).toHaveBeenCalledWith( "[google-meet] agent audio bridge starting: transcriptionProvider=openai transcriptionModel=gpt-4o-transcribe tts=telephony audioFormat=pcm16-24khz", ); inputStdout.write(Buffer.from([1, 0, 2, 0, 3, 0, 4, 0])); callbacks?.onTranscript?.("Please summarize the launch."); await vi.advanceTimersByTimeAsync(GOOGLE_MEET_AGENT_TRANSCRIPT_DEBOUNCE_MS); expect(sendAudio).toHaveBeenCalledTimes(1); const audioChunk = mockCallArg(sendAudio, 0) as Buffer; expect(Buffer.isBuffer(audioChunk)).toBe(true); expect(audioChunk.byteLength).toBeGreaterThan(0); expect(runtime.agent.runEmbeddedAgent).toHaveBeenCalled(); expect(runtime.tts.textToSpeechTelephony).toHaveBeenCalledWith({ text: "Use the Portugal launch data.", cfg: {}, }); expect(noopLogger.info).toHaveBeenCalledWith( "[google-meet] agent TTS: provider=elevenlabs model=eleven_multilingual_v2 voice=pMsXgVXv3BLzUgSXRplE outputFormat=pcm16 sampleRate=24000", ); expect(Buffer.concat(outputStdinWrites)).toEqual(Buffer.from([1, 0, 2, 0])); const health = handle.getHealth(); expect(health.providerConnected).toBe(true); expect(health.audioInputActive).toBe(true); expect(health.audioOutputActive).toBe(true); expect(health.realtimeTranscriptLines).toBe(2); expect(health.lastRealtimeTranscriptRole).toBe("assistant"); const talkEventTypes = health.recentTalkEvents?.map((event) => event.type) ?? []; expect(talkEventTypes).toEqual([ "session.started", "session.ready", "turn.started", "input.audio.delta", "input.audio.committed", "transcript.done", "output.text.done", "output.audio.started", "output.audio.delta", "output.audio.done", "turn.ended", ]); expect(talkEventTypes.indexOf("output.text.done")).toBeLessThan( talkEventTypes.indexOf("output.audio.started"), ); await handle.stop(); }); it("preserves telephony TTS output formats when routing Google Meet agent audio", () => { const ulaw = Buffer.from([0xff, 0x7f, 0x00]); const pcmBridgeConfig = resolveGoogleMeetConfig({ chrome: { audioFormat: "pcm16-24khz" } }); const ulawBridgeConfig = resolveGoogleMeetConfig({ chrome: { audioFormat: "g711-ulaw-8khz" } }); expect( convertGoogleMeetTtsAudioForBridge(ulaw, 8_000, ulawBridgeConfig, "raw-8khz-8bit-mono-mulaw"), ).toEqual(ulaw); const pcmForMeet = convertGoogleMeetTtsAudioForBridge( ulaw, 8_000, pcmBridgeConfig, "ulaw_8000", ); expect(pcmForMeet.byteLength).toBe(18); expect(pcmForMeet).not.toEqual(ulaw); expect(() => convertGoogleMeetTtsAudioForBridge(Buffer.from([1, 2, 3]), 8_000, pcmBridgeConfig, "mp3"), ).toThrow("Unsupported telephony TTS output format"); }); it("pipes Chrome command-pair audio through the realtime provider", async () => { let callbacks: Parameters[0] | undefined; const sendAudio = vi.fn(); const bridge = { supportsToolResultContinuation: true, connect: vi.fn(async () => {}), sendAudio, setMediaTimestamp: vi.fn(), handleBargeIn: vi.fn(), submitToolResult: vi.fn(), acknowledgeMark: vi.fn(), close: vi.fn(), triggerGreeting: vi.fn(), isConnected: vi.fn(() => true), }; const provider: RealtimeVoiceProviderPlugin = { id: "openai", label: "OpenAI", defaultModel: "gpt-realtime-2", autoSelectOrder: 1, resolveConfig: ({ rawConfig }) => rawConfig, isConfigured: () => true, createBridge: (req) => { callbacks = req; return bridge; }, }; const inputStdout = new PassThrough(); const outputStdinWrites: Buffer[] = []; const replacementOutputStdinWrites: Buffer[] = []; const makeProcess = (stdio: { stdin?: { write(chunk: unknown): unknown } | null; stdout?: { on(event: "data", listener: (chunk: unknown) => void): unknown } | null; }): TestBridgeProcess => { const proc = new EventEmitter() as unknown as TestBridgeProcess; proc.stdin = stdio.stdin; proc.stdout = stdio.stdout; proc.stderr = new PassThrough(); proc.killed = false; proc.kill = vi.fn(() => { proc.killed = true; return true; }); return proc; }; const outputStdin = new Writable({ write(chunk, _encoding, done) { outputStdinWrites.push(Buffer.from(chunk)); done(); }, }); const replacementOutputStdin = new Writable({ write(chunk, _encoding, done) { replacementOutputStdinWrites.push(Buffer.from(chunk)); done(); }, }); const inputProcess = makeProcess({ stdout: inputStdout, stdin: null }); const outputProcess = makeProcess({ stdin: outputStdin, stdout: null }); const replacementOutputProcess = makeProcess({ stdin: replacementOutputStdin, stdout: null }); const spawnMock = vi .fn() .mockReturnValueOnce(outputProcess) .mockReturnValueOnce(inputProcess) .mockReturnValueOnce(replacementOutputProcess); const fullConfig = { models: { providers: {} } } as never; const sessionStore: Record = {}; const runtime = { agent: { resolveAgentDir: vi.fn(() => "/tmp/agent"), resolveAgentWorkspaceDir: vi.fn(() => "/tmp/workspace"), ensureAgentWorkspace: vi.fn(async () => {}), session: createMockSessionRuntime(sessionStore), runEmbeddedAgent: vi.fn(async (_request: unknown) => ({ payloads: [{ text: "Use the Portugal launch data." }], meta: {}, })), resolveAgentTimeoutMs: vi.fn(() => 1000), }, }; const handle = await startCommandRealtimeAudioBridge({ config: resolveGoogleMeetConfig({ realtime: { strategy: "bidi", provider: "openai", model: "gpt-realtime", agentId: "jay" }, }), fullConfig, runtime: runtime as never, meetingSessionId: "meet-1", inputCommand: ["capture-meet"], outputCommand: ["play-meet"], logger: noopLogger, providers: [provider], spawn: spawnMock, }); expect(noopLogger.info).toHaveBeenCalledWith( "[google-meet] realtime voice bridge starting: strategy=bidi provider=openai model=gpt-realtime audioFormat=pcm16-24khz", ); expect(callbacks?.cfg).toBe(fullConfig); inputStdout.write(Buffer.from([1, 2, 3])); callbacks?.onAudio(Buffer.from([4, 5])); callbacks?.onMark?.("mark-1"); callbacks?.onClearAudio(); callbacks?.onAudio(Buffer.from([6, 7])); callbacks?.onReady?.(); callbacks?.onTranscript?.("assistant", "How can I help you?", true); callbacks?.onTranscript?.("user", "Please summarize the launch.", true); callbacks?.onEvent?.({ direction: "client", type: "response.create" }); callbacks?.onEvent?.({ direction: "server", type: "response.done", detail: "status=completed", }); callbacks?.onToolCall?.({ itemId: "item-1", callId: "tool-call-1", name: "openclaw_agent_consult", args: { question: "What should I say about launch timing?" }, }); expect(bridge.submitToolResult).toHaveBeenCalled(); const firstToolResultCall = mockCall(bridge.submitToolResult); expect(firstToolResultCall[0]).toBe("tool-call-1"); expect(firstToolResultCall[2]).toStrictEqual({ willContinue: true }); const progressPayload = requireRecord(firstToolResultCall[1], "tool progress payload"); expect(progressPayload.status).toBe("working"); expect(progressPayload.tool).toBe("openclaw_agent_consult"); expect(spawnMock).toHaveBeenNthCalledWith(1, "play-meet", [], { stdio: ["pipe", "ignore", "pipe"], }); expect(spawnMock).toHaveBeenNthCalledWith(2, "capture-meet", [], { stdio: ["ignore", "pipe", "pipe"], }); expect(sendAudio).toHaveBeenCalledWith(Buffer.from([1, 2, 3])); expect(outputStdinWrites).toEqual([Buffer.from([4, 5])]); expect(outputProcess.kill).toHaveBeenCalledWith("SIGKILL"); expect(replacementOutputStdinWrites).toEqual([Buffer.from([6, 7])]); outputProcess.emit("error", new Error("stale output process failed after clear")); outputStdin.emit("error", new Error("stale output pipe closed after clear")); expect(bridge.close).not.toHaveBeenCalled(); expect(bridge.acknowledgeMark).toHaveBeenCalled(); expect(bridge.triggerGreeting).not.toHaveBeenCalled(); handle.speak("Say exactly: hello from the meeting."); expect(bridge.triggerGreeting).toHaveBeenLastCalledWith("Say exactly: hello from the meeting."); const health = handle.getHealth(); expect(health.providerConnected).toBe(true); expect(health.realtimeReady).toBe(true); expect(health.audioInputActive).toBe(true); expect(health.audioOutputActive).toBe(true); expect(health.lastInputBytes).toBe(3); expect(health.lastOutputBytes).toBe(4); expect(health.realtimeTranscriptLines).toBe(2); expect(health.lastRealtimeTranscriptRole).toBe("user"); expect(health.lastRealtimeTranscriptText).toBe("Please summarize the launch."); expect(health.lastRealtimeEventType).toBe("server:response.done"); expect(health.lastRealtimeEventDetail).toBe("status=completed"); expect(health.clearCount).toBe(1); expect(health.recentRealtimeTranscript).toHaveLength(2); expect(health.recentRealtimeTranscript?.[0]?.role).toBe("assistant"); expect(health.recentRealtimeTranscript?.[0]?.text).toBe("How can I help you?"); expect(health.recentRealtimeTranscript?.[1]?.role).toBe("user"); expect(health.recentRealtimeTranscript?.[1]?.text).toBe("Please summarize the launch."); expect(health.recentRealtimeEvents).toHaveLength(2); expect(health.recentRealtimeEvents?.[0]?.direction).toBe("client"); expect(health.recentRealtimeEvents?.[0]?.type).toBe("response.create"); expect(health.recentRealtimeEvents?.[1]?.direction).toBe("server"); expect(health.recentRealtimeEvents?.[1]?.type).toBe("response.done"); expect(health.recentRealtimeEvents?.[1]?.detail).toBe("status=completed"); if (!callbacks) { throw new Error("Expected realtime bridge callbacks"); } expect(callbacks.audioFormat).toStrictEqual({ encoding: "pcm16", sampleRateHz: 24000, channels: 1, }); expect(callbacks.autoRespondToAudio).toBe(true); expect(callbacks.tools?.map((tool) => tool.name)).toContain("openclaw_agent_consult"); await vi.waitFor(() => { expect(bridge.submitToolResult).toHaveBeenLastCalledWith( "tool-call-1", { text: "Use the Portugal launch data.", }, undefined, ); }); const talkEventTypes = handle.getHealth().recentTalkEvents?.map((event) => event.type) ?? []; for (const type of [ "session.started", "session.ready", "input.audio.delta", "output.audio.delta", "output.audio.done", "transcript.done", "output.text.done", "tool.call", "tool.progress", "tool.result", "turn.ended", ]) { expect(talkEventTypes).toContain(type); } expect(runtime.agent.runEmbeddedAgent).toHaveBeenCalledTimes(1); const agentRequest = requireRecord( mockCallArg(runtime.agent.runEmbeddedAgent, 0), "embedded agent request", ); expect(agentRequest.messageProvider).toBe("google-meet"); expect(agentRequest.agentId).toBe("jay"); expect(agentRequest.spawnedBy).toBe("agent:jay:main"); expect(agentRequest.sessionKey).toBe("agent:jay:subagent:google-meet:meet-1"); expect(agentRequest.sandboxSessionKey).toBe("agent:jay:subagent:google-meet:meet-1"); expect(agentRequest.thinkLevel).toBe("high"); expect(agentRequest.toolsAllow).toStrictEqual([ "read", "web_search", "web_fetch", "x_search", "memory_search", "memory_get", ]); expect(sessionStore).toHaveProperty("agent:jay:subagent:google-meet:meet-1"); await handle.stop(); expect(bridge.close).toHaveBeenCalled(); expect(inputProcess.kill).toHaveBeenCalledWith("SIGTERM"); expect(replacementOutputProcess.kill).toHaveBeenCalledWith("SIGTERM"); }); it("defaults Chrome command-pair realtime to agent-driven talk-back", async () => { vi.useFakeTimers(); try { let callbacks: Parameters[0] | undefined; const sendUserMessage = vi.fn(); const bridge = { connect: vi.fn(async () => {}), sendAudio: vi.fn(), sendUserMessage, setMediaTimestamp: vi.fn(), submitToolResult: vi.fn(), acknowledgeMark: vi.fn(), close: vi.fn(), triggerGreeting: vi.fn(), isConnected: vi.fn(() => true), }; const provider: RealtimeVoiceProviderPlugin = { id: "openai", label: "OpenAI", defaultModel: "gpt-realtime-2", autoSelectOrder: 1, resolveConfig: ({ rawConfig }) => rawConfig, isConfigured: () => true, createBridge: (req) => { callbacks = req; return bridge; }, }; const inputStdout = new PassThrough(); const makeProcess = (stdio: { stdin?: { write(chunk: unknown): unknown } | null; stdout?: { on(event: "data", listener: (chunk: unknown) => void): unknown } | null; }): TestBridgeProcess => { const proc = new EventEmitter() as unknown as TestBridgeProcess; proc.stdin = stdio.stdin; proc.stdout = stdio.stdout; proc.stderr = new PassThrough(); proc.killed = false; proc.kill = vi.fn(() => { proc.killed = true; return true; }); return proc; }; const outputProcess = makeProcess({ stdin: new Writable({ write(_chunk, _encoding, done) { done(); }, }), stdout: null, }); const inputProcess = makeProcess({ stdout: inputStdout, stdin: null }); const spawnMock = vi .fn() .mockReturnValueOnce(outputProcess) .mockReturnValueOnce(inputProcess); const sessionStore: Record = {}; const runtime = { agent: { resolveAgentDir: vi.fn(() => "/tmp/agent"), resolveAgentWorkspaceDir: vi.fn(() => "/tmp/workspace"), ensureAgentWorkspace: vi.fn(async () => {}), session: createMockSessionRuntime(sessionStore), runEmbeddedAgent: vi.fn(async (_request: unknown) => ({ payloads: [{ text: "The launch is still on track." }], meta: {}, })), resolveAgentTimeoutMs: vi.fn(() => 1000), }, }; const handle = await startCommandRealtimeAudioBridge({ config: resolveGoogleMeetConfig({ realtime: { provider: "openai", agentId: "jay" } }), fullConfig: {} as never, runtime: runtime as never, meetingSessionId: "meet-1", inputCommand: ["capture-meet"], outputCommand: ["play-meet"], logger: noopLogger, providers: [provider], spawn: spawnMock, }); if (!callbacks) { throw new Error("Expected realtime bridge callbacks"); } expect(callbacks.autoRespondToAudio).toBe(false); expect(callbacks.tools).toStrictEqual([]); callbacks?.onTranscript?.("user", "Are we still on track?", true); callbacks?.onTranscript?.("user", "Please include launch blockers.", true); await vi.advanceTimersByTimeAsync(GOOGLE_MEET_AGENT_TRANSCRIPT_DEBOUNCE_MS); await vi.waitFor(() => { expect(runtime.agent.runEmbeddedAgent).toHaveBeenCalledTimes(1); }); const consultArgs = requireRecord( (runtime.agent.runEmbeddedAgent.mock.calls as unknown[][])[0]?.[0], "default talk-back agent request", ); expect(consultArgs.agentId).toBe("jay"); expect(consultArgs.spawnedBy).toBe("agent:jay:main"); expect(consultArgs.sessionKey).toBe("agent:jay:subagent:google-meet:meet-1"); expect(consultArgs.sandboxSessionKey).toBe("agent:jay:subagent:google-meet:meet-1"); expect(JSON.stringify(consultArgs)).toContain( "Are we still on track?\\nPlease include launch blockers.", ); expect(sendUserMessage).toHaveBeenCalledTimes(1); const sentUserMessage = mockCallArg(sendUserMessage, 0) as string; expect(typeof sentUserMessage).toBe("string"); expect(sentUserMessage).toContain(JSON.stringify("The launch is still on track.")); expect(sessionStore).toHaveProperty("agent:jay:subagent:google-meet:meet-1"); await handle.stop(); } finally { vi.useRealTimers(); } }); it("tracks queued playback time when suppressing realtime input echo", () => { const first = extendGoogleMeetOutputEchoSuppression({ audio: Buffer.alloc(48_000), audioFormat: "pcm16-24khz", nowMs: 1_000, lastOutputPlayableUntilMs: 0, suppressInputUntilMs: 0, }); const second = extendGoogleMeetOutputEchoSuppression({ audio: Buffer.alloc(48_000), audioFormat: "pcm16-24khz", nowMs: 1_100, lastOutputPlayableUntilMs: first.lastOutputPlayableUntilMs, suppressInputUntilMs: first.suppressInputUntilMs, }); expect(first.durationMs).toBe(1_000); expect(first.lastOutputPlayableUntilMs).toBe(2_000); expect(first.suppressInputUntilMs).toBe(5_000); expect(second.durationMs).toBe(1_000); expect(second.lastOutputPlayableUntilMs).toBe(3_000); expect(second.suppressInputUntilMs).toBe(6_000); }); it("detects assistant transcript echoes before agent consult", () => { const nowMs = Date.parse("2026-05-04T01:00:00.000Z"); const transcript = [ { at: new Date(nowMs - 1_000).toISOString(), role: "assistant" as const, text: "Hi Molty, glad to have you here. Let me know if there's anything specific you'd like to cover or if you need any support during the meeting.", }, ]; expect( isGoogleMeetLikelyAssistantEchoTranscript({ transcript, text: "Let me know if there's anything specific you'd like to cover or if you need any support during the", nowMs, }), ).toBe(true); expect( isGoogleMeetLikelyAssistantEchoTranscript({ transcript, text: "Tell me a story.", nowMs, }), ).toBe(false); expect( isGoogleMeetLikelyAssistantEchoTranscript({ transcript, text: "yes yes yes yes", nowMs, }), ).toBe(false); }); it("uses a local barge-in input command to clear active Chrome playback", async () => { let callbacks: | { onAudio: (audio: Buffer) => void; } | undefined; const sendAudio = vi.fn(); const bridge = { connect: vi.fn(async () => {}), sendAudio, setMediaTimestamp: vi.fn(), handleBargeIn: vi.fn(), submitToolResult: vi.fn(), acknowledgeMark: vi.fn(), close: vi.fn(), isConnected: vi.fn(() => true), }; const provider: RealtimeVoiceProviderPlugin = { id: "openai", label: "OpenAI", autoSelectOrder: 1, resolveConfig: ({ rawConfig }) => rawConfig, isConfigured: () => true, createBridge: (req) => { callbacks = req; return bridge; }, }; const inputStdout = new PassThrough(); const bargeInStdout = new PassThrough(); const outputStdin = new Writable({ write(_chunk, _encoding, done) { done(); }, }); const replacementOutputStdin = new Writable({ write(_chunk, _encoding, done) { done(); }, }); const makeProcess = (stdio: { stdin?: { write(chunk: unknown): unknown } | null; stdout?: { on(event: "data", listener: (chunk: unknown) => void): unknown } | null; }): TestBridgeProcess => { const proc = new EventEmitter() as unknown as TestBridgeProcess; proc.stdin = stdio.stdin; proc.stdout = stdio.stdout; proc.stderr = new PassThrough(); proc.killed = false; proc.kill = vi.fn(() => { proc.killed = true; return true; }); return proc; }; const outputProcess = makeProcess({ stdin: outputStdin, stdout: null }); const inputProcess = makeProcess({ stdout: inputStdout, stdin: null }); const bargeInProcess = makeProcess({ stdout: bargeInStdout, stdin: null }); const replacementOutputProcess = makeProcess({ stdin: replacementOutputStdin, stdout: null }); const spawnMock = vi .fn() .mockReturnValueOnce(outputProcess) .mockReturnValueOnce(inputProcess) .mockReturnValueOnce(bargeInProcess) .mockReturnValueOnce(replacementOutputProcess); const handle = await startCommandRealtimeAudioBridge({ config: resolveGoogleMeetConfig({ chrome: { bargeInInputCommand: ["capture-human"], bargeInRmsThreshold: 10, bargeInPeakThreshold: 10, bargeInCooldownMs: 1, }, realtime: { provider: "openai", model: "gpt-realtime" }, }), fullConfig: {} as never, runtime: {} as never, meetingSessionId: "meet-1", inputCommand: ["capture-meet"], outputCommand: ["play-meet"], logger: noopLogger, providers: [provider], spawn: spawnMock, }); callbacks?.onAudio(Buffer.alloc(48_000)); inputStdout.write(Buffer.from([1, 2, 3, 4])); bargeInStdout.write(Buffer.from([0xff, 0x7f, 0xff, 0x7f])); expect(spawnMock).toHaveBeenNthCalledWith(3, "capture-human", [], { stdio: ["ignore", "pipe", "pipe"], }); expect(bridge.handleBargeIn).toHaveBeenCalled(); expect(outputProcess.kill).toHaveBeenCalledWith("SIGKILL"); expect(sendAudio).not.toHaveBeenCalledWith(Buffer.from([1, 2, 3, 4])); const health = handle.getHealth(); expect(health.clearCount).toBe(1); expect(health.suppressedInputBytes).toBe(4); await handle.stop(); expect(inputProcess.kill).toHaveBeenCalledWith("SIGTERM"); expect(bargeInProcess.kill).toHaveBeenCalledWith("SIGTERM"); expect(replacementOutputProcess.kill).toHaveBeenCalledWith("SIGTERM"); }); it("pipes paired-node command-pair audio through the realtime provider", async () => { let callbacks: Parameters[0] | undefined; const sendAudio = vi.fn(); const bridge = { supportsToolResultContinuation: true, connect: vi.fn(async () => {}), sendAudio, setMediaTimestamp: vi.fn(), submitToolResult: vi.fn(), acknowledgeMark: vi.fn(), close: vi.fn(), triggerGreeting: vi.fn(), isConnected: vi.fn(() => true), }; const provider: RealtimeVoiceProviderPlugin = { id: "openai", label: "OpenAI", autoSelectOrder: 1, resolveConfig: ({ rawConfig }) => rawConfig, isConfigured: () => true, createBridge: (req) => { callbacks = req; return bridge; }, }; let pullCount = 0; let idlePullStarted = false; let releaseIdlePull: (() => void) | undefined; const fullConfig = { models: { providers: {} } } as never; const sessionStore: Record = {}; const runtime = { nodes: { invoke: vi.fn(async ({ params }: { params?: { action?: string; base64?: string } }) => { if (params?.action === "pullAudio") { pullCount += 1; if (pullCount === 1) { return { bridgeId: "bridge-1", base64: Buffer.from([9, 8, 7]).toString("base64") }; } idlePullStarted = true; await new Promise((resolve) => { releaseIdlePull = resolve; }); return { bridgeId: "bridge-1" }; } releaseIdlePull?.(); releaseIdlePull = undefined; return { ok: true }; }), }, agent: { resolveAgentDir: vi.fn(() => "/tmp/agent"), resolveAgentWorkspaceDir: vi.fn(() => "/tmp/workspace"), ensureAgentWorkspace: vi.fn(async () => {}), session: createMockSessionRuntime(sessionStore), runEmbeddedAgent: vi.fn(async () => ({ payloads: [{ text: "Use the launch update." }], meta: {}, })), resolveAgentTimeoutMs: vi.fn(() => 1000), }, }; const handle = await startNodeRealtimeAudioBridge({ config: resolveGoogleMeetConfig({ realtime: { strategy: "bidi", provider: "openai", model: "gpt-realtime" }, }), fullConfig, runtime: runtime as never, meetingSessionId: "meet-1", nodeId: "node-1", bridgeId: "bridge-1", logger: noopLogger, providers: [provider], }); expect(noopLogger.info).toHaveBeenCalledWith( "[google-meet] realtime voice bridge starting: strategy=bidi provider=openai model=gpt-realtime audioFormat=pcm16-24khz", ); expect(callbacks?.cfg).toBe(fullConfig); callbacks?.onAudio(Buffer.from([1, 2, 3])); callbacks?.onClearAudio(); callbacks?.onReady?.(); callbacks?.onTranscript?.("assistant", "How can I help from the node?", true); callbacks?.onEvent?.({ direction: "server", type: "response.done", detail: "status=completed", }); callbacks?.onToolCall?.({ itemId: "item-1", callId: "tool-call-1", name: "openclaw_agent_consult", args: { question: "What should I say?" }, }); expect(bridge.submitToolResult).toHaveBeenCalled(); const firstToolResultCall = mockCall(bridge.submitToolResult); expect(firstToolResultCall[0]).toBe("tool-call-1"); expect(firstToolResultCall[2]).toStrictEqual({ willContinue: true }); const progressPayload = requireRecord(firstToolResultCall[1], "node tool progress payload"); expect(progressPayload.status).toBe("working"); expect(progressPayload.tool).toBe("openclaw_agent_consult"); await vi.waitFor(() => { expect(sendAudio).toHaveBeenCalledWith(Buffer.from([9, 8, 7])); }); await vi.waitFor(() => { const pushCall = runtime.nodes.invoke.mock.calls .map(([call]) => call) .find((call) => isRecord(call.params) && call.params.action === "pushAudio"); const push = requireRecord(pushCall, "node push audio call"); const params = requireRecord(push.params, "node push audio params"); expect(push.nodeId).toBe("node-1"); expect(push.command).toBe("googlemeet.chrome"); expect(params.bridgeId).toBe("bridge-1"); expect(params.base64).toBe(Buffer.from([1, 2, 3]).toString("base64")); }); await vi.waitFor(() => { const clearCall = runtime.nodes.invoke.mock.calls .map(([call]) => call) .find((call) => isRecord(call.params) && call.params.action === "clearAudio"); const clear = requireRecord(clearCall, "node clear audio call"); expect(clear.nodeId).toBe("node-1"); expect(clear.command).toBe("googlemeet.chrome"); expect(clear.params).toStrictEqual({ action: "clearAudio", bridgeId: "bridge-1" }); expect(clear.timeoutMs).toBe(5_000); }); await vi.waitFor(() => { expect(bridge.submitToolResult).toHaveBeenLastCalledWith( "tool-call-1", { text: "Use the launch update.", }, undefined, ); }); expect(bridge.triggerGreeting).not.toHaveBeenCalled(); handle.speak("Say exactly: hello from the node."); expect(bridge.triggerGreeting).toHaveBeenLastCalledWith("Say exactly: hello from the node."); if (!callbacks) { throw new Error("Expected node realtime callbacks"); } expect(callbacks.audioFormat).toStrictEqual({ encoding: "pcm16", sampleRateHz: 24000, channels: 1, }); expect(callbacks.autoRespondToAudio).toBe(true); expect(callbacks.tools?.map((tool) => tool.name)).toContain("openclaw_agent_consult"); expect(handle.type).toBe("node-command-pair"); expect(handle.providerId).toBe("openai"); expect(handle.nodeId).toBe("node-1"); expect(handle.bridgeId).toBe("bridge-1"); const nodeHealth = handle.getHealth(); expect(nodeHealth.providerConnected).toBe(true); expect(nodeHealth.realtimeReady).toBe(true); expect(nodeHealth.audioInputActive).toBe(true); expect(nodeHealth.audioOutputActive).toBe(true); expect(nodeHealth.lastInputBytes).toBe(3); expect(nodeHealth.lastOutputBytes).toBe(3); expect(nodeHealth.realtimeTranscriptLines).toBe(1); expect(nodeHealth.lastRealtimeTranscriptRole).toBe("assistant"); expect(nodeHealth.lastRealtimeTranscriptText).toBe("How can I help from the node?"); expect(nodeHealth.lastRealtimeEventType).toBe("server:response.done"); expect(nodeHealth.lastRealtimeEventDetail).toBe("status=completed"); expect(nodeHealth.clearCount).toBe(1); const talkEvents = nodeHealth.recentTalkEvents ?? []; const talkEventTypes = talkEvents.map((event) => event.type); for (const type of [ "session.started", "session.ready", "input.audio.delta", "output.audio.delta", "output.audio.done", "output.text.done", "tool.call", "tool.progress", "tool.result", "turn.ended", ]) { expect(talkEventTypes).toContain(type); } expect(talkEvents[0]?.sessionId).toBe("google-meet:meet-1:bridge-1:node-realtime"); await vi.waitFor(() => { expect(idlePullStarted).toBe(true); }); await handle.stop(); expect(bridge.close).toHaveBeenCalled(); const stopCall = runtime.nodes.invoke.mock.calls .map(([call]) => call) .find((call) => isRecord(call.params) && call.params.action === "stop"); const stop = requireRecord(stopCall, "node stop call"); expect(stop.nodeId).toBe("node-1"); expect(stop.command).toBe("googlemeet.chrome"); expect(stop.params).toStrictEqual({ action: "stop", bridgeId: "bridge-1" }); expect(stop.timeoutMs).toBe(5_000); }); it("keeps paired-node realtime audio alive after transient input pull failures", async () => { vi.useFakeTimers(); const sendAudio = vi.fn(); const bridge = { connect: vi.fn(async () => {}), sendAudio, setMediaTimestamp: vi.fn(), submitToolResult: vi.fn(), acknowledgeMark: vi.fn(), close: vi.fn(), triggerGreeting: vi.fn(), isConnected: vi.fn(() => true), }; const provider: RealtimeVoiceProviderPlugin = { id: "openai", label: "OpenAI", autoSelectOrder: 1, resolveConfig: ({ rawConfig }) => rawConfig, isConfigured: () => true, createBridge: () => bridge, }; let pullCount = 0; let idlePullStarted = false; let releaseIdlePull: (() => void) | undefined; const runtime = { nodes: { invoke: vi.fn(async ({ params }: { params?: { action?: string } }) => { if (params?.action === "pullAudio") { pullCount += 1; if (pullCount === 1) { throw new Error("transient node timeout"); } if (pullCount === 2) { return { bridgeId: "bridge-1", base64: Buffer.from([5, 4, 3]).toString("base64") }; } idlePullStarted = true; await new Promise((resolve) => { releaseIdlePull = resolve; }); return { bridgeId: "bridge-1" }; } releaseIdlePull?.(); releaseIdlePull = undefined; return { ok: true }; }), }, }; let handle: Awaited> | undefined; try { handle = await startNodeRealtimeAudioBridge({ config: resolveGoogleMeetConfig({ realtime: { provider: "openai", model: "gpt-realtime" }, }), fullConfig: {} as never, runtime: runtime as never, meetingSessionId: "meet-1", nodeId: "node-1", bridgeId: "bridge-1", logger: noopLogger, providers: [provider], }); await vi.advanceTimersByTimeAsync(250); await vi.waitFor(() => { expect(sendAudio).toHaveBeenCalledWith(Buffer.from([5, 4, 3])); }); expect(bridge.close).not.toHaveBeenCalled(); const health = handle.getHealth(); expect(health.audioInputActive).toBe(true); expect(health.lastInputBytes).toBe(3); expect(health.consecutiveInputErrors).toBe(0); await vi.waitFor(() => { expect(idlePullStarted).toBe(true); }); await handle.stop(); } finally { await handle?.stop(); vi.useRealTimers(); } }); it("stops paired-node realtime audio after repeated input pull failures", async () => { vi.useFakeTimers(); const bridge = { connect: vi.fn(async () => {}), sendAudio: vi.fn(), setMediaTimestamp: vi.fn(), submitToolResult: vi.fn(), acknowledgeMark: vi.fn(), close: vi.fn(), triggerGreeting: vi.fn(), isConnected: vi.fn(() => true), }; const provider: RealtimeVoiceProviderPlugin = { id: "openai", label: "OpenAI", autoSelectOrder: 1, resolveConfig: ({ rawConfig }) => rawConfig, isConfigured: () => true, createBridge: () => bridge, }; const runtime = { nodes: { invoke: vi.fn(async ({ params }: { params?: { action?: string } }) => { if (params?.action === "pullAudio") { throw new Error("node invoke timeout"); } return { ok: true }; }), }, }; try { const handle = await startNodeRealtimeAudioBridge({ config: resolveGoogleMeetConfig({ realtime: { provider: "openai", model: "gpt-realtime" }, }), fullConfig: {} as never, runtime: runtime as never, meetingSessionId: "meet-1", nodeId: "node-1", bridgeId: "bridge-1", logger: noopLogger, providers: [provider], }); await vi.advanceTimersByTimeAsync(1_000); await vi.waitFor( () => { expect(bridge.close).toHaveBeenCalled(); }, { timeout: 3_000 }, ); const health = handle.getHealth(); expect(health.bridgeClosed).toBe(true); expect(health.consecutiveInputErrors).toBe(5); expect(health.lastInputError).toBe("node invoke timeout"); const stopCall = runtime.nodes.invoke.mock.calls .map(([call]) => call) .find((call) => isRecord(call.params) && call.params.action === "stop"); const stop = requireRecord(stopCall, "failed pull stop call"); expect(stop.nodeId).toBe("node-1"); expect(stop.command).toBe("googlemeet.chrome"); expect(stop.params).toStrictEqual({ action: "stop", bridgeId: "bridge-1" }); expect(stop.timeoutMs).toBe(5_000); } finally { vi.useRealTimers(); } }); it("exposes node-host list and stop-by-url bridge actions", async () => { const listed = JSON.parse( await handleGoogleMeetNodeHostCommand( JSON.stringify({ action: "list", url: "https://meet.google.com/abc-defg-hij" }), ), ); expect(listed).toEqual({ bridges: [] }); await expect( handleGoogleMeetNodeHostCommand(JSON.stringify({ action: "stopByUrl" })), ).rejects.toThrow("url required"); }); });