fix(googlechat): land #30965 thread reply option support (@novan)

Landed from contributor PR #30965 by @novan.

Co-authored-by: novan <novan@users.noreply.github.com>
This commit is contained in:
Peter Steinberger
2026-03-02 03:16:48 +00:00
parent 355b4c62bc
commit cdbed3c9b1
3 changed files with 54 additions and 2 deletions

View File

@@ -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.

View File

@@ -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=");
});
});

View File

@@ -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),