From 2de095746bc2409b608538780a8e10928da7a000 Mon Sep 17 00:00:00 2001 From: Peter Steinberger Date: Sun, 23 Aug 2026 23:30:48 -0700 Subject: [PATCH] test(msteams): cover agent reaction removal parity (#128560) Parity assessment for Microsoft's Lobster patch 0018 via giodl73-repo/lobster-plugins-and-patches. --- .../msteams/src/channel.actions.test.ts | 105 +++++++++++------- .../src/graph-messages.actions.test.ts | 27 +++++ 2 files changed, 93 insertions(+), 39 deletions(-) diff --git a/extensions/msteams/src/channel.actions.test.ts b/extensions/msteams/src/channel.actions.test.ts index c4172ec63e3b..ad8b9266743f 100644 --- a/extensions/msteams/src/channel.actions.test.ts +++ b/extensions/msteams/src/channel.actions.test.ts @@ -20,6 +20,7 @@ const { sendAdaptiveCardMSTeamsMock, sendMessageMSTeamsMock, unpinMessageMSTeamsMock, + unreactMessageMSTeamsMock, } = vi.hoisted(() => ({ addParticipantMSTeamsMock: vi.fn(), editMessageMSTeamsMock: vi.fn(), @@ -37,6 +38,7 @@ const { sendAdaptiveCardMSTeamsMock: vi.fn(), sendMessageMSTeamsMock: vi.fn(), unpinMessageMSTeamsMock: vi.fn(), + unreactMessageMSTeamsMock: vi.fn(), })); vi.mock("./channel.runtime.js", () => ({ msTeamsChannelRuntime: { @@ -56,6 +58,7 @@ vi.mock("./channel.runtime.js", () => ({ sendAdaptiveCardMSTeams: sendAdaptiveCardMSTeamsMock, sendMessageMSTeams: sendMessageMSTeamsMock, unpinMessageMSTeams: unpinMessageMSTeamsMock, + unreactMessageMSTeams: unreactMessageMSTeamsMock, }, })); @@ -76,6 +79,7 @@ const actionMocks = [ sendAdaptiveCardMSTeamsMock, sendMessageMSTeamsMock, unpinMessageMSTeamsMock, + unreactMessageMSTeamsMock, ]; const currentChannelId = "conversation:19:ctx@thread.tacv2"; const graphTeamId = "11111111-1111-1111-1111-111111111111"; @@ -1090,30 +1094,43 @@ describe("msteamsPlugin message actions", () => { expect(properties).toHaveProperty("pinnedMessageId"); }); - it.each([ - { - chatType: "channel", - conversationTarget: "conversation:19:c@thread.tacv2", - currentMessagingTarget: "team-1/19:c@thread.tacv2", - expectedTarget: "team-1/19:c@thread.tacv2", - }, - { - chatType: "group", - conversationTarget: "conversation:19:g@thread.v2", - currentMessagingTarget: undefined, - expectedTarget: "conversation:19:g@thread.v2", - }, - { - chatType: "direct", - conversationTarget: "conversation:a:dm", - currentMessagingTarget: undefined, - expectedTarget: "conversation:a:dm", - }, - ] as const)( - "routes agent react actions and preserves their result shape for $chatType turns", - async ({ chatType, conversationTarget, currentMessagingTarget, expectedTarget }) => { + it.each( + ( + [ + { + chatType: "channel", + conversationTarget: "conversation:19:c@thread.tacv2", + currentMessagingTarget: "team-1/19:c@thread.tacv2", + expectedTarget: "team-1/19:c@thread.tacv2", + }, + { + chatType: "group", + conversationTarget: "conversation:19:g@thread.v2", + currentMessagingTarget: undefined, + expectedTarget: "conversation:19:g@thread.v2", + }, + { + chatType: "direct", + conversationTarget: "conversation:a:dm", + currentMessagingTarget: undefined, + expectedTarget: "conversation:a:dm", + }, + ] as const + ).flatMap((conversation) => + [false, true].map((remove) => ({ + conversation, + chatType: conversation.chatType, + operation: remove ? "remove" : "add", + remove, + })), + ), + )( + "routes agent reaction $operation actions and preserves their result shape for $chatType turns", + async ({ conversation, remove }) => { + const { chatType, conversationTarget, currentMessagingTarget, expectedTarget } = conversation; + const resultDetails = { ...(remove ? { removed: true } : {}), reactionType }; await expectSuccessfulAction({ - mockFn: reactMessageMSTeamsMock, + mockFn: remove ? unreactMessageMSTeamsMock : reactMessageMSTeamsMock, mockResult: { ok: true }, action: "react", cfg: unrestrictedReadCfg, @@ -1123,6 +1140,7 @@ describe("msteamsPlugin message actions", () => { ...(chatType === "channel" ? { target: conversationTarget } : {}), messageId: padded("msg-react"), emoji: padded(reactionType), + ...(remove ? { remove: true } : {}), }, toolContext: { currentChannelProvider: "msteams", @@ -1135,11 +1153,11 @@ describe("msteamsPlugin message actions", () => { messageId: "msg-react", reactionType, }, - details: okMSTeamsActionDetails("react", { reactionType }), + details: okMSTeamsActionDetails("react", resultDetails), contentDetails: { channel: "msteams", action: "react", - reactionType, + ...resultDetails, ok: true, }, }); @@ -1258,20 +1276,24 @@ describe("msteamsPlugin message actions", () => { }); }); - it("reports the allowed reaction types when emoji is missing", async () => { - await expectActionParamError( - "react", - { - to: targetChannelId, - messageId: "msg-4", - }, - reactMissingEmojiError, - { - error: reactMissingEmojiDetail, - validTypes: reactionTypes, - }, - ); - }); + it.each([false, true])( + "reports the allowed reaction types when emoji is missing (remove=%s)", + async (remove) => { + await expectActionParamError( + "react", + { + to: targetChannelId, + messageId: "msg-4", + ...(remove ? { remove: true } : {}), + }, + reactMissingEmojiError, + { + error: reactMissingEmojiDetail, + validTypes: reactionTypes, + }, + ); + }, + ); it("requires a non-empty search query after trimming", async () => { await expectActionError( @@ -1342,6 +1364,11 @@ describe("msteamsPlugin message actions", () => { params: { to: graphChannelTarget, messageId: "msg-1", emoji: "like" }, runtimeMock: reactMessageMSTeamsMock, }, + { + action: "react", + params: { to: graphChannelTarget, messageId: "msg-1", emoji: "like", remove: true }, + runtimeMock: unreactMessageMSTeamsMock, + }, ])("rejects a blocked $action target before the provider operation", async (testCase) => { await expect( runAction({ diff --git a/extensions/msteams/src/graph-messages.actions.test.ts b/extensions/msteams/src/graph-messages.actions.test.ts index c666cff750e7..ce4c00ad2f75 100644 --- a/extensions/msteams/src/graph-messages.actions.test.ts +++ b/extensions/msteams/src/graph-messages.actions.test.ts @@ -131,6 +131,7 @@ describe("reactMessageMSTeams", () => { }); expect(result).toEqual({ ok: true }); + expect(mockState.resolveGraphToken).toHaveBeenCalledWith({}, { preferDelegated: true }); expect(mockState.mutateGraphJson).toHaveBeenCalledWith({ token: TOKEN, path: `/chats/${encodeURIComponent(CHAT_ID)}/messages/msg-1/setReaction`, @@ -236,6 +237,7 @@ describe("unreactMessageMSTeams", () => { }); expect(result).toEqual({ ok: true }); + expect(mockState.resolveGraphToken).toHaveBeenCalledWith({}, { preferDelegated: true }); expect(mockState.mutateGraphJson).toHaveBeenCalledWith({ token: TOKEN, path: `/chats/${encodeURIComponent(CHAT_ID)}/messages/msg-1/unsetReaction`, @@ -264,4 +266,29 @@ describe("unreactMessageMSTeams", () => { beta: true, }); }); + + it.each([ + { reactionType: " LAUGH ", expectedReaction: "😆" }, + { reactionType: " 🎉 ", expectedReaction: "🎉" }, + ])( + "normalizes $reactionType when removing a reaction", + async ({ reactionType, expectedReaction }) => { + mockState.mutateGraphJson.mockResolvedValue(undefined); + + await unreactMessageMSTeams({ + cfg: {} as OpenClawConfig, + to: CHAT_ID, + messageId: "msg-1", + reactionType, + }); + + expect(mockState.mutateGraphJson).toHaveBeenCalledWith({ + token: TOKEN, + path: `/chats/${encodeURIComponent(CHAT_ID)}/messages/msg-1/unsetReaction`, + method: "POST", + body: { reactionType: expectedReaction }, + beta: true, + }); + }, + ); });