fix(mattermost): harden react remove flag parsing

This commit is contained in:
Peter Steinberger
2026-02-17 02:55:46 +01:00
parent d6226355e6
commit afa5533253
3 changed files with 109 additions and 1 deletions

View File

@@ -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", () => {

View File

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

View File

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