From afa5533253dc004ae6d63d214611353fbc0a2e24 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Tue, 17 Feb 2026 02:55:46 +0100 Subject: [PATCH] fix(mattermost): harden react remove flag parsing --- extensions/mattermost/src/channel.test.ts | 52 +++++++++++++++++ extensions/mattermost/src/channel.ts | 2 +- .../src/mattermost/monitor-websocket.test.ts | 56 +++++++++++++++++++ 3 files changed, 109 insertions(+), 1 deletion(-) diff --git a/extensions/mattermost/src/channel.test.ts b/extensions/mattermost/src/channel.test.ts index cf178ab833..f6cb574fbf 100644 --- a/extensions/mattermost/src/channel.test.ts +++ b/extensions/mattermost/src/channel.test.ts @@ -187,6 +187,58 @@ describe("mattermostPlugin", () => { (globalThis as any).fetch = prevFetch; } }); + + it("only treats boolean remove flag as removal", async () => { + const cfg: OpenClawConfig = { + channels: { + mattermost: { + enabled: true, + botToken: "test-token", + baseUrl: "https://chat.example.com", + }, + }, + }; + + const fetchImpl = vi.fn(async (url: any, init?: any) => { + if (String(url).endsWith("/api/v4/users/me")) { + return new Response(JSON.stringify({ id: "BOT123" }), { + status: 200, + headers: { "content-type": "application/json" }, + }); + } + if (String(url).endsWith("/api/v4/reactions")) { + expect(init?.method).toBe("POST"); + expect(JSON.parse(init?.body)).toEqual({ + user_id: "BOT123", + post_id: "POST1", + emoji_name: "thumbsup", + }); + return new Response(JSON.stringify({ ok: true }), { + status: 201, + headers: { "content-type": "application/json" }, + }); + } + throw new Error(`unexpected url: ${url}`); + }); + + const prevFetch = globalThis.fetch; + (globalThis as any).fetch = fetchImpl; + try { + const result = await mattermostPlugin.actions?.handleAction?.({ + channel: "mattermost", + action: "react", + params: { messageId: "POST1", emoji: "thumbsup", remove: "true" }, + cfg, + accountId: "default", + } as any); + + expect(result?.content).toEqual([ + { type: "text", text: "Reacted with :thumbsup: on POST1" }, + ]); + } finally { + (globalThis as any).fetch = prevFetch; + } + }); }); describe("config", () => { diff --git a/extensions/mattermost/src/channel.ts b/extensions/mattermost/src/channel.ts index 43ee15fef1..9585b1e718 100644 --- a/extensions/mattermost/src/channel.ts +++ b/extensions/mattermost/src/channel.ts @@ -83,7 +83,7 @@ const mattermostMessageActions: ChannelMessageActionAdapter = { throw new Error("Mattermost react requires emoji"); } - const remove = Boolean((params as any)?.remove); + const remove = (params as any)?.remove === true; if (remove) { const result = await removeMattermostReaction({ cfg, diff --git a/extensions/mattermost/src/mattermost/monitor-websocket.test.ts b/extensions/mattermost/src/mattermost/monitor-websocket.test.ts index fee581b62c..c17e2c829a 100644 --- a/extensions/mattermost/src/mattermost/monitor-websocket.test.ts +++ b/extensions/mattermost/src/mattermost/monitor-websocket.test.ts @@ -170,4 +170,60 @@ describe("mattermost websocket monitor", () => { expect(patches.some((patch) => patch.connected === true)).toBe(true); expect(patches.filter((patch) => patch.connected === false)).toHaveLength(2); }); + + it("dispatches reaction events to the reaction handler", async () => { + const socket = new FakeWebSocket(); + const onPosted = vi.fn(async () => {}); + const onReaction = vi.fn(async (payload) => payload); + const connectOnce = createMattermostConnectOnce({ + wsUrl: "wss://example.invalid/api/v4/websocket", + botToken: "token", + runtime: testRuntime(), + nextSeq: () => 1, + onPosted, + onReaction, + webSocketFactory: () => socket, + }); + + socket.emitOpen(); + socket.emitMessage( + Buffer.from( + JSON.stringify({ + event: "reaction_added", + data: { + reaction: JSON.stringify({ + user_id: "user-1", + post_id: "post-1", + emoji_name: "thumbsup", + }), + }, + }), + ), + ); + socket.emitClose(1000); + + await connectOnce(); + + expect(onReaction).toHaveBeenCalledTimes(1); + expect(onPosted).not.toHaveBeenCalled(); + const payload = onReaction.mock.calls[0]?.[0]; + expect(payload).toMatchObject({ + event: "reaction_added", + data: { + reaction: JSON.stringify({ + user_id: "user-1", + post_id: "post-1", + emoji_name: "thumbsup", + }), + }, + }); + expect(payload.data?.reaction).toBe( + JSON.stringify({ + user_id: "user-1", + post_id: "post-1", + emoji_name: "thumbsup", + }), + ); + expect(payload.data?.reaction).toBeDefined(); + }); });