




















@@ -3,6 +3,7 @@ import { existsSync, mkdtempSync, readFileSync, rmSync } from "node:fs";
33import { tmpdir } from "node:os";
44import path from "node:path";
55import { PassThrough, Writable } from "node:stream";
6+import { createContext, Script } from "node:vm";
67import type { RealtimeVoiceProviderPlugin } from "openclaw/plugin-sdk/realtime-voice";
78import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
89import plugin, { __testing as googleMeetPluginTesting } from "./index.js";
@@ -1635,7 +1636,25 @@ describe("google-meet plugin", () => {
16351636const { methods, runCommandWithTimeout } = setup({
16361637defaultMode: "transcribe",
16371638});
1638-const callGatewayFromCli = mockLocalMeetBrowserRequest();
1639+const callGatewayFromCli = mockLocalMeetBrowserRequest({
1640+inCall: true,
1641+micMuted: true,
1642+captioning: true,
1643+captionsEnabledAttempted: true,
1644+transcriptLines: 1,
1645+lastCaptionAt: "2026-04-27T10:00:00.000Z",
1646+lastCaptionSpeaker: "Alice",
1647+lastCaptionText: "Can everyone hear the agent?",
1648+recentTranscript: [
1649+{
1650+at: "2026-04-27T10:00:00.000Z",
1651+speaker: "Alice",
1652+text: "Can everyone hear the agent?",
1653+},
1654+],
1655+title: "Meet call",
1656+url: "https://meet.google.com/abc-defg-hij",
1657+});
16391658const handler = methods.get("googlemeet.join") as
16401659| ((ctx: {
16411660params: Record<string, unknown>;
@@ -1666,17 +1685,292 @@ describe("google-meet plugin", () => {
16661685([, , request]) => (request as { path?: string }).path === "/permissions/grant",
16671686),
16681687).toBe(false);
1688+expect(respond.mock.calls[0]?.[1]).toMatchObject({
1689+session: {
1690+chrome: {
1691+health: {
1692+captioning: true,
1693+captionsEnabledAttempted: true,
1694+transcriptLines: 1,
1695+lastCaptionSpeaker: "Alice",
1696+lastCaptionText: "Can everyone hear the agent?",
1697+recentTranscript: [
1698+{
1699+speaker: "Alice",
1700+text: "Can everyone hear the agent?",
1701+},
1702+],
1703+},
1704+},
1705+},
1706+});
16691707const actCall = callGatewayFromCli.mock.calls.find(
16701708([, , request]) => (request as { path?: string }).path === "/act",
16711709);
16721710expect(String((actCall?.[2] as { body?: { fn?: string } } | undefined)?.body?.fn)).toContain(
16731711"const allowMicrophone = false",
16741712);
1713+expect(String((actCall?.[2] as { body?: { fn?: string } } | undefined)?.body?.fn)).toContain(
1714+"const captureCaptions = true",
1715+);
16751716} finally {
16761717Object.defineProperty(process, "platform", { value: originalPlatform });
16771718}
16781719});
167917201721+it("refreshes observe-only caption health when status is requested", async () => {
1722+let openedTab = false;
1723+let actCount = 0;
1724+const callGatewayFromCli = vi.fn(
1725+async (
1726+_method: string,
1727+_opts: unknown,
1728+params?: unknown,
1729+_extra?: unknown,
1730+): Promise<Record<string, unknown>> => {
1731+const request = params as {
1732+path?: string;
1733+body?: { targetId?: string; url?: string };
1734+};
1735+if (request.path === "/tabs") {
1736+return openedTab
1737+ ? {
1738+tabs: [
1739+{
1740+targetId: "local-meet-tab",
1741+title: "Meet",
1742+url: "https://meet.google.com/abc-defg-hij",
1743+},
1744+],
1745+}
1746+ : { tabs: [] };
1747+}
1748+if (request.path === "/tabs/open") {
1749+openedTab = true;
1750+return {
1751+targetId: "local-meet-tab",
1752+title: "Meet",
1753+url: request.body?.url ?? "https://meet.google.com/abc-defg-hij",
1754+};
1755+}
1756+if (request.path === "/tabs/focus") {
1757+return { ok: true };
1758+}
1759+if (request.path === "/act") {
1760+actCount += 1;
1761+return {
1762+result: JSON.stringify(
1763+actCount === 1
1764+ ? {
1765+inCall: true,
1766+captioning: false,
1767+captionsEnabledAttempted: true,
1768+transcriptLines: 0,
1769+title: "Meet call",
1770+url: "https://meet.google.com/abc-defg-hij",
1771+}
1772+ : {
1773+inCall: true,
1774+captioning: true,
1775+captionsEnabledAttempted: true,
1776+transcriptLines: 1,
1777+lastCaptionAt: "2026-04-27T10:00:00.000Z",
1778+lastCaptionSpeaker: "Alice",
1779+lastCaptionText: "Please capture this.",
1780+recentTranscript: [
1781+{
1782+at: "2026-04-27T10:00:00.000Z",
1783+speaker: "Alice",
1784+text: "Please capture this.",
1785+},
1786+],
1787+title: "Meet call",
1788+url: "https://meet.google.com/abc-defg-hij",
1789+},
1790+),
1791+};
1792+}
1793+throw new Error(`unexpected browser request path ${request.path}`);
1794+},
1795+);
1796+chromeTransportTesting.setDepsForTest({ callGatewayFromCli });
1797+const { methods } = setup({
1798+defaultMode: "transcribe",
1799+defaultTransport: "chrome",
1800+});
1801+1802+const join = (await invokeGoogleMeetGatewayMethodForTest(methods, "googlemeet.join", {
1803+url: "https://meet.google.com/abc-defg-hij",
1804+})) as { session: { id: string; chrome?: { health?: { transcriptLines?: number } } } };
1805+expect(join.session.chrome?.health?.transcriptLines).toBe(0);
1806+1807+const status = (await invokeGoogleMeetGatewayMethodForTest(methods, "googlemeet.status", {
1808+sessionId: join.session.id,
1809+})) as {
1810+session?: {
1811+chrome?: {
1812+health?: {
1813+captioning?: boolean;
1814+transcriptLines?: number;
1815+lastCaptionText?: string;
1816+};
1817+};
1818+};
1819+};
1820+1821+expect(status.session?.chrome?.health).toMatchObject({
1822+captioning: true,
1823+transcriptLines: 1,
1824+lastCaptionText: "Please capture this.",
1825+});
1826+expect(callGatewayFromCli).toHaveBeenCalledWith(
1827+"browser.request",
1828+expect.any(Object),
1829+expect.objectContaining({
1830+method: "POST",
1831+path: "/tabs/focus",
1832+body: { targetId: "local-meet-tab" },
1833+}),
1834+{ progress: false },
1835+);
1836+});
1837+1838+it("does not mutate realtime browser prompts when status is requested", async () => {
1839+let openedTab = false;
1840+const { methods, nodesInvoke } = setup(
1841+{
1842+defaultMode: "realtime",
1843+defaultTransport: "chrome-node",
1844+},
1845+{
1846+nodesInvokeHandler: async ({ command, params }) => {
1847+const raw = params as { path?: string; body?: { url?: string; targetId?: string } };
1848+if (command === "browser.proxy") {
1849+if (raw.path === "/tabs") {
1850+return { payload: { result: { running: true, tabs: [] } } };
1851+}
1852+if (raw.path === "/tabs/open") {
1853+openedTab = true;
1854+return {
1855+payload: {
1856+result: {
1857+targetId: "tab-1",
1858+title: "Meet",
1859+url: raw.body?.url ?? "https://meet.google.com/abc-defg-hij",
1860+},
1861+},
1862+};
1863+}
1864+if (raw.path === "/tabs/focus" || raw.path === "/permissions/grant") {
1865+return { payload: { result: { ok: true } } };
1866+}
1867+if (raw.path === "/act") {
1868+return {
1869+payload: {
1870+result: {
1871+ok: true,
1872+targetId: raw.body?.targetId ?? "tab-1",
1873+result: JSON.stringify({
1874+inCall: false,
1875+manualActionRequired: true,
1876+manualActionReason: "meet-audio-choice-required",
1877+manualActionMessage: "Choose the Meet microphone path manually.",
1878+title: "Meet",
1879+url: "https://meet.google.com/abc-defg-hij",
1880+}),
1881+},
1882+},
1883+};
1884+}
1885+}
1886+if (command === "googlemeet.chrome") {
1887+return { payload: { launched: openedTab } };
1888+}
1889+throw new Error(`unexpected invoke ${command}`);
1890+},
1891+},
1892+);
1893+1894+const join = (await invokeGoogleMeetGatewayMethodForTest(methods, "googlemeet.join", {
1895+url: "https://meet.google.com/abc-defg-hij",
1896+})) as { session: { id: string } };
1897+nodesInvoke.mockClear();
1898+1899+const status = (await invokeGoogleMeetGatewayMethodForTest(methods, "googlemeet.status", {
1900+sessionId: join.session.id,
1901+})) as { session?: { chrome?: { health?: { manualActionRequired?: boolean } } } };
1902+1903+expect(status.session?.chrome?.health?.manualActionRequired).toBe(true);
1904+expect(nodesInvoke).not.toHaveBeenCalledWith(
1905+expect.objectContaining({ command: "browser.proxy" }),
1906+);
1907+});
1908+1909+it("retries caption enable until the captions button is available", () => {
1910+const makeButton = (label: string) => ({
1911+disabled: false,
1912+innerText: "",
1913+textContent: "",
1914+click: vi.fn(),
1915+getAttribute: vi.fn((name: string) => (name === "aria-label" ? label : null)),
1916+});
1917+const leaveButton = makeButton("Leave call");
1918+const captionButton = makeButton("Turn on captions");
1919+const page = {
1920+buttons: [leaveButton],
1921+};
1922+const windowState: Record<string, unknown> = {};
1923+const document = {
1924+body: { innerText: "", textContent: "" },
1925+title: "Meet",
1926+querySelector: vi.fn(() => null),
1927+querySelectorAll: vi.fn((selector: string) => {
1928+if (selector === "button") {
1929+return page.buttons;
1930+}
1931+if (selector === "input") {
1932+return [];
1933+}
1934+return [];
1935+}),
1936+};
1937+const context = createContext({
1938+ Date,
1939+JSON,
1940+ String,
1941+ document,
1942+location: {
1943+href: "https://meet.google.com/abc-defg-hij",
1944+hostname: "meet.google.com",
1945+},
1946+MutationObserver: class {
1947+observe = vi.fn();
1948+},
1949+window: windowState,
1950+});
1951+const inspect = new Script(
1952+`(${chromeTransportTesting.meetStatusScriptForTest({
1953+ allowMicrophone: false,
1954+ autoJoin: false,
1955+ captureCaptions: true,
1956+ guestName: "OpenClaw Agent",
1957+ })})`,
1958+).runInContext(context) as () => string;
1959+1960+const first = JSON.parse(inspect()) as { captionsEnabledAttempted?: boolean };
1961+const stateAfterFirst = windowState.__openclawMeetCaptions as { enabledAttempted?: boolean };
1962+expect(first.captionsEnabledAttempted).toBe(false);
1963+expect(stateAfterFirst.enabledAttempted).toBe(false);
1964+expect(captionButton.click).not.toHaveBeenCalled();
1965+1966+page.buttons = [leaveButton, captionButton];
1967+const second = JSON.parse(inspect()) as { captionsEnabledAttempted?: boolean };
1968+const stateAfterSecond = windowState.__openclawMeetCaptions as { enabledAttempted?: boolean };
1969+expect(second.captionsEnabledAttempted).toBe(true);
1970+expect(stateAfterSecond.enabledAttempted).toBe(true);
1971+expect(captionButton.click).toHaveBeenCalledTimes(1);
1972+});
1973+16801974it("joins Chrome on a paired node without local Chrome or BlackHole", async () => {
16811975const { methods, nodesList, nodesInvoke } = setup(
16821976{
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。