import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime"; import { beforeEach, describe, expect, it, vi } from "vitest"; import { msteamsActionsAdapter } from "./actions.js"; import { msteamsPlugin } from "./channel.js";
it("returns an error when unpin is called without pinnedMessageId or messageId", async () => {
await expectActionParamError( "unpin",
{ target: targetChannelId }, "Unpin requires a target (to) and pinnedMessageId.",
);
});
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("requires a non-empty search query after trimming", async () => {
await expectActionParamError( "search",
{
to: targetChannelId,
query: " ",
},
searchMissingQueryError,
);
});
it("routes channel fallback targets via teamId/channelId for react actions", async () => { // When an action is invoked in a Teams channel context and `target` is // omitted, the action handler falls back to `toolContext.currentChannelId`. // For channel turns, buildToolContext populates that field with the // compound `teamId/channelId` form (see buildToolContext below), so the // runtime call must receive that compound form — NOT a bare // `conversation:<id>` — so Graph API routes through // `/teams/{teamId}/channels/{channelId}` rather than `/chats/{id}`. const teamChannelTarget = "team-1/19:channel-abc@thread.tacv2";
await expectSuccessfulAction({
mockFn: reactMessageMSTeamsMock,
mockResult: { ok: true },
action: "react",
actionParams: {
messageId: "msg-channel-react",
emoji: reactionType,
},
toolContext: {
currentChannelId: "conversation:19:channel-abc@thread.tacv2",
currentGraphChannelId: teamChannelTarget,
},
runtimeParams: {
to: teamChannelTarget,
messageId: "msg-channel-react",
reactionType,
},
details: okMSTeamsActionDetails("react", {
reactionType,
}),
contentDetails: {
channel: "msteams",
action: "react",
reactionType,
ok: true,
},
});
});
it("preserves explicit teamId/channelId target over toolContext fallback", async () => { // Even in a channel context with a compound currentChannelId, an // explicit `target` param must take precedence. const teamChannelTarget = "team-2/19:channel-def@thread.tacv2"; const explicitTarget = "team-explicit/19:other@thread.tacv2";
await expectSuccessfulAction({
mockFn: reactMessageMSTeamsMock,
mockResult: { ok: true },
action: "react",
actionParams: {
target: explicitTarget,
messageId: "msg-explicit",
emoji: reactionType,
},
toolContext: {
currentChannelId: teamChannelTarget,
currentGraphChannelId: teamChannelTarget,
},
runtimeParams: {
to: explicitTarget,
messageId: "msg-explicit",
reactionType,
},
details: okMSTeamsActionDetails("react", {
reactionType,
}),
contentDetails: {
channel: "msteams",
action: "react",
reactionType,
ok: true,
},
});
});
it("keeps chat conversation fallback targets as-is for DM react actions", async () => { // DM/group-chat turns continue to set currentChannelId to a // `conversation:<id>` string (no `teamId/` prefix), which the runtime // will resolve through `/chats/{id}`. const dmFallback = "conversation:19:chat-dm@thread.skype";
await expectSuccessfulAction({
mockFn: reactMessageMSTeamsMock,
mockResult: { ok: true },
action: "react",
actionParams: {
messageId: "msg-dm-react",
emoji: reactionType,
},
toolContext: {
currentChannelId: dmFallback,
},
runtimeParams: {
to: dmFallback,
messageId: "msg-dm-react",
reactionType,
},
details: okMSTeamsActionDetails("react", {
reactionType,
}),
contentDetails: {
channel: "msteams",
action: "react",
reactionType,
ok: true,
},
});
});
});
it("uses NativeChannelId for channel turns so actions route via teamId/channelId", () => { // Teams channel inbound messages carry the compound `teamId/channelId` // on NativeChannelId. buildToolContext must prefer it over the bare // `conversation:<id>` in To so action fallbacks route via // `/teams/{teamId}/channels/{channelId}`. const result = callBuildToolContext({
To: "conversation:19:channel-abc@thread.tacv2",
NativeChannelId: "team-1/19:channel-abc@thread.tacv2",
ReplyToId: "reply-1",
});
expect(result?.currentChannelId).toBe("conversation:19:channel-abc@thread.tacv2");
expect(result?.currentGraphChannelId).toBe("team-1/19:channel-abc@thread.tacv2");
expect(result?.currentThreadTs).toBe("reply-1");
});
it("falls back to To for DM turns (no NativeChannelId)", () => { const result = callBuildToolContext({
To: "user:aad-user-1",
});
expect(result?.currentChannelId).toBe("user:aad-user-1");
expect(result?.currentGraphChannelId).toBeUndefined();
});
it("falls back to To for group chat turns (no NativeChannelId)", () => { const result = callBuildToolContext({
To: "conversation:19:groupchat@thread.v2",
});
expect(result?.currentChannelId).toBe("conversation:19:groupchat@thread.v2");
expect(result?.currentGraphChannelId).toBeUndefined();
});
it("ignores NativeChannelId that does not encode a teamId/channelId pair", () => { // Safety: only compound forms (with "/") should preempt the To fallback. // A bare native id without a team prefix must not accidentally route // through channel Graph paths. const result = callBuildToolContext({
To: "conversation:19:chat@thread.v2",
NativeChannelId: "19:chat@thread.v2",
});
expect(result?.currentChannelId).toBe("conversation:19:chat@thread.v2");
expect(result?.currentGraphChannelId).toBeUndefined();
});
});
Messung V0.5 in Prozent
¤ Dauer der Verarbeitung: 0.12 Sekunden
(vorverarbeitet am 2026-05-26)
¤
Die Informationen auf dieser Webseite wurden
nach bestem Wissen sorgfältig zusammengestellt. Es wird jedoch weder Vollständigkeit, noch Richtigkeit,
noch Qualität der bereit gestellten Informationen zugesichert.
Bemerkung:
Die farbliche Syntaxdarstellung und die Messung sind noch experimentell.