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 type { RealtimeVoiceProviderPlugin } from "openclaw/plugin-sdk/realtime-voice"; import { 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 { 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 } from "./src/transports/twilio.js"; import type { GoogleMeetSession } from "./src/transports/types.js"; const voiceCallMocks = vi.hoisted(() => ({ joinMeetViaVoiceCallGateway: vi.fn(async () => ({ callId: "call-1", dtmfSent: true })), endMeetVoiceCallGatewayCall: 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", () => ({ fetchWithSsrFGuard: fetchGuardMocks.fetchWithSsrFGuard, })); vi.mock("./src/voice-call-gateway.js", () => ({ joinMeetViaVoiceCallGateway: voiceCallMocks.joinMeetViaVoiceCallGateway, endMeetVoiceCallGatewayCall: voiceCallMocks.endMeetVoiceCallGatewayCall, })); 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 >, ); return harness; } function jsonResponse(value: unknown): Response { return new Response(JSON.stringify(value), { status: 200, headers: { "Content-Type": "application/json" }, }); } 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 = { 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?: { 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(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(); }); afterEach(() => { vi.unstubAllGlobals(); chromeTransportTesting.setDepsForTest(null); googleMeetPluginTesting.setCallGatewayFromCliForTests(); }); it("defaults to chrome realtime with safe read-only tools", () => { expect(resolveGoogleMeetConfig({})).toMatchObject({ enabled: true, defaults: {}, preview: { enrollmentAcknowledged: false }, defaultTransport: "chrome", defaultMode: "realtime", chrome: { audioBackend: "blackhole-2ch", launch: true, guestName: "OpenClaw Agent", reuseExistingTab: true, autoJoin: true, waitForInCallMs: 20000, audioFormat: "pcm16-24khz", audioInputCommand: [ "sox", "-q", "-t", "coreaudio", "BlackHole 2ch", "-t", "raw", "-r", "24000", "-c", "1", "-e", "signed-integer", "-b", "16", "-L", "-", ], audioOutputCommand: [ "sox", "-q", "-t", "raw", "-r", "24000", "-c", "1", "-e", "signed-integer", "-b", "16", "-L", "-", "-t", "coreaudio", "BlackHole 2ch", ], }, voiceCall: { enabled: true, requestTimeoutMs: 30000, dtmfDelayMs: 2500 }, realtime: { provider: "openai", introMessage: "Say exactly: I'm here and listening.", toolPolicy: "safe-read-only", }, oauth: {}, auth: { provider: "google-oauth" }, }); expect(resolveGoogleMeetConfig({}).realtime.instructions).toContain("openclaw_agent_consult"); }); it("resolves the realtime consult agent id", () => { expect( resolveGoogleMeetConfig({ realtime: { agentId: " jay ", }, }).realtime.agentId, ).toBe("jay"); }); it("keeps legacy command-pair audio format when custom commands omit a format", () => { expect( resolveGoogleMeetConfig({ chrome: { audioInputCommand: ["capture-legacy"], audioOutputCommand: ["play-legacy"], }, }).chrome, ).toMatchObject({ audioFormat: "g711-ulaw-8khz", audioInputCommand: ["capture-legacy"], audioOutputCommand: ["play-legacy"], }); }); it("uses env fallbacks for OAuth, preview, and default meeting values", () => { expect( 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", }, ), ).toMatchObject({ defaults: { meeting: "https://meet.google.com/abc-defg-hij" }, preview: { enrollmentAcknowledged: true }, oauth: { clientId: "client-id", clientSecret: "client-secret", refreshToken: "refresh-token", accessToken: "access-token", expiresAt: 123456, }, }); }); 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).toContainEqual({ 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 } = setup(); expect(nodeHostCommands).toContainEqual( expect.objectContaining({ command: "googlemeet.chrome", cap: "google-meet", handle: expect.any(Function), }), ); }); 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"); expect(tool.parameters).toMatchObject({ type: "object", properties: { action: { type: "string", enum: [ "join", "create", "status", "setup_status", "resolve_space", "preflight", "latest", "calendar_events", "artifacts", "attendance", "export", "recover_current_tab", "leave", "speak", "test_speech", ], description: expect.stringContaining("recover_current_tab"), }, transport: { type: "string", enum: ["chrome", "chrome-node", "twilio"] }, mode: { type: "string", enum: ["realtime", "transcribe"] }, }, }); }); 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"); await expect( findGoogleMeetCalendarEvent({ accessToken: "token", now: new Date("2026-04-25T09:50:00Z"), timeMin: "2026-04-25T00:00:00Z", timeMax: "2026-04-26T00:00:00Z", }), ).resolves.toMatchObject({ calendarId: "primary", meetingUri: "https://meet.google.com/abc-defg-hij", event: { summary: "Project sync" }, }); await expect( listGoogleMeetCalendarEvents({ accessToken: "token", now: new Date("2026-04-25T09:50:00Z"), timeMin: "2026-04-25T00:00:00Z", timeMax: "2026-04-26T00:00:00Z", }), ).resolves.toMatchObject({ events: [ { meetingUri: "https://meet.google.com/abc-defg-hij", selected: true, }, ], }); 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"); expect(fetchGuardMocks.fetchWithSsrFGuard).toHaveBeenCalledWith( expect.objectContaining({ policy: { allowedHostnames: ["www.googleapis.com"] }, auditContext: "google-meet.calendar.events.list", }), ); }); 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); await expect( fetchGoogleMeetSpace({ accessToken: "token", meeting: "spaces/abc-defg-hij", }), ).resolves.toMatchObject({ name: "spaces/abc-defg-hij" }); expect(fetchGuardMocks.fetchWithSsrFGuard).toHaveBeenCalledWith( expect.objectContaining({ url: "https://meet.googleapis.com/v2/spaces/abc-defg-hij", init: expect.objectContaining({ headers: expect.objectContaining({ Authorization: "Bearer token" }), }), policy: { allowedHostnames: ["meet.googleapis.com"] }, auditContext: "google-meet.spaces.get", }), ); expect(fetchMock).toHaveBeenCalledWith( "https://meet.googleapis.com/v2/spaces/abc-defg-hij", expect.objectContaining({ headers: expect.objectContaining({ Authorization: "Bearer token" }), }), ); }); 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); await expect(createGoogleMeetSpace({ accessToken: "token" })).resolves.toMatchObject({ meetingUri: "https://meet.google.com/new-abcd-xyz", space: { name: "spaces/new-space" }, }); expect(fetchGuardMocks.fetchWithSsrFGuard).toHaveBeenCalledWith( expect.objectContaining({ url: "https://meet.googleapis.com/v2/spaces", init: expect.objectContaining({ method: "POST", headers: expect.objectContaining({ Authorization: "Bearer token" }), body: "{}", }), policy: { allowedHostnames: ["meet.googleapis.com"] }, auditContext: "google-meet.spaces.create", }), ); }); it("lists Meet artifact metadata for the latest conference record by default", async () => { const fetchMock = stubMeetArtifactsApi(); await expect( fetchGoogleMeetArtifacts({ accessToken: "token", meeting: "abc-defg-hij", pageSize: 2, }), ).resolves.toMatchObject({ input: "abc-defg-hij", space: { name: "spaces/abc-defg-hij" }, conferenceRecords: [{ name: "conferenceRecords/rec-1" }], artifacts: [ { conferenceRecord: { name: "conferenceRecords/rec-1" }, participants: [{ name: "conferenceRecords/rec-1/participants/p1" }], recordings: [{ name: "conferenceRecords/rec-1/recordings/r1" }], transcripts: [{ name: "conferenceRecords/rec-1/transcripts/t1" }], transcriptEntries: [ { transcript: "conferenceRecords/rec-1/transcripts/t1", entries: [ { name: "conferenceRecords/rec-1/transcripts/t1/entries/e1", text: "Hello from the transcript.", }, ], }, ], smartNotes: [{ name: "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(fetchGuardMocks.fetchWithSsrFGuard).toHaveBeenCalledWith( expect.objectContaining({ url: "https://meet.googleapis.com/v2/conferenceRecords/rec-1/smartNotes?pageSize=2", auditContext: "google-meet.conferenceRecords.smartNotes.list", }), ); expect(fetchGuardMocks.fetchWithSsrFGuard).toHaveBeenCalledWith( expect.objectContaining({ url: "https://meet.googleapis.com/v2/conferenceRecords/rec-1/transcripts/t1/entries?pageSize=2", auditContext: "google-meet.conferenceRecords.transcripts.entries.list", }), ); }); 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(); await expect( fetchGoogleMeetArtifacts({ accessToken: "token", conferenceRecord: "rec-1", includeDocumentBodies: true, }), ).resolves.toMatchObject({ artifacts: [ { transcripts: [{ documentText: "Transcript document body." }], smartNotes: [{ documentText: "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.every((url) => url.searchParams.get("mimeType") === "text/plain")).toBe(true); }); it("fetches only the latest Meet conference record for a meeting", async () => { const fetchMock = stubMeetArtifactsApi(); await expect( fetchLatestGoogleMeetConferenceRecord({ accessToken: "token", meeting: "abc-defg-hij", }), ).resolves.toMatchObject({ input: "abc-defg-hij", space: { name: "spaces/abc-defg-hij" }, conferenceRecord: { name: "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(); await expect( fetchGoogleMeetAttendance({ accessToken: "token", conferenceRecord: "rec-1", pageSize: 3, }), ).resolves.toMatchObject({ input: "rec-1", conferenceRecords: [{ name: "conferenceRecords/rec-1" }], attendance: [ { conferenceRecord: "conferenceRecords/rec-1", participant: "conferenceRecords/rec-1/participants/p1", displayName: "Alice", user: "users/alice", sessions: [ { name: "conferenceRecords/rec-1/participants/p1/participantSessions/s1", }, ], }, ], }); expect(fetchMock).toHaveBeenCalledWith( "https://meet.googleapis.com/v2/conferenceRecords/rec-1", expect.objectContaining({ headers: expect.objectContaining({ Authorization: "Bearer token" }), }), ); }); 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); await expect( fetchGoogleMeetAttendance({ accessToken: "token", conferenceRecord: "rec-1", }), ).resolves.toMatchObject({ attendance: [ { displayName: "Alice", participants: [ "conferenceRecords/rec-1/participants/p1", "conferenceRecords/rec-1/participants/p2", ], firstJoinTime: "2026-04-25T10:10:00.000Z", lastLeaveTime: "2026-04-25T10:50:00.000Z", durationMs: 1_800_000, late: true, earlyLeave: true, sessions: [ { name: expect.stringContaining("/p1/") }, { name: expect.stringContaining("/p2/") }, ], }, ], }); }); it("surfaces Developer Preview acknowledgment blockers in preflight reports", () => { expect( buildGoogleMeetPreflightReport({ input: "abc-defg-hij", space: { name: "spaces/abc-defg-hij" }, previewAcknowledged: false, tokenSource: "cached-access-token", }), ).toMatchObject({ resolvedSpaceName: "spaces/abc-defg-hij", previewAcknowledged: false, blockers: [expect.stringContaining("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#"); }); 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", }); expect(result.details.session).toMatchObject({ transport: "twilio", mode: "realtime", twilio: { dialInNumber: "+15551234567", pinProvided: true, dtmfSequence: "123456#", voiceCallId: "call-1", dtmfSent: true, }, }); expect(voiceCallMocks.joinMeetViaVoiceCallGateway).toHaveBeenCalledWith({ config: expect.objectContaining({ defaultTransport: "twilio" }), dialInNumber: "+15551234567", dtmfSequence: "123456#", }); }); 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 }); expect(voiceCallMocks.endMeetVoiceCallGatewayCall).toHaveBeenCalledWith({ config: expect.objectContaining({ defaultTransport: "twilio" }), callId: "call-1", }); }); 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("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).toEqual([expect.objectContaining({ displayName: "Alice" })]); }); 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).toEqual( expect.arrayContaining([path.join(tempDir, "manifest.json")]), ); expect(result.details.zipFile).toBe(`${tempDir}.zip`); const manifest = JSON.parse(readFileSync(path.join(tempDir, "manifest.json"), "utf8")); expect(manifest).toMatchObject({ request: { conferenceRecord: "rec-1", includeDocumentBodies: true, }, counts: { attendanceRows: 1, warnings: 0, }, files: expect.arrayContaining(["summary.md", "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).toMatchObject({ dryRun: true, manifest: { files: expect.arrayContaining(["summary.md", "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).toMatchObject({ name: "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).toMatchObject({ meetingUri: "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).toEqual([ expect.objectContaining({ selected: true, meetingUri: "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); expect(result.details.checks).toEqual( expect.arrayContaining([ expect.objectContaining({ id: "chrome-node-connected", ok: false, message: expect.stringContaining("parallels-macos"), }), ]), ); const check = result.details.checks?.find( (item) => (item as { id?: unknown }).id === "chrome-node-connected", ) as { message?: string } | undefined; 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); expect(result.details.checks).toEqual( expect.arrayContaining([ expect.objectContaining({ id: "chrome-local-audio-device", ok: false, message: expect.stringContaining("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); expect(result.details.checks).toEqual( expect.arrayContaining([ expect.objectContaining({ id: "chrome-local-audio-commands", ok: false, message: "Chrome audio command missing: sox", }), ]), ); } 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" }, }, }, }, }, }, ); 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(result.details.checks).toEqual( expect.arrayContaining([ expect.objectContaining({ id: "twilio-voice-call-plugin", ok: true, }), expect.objectContaining({ id: "twilio-voice-call-credentials", ok: true, }), ]), ); }); it("reports missing voice-call wiring for Twilio transport", async () => { vi.stubEnv("TWILIO_ACCOUNT_SID", ""); vi.stubEnv("TWILIO_AUTH_TOKEN", ""); vi.stubEnv("TWILIO_FROM_NUMBER", ""); const { tools } = setup( { defaultTransport: "twilio" }, { 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" }); expect(result.details.ok).toBe(false); expect(result.details.checks).toEqual( expect.arrayContaining([ expect.objectContaining({ id: "twilio-voice-call-plugin", ok: false, }), expect.objectContaining({ id: "twilio-voice-call-credentials", ok: false, }), ]), ); }); it("opens local Chrome Meet through browser control after the BlackHole check", async () => { const originalPlatform = process.platform; Object.defineProperty(process, "platform", { value: "darwin" }); try { const { methods, runCommandWithTimeout } = setup({ defaultMode: "transcribe", }); 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, }); expect(respond.mock.calls[0]?.[0]).toBe(true); expect(runCommandWithTimeout).toHaveBeenNthCalledWith( 1, ["/usr/sbin/system_profiler", "SPAudioDataType"], { timeoutMs: 10000 }, ); expect(runCommandWithTimeout).toHaveBeenCalledTimes(1); expect(callGatewayFromCli).toHaveBeenCalledWith( "browser.request", expect.any(Object), expect.objectContaining({ method: "POST", path: "/tabs/open", body: { url: "https://meet.google.com/abc-defg-hij" }, }), { progress: false }, ); expect(callGatewayFromCli).toHaveBeenCalledWith( "browser.request", expect.any(Object), expect.objectContaining({ method: "POST", path: "/permissions/grant", body: expect.objectContaining({ origin: "https://meet.google.com", permissions: ["audioCapture", "videoCapture"], optionalPermissions: ["speakerSelection"], }), }), { progress: false }, ); } finally { 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, }); expect(respond.mock.calls[0]?.[0]).toBe(true); expect(nodesList.mock.calls[0]).toEqual([]); expect(nodesInvoke).toHaveBeenCalledWith( expect.objectContaining({ nodeId: "node-1", command: "googlemeet.chrome", params: expect.objectContaining({ action: "stopByUrl", url: "https://meet.google.com/abc-defg-hij", mode: "transcribe", }), }), ); expect(nodesInvoke).toHaveBeenCalledWith( expect.objectContaining({ nodeId: "node-1", command: "browser.proxy", params: expect.objectContaining({ path: "/tabs/open", body: { url: "https://meet.google.com/abc-defg-hij" }, }), }), ); expect(nodesInvoke).toHaveBeenCalledWith( expect.objectContaining({ nodeId: "node-1", command: "googlemeet.chrome", params: expect.objectContaining({ action: "start", url: "https://meet.google.com/abc-defg-hij", mode: "transcribe", launch: false, }), }), ); expect(respond.mock.calls[0]?.[1]).toMatchObject({ session: { transport: "chrome-node", chrome: { nodeId: "node-1", launched: 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); expect(second.mock.calls[0]?.[1]).toMatchObject({ session: { chrome: { health: { inCall: true, micMuted: false } }, notes: expect.arrayContaining(["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); expect(second.mock.calls[0]?.[1]).toMatchObject({ session: { notes: expect.arrayContaining(["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, }); expect(nodesInvoke).toHaveBeenCalledWith( expect.objectContaining({ params: expect.objectContaining({ path: "/tabs/focus", body: { targetId: "existing-meet-tab" }, }), }), ); expect(nodesInvoke).not.toHaveBeenCalledWith( expect.objectContaining({ params: expect.objectContaining({ path: "/tabs/open" }), }), ); }); 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; browser?: unknown } }>; }; const result = await tool.execute("id", { action: "recover_current_tab", url: "https://meet.google.com/abc-defg-hij", }); expect(result.details).toMatchObject({ found: true, targetId: "existing-meet-tab", browser: { manualActionRequired: true, manualActionReason: "meet-admission-required", }, }); expect(nodesInvoke).toHaveBeenCalledWith( expect.objectContaining({ params: expect.objectContaining({ path: "/tabs/focus", body: { targetId: "existing-meet-tab" }, }), }), ); expect(nodesInvoke).not.toHaveBeenCalledWith( expect.objectContaining({ params: expect.objectContaining({ path: "/tabs/open" }), }), ); }); 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" }); const tool = tools[0] as { execute: ( id: string, params: unknown, ) => Promise<{ details: { transport?: string; found?: boolean; browser?: unknown } }>; }; const result = await tool.execute("id", { action: "recover_current_tab", url: "https://meet.google.com/abc-defg-hij", }); expect(result.details).toMatchObject({ transport: "chrome", found: true, targetId: "local-meet-tab", browser: { manualActionRequired: true, manualActionReason: "meet-admission-required", }, }); expect(callGatewayFromCli).toHaveBeenCalledWith( "browser.request", expect.any(Object), expect.objectContaining({ method: "POST", path: "/tabs/focus" }), { 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.", }); expect(nodesInvoke).toHaveBeenCalledWith( expect.objectContaining({ command: "googlemeet.chrome", params: expect.objectContaining({ action: "start" }), }), ); expect(result.details).toMatchObject({ createdSession: true }); }); 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: "realtime", 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, provider: "openai", toolPolicy: "safe-read-only" }, chrome: { audioBackend: "blackhole-2ch", launched: true }, notes: [], }; 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).toHaveBeenCalledWith(expect.objectContaining({ message: "Say exactly: hello." })); expect(speak).not.toHaveBeenCalled(); expect(result.spoken).toBe(true); }); 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; 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).toMatchObject({ manualActionRequired: true, manualActionReason: "google-login-required", session: { chrome: { health: { manualActionRequired: true, manualActionReason: "google-login-required", }, }, }, }); }); 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({ chrome: { 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, }); expect(respond.mock.calls[0]?.[0]).toBe(true); expect(runCommandWithTimeout).toHaveBeenNthCalledWith(2, ["bridge", "status"], { timeoutMs: 30000, }); expect(runCommandWithTimeout).toHaveBeenNthCalledWith(3, ["bridge", "start"], { timeoutMs: 30000, }); expect(callGatewayFromCli).toHaveBeenCalledWith( "browser.request", expect.any(Object), expect.objectContaining({ method: "POST", path: "/tabs/open", body: { url: "https://meet.google.com/abc-defg-hij" }, }), { progress: false }, ); } finally { Object.defineProperty(process, "platform", { value: originalPlatform }); } }); it("pipes Chrome command-pair audio through the realtime provider", async () => { let callbacks: | { onAudio: (audio: Buffer) => void; onClearAudio: () => void; onMark?: (markName: string) => void; onToolCall?: (event: { itemId: string; callId: string; name: string; args: unknown; }) => void; onReady?: () => void; tools?: unknown[]; } | 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; }, }; 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 sessionStore: Record = {}; const runtime = { agent: { resolveAgentDir: vi.fn(() => "/tmp/agent"), resolveAgentWorkspaceDir: vi.fn(() => "/tmp/workspace"), ensureAgentWorkspace: vi.fn(async () => {}), session: { resolveStorePath: vi.fn(() => "/tmp/sessions.json"), loadSessionStore: vi.fn(() => sessionStore), saveSessionStore: vi.fn(async () => {}), resolveSessionFilePath: vi.fn(() => "/tmp/session.json"), }, runEmbeddedPiAgent: vi.fn(async () => ({ payloads: [{ text: "Use the Portugal launch data." }], meta: {}, })), resolveAgentTimeoutMs: vi.fn(() => 1000), }, }; const handle = await startCommandRealtimeAudioBridge({ config: resolveGoogleMeetConfig({ realtime: { provider: "openai", model: "gpt-realtime", agentId: "jay" }, }), fullConfig: {} as never, runtime: runtime as never, meetingSessionId: "meet-1", inputCommand: ["capture-meet"], outputCommand: ["play-meet"], logger: noopLogger, providers: [provider], spawn: spawnMock, }); 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?.onToolCall?.({ itemId: "item-1", callId: "tool-call-1", name: "openclaw_agent_consult", args: { question: "What should I say about launch timing?" }, }); expect(bridge.submitToolResult).toHaveBeenNthCalledWith( 1, "tool-call-1", expect.objectContaining({ status: "working", tool: "openclaw_agent_consult", }), { willContinue: true }, ); 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("SIGTERM"); expect(replacementOutputStdinWrites).toEqual([Buffer.from([6, 7])]); outputProcess.emit("error", new Error("stale output process failed 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."); expect(handle.getHealth()).toMatchObject({ providerConnected: true, realtimeReady: true, audioInputActive: true, audioOutputActive: true, lastInputBytes: 3, lastOutputBytes: 4, clearCount: 1, }); expect(callbacks).toMatchObject({ audioFormat: { encoding: "pcm16", sampleRateHz: 24000, channels: 1, }, tools: [ expect.objectContaining({ name: "openclaw_agent_consult", }), ], }); await vi.waitFor(() => { expect(bridge.submitToolResult).toHaveBeenLastCalledWith( "tool-call-1", { text: "Use the Portugal launch data.", }, undefined, ); }); expect(runtime.agent.runEmbeddedPiAgent).toHaveBeenCalledWith( expect.objectContaining({ messageProvider: "google-meet", agentId: "jay", sessionKey: "agent:jay:google-meet:meet-1", sandboxSessionKey: "agent:jay:google-meet:meet-1", thinkLevel: "high", toolsAllow: ["read", "web_search", "web_fetch", "x_search", "memory_search", "memory_get"], }), ); expect(sessionStore).toHaveProperty("agent:jay:google-meet:meet-1"); await handle.stop(); expect(bridge.close).toHaveBeenCalled(); expect(inputProcess.kill).toHaveBeenCalledWith("SIGTERM"); expect(outputProcess.kill).toHaveBeenCalledWith("SIGTERM"); }); it("pipes paired-node command-pair audio through the realtime provider", async () => { let callbacks: | { onAudio: (audio: Buffer) => void; onClearAudio: () => void; onToolCall?: (event: { itemId: string; callId: string; name: string; args: unknown; }) => void; onReady?: () => void; tools?: unknown[]; } | 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; 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") }; } await new Promise((resolve) => setTimeout(resolve, 1_000)); return { bridgeId: "bridge-1" }; } return { ok: true }; }), }, agent: { resolveAgentDir: vi.fn(() => "/tmp/agent"), resolveAgentWorkspaceDir: vi.fn(() => "/tmp/workspace"), ensureAgentWorkspace: vi.fn(async () => {}), session: { resolveStorePath: vi.fn(() => "/tmp/sessions.json"), loadSessionStore: vi.fn(() => ({})), saveSessionStore: vi.fn(async () => {}), resolveSessionFilePath: vi.fn(() => "/tmp/session.json"), }, runEmbeddedPiAgent: vi.fn(async () => ({ payloads: [{ text: "Use the launch update." }], meta: {}, })), resolveAgentTimeoutMs: vi.fn(() => 1000), }, }; 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], }); callbacks?.onAudio(Buffer.from([1, 2, 3])); callbacks?.onClearAudio(); callbacks?.onReady?.(); callbacks?.onToolCall?.({ itemId: "item-1", callId: "tool-call-1", name: "openclaw_agent_consult", args: { question: "What should I say?" }, }); expect(bridge.submitToolResult).toHaveBeenNthCalledWith( 1, "tool-call-1", expect.objectContaining({ status: "working", tool: "openclaw_agent_consult", }), { willContinue: true }, ); await vi.waitFor(() => { expect(sendAudio).toHaveBeenCalledWith(Buffer.from([9, 8, 7])); }); await vi.waitFor(() => { expect(runtime.nodes.invoke).toHaveBeenCalledWith( expect.objectContaining({ nodeId: "node-1", command: "googlemeet.chrome", params: expect.objectContaining({ action: "pushAudio", bridgeId: "bridge-1", base64: Buffer.from([1, 2, 3]).toString("base64"), }), }), ); }); await vi.waitFor(() => { expect(runtime.nodes.invoke).toHaveBeenCalledWith( expect.objectContaining({ nodeId: "node-1", command: "googlemeet.chrome", params: { action: "clearAudio", bridgeId: "bridge-1", }, timeoutMs: 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."); expect(callbacks).toMatchObject({ audioFormat: { encoding: "pcm16", sampleRateHz: 24000, channels: 1, }, tools: [ expect.objectContaining({ name: "openclaw_agent_consult", }), ], }); expect(handle).toMatchObject({ type: "node-command-pair", providerId: "openai", nodeId: "node-1", bridgeId: "bridge-1", }); expect(handle.getHealth()).toMatchObject({ providerConnected: true, realtimeReady: true, audioInputActive: true, audioOutputActive: true, lastInputBytes: 3, lastOutputBytes: 3, clearCount: 1, }); await handle.stop(); expect(bridge.close).toHaveBeenCalled(); expect(runtime.nodes.invoke).toHaveBeenCalledWith( expect.objectContaining({ nodeId: "node-1", command: "googlemeet.chrome", params: { action: "stop", bridgeId: "bridge-1" }, timeoutMs: 5_000, }), ); }); it("keeps paired-node realtime audio alive after transient input pull failures", async () => { 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; 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") }; } await new Promise((resolve) => setTimeout(resolve, 1_000)); return { bridgeId: "bridge-1" }; } return { ok: true }; }), }, }; 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.waitFor(() => { expect(sendAudio).toHaveBeenCalledWith(Buffer.from([5, 4, 3])); }); expect(bridge.close).not.toHaveBeenCalled(); expect(handle.getHealth()).toMatchObject({ audioInputActive: true, lastInputBytes: 3, consecutiveInputErrors: 0, }); await handle.stop(); }); it("stops paired-node realtime audio after repeated input pull failures", async () => { 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 }; }), }, }; 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.waitFor( () => { expect(bridge.close).toHaveBeenCalled(); }, { timeout: 3_000 }, ); expect(handle.getHealth()).toMatchObject({ bridgeClosed: true, consecutiveInputErrors: 5, lastInputError: "node invoke timeout", }); expect(runtime.nodes.invoke).toHaveBeenCalledWith( expect.objectContaining({ nodeId: "node-1", command: "googlemeet.chrome", params: { action: "stop", bridgeId: "bridge-1" }, timeoutMs: 5_000, }), ); }); 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"); }); });