

























@@ -1865,6 +1865,206 @@ describe("google-meet plugin", () => {
18651865}
18661866});
186718671868+it("grants local Chrome Meet media permissions against the opened tab", async () => {
1869+const callGatewayFromCli = mockLocalMeetBrowserRequest({
1870+inCall: true,
1871+micMuted: false,
1872+title: "Meet call",
1873+url: "https://meet.google.com/abc-defg-hij",
1874+});
1875+const { methods } = setup({
1876+defaultMode: "realtime",
1877+defaultTransport: "chrome",
1878+chrome: {
1879+audioBridgeCommand: ["bridge", "start"],
1880+},
1881+realtime: { introMessage: "" },
1882+});
1883+const handler = methods.get("googlemeet.join") as
1884+| ((ctx: {
1885+params: Record<string, unknown>;
1886+respond: ReturnType<typeof vi.fn>;
1887+}) => Promise<void>)
1888+| undefined;
1889+const respond = vi.fn();
1890+1891+await handler?.({
1892+params: { url: "https://meet.google.com/abc-defg-hij" },
1893+ respond,
1894+});
1895+1896+expect(respond.mock.calls[0]?.[0]).toBe(true);
1897+expect(callGatewayFromCli).toHaveBeenCalledWith(
1898+"browser.request",
1899+expect.any(Object),
1900+expect.objectContaining({
1901+method: "POST",
1902+path: "/permissions/grant",
1903+body: expect.objectContaining({
1904+origin: "https://meet.google.com",
1905+permissions: ["audioCapture", "videoCapture"],
1906+targetId: "local-meet-tab",
1907+}),
1908+}),
1909+{ progress: false },
1910+);
1911+});
1912+1913+it("starts the local realtime audio bridge after Meet is inspected", async () => {
1914+const events: string[] = [];
1915+const callGatewayFromCli = vi.fn(
1916+async (
1917+_method: string,
1918+_opts: unknown,
1919+params?: unknown,
1920+_extra?: unknown,
1921+): Promise<Record<string, unknown>> => {
1922+const request = params as {
1923+path?: string;
1924+body?: { fn?: string; targetId?: string; url?: string };
1925+};
1926+events.push(`browser:${request.path}`);
1927+if (request.path === "/tabs") {
1928+return { tabs: [] };
1929+}
1930+if (request.path === "/tabs/open") {
1931+return {
1932+targetId: "local-meet-tab",
1933+title: "Meet",
1934+url: request.body?.url ?? "https://meet.google.com/abc-defg-hij",
1935+};
1936+}
1937+if (request.path === "/tabs/focus" || request.path === "/permissions/grant") {
1938+return { ok: true };
1939+}
1940+if (request.path === "/act") {
1941+return {
1942+result: JSON.stringify({
1943+inCall: true,
1944+micMuted: false,
1945+title: "Meet call",
1946+url: "https://meet.google.com/abc-defg-hij",
1947+}),
1948+};
1949+}
1950+throw new Error(`unexpected browser request path ${request.path}`);
1951+},
1952+);
1953+chromeTransportTesting.setDepsForTest({ callGatewayFromCli });
1954+const { methods } = setup(
1955+{
1956+defaultMode: "realtime",
1957+defaultTransport: "chrome",
1958+chrome: {
1959+audioBridgeCommand: ["bridge", "start"],
1960+},
1961+realtime: { introMessage: "" },
1962+},
1963+{
1964+runCommandWithTimeoutHandler: async (argv) => {
1965+events.push(`command:${argv.join(" ")}`);
1966+return argv[0] === "/usr/sbin/system_profiler"
1967+ ? { code: 0, stdout: "BlackHole 2ch", stderr: "" }
1968+ : { code: 0, stdout: "", stderr: "" };
1969+},
1970+},
1971+);
1972+const handler = methods.get("googlemeet.join") as
1973+| ((ctx: {
1974+params: Record<string, unknown>;
1975+respond: ReturnType<typeof vi.fn>;
1976+}) => Promise<void>)
1977+| undefined;
1978+const respond = vi.fn();
1979+1980+await handler?.({
1981+params: { url: "https://meet.google.com/abc-defg-hij" },
1982+ respond,
1983+});
1984+1985+expect(respond.mock.calls[0]?.[0]).toBe(true);
1986+expect(events.indexOf("browser:/act")).toBeGreaterThan(-1);
1987+expect(events.indexOf("command:bridge start")).toBeGreaterThan(events.indexOf("browser:/act"));
1988+});
1989+1990+it("does not start the local realtime audio bridge while Meet admission is pending", async () => {
1991+const events: string[] = [];
1992+const callGatewayFromCli = vi.fn(
1993+async (
1994+_method: string,
1995+_opts: unknown,
1996+params?: unknown,
1997+_extra?: unknown,
1998+): Promise<Record<string, unknown>> => {
1999+const request = params as { path?: string; body?: { targetId?: string; url?: string } };
2000+events.push(`browser:${request.path}`);
2001+if (request.path === "/tabs") {
2002+return { tabs: [] };
2003+}
2004+if (request.path === "/tabs/open") {
2005+return {
2006+targetId: "local-meet-tab",
2007+title: "Meet",
2008+url: request.body?.url ?? "https://meet.google.com/abc-defg-hij",
2009+};
2010+}
2011+if (request.path === "/tabs/focus" || request.path === "/permissions/grant") {
2012+return { ok: true };
2013+}
2014+if (request.path === "/act") {
2015+return {
2016+result: JSON.stringify({
2017+inCall: false,
2018+lobbyWaiting: true,
2019+manualActionRequired: true,
2020+manualActionReason: "meet-admission-required",
2021+manualActionMessage: "Admit the OpenClaw browser participant in Google Meet.",
2022+title: "Meet",
2023+url: "https://meet.google.com/abc-defg-hij",
2024+}),
2025+};
2026+}
2027+throw new Error(`unexpected browser request path ${request.path}`);
2028+},
2029+);
2030+chromeTransportTesting.setDepsForTest({ callGatewayFromCli });
2031+const { methods } = setup(
2032+{
2033+defaultMode: "realtime",
2034+defaultTransport: "chrome",
2035+chrome: {
2036+audioBridgeCommand: ["bridge", "start"],
2037+waitForInCallMs: 1,
2038+},
2039+realtime: { introMessage: "" },
2040+},
2041+{
2042+runCommandWithTimeoutHandler: async (argv) => {
2043+events.push(`command:${argv.join(" ")}`);
2044+return argv[0] === "/usr/sbin/system_profiler"
2045+ ? { code: 0, stdout: "BlackHole 2ch", stderr: "" }
2046+ : { code: 0, stdout: "", stderr: "" };
2047+},
2048+},
2049+);
2050+const handler = methods.get("googlemeet.join") as
2051+| ((ctx: {
2052+params: Record<string, unknown>;
2053+respond: ReturnType<typeof vi.fn>;
2054+}) => Promise<void>)
2055+| undefined;
2056+const respond = vi.fn();
2057+2058+await handler?.({
2059+params: { url: "https://meet.google.com/abc-defg-hij" },
2060+ respond,
2061+});
2062+2063+expect(respond.mock.calls[0]?.[0]).toBe(true);
2064+expect(events).toContain("browser:/act");
2065+expect(events).not.toContain("command:bridge start");
2066+});
2067+18682068it("refreshes observe-only caption health when status is requested", async () => {
18692069let openedTab = false;
18702070let actCount = 0;
@@ -2790,7 +2990,8 @@ describe("google-meet plugin", () => {
27902990chrome: {
27912991health: {
27922992inCall: true,
2793-speechReady: true,
2993+speechReady: false,
2994+speechBlockedReason: "audio-bridge-unavailable",
27942995},
27952996},
27962997},
@@ -3239,21 +3440,7 @@ describe("google-meet plugin", () => {
32393440});
3240344132413442it("pipes Chrome command-pair audio through the realtime provider", async () => {
3242-let callbacks:
3243-| {
3244-onAudio: (audio: Buffer) => void;
3245-onClearAudio: () => void;
3246-onMark?: (markName: string) => void;
3247-onToolCall?: (event: {
3248-itemId: string;
3249-callId: string;
3250-name: string;
3251-args: unknown;
3252-}) => void;
3253-onReady?: () => void;
3254-tools?: unknown[];
3255-}
3256-| undefined;
3443+let callbacks: Parameters<RealtimeVoiceProviderPlugin["createBridge"]>[0] | undefined;
32573444const sendAudio = vi.fn();
32583445const bridge = {
32593446supportsToolResultContinuation: true,
@@ -3357,6 +3544,14 @@ describe("google-meet plugin", () => {
33573544callbacks?.onClearAudio();
33583545callbacks?.onAudio(Buffer.from([6, 7]));
33593546callbacks?.onReady?.();
3547+callbacks?.onTranscript?.("assistant", "How can I help you?", true);
3548+callbacks?.onTranscript?.("user", "Please summarize the launch.", true);
3549+callbacks?.onEvent?.({ direction: "client", type: "response.create" });
3550+callbacks?.onEvent?.({
3551+direction: "server",
3552+type: "response.done",
3553+detail: "status=completed",
3554+});
33603555callbacks?.onToolCall?.({
33613556itemId: "item-1",
33623557callId: "tool-call-1",
@@ -3396,6 +3591,23 @@ describe("google-meet plugin", () => {
33963591audioOutputActive: true,
33973592lastInputBytes: 3,
33983593lastOutputBytes: 4,
3594+realtimeTranscriptLines: 2,
3595+lastRealtimeTranscriptRole: "user",
3596+lastRealtimeTranscriptText: "Please summarize the launch.",
3597+lastRealtimeEventType: "server:response.done",
3598+lastRealtimeEventDetail: "status=completed",
3599+recentRealtimeTranscript: [
3600+expect.objectContaining({ role: "assistant", text: "How can I help you?" }),
3601+expect.objectContaining({ role: "user", text: "Please summarize the launch." }),
3602+],
3603+recentRealtimeEvents: [
3604+expect.objectContaining({ direction: "client", type: "response.create" }),
3605+expect.objectContaining({
3606+direction: "server",
3607+type: "response.done",
3608+detail: "status=completed",
3609+}),
3610+],
33993611clearCount: 1,
34003612});
34013613expect(callbacks).toMatchObject({
@@ -3545,20 +3757,7 @@ describe("google-meet plugin", () => {
35453757});
3546375835473759it("pipes paired-node command-pair audio through the realtime provider", async () => {
3548-let callbacks:
3549-| {
3550-onAudio: (audio: Buffer) => void;
3551-onClearAudio: () => void;
3552-onToolCall?: (event: {
3553-itemId: string;
3554-callId: string;
3555-name: string;
3556-args: unknown;
3557-}) => void;
3558-onReady?: () => void;
3559-tools?: unknown[];
3560-}
3561-| undefined;
3760+let callbacks: Parameters<RealtimeVoiceProviderPlugin["createBridge"]>[0] | undefined;
35623761const sendAudio = vi.fn();
35633762const bridge = {
35643763supportsToolResultContinuation: true,
@@ -3633,6 +3832,12 @@ describe("google-meet plugin", () => {
36333832callbacks?.onAudio(Buffer.from([1, 2, 3]));
36343833callbacks?.onClearAudio();
36353834callbacks?.onReady?.();
3835+callbacks?.onTranscript?.("assistant", "How can I help from the node?", true);
3836+callbacks?.onEvent?.({
3837+direction: "server",
3838+type: "response.done",
3839+detail: "status=completed",
3840+});
36363841callbacks?.onToolCall?.({
36373842itemId: "item-1",
36383843callId: "tool-call-1",
@@ -3715,6 +3920,11 @@ describe("google-meet plugin", () => {
37153920audioOutputActive: true,
37163921lastInputBytes: 3,
37173922lastOutputBytes: 3,
3923+realtimeTranscriptLines: 1,
3924+lastRealtimeTranscriptRole: "assistant",
3925+lastRealtimeTranscriptText: "How can I help from the node?",
3926+lastRealtimeEventType: "server:response.done",
3927+lastRealtimeEventDetail: "status=completed",
37183928clearCount: 1,
37193929});
37203930此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。