

























@@ -99,6 +99,51 @@ async function runCreateMeetBrowserScript(params: { buttonText: string }) {
9999return { button, result: await fn() };
100100}
101101102+function requireRecord(value: unknown, label: string): Record<string, unknown> {
103+expect(value, label).toBeTypeOf("object");
104+expect(value, label).not.toBeNull();
105+return value as Record<string, unknown>;
106+}
107+108+function mockCalls(mock: unknown, label: string): Array<Array<unknown>> {
109+const mockState = (mock as { mock?: { calls?: Array<Array<unknown>> } }).mock;
110+expect(mockState, `${label}.mock`).toBeDefined();
111+expect(Array.isArray(mockState?.calls), `${label}.mock.calls`).toBe(true);
112+return mockState?.calls ?? [];
113+}
114+115+function findMockCall(mock: unknown, label: string, predicate: (call: Array<unknown>) => boolean) {
116+const call = mockCalls(mock, label).find(predicate);
117+expect(call, label).toBeDefined();
118+return call as Array<unknown>;
119+}
120+121+function responsePayload(respond: unknown): Record<string, unknown> {
122+const calls = mockCalls(respond, "respond");
123+expect(calls[0]?.[0]).toBe(true);
124+return requireRecord(calls[0]?.[1], "response payload");
125+}
126+127+function responseErrorPayload(respond: unknown): Record<string, unknown> {
128+const calls = mockCalls(respond, "respond");
129+expect(calls[0]?.[0]).toBe(false);
130+return requireRecord(calls[0]?.[1], "response payload");
131+}
132+133+function findNodeInvokeParams(
134+nodesInvoke: unknown,
135+label: string,
136+predicate: (params: Record<string, unknown>) => boolean,
137+) {
138+const call = findMockCall(nodesInvoke, label, ([value]) => {
139+if (!value || typeof value !== "object") {
140+return false;
141+}
142+return predicate(value as Record<string, unknown>);
143+});
144+return requireRecord(call[0], label);
145+}
146+102147describe("google-meet create flow", () => {
103148beforeEach(() => {
104149vi.clearAllMocks();
@@ -164,12 +209,15 @@ describe("google-meet create flow", () => {
164209);
165210expect(stdout.output()).toContain("meeting uri: https://meet.google.com/new-abcd-xyz");
166211expect(stdout.output()).toContain("space: spaces/new-space");
167-expect(fetchMock).toHaveBeenCalledWith(
168-"https://meet.googleapis.com/v2/spaces",
169-expect.objectContaining({
170-method: "POST",
171-body: JSON.stringify({ config: { accessType: "OPEN", entryPointAccess: "ALL" } }),
172-}),
212+const createSpaceCall = findMockCall(
213+fetchMock,
214+"create space fetch",
215+([url]) => url === "https://meet.googleapis.com/v2/spaces",
216+);
217+const createSpaceInit = requireRecord(createSpaceCall[1], "create space init");
218+expect(createSpaceInit.method).toBe("POST");
219+expect(createSpaceInit.body).toBe(
220+JSON.stringify({ config: { accessType: "OPEN", entryPointAccess: "ALL" } }),
173221);
174222} finally {
175223stdout.restore();
@@ -228,22 +276,27 @@ describe("google-meet create flow", () => {
228276229277await handler?.({ params: { join: false }, respond });
230278231-expect(respond.mock.calls[0]?.[0]).toBe(true);
232-expect(respond.mock.calls[0]?.[1]).toMatchObject({
233-source: "browser",
234-meetingUri: "https://meet.google.com/browser-made-url",
235-joined: false,
236-browser: { nodeId: "node-1", targetId: "tab-1" },
279+const payload = responsePayload(respond);
280+expect(payload.source).toBe("browser");
281+expect(payload.meetingUri).toBe("https://meet.google.com/browser-made-url");
282+expect(payload.joined).toBe(false);
283+const browser = requireRecord(payload.browser, "browser payload");
284+expect(browser.nodeId).toBe("node-1");
285+expect(browser.targetId).toBe("tab-1");
286+findNodeInvokeParams(nodesInvoke, "open create tab", (params) => {
287+if (params.command !== "browser.proxy") {
288+return false;
289+}
290+if (!params.params || typeof params.params !== "object") {
291+return false;
292+}
293+const proxy = params.params as Record<string, unknown>;
294+if (!proxy.body || typeof proxy.body !== "object") {
295+return false;
296+}
297+const body = proxy.body as Record<string, unknown>;
298+return proxy.path === "/tabs/open" && body.url === "https://meet.google.com/new";
237299});
238-expect(nodesInvoke).toHaveBeenCalledWith(
239-expect.objectContaining({
240-command: "browser.proxy",
241-params: expect.objectContaining({
242-path: "/tabs/open",
243-body: { url: "https://meet.google.com/new" },
244-}),
245-}),
246-);
247300});
248301249302it("rejects access policy flags when tool create would use browser fallback", async () => {
@@ -322,23 +375,22 @@ describe("google-meet create flow", () => {
322375323376await handler?.({ params: {}, respond });
324377325-expect(respond.mock.calls[0]?.[0]).toBe(false);
326-expect(respond.mock.calls[0]?.[1]).toMatchObject({
327-source: "browser",
328-error:
329-"google-login-required: Sign in to Google in the OpenClaw browser profile, then retry meeting creation.",
330-manualActionRequired: true,
331-manualActionReason: "google-login-required",
332-manualActionMessage:
333-"Sign in to Google in the OpenClaw browser profile, then retry meeting creation.",
334-browser: {
335-nodeId: "node-1",
336-targetId: "login-tab",
337-browserUrl: "https://accounts.google.com/signin",
338-browserTitle: "Sign in - Google Accounts",
339-notes: ["Sign-in page detected."],
340-},
341-});
378+const payload = responseErrorPayload(respond);
379+expect(payload.source).toBe("browser");
380+expect(payload.error).toBe(
381+"google-login-required: Sign in to Google in the OpenClaw browser profile, then retry meeting creation.",
382+);
383+expect(payload.manualActionRequired).toBe(true);
384+expect(payload.manualActionReason).toBe("google-login-required");
385+expect(payload.manualActionMessage).toBe(
386+"Sign in to Google in the OpenClaw browser profile, then retry meeting creation.",
387+);
388+const browser = requireRecord(payload.browser, "browser payload");
389+expect(browser.nodeId).toBe("node-1");
390+expect(browser.targetId).toBe("login-tab");
391+expect(browser.browserUrl).toBe("https://accounts.google.com/signin");
392+expect(browser.browserTitle).toBe("Sign in - Google Accounts");
393+expect(browser.notes).toEqual(["Sign-in page detected."]);
342394});
343395344396it("creates and joins a Meet through the create tool action by default", async () => {
@@ -412,28 +464,32 @@ describe("google-meet create flow", () => {
412464id: string,
413465params: unknown,
414466) => Promise<{
415-details: { joined?: boolean; meetingUri?: string; join?: { session: { url: string } } };
467+details: {
468+source?: string;
469+joined?: boolean;
470+meetingUri?: string;
471+join?: { session: { url: string } };
472+};
416473}>;
417474};
418475419476const result = await tool.execute("id", { action: "create" });
420477421-expect(result.details).toMatchObject({
422-source: "browser",
423-joined: true,
424-meetingUri: "https://meet.google.com/new-abcd-xyz",
425-join: { session: { url: "https://meet.google.com/new-abcd-xyz" } },
478+expect(result.details.source).toBe("browser");
479+expect(result.details.joined).toBe(true);
480+expect(result.details.meetingUri).toBe("https://meet.google.com/new-abcd-xyz");
481+expect(result.details.join?.session.url).toBe("https://meet.google.com/new-abcd-xyz");
482+findNodeInvokeParams(nodesInvoke, "googlemeet chrome start", (params) => {
483+if (params.command !== "googlemeet.chrome") {
484+return false;
485+}
486+const chromeParams = requireRecord(params.params, "chrome params");
487+return (
488+chromeParams.action === "start" &&
489+chromeParams.url === "https://meet.google.com/new-abcd-xyz" &&
490+chromeParams.launch === false
491+);
426492});
427-expect(nodesInvoke).toHaveBeenCalledWith(
428-expect.objectContaining({
429-command: "googlemeet.chrome",
430-params: expect.objectContaining({
431-action: "start",
432-url: "https://meet.google.com/new-abcd-xyz",
433-launch: false,
434-}),
435-}),
436-);
437493});
438494439495it("returns structured manual action from the create tool action", async () => {
@@ -486,19 +542,17 @@ describe("google-meet create flow", () => {
486542487543const result = await tool.execute("id", { action: "create" });
488544489-expect(result.details).toMatchObject({
490-source: "browser",
491-manualActionRequired: true,
492-manualActionReason: "meet-permission-required",
493-manualActionMessage:
494-"Allow microphone/camera permissions for Meet in the OpenClaw browser profile, then retry meeting creation.",
495-browser: {
496-nodeId: "node-1",
497-targetId: "permission-tab",
498-browserUrl: "https://meet.google.com/new",
499-browserTitle: "Meet",
500-},
501-});
545+expect(result.details.source).toBe("browser");
546+expect(result.details.manualActionRequired).toBe(true);
547+expect(result.details.manualActionReason).toBe("meet-permission-required");
548+expect(result.details.manualActionMessage).toBe(
549+"Allow microphone/camera permissions for Meet in the OpenClaw browser profile, then retry meeting creation.",
550+);
551+const browser = requireRecord(result.details.browser, "browser details");
552+expect(browser.nodeId).toBe("node-1");
553+expect(browser.targetId).toBe("permission-tab");
554+expect(browser.browserUrl).toBe("https://meet.google.com/new");
555+expect(browser.browserTitle).toBe("Meet");
502556});
503557504558it("reuses an existing browser create tab instead of opening duplicates", async () => {
@@ -557,25 +611,35 @@ describe("google-meet create flow", () => {
557611558612await handler?.({ params: { join: false }, respond });
559613560-expect(respond.mock.calls[0]?.[0]).toBe(true);
561-expect(respond.mock.calls[0]?.[1]).toMatchObject({
562-source: "browser",
563-meetingUri: "https://meet.google.com/reu-sedx-tab",
564-browser: { nodeId: "node-1", targetId: "existing-create-tab" },
614+const payload = responsePayload(respond);
615+expect(payload.source).toBe("browser");
616+expect(payload.meetingUri).toBe("https://meet.google.com/reu-sedx-tab");
617+const browser = requireRecord(payload.browser, "browser payload");
618+expect(browser.nodeId).toBe("node-1");
619+expect(browser.targetId).toBe("existing-create-tab");
620+findNodeInvokeParams(nodesInvoke, "focus existing tab", (params) => {
621+if (!params.params || typeof params.params !== "object") {
622+return false;
623+}
624+const proxy = params.params as Record<string, unknown>;
625+if (!proxy.body || typeof proxy.body !== "object") {
626+return false;
627+}
628+const body = proxy.body as Record<string, unknown>;
629+return proxy.path === "/tabs/focus" && body.targetId === "existing-create-tab";
565630});
566-expect(nodesInvoke).toHaveBeenCalledWith(
567-expect.objectContaining({
568-params: expect.objectContaining({
569-path: "/tabs/focus",
570-body: { targetId: "existing-create-tab" },
571-}),
572-}),
573-);
574-expect(nodesInvoke).not.toHaveBeenCalledWith(
575-expect.objectContaining({
576-params: expect.objectContaining({ path: "/tabs/open" }),
577-}),
578-);
631+const openedCreateTab = mockCalls(nodesInvoke, "nodes invoke").some(([value]) => {
632+if (!value || typeof value !== "object") {
633+return false;
634+}
635+const params = value as Record<string, unknown>;
636+if (!params.params || typeof params.params !== "object") {
637+return false;
638+}
639+const proxy = params.params as Record<string, unknown>;
640+return proxy.path === "/tabs/open";
641+});
642+expect(openedCreateTab).toBe(false);
579643});
580644581645it.each([
@@ -589,10 +653,8 @@ describe("google-meet create flow", () => {
589653async (buttonText, note) => {
590654const { button, result } = await runCreateMeetBrowserScript({ buttonText });
591655592-expect(result).toMatchObject({
593-retryAfterMs: 1000,
594-notes: [note],
595-});
656+expect(result.retryAfterMs).toBe(1000);
657+expect(result.notes).toEqual([note]);
596658expect(button.click).toHaveBeenCalledTimes(1);
597659expect(result.meetingUri).toBeUndefined();
598660expect(result.manualActionReason).toBeUndefined();
此内容由惯性聚合(RSS阅读器)自动聚合整理,仅供阅读参考。 原文来自 — 版权归原作者所有。