Skip to content

Commit f71ee71

Browse files
msteams: add group management actions (add/remove participant, rename) (#57530)
* msteams: add group management actions (addParticipant, removeParticipant, renameGroup) * fix(msteams): restore group-management plugin contracts * fix(msteams): satisfy plugin guardrails * msteams: restore doctor adapter lost in main merge * fix(msteams): restore message tool schema imports * msteams: fix graph action routing and member paging --------- Co-authored-by: Brad Groux <[email protected]>
1 parent 51312b7 commit f71ee71

8 files changed

Lines changed: 1316 additions & 21 deletions

File tree

extensions/msteams/src/channel.actions.test.ts

Lines changed: 82 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,10 @@ import { msteamsPlugin } from "./channel.js";
66
const {
77
editMessageMSTeamsMock,
88
deleteMessageMSTeamsMock,
9+
getChannelInfoMSTeamsMock,
10+
getMemberInfoMSTeamsMock,
911
getMessageMSTeamsMock,
12+
listChannelsMSTeamsMock,
1013
listReactionsMSTeamsMock,
1114
pinMessageMSTeamsMock,
1215
reactMessageMSTeamsMock,
@@ -17,7 +20,10 @@ const {
1720
} = vi.hoisted(() => ({
1821
editMessageMSTeamsMock: vi.fn(),
1922
deleteMessageMSTeamsMock: vi.fn(),
23+
getChannelInfoMSTeamsMock: vi.fn(),
24+
getMemberInfoMSTeamsMock: vi.fn(),
2025
getMessageMSTeamsMock: vi.fn(),
26+
listChannelsMSTeamsMock: vi.fn(),
2127
listReactionsMSTeamsMock: vi.fn(),
2228
pinMessageMSTeamsMock: vi.fn(),
2329
reactMessageMSTeamsMock: vi.fn(),
@@ -31,7 +37,10 @@ vi.mock("./channel.runtime.js", () => ({
3137
msTeamsChannelRuntime: {
3238
editMessageMSTeams: editMessageMSTeamsMock,
3339
deleteMessageMSTeams: deleteMessageMSTeamsMock,
40+
getChannelInfoMSTeams: getChannelInfoMSTeamsMock,
41+
getMemberInfoMSTeams: getMemberInfoMSTeamsMock,
3442
getMessageMSTeams: getMessageMSTeamsMock,
43+
listChannelsMSTeams: listChannelsMSTeamsMock,
3544
listReactionsMSTeams: listReactionsMSTeamsMock,
3645
pinMessageMSTeams: pinMessageMSTeamsMock,
3746
reactMessageMSTeams: reactMessageMSTeamsMock,
@@ -45,7 +54,10 @@ vi.mock("./channel.runtime.js", () => ({
4554
const actionMocks = [
4655
editMessageMSTeamsMock,
4756
deleteMessageMSTeamsMock,
57+
getChannelInfoMSTeamsMock,
58+
getMemberInfoMSTeamsMock,
4859
getMessageMSTeamsMock,
60+
listChannelsMSTeamsMock,
4961
listReactionsMSTeamsMock,
5062
pinMessageMSTeamsMock,
5163
reactMessageMSTeamsMock,
@@ -101,6 +113,7 @@ async function runAction(params: {
101113
params?: Record<string, unknown>;
102114
toolContext?: Record<string, unknown>;
103115
mediaLocalRoots?: readonly string[];
116+
mediaReadFile?: (filePath: string) => Promise<Buffer>;
104117
}) {
105118
const handleAction = requireMSTeamsHandleAction();
106119
return await handleAction({
@@ -109,6 +122,7 @@ async function runAction(params: {
109122
cfg: params.cfg ?? {},
110123
params: params.params ?? {},
111124
mediaLocalRoots: params.mediaLocalRoots,
125+
mediaReadFile: params.mediaReadFile,
112126
toolContext: params.toolContext,
113127
} as Parameters<ReturnType<typeof requireMSTeamsHandleAction>>[0]);
114128
}
@@ -167,6 +181,7 @@ async function expectSuccessfulAction(params: {
167181
actionParams?: Parameters<typeof runAction>[0]["params"];
168182
toolContext?: Parameters<typeof runAction>[0]["toolContext"];
169183
mediaLocalRoots?: Parameters<typeof runAction>[0]["mediaLocalRoots"];
184+
mediaReadFile?: Parameters<typeof runAction>[0]["mediaReadFile"];
170185
runtimeParams: Record<string, unknown>;
171186
details: Record<string, unknown>;
172187
contentDetails?: Record<string, unknown>;
@@ -176,6 +191,7 @@ async function expectSuccessfulAction(params: {
176191
action: params.action,
177192
params: params.actionParams,
178193
mediaLocalRoots: params.mediaLocalRoots,
194+
mediaReadFile: params.mediaReadFile,
179195
toolContext: params.toolContext,
180196
});
181197
expectActionRuntimeCall(params.mockFn, params.runtimeParams);
@@ -233,6 +249,7 @@ describe("msteamsPlugin message actions", () => {
233249
});
234250

235251
it("routes upload-file through sendMessageMSTeams with filename override", async () => {
252+
const mediaReadFile = vi.fn(async () => Buffer.from("pdf"));
236253
await expectSuccessfulAction({
237254
mockFn: sendMessageMSTeamsMock,
238255
mockResult: {
@@ -247,12 +264,14 @@ describe("msteamsPlugin message actions", () => {
247264
filename: "Q1-report.pdf",
248265
},
249266
mediaLocalRoots: ["/tmp"],
267+
mediaReadFile,
250268
runtimeParams: {
251269
to: targetChannelId,
252270
text: "Quarterly report",
253271
mediaUrl: " /tmp/report.pdf ",
254272
filename: "Q1-report.pdf",
255273
mediaLocalRoots: ["/tmp"],
274+
mediaReadFile,
256275
},
257276
details: {
258277
ok: true,
@@ -269,6 +288,69 @@ describe("msteamsPlugin message actions", () => {
269288
});
270289
});
271290

291+
it("routes member-info through the Teams runtime", async () => {
292+
await expectSuccessfulAction({
293+
mockFn: getMemberInfoMSTeamsMock,
294+
mockResult: { member: { id: "user-1" } },
295+
action: "member-info",
296+
actionParams: { userId: " user-1 " },
297+
runtimeParams: { userId: "user-1" },
298+
details: okMSTeamsActionDetails("member-info", {
299+
member: { id: "user-1" },
300+
}),
301+
contentDetails: {
302+
ok: true,
303+
channel: "msteams",
304+
action: "member-info",
305+
member: { id: "user-1" },
306+
},
307+
});
308+
});
309+
310+
it("routes channel-list through the Teams runtime", async () => {
311+
await expectSuccessfulAction({
312+
mockFn: listChannelsMSTeamsMock,
313+
mockResult: { channels: [{ id: "channel-1" }] },
314+
action: "channel-list",
315+
actionParams: { teamId: " team-1 " },
316+
runtimeParams: { teamId: "team-1" },
317+
details: okMSTeamsActionDetails("channel-list", {
318+
channels: [{ id: "channel-1" }],
319+
}),
320+
contentDetails: {
321+
ok: true,
322+
channel: "msteams",
323+
action: "channel-list",
324+
channels: [{ id: "channel-1" }],
325+
},
326+
});
327+
});
328+
329+
it("routes channel-info through the Teams runtime", async () => {
330+
await expectSuccessfulAction({
331+
mockFn: getChannelInfoMSTeamsMock,
332+
mockResult: { channel: { id: "channel-1" } },
333+
action: "channel-info",
334+
actionParams: {
335+
teamId: " team-1 ",
336+
channelId: " channel-1 ",
337+
},
338+
runtimeParams: {
339+
teamId: "team-1",
340+
channelId: "channel-1",
341+
},
342+
details: okMSTeamsActionDetails("channel-info", {
343+
channelInfo: { id: "channel-1" },
344+
}),
345+
contentDetails: {
346+
ok: true,
347+
channel: "msteams",
348+
action: "channel-info",
349+
channelInfo: { id: "channel-1" },
350+
},
351+
});
352+
});
353+
272354
it("accepts target as an alias for pin actions", async () => {
273355
await expectSuccessfulAction({
274356
mockFn: pinMessageMSTeamsMock,

extensions/msteams/src/channel.runtime.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,11 @@ import {
22
listMSTeamsDirectoryGroupsLive as listMSTeamsDirectoryGroupsLiveImpl,
33
listMSTeamsDirectoryPeersLive as listMSTeamsDirectoryPeersLiveImpl,
44
} from "./directory-live.js";
5+
import {
6+
addParticipantMSTeams as addParticipantMSTeamsImpl,
7+
removeParticipantMSTeams as removeParticipantMSTeamsImpl,
8+
renameGroupMSTeams as renameGroupMSTeamsImpl,
9+
} from "./graph-group-management.js";
510
import { getMemberInfoMSTeams as getMemberInfoMSTeamsImpl } from "./graph-members.js";
611
import {
712
getMessageMSTeams as getMessageMSTeamsImpl,
@@ -32,6 +37,7 @@ import {
3237
// is currently wired through its own test surface; do not re-import it here
3338
// until channel.ts is migrated to that signature, otherwise identifiers collide.
3439
export const msTeamsChannelRuntime = {
40+
addParticipantMSTeams: addParticipantMSTeamsImpl,
3541
deleteMessageMSTeams: deleteMessageMSTeamsImpl,
3642
editMessageMSTeams: editMessageMSTeamsImpl,
3743
getChannelInfoMSTeams: getChannelInfoMSTeamsImpl,
@@ -42,6 +48,8 @@ export const msTeamsChannelRuntime = {
4248
listReactionsMSTeams: listReactionsMSTeamsImpl,
4349
pinMessageMSTeams: pinMessageMSTeamsImpl,
4450
reactMessageMSTeams: reactMessageMSTeamsImpl,
51+
removeParticipantMSTeams: removeParticipantMSTeamsImpl,
52+
renameGroupMSTeams: renameGroupMSTeamsImpl,
4553
searchMessagesMSTeams: searchMessagesMSTeamsImpl,
4654
unpinMessageMSTeams: unpinMessageMSTeamsImpl,
4755
unreactMessageMSTeams: unreactMessageMSTeamsImpl,
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
import type { OpenClawConfig } from "openclaw/plugin-sdk/config-runtime";
2+
import { describe, expect, it } from "vitest";
3+
import { msteamsPlugin } from "./channel.js";
4+
import { msTeamsApprovalAuth } from "./approval-auth.js";
5+
6+
function createConfiguredMSTeamsCfg(): OpenClawConfig {
7+
return {
8+
channels: {
9+
msteams: {
10+
appId: "app-id",
11+
appPassword: "secret",
12+
tenantId: "tenant-id",
13+
},
14+
},
15+
};
16+
}
17+
18+
describe("msteamsPlugin", () => {
19+
it("exposes approval auth through approvalCapability", () => {
20+
expect(msteamsPlugin.approvalCapability).toBe(msTeamsApprovalAuth);
21+
});
22+
23+
it("advertises legacy and group-management message-tool actions together", () => {
24+
const actions = msteamsPlugin.actions?.describeMessageTool?.({
25+
cfg: createConfiguredMSTeamsCfg(),
26+
})?.actions;
27+
28+
expect(actions).toEqual(
29+
expect.arrayContaining([
30+
"upload-file",
31+
"member-info",
32+
"channel-list",
33+
"channel-info",
34+
"addParticipant",
35+
"removeParticipant",
36+
"renameGroup",
37+
]),
38+
);
39+
});
40+
41+
it("reuses the shared Teams target-id matcher for explicit targets", () => {
42+
const looksLikeId = msteamsPlugin.messaging?.targetResolver?.looksLikeId;
43+
44+
expect(looksLikeId?.("29:1a2b3c4d5e6f")).toBe(true);
45+
expect(looksLikeId?.("a:1bfPersonalChat")).toBe(true);
46+
expect(looksLikeId?.("user:Jane Doe")).toBe(false);
47+
});
48+
});

0 commit comments

Comments
 (0)