From cdbed3c9b1c4c5d35b6989d184c8335cb2d6d456 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Mon, 2 Mar 2026 03:16:48 +0000 Subject: [PATCH] fix(googlechat): land #30965 thread reply option support (@novan) Landed from contributor PR #30965 by @novan. Co-authored-by: novan --- CHANGELOG.md | 1 + extensions/googlechat/src/api.test.ts | 49 ++++++++++++++++++++++++++- extensions/googlechat/src/api.ts | 6 +++- 3 files changed, 54 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f571692b2..9e4d0c2f7 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -112,6 +112,7 @@ Docs: https://docs.openclaw.ai ### Fixes +- Google Chat/Thread replies: set `messageReplyOption=REPLY_MESSAGE_FALLBACK_TO_NEW_THREAD` on threaded sends so replies attach to existing threads instead of silently failing thread placement. Landed from contributor PR #30965 by @novan. Thanks @novan. - Mattermost/Private channel policy routing: map Mattermost private channel type `P` to group chat type so `groupPolicy`/`groupAllowFrom` gates apply correctly instead of being treated as open public channels. Landed from contributor PR #30891 by @BlueBirdBack. Thanks @BlueBirdBack. - Models/Custom provider keys: trim custom provider map keys during normalization so image-capable models remain discoverable when provider keys are configured with leading/trailing whitespace. Landed from contributor PR #31202 by @stakeswky. Thanks @stakeswky. - Discord/Agent component interactions: accept Components v2 `cid` payloads alongside legacy `componentId`, and safely decode percent-encoded IDs without throwing on malformed `%` sequences. Landed from contributor PR #29013 by @Jacky1n7. Thanks @Jacky1n7. diff --git a/extensions/googlechat/src/api.test.ts b/extensions/googlechat/src/api.test.ts index b98b247a6..a8a6b763a 100644 --- a/extensions/googlechat/src/api.test.ts +++ b/extensions/googlechat/src/api.test.ts @@ -1,6 +1,6 @@ import { afterEach, describe, expect, it, vi } from "vitest"; import type { ResolvedGoogleChatAccount } from "./accounts.js"; -import { downloadGoogleChatMedia } from "./api.js"; +import { downloadGoogleChatMedia, sendGoogleChatMessage } from "./api.js"; vi.mock("./auth.js", () => ({ getGoogleChatAccessToken: vi.fn().mockResolvedValue("token"), @@ -59,3 +59,50 @@ describe("downloadGoogleChatMedia", () => { ).rejects.toThrow(/max bytes/i); }); }); + +describe("sendGoogleChatMessage", () => { + afterEach(() => { + vi.unstubAllGlobals(); + }); + + it("adds messageReplyOption when sending to an existing thread", async () => { + const fetchMock = vi + .fn() + .mockResolvedValue( + new Response(JSON.stringify({ name: "spaces/AAA/messages/123" }), { status: 200 }), + ); + vi.stubGlobal("fetch", fetchMock); + + await sendGoogleChatMessage({ + account, + space: "spaces/AAA", + text: "hello", + thread: "spaces/AAA/threads/xyz", + }); + + const [url, init] = fetchMock.mock.calls[0] ?? []; + expect(String(url)).toContain("messageReplyOption=REPLY_MESSAGE_FALLBACK_TO_NEW_THREAD"); + expect(JSON.parse(String(init?.body))).toMatchObject({ + text: "hello", + thread: { name: "spaces/AAA/threads/xyz" }, + }); + }); + + it("does not set messageReplyOption for non-thread sends", async () => { + const fetchMock = vi + .fn() + .mockResolvedValue( + new Response(JSON.stringify({ name: "spaces/AAA/messages/124" }), { status: 200 }), + ); + vi.stubGlobal("fetch", fetchMock); + + await sendGoogleChatMessage({ + account, + space: "spaces/AAA", + text: "hello", + }); + + const [url] = fetchMock.mock.calls[0] ?? []; + expect(String(url)).not.toContain("messageReplyOption="); + }); +}); diff --git a/extensions/googlechat/src/api.ts b/extensions/googlechat/src/api.ts index f8bcd65fc..c71c44916 100644 --- a/extensions/googlechat/src/api.ts +++ b/extensions/googlechat/src/api.ts @@ -128,7 +128,11 @@ export async function sendGoogleChatMessage(params: { ...(item.contentName ? { contentName: item.contentName } : {}), })); } - const url = `${CHAT_API_BASE}/${space}/messages`; + const urlObj = new URL(`${CHAT_API_BASE}/${space}/messages`); + if (thread) { + urlObj.searchParams.set("messageReplyOption", "REPLY_MESSAGE_FALLBACK_TO_NEW_THREAD"); + } + const url = urlObj.toString(); const result = await fetchJson<{ name?: string }>(account, url, { method: "POST", body: JSON.stringify(body),