From 6e8aaef1cc98e8bc7a4e0b4cf3b7209cca307971 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 27 Apr 2026 14:29:08 +0100 Subject: [PATCH] fix(google-meet): avoid duplicate test speech --- extensions/google-meet/index.test.ts | 36 ++++++++++++++++++- extensions/google-meet/src/runtime.ts | 30 ++++++++-------- .../google-meet/src/transports/types.ts | 1 + 3 files changed, 52 insertions(+), 15 deletions(-) diff --git a/extensions/google-meet/index.test.ts b/extensions/google-meet/index.test.ts index feb2c30893c..84786ef3d59 100644 --- a/extensions/google-meet/index.test.ts +++ b/extensions/google-meet/index.test.ts @@ -24,7 +24,7 @@ import { import { handleGoogleMeetNodeHostCommand } from "./src/node-host.js"; import { startNodeRealtimeAudioBridge } from "./src/realtime-node.js"; import { startCommandRealtimeAudioBridge } from "./src/realtime.js"; -import { normalizeMeetUrl } from "./src/runtime.js"; +import { GoogleMeetRuntime, normalizeMeetUrl } from "./src/runtime.js"; import { invokeGoogleMeetGatewayMethodForTest, noopLogger, @@ -32,6 +32,7 @@ import { } 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 })), @@ -1837,6 +1838,39 @@ describe("google-meet plugin", () => { 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( { diff --git a/extensions/google-meet/src/runtime.ts b/extensions/google-meet/src/runtime.ts index ce9491ff259..afa6ebd902a 100644 --- a/extensions/google-meet/src/runtime.ts +++ b/extensions/google-meet/src/runtime.ts @@ -212,16 +212,18 @@ export class GoogleMeetRuntime { session.transport === transport && session.mode === mode, ); + const speechInstructions = request.message ?? this.params.config.realtime.introMessage; if (reusable) { reusable.notes = [ ...reusable.notes.filter((note) => note !== "Reused existing active Meet session."), "Reused existing active Meet session.", ]; reusable.updatedAt = nowIso(); - if (request.message || this.params.config.realtime.introMessage) { - this.speak(reusable.id, request.message); - } - return { session: reusable }; + const spoken = + mode === "realtime" && speechInstructions + ? this.speak(reusable.id, speechInstructions).spoken + : false; + return { session: reusable, spoken }; } const createdAt = nowIso(); @@ -347,10 +349,11 @@ export class GoogleMeetRuntime { } this.#sessions.set(session.id, session); - if (mode === "realtime" && this.params.config.realtime.introMessage) { - this.speak(session.id, request.message); - } - return { session }; + const spoken = + mode === "realtime" && speechInstructions + ? this.speak(session.id, speechInstructions).spoken + : false; + return { session, spoken }; } async leave(sessionId: string): Promise<{ found: boolean; session?: GoogleMeetSession }> { @@ -398,11 +401,10 @@ export class GoogleMeetRuntime { session: GoogleMeetSession; }> { const before = new Set(this.list().map((session) => session.id)); - const result = await this.join(request); - const spoken = this.speak( - result.session.id, - request.message ?? "Say exactly: Google Meet speech test complete.", - ).spoken; + const result = await this.join({ + ...request, + message: request.message ?? "Say exactly: Google Meet speech test complete.", + }); const health = result.session.chrome?.health; return { createdSession: !before.has(result.session.id), @@ -410,7 +412,7 @@ export class GoogleMeetRuntime { manualActionRequired: health?.manualActionRequired, manualActionReason: health?.manualActionReason, manualActionMessage: health?.manualActionMessage, - spoken, + spoken: result.spoken ?? false, session: result.session, }; } diff --git a/extensions/google-meet/src/transports/types.ts b/extensions/google-meet/src/transports/types.ts index d61ab85494f..30f9738ce31 100644 --- a/extensions/google-meet/src/transports/types.ts +++ b/extensions/google-meet/src/transports/types.ts @@ -83,4 +83,5 @@ export type GoogleMeetSession = { export type GoogleMeetJoinResult = { session: GoogleMeetSession; + spoken?: boolean; };