fix(google-meet): avoid duplicate test speech

This commit is contained in:
Peter Steinberger
2026-04-27 14:29:08 +01:00
parent 73ba282b54
commit 6e8aaef1cc
3 changed files with 52 additions and 15 deletions

View File

@@ -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(
{

View File

@@ -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,
};
}

View File

@@ -83,4 +83,5 @@ export type GoogleMeetSession = {
export type GoogleMeetJoinResult = {
session: GoogleMeetSession;
spoken?: boolean;
};