@openclaw/msteams 2026.5.2 → 2026.5.3-beta.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (197) hide show
  1. package/dist/api.js +3 -0
  2. package/dist/channel-D7hdreTh.js +984 -0
  3. package/dist/channel-config-api.js +2 -0
  4. package/dist/channel-plugin-api.js +2 -0
  5. package/dist/channel.runtime-BC1ruIfN.js +573 -0
  6. package/dist/config-schema-B8QezH6t.js +15 -0
  7. package/dist/contract-api.js +2 -0
  8. package/dist/graph-users-9uQJepqr.js +1354 -0
  9. package/dist/index.js +22 -0
  10. package/dist/oauth-BWJyilR1.js +114 -0
  11. package/dist/oauth.token-xxpoLWy5.js +115 -0
  12. package/dist/policy-DTnU2GR7.js +142 -0
  13. package/dist/probe-D_H8yFps.js +2194 -0
  14. package/dist/resolve-allowlist-D41JSziq.js +219 -0
  15. package/dist/runtime-api-DV1iVMn1.js +28 -0
  16. package/dist/runtime-api.js +2 -0
  17. package/dist/secret-contract-BuoEXmPS.js +35 -0
  18. package/dist/secret-contract-api.js +2 -0
  19. package/dist/setup-entry.js +15 -0
  20. package/dist/setup-plugin-api.js +64 -0
  21. package/dist/setup-surface-BLkFQYIQ.js +313 -0
  22. package/dist/src-CFp1QpFd.js +4064 -0
  23. package/dist/test-api.js +2 -0
  24. package/package.json +14 -6
  25. package/api.ts +0 -3
  26. package/channel-config-api.ts +0 -1
  27. package/channel-plugin-api.ts +0 -2
  28. package/config-api.ts +0 -4
  29. package/contract-api.ts +0 -4
  30. package/index.ts +0 -20
  31. package/runtime-api.ts +0 -73
  32. package/secret-contract-api.ts +0 -5
  33. package/setup-entry.ts +0 -13
  34. package/setup-plugin-api.ts +0 -3
  35. package/src/ai-entity.ts +0 -7
  36. package/src/approval-auth.ts +0 -44
  37. package/src/attachments/bot-framework.test.ts +0 -461
  38. package/src/attachments/bot-framework.ts +0 -362
  39. package/src/attachments/download.ts +0 -311
  40. package/src/attachments/graph.test.ts +0 -416
  41. package/src/attachments/graph.ts +0 -484
  42. package/src/attachments/html.ts +0 -122
  43. package/src/attachments/payload.ts +0 -14
  44. package/src/attachments/remote-media.test.ts +0 -137
  45. package/src/attachments/remote-media.ts +0 -112
  46. package/src/attachments/shared.test.ts +0 -530
  47. package/src/attachments/shared.ts +0 -626
  48. package/src/attachments/types.ts +0 -47
  49. package/src/attachments.graph.test.ts +0 -342
  50. package/src/attachments.helpers.test.ts +0 -246
  51. package/src/attachments.test-helpers.ts +0 -17
  52. package/src/attachments.test.ts +0 -687
  53. package/src/attachments.ts +0 -18
  54. package/src/block-streaming-config.test.ts +0 -61
  55. package/src/channel-api.ts +0 -1
  56. package/src/channel.actions.test.ts +0 -742
  57. package/src/channel.directory.test.ts +0 -200
  58. package/src/channel.runtime.ts +0 -56
  59. package/src/channel.setup.ts +0 -77
  60. package/src/channel.test.ts +0 -128
  61. package/src/channel.ts +0 -1136
  62. package/src/config-schema.ts +0 -6
  63. package/src/config-ui-hints.ts +0 -12
  64. package/src/conversation-store-fs.test.ts +0 -74
  65. package/src/conversation-store-fs.ts +0 -149
  66. package/src/conversation-store-helpers.test.ts +0 -202
  67. package/src/conversation-store-helpers.ts +0 -105
  68. package/src/conversation-store-memory.ts +0 -51
  69. package/src/conversation-store.shared.test.ts +0 -225
  70. package/src/conversation-store.ts +0 -71
  71. package/src/directory-live.test.ts +0 -156
  72. package/src/directory-live.ts +0 -111
  73. package/src/doctor.ts +0 -27
  74. package/src/errors.test.ts +0 -133
  75. package/src/errors.ts +0 -246
  76. package/src/feedback-reflection-prompt.ts +0 -117
  77. package/src/feedback-reflection-store.ts +0 -114
  78. package/src/feedback-reflection.test.ts +0 -237
  79. package/src/feedback-reflection.ts +0 -283
  80. package/src/file-consent-helpers.test.ts +0 -326
  81. package/src/file-consent-helpers.ts +0 -126
  82. package/src/file-consent-invoke.ts +0 -150
  83. package/src/file-consent.test.ts +0 -363
  84. package/src/file-consent.ts +0 -287
  85. package/src/graph-chat.ts +0 -55
  86. package/src/graph-group-management.test.ts +0 -318
  87. package/src/graph-group-management.ts +0 -168
  88. package/src/graph-members.test.ts +0 -89
  89. package/src/graph-members.ts +0 -48
  90. package/src/graph-messages.actions.test.ts +0 -243
  91. package/src/graph-messages.read.test.ts +0 -391
  92. package/src/graph-messages.search.test.ts +0 -213
  93. package/src/graph-messages.test-helpers.ts +0 -50
  94. package/src/graph-messages.ts +0 -534
  95. package/src/graph-teams.test.ts +0 -215
  96. package/src/graph-teams.ts +0 -114
  97. package/src/graph-thread.test.ts +0 -246
  98. package/src/graph-thread.ts +0 -146
  99. package/src/graph-upload.test.ts +0 -258
  100. package/src/graph-upload.ts +0 -531
  101. package/src/graph-users.ts +0 -29
  102. package/src/graph.test.ts +0 -516
  103. package/src/graph.ts +0 -293
  104. package/src/inbound.test.ts +0 -221
  105. package/src/inbound.ts +0 -148
  106. package/src/index.ts +0 -4
  107. package/src/media-helpers.test.ts +0 -202
  108. package/src/media-helpers.ts +0 -105
  109. package/src/mentions.test.ts +0 -244
  110. package/src/mentions.ts +0 -114
  111. package/src/messenger.test.ts +0 -865
  112. package/src/messenger.ts +0 -605
  113. package/src/monitor-handler/access.ts +0 -125
  114. package/src/monitor-handler/inbound-media.test.ts +0 -289
  115. package/src/monitor-handler/inbound-media.ts +0 -180
  116. package/src/monitor-handler/message-handler-mock-support.test-support.ts +0 -28
  117. package/src/monitor-handler/message-handler.authz.test.ts +0 -669
  118. package/src/monitor-handler/message-handler.dm-media.test.ts +0 -54
  119. package/src/monitor-handler/message-handler.test-support.ts +0 -100
  120. package/src/monitor-handler/message-handler.thread-parent.test.ts +0 -223
  121. package/src/monitor-handler/message-handler.thread-session.test.ts +0 -77
  122. package/src/monitor-handler/message-handler.ts +0 -1000
  123. package/src/monitor-handler/reaction-handler.test.ts +0 -267
  124. package/src/monitor-handler/reaction-handler.ts +0 -210
  125. package/src/monitor-handler/thread-session.ts +0 -17
  126. package/src/monitor-handler.adaptive-card.test.ts +0 -162
  127. package/src/monitor-handler.feedback-authz.test.ts +0 -314
  128. package/src/monitor-handler.file-consent.test.ts +0 -423
  129. package/src/monitor-handler.sso.test.ts +0 -563
  130. package/src/monitor-handler.test-helpers.ts +0 -180
  131. package/src/monitor-handler.ts +0 -534
  132. package/src/monitor-handler.types.ts +0 -27
  133. package/src/monitor-types.ts +0 -6
  134. package/src/monitor.lifecycle.test.ts +0 -278
  135. package/src/monitor.test.ts +0 -119
  136. package/src/monitor.ts +0 -442
  137. package/src/oauth.flow.ts +0 -77
  138. package/src/oauth.shared.ts +0 -37
  139. package/src/oauth.test.ts +0 -305
  140. package/src/oauth.token.ts +0 -158
  141. package/src/oauth.ts +0 -130
  142. package/src/outbound.test.ts +0 -130
  143. package/src/outbound.ts +0 -71
  144. package/src/pending-uploads-fs.test.ts +0 -246
  145. package/src/pending-uploads-fs.ts +0 -235
  146. package/src/pending-uploads.test.ts +0 -173
  147. package/src/pending-uploads.ts +0 -121
  148. package/src/policy.test.ts +0 -240
  149. package/src/policy.ts +0 -262
  150. package/src/polls-store-memory.ts +0 -32
  151. package/src/polls.test.ts +0 -160
  152. package/src/polls.ts +0 -323
  153. package/src/presentation.ts +0 -68
  154. package/src/probe.test.ts +0 -77
  155. package/src/probe.ts +0 -132
  156. package/src/reply-dispatcher.test.ts +0 -437
  157. package/src/reply-dispatcher.ts +0 -346
  158. package/src/reply-stream-controller.test.ts +0 -235
  159. package/src/reply-stream-controller.ts +0 -147
  160. package/src/resolve-allowlist.test.ts +0 -250
  161. package/src/resolve-allowlist.ts +0 -309
  162. package/src/revoked-context.ts +0 -17
  163. package/src/runtime.ts +0 -9
  164. package/src/sdk-types.ts +0 -59
  165. package/src/sdk.test.ts +0 -666
  166. package/src/sdk.ts +0 -884
  167. package/src/secret-contract.ts +0 -49
  168. package/src/secret-input.ts +0 -7
  169. package/src/send-context.ts +0 -231
  170. package/src/send.test.ts +0 -493
  171. package/src/send.ts +0 -637
  172. package/src/sent-message-cache.test.ts +0 -15
  173. package/src/sent-message-cache.ts +0 -56
  174. package/src/session-route.ts +0 -40
  175. package/src/setup-core.ts +0 -160
  176. package/src/setup-surface.test.ts +0 -202
  177. package/src/setup-surface.ts +0 -320
  178. package/src/sso-token-store.test.ts +0 -72
  179. package/src/sso-token-store.ts +0 -166
  180. package/src/sso.ts +0 -300
  181. package/src/storage.ts +0 -25
  182. package/src/store-fs.ts +0 -44
  183. package/src/streaming-message.test.ts +0 -262
  184. package/src/streaming-message.ts +0 -297
  185. package/src/test-runtime.ts +0 -16
  186. package/src/thread-parent-context.test.ts +0 -224
  187. package/src/thread-parent-context.ts +0 -159
  188. package/src/token-response.ts +0 -11
  189. package/src/token.test.ts +0 -259
  190. package/src/token.ts +0 -195
  191. package/src/user-agent.test.ts +0 -86
  192. package/src/user-agent.ts +0 -53
  193. package/src/webhook-timeouts.ts +0 -27
  194. package/src/welcome-card.test.ts +0 -81
  195. package/src/welcome-card.ts +0 -57
  196. package/test-api.ts +0 -1
  197. package/tsconfig.json +0 -16
@@ -1,865 +0,0 @@
1
- import { mkdtemp, rm, writeFile } from "node:fs/promises";
2
- import path from "node:path";
3
- import { SILENT_REPLY_TOKEN } from "openclaw/plugin-sdk/reply-chunking";
4
- import type { PluginRuntime } from "openclaw/plugin-sdk/runtime-store";
5
- import { resolvePreferredOpenClawTmpDir } from "openclaw/plugin-sdk/temp-path";
6
- import { beforeEach, describe, expect, it, vi } from "vitest";
7
- import type { StoredConversationReference } from "./conversation-store.js";
8
- const graphUploadMockState = vi.hoisted(() => ({
9
- uploadAndShareOneDrive: vi.fn(),
10
- uploadAndShareSharePoint: vi.fn(),
11
- getDriveItemProperties: vi.fn(),
12
- }));
13
-
14
- vi.mock("./graph-upload.js", () => {
15
- return {
16
- uploadAndShareOneDrive: graphUploadMockState.uploadAndShareOneDrive,
17
- uploadAndShareSharePoint: graphUploadMockState.uploadAndShareSharePoint,
18
- getDriveItemProperties: graphUploadMockState.getDriveItemProperties,
19
- };
20
- });
21
-
22
- import {
23
- buildActivity,
24
- buildConversationReference,
25
- renderReplyPayloadsToMessages,
26
- sendMSTeamsMessages,
27
- type MSTeamsAdapter,
28
- } from "./messenger.js";
29
- import { setMSTeamsRuntime } from "./runtime.js";
30
-
31
- const chunkMarkdownText = (text: string, limit: number) => {
32
- if (!text) {
33
- return [];
34
- }
35
- if (limit <= 0 || text.length <= limit) {
36
- return [text];
37
- }
38
- const chunks: string[] = [];
39
- for (let index = 0; index < text.length; index += limit) {
40
- chunks.push(text.slice(index, index + limit));
41
- }
42
- return chunks;
43
- };
44
-
45
- const runtimeStub = {
46
- config: {
47
- loadConfig: () => ({}),
48
- },
49
- channel: {
50
- text: {
51
- chunkMarkdownText,
52
- chunkMarkdownTextWithMode: chunkMarkdownText,
53
- resolveMarkdownTableMode: () => "code",
54
- convertMarkdownTables: (text: string) => text,
55
- },
56
- },
57
- } as unknown as PluginRuntime;
58
-
59
- const noopUpdateActivity = async () => {};
60
- const noopDeleteActivity = async () => {};
61
-
62
- const createNoopAdapter = (): MSTeamsAdapter => ({
63
- continueConversation: async () => {},
64
- process: async () => {},
65
- updateActivity: noopUpdateActivity,
66
- deleteActivity: noopDeleteActivity,
67
- });
68
-
69
- const createRecordedSendActivity = (
70
- sink: string[],
71
- failFirstWithStatusCode?: number,
72
- ): ((activity: unknown) => Promise<{ id: string }>) => {
73
- let attempts = 0;
74
- return async (activity: unknown) => {
75
- const { text } = activity as { text?: string };
76
- const content = text ?? "";
77
- sink.push(content);
78
- attempts += 1;
79
- if (failFirstWithStatusCode !== undefined && attempts === 1) {
80
- throw Object.assign(new Error("send failed"), { statusCode: failFirstWithStatusCode });
81
- }
82
- return { id: `id:${content}` };
83
- };
84
- };
85
-
86
- const REVOCATION_ERROR = "Cannot perform 'set' on a proxy that has been revoked";
87
-
88
- function requireConversationId(ref: { conversation?: { id?: string } }) {
89
- if (!ref.conversation?.id) {
90
- throw new Error("expected Teams top-level send to preserve conversation id");
91
- }
92
- return ref.conversation.id;
93
- }
94
-
95
- function requireSentMessage(sent: Array<{ text?: string; entities?: unknown[] }>) {
96
- const firstSent = sent[0];
97
- if (!firstSent?.text) {
98
- throw new Error("expected Teams message send to include rendered text");
99
- }
100
- return firstSent;
101
- }
102
-
103
- const createFallbackAdapter = (proactiveSent: string[]): MSTeamsAdapter => ({
104
- continueConversation: async (_appId, _reference, logic) => {
105
- await logic({
106
- sendActivity: createRecordedSendActivity(proactiveSent),
107
- updateActivity: noopUpdateActivity,
108
- deleteActivity: noopDeleteActivity,
109
- });
110
- },
111
- process: async () => {},
112
- updateActivity: noopUpdateActivity,
113
- deleteActivity: noopDeleteActivity,
114
- });
115
-
116
- describe("msteams messenger", () => {
117
- beforeEach(() => {
118
- setMSTeamsRuntime(runtimeStub);
119
- graphUploadMockState.uploadAndShareOneDrive.mockReset();
120
- graphUploadMockState.uploadAndShareSharePoint.mockReset();
121
- graphUploadMockState.getDriveItemProperties.mockReset();
122
- graphUploadMockState.uploadAndShareOneDrive.mockResolvedValue({
123
- itemId: "item123",
124
- webUrl: "https://onedrive.example.com/item123",
125
- shareUrl: "https://onedrive.example.com/share/item123",
126
- name: "upload.txt",
127
- });
128
- });
129
-
130
- describe("renderReplyPayloadsToMessages", () => {
131
- it("filters silent replies", () => {
132
- const messages = renderReplyPayloadsToMessages([{ text: SILENT_REPLY_TOKEN }], {
133
- textChunkLimit: 4000,
134
- tableMode: "code",
135
- });
136
- expect(messages).toEqual([]);
137
- });
138
-
139
- it("does not filter non-exact silent reply prefixes", () => {
140
- const messages = renderReplyPayloadsToMessages(
141
- [{ text: `${SILENT_REPLY_TOKEN} -- ignored` }],
142
- { textChunkLimit: 4000, tableMode: "code" },
143
- );
144
- expect(messages).toEqual([{ text: `${SILENT_REPLY_TOKEN} -- ignored` }]);
145
- });
146
-
147
- it("splits media into separate messages by default", () => {
148
- const messages = renderReplyPayloadsToMessages(
149
- [{ text: "hi", mediaUrl: "https://example.com/a.png" }],
150
- { textChunkLimit: 4000, tableMode: "code" },
151
- );
152
- expect(messages).toEqual([{ text: "hi" }, { mediaUrl: "https://example.com/a.png" }]);
153
- });
154
-
155
- it("supports inline media mode", () => {
156
- const messages = renderReplyPayloadsToMessages(
157
- [{ text: "hi", mediaUrl: "https://example.com/a.png" }],
158
- { textChunkLimit: 4000, mediaMode: "inline", tableMode: "code" },
159
- );
160
- expect(messages).toEqual([{ text: "hi", mediaUrl: "https://example.com/a.png" }]);
161
- });
162
-
163
- it("chunks long text when enabled", () => {
164
- const long = "hello ".repeat(200);
165
- const messages = renderReplyPayloadsToMessages([{ text: long }], {
166
- textChunkLimit: 50,
167
- tableMode: "code",
168
- });
169
- expect(messages.length).toBeGreaterThan(1);
170
- });
171
- });
172
-
173
- describe("sendMSTeamsMessages", () => {
174
- function createRevokedThreadContext(params?: { failAfterAttempt?: number; sent?: string[] }) {
175
- let attempt = 0;
176
- return {
177
- sendActivity: async (activity: unknown) => {
178
- const { text } = activity as { text?: string };
179
- const content = text ?? "";
180
- attempt += 1;
181
- if (params?.failAfterAttempt && attempt < params.failAfterAttempt) {
182
- params.sent?.push(content);
183
- return { id: `id:${content}` };
184
- }
185
- throw new TypeError(REVOCATION_ERROR);
186
- },
187
- updateActivity: noopUpdateActivity,
188
- deleteActivity: noopDeleteActivity,
189
- };
190
- }
191
-
192
- const baseRef: StoredConversationReference = {
193
- activityId: "activity123",
194
- user: { id: "user123", name: "User" },
195
- agent: { id: "bot123", name: "Bot" },
196
- conversation: { id: "19:abc@thread.tacv2;messageid=deadbeef" },
197
- channelId: "msteams",
198
- serviceUrl: "https://service.example.com",
199
- };
200
-
201
- async function sendAndCaptureRevokeFallbackReference(params: {
202
- conversation: StoredConversationReference["conversation"];
203
- activityId?: string;
204
- threadId?: string;
205
- }) {
206
- const proactiveSent: string[] = [];
207
- let capturedReference: unknown;
208
- const conversationRef: StoredConversationReference = {
209
- activityId: params.activityId ?? "activity456",
210
- user: { id: "user123", name: "User" },
211
- agent: { id: "bot123", name: "Bot" },
212
- conversation: params.conversation,
213
- channelId: "msteams",
214
- serviceUrl: "https://service.example.com",
215
- ...(params.threadId ? { threadId: params.threadId } : {}),
216
- };
217
- const adapter: MSTeamsAdapter = {
218
- continueConversation: async (_appId, reference, logic) => {
219
- capturedReference = reference;
220
- await logic({
221
- sendActivity: createRecordedSendActivity(proactiveSent),
222
- updateActivity: noopUpdateActivity,
223
- deleteActivity: noopDeleteActivity,
224
- });
225
- },
226
- process: async () => {},
227
- updateActivity: noopUpdateActivity,
228
- deleteActivity: noopDeleteActivity,
229
- };
230
-
231
- await sendMSTeamsMessages({
232
- replyStyle: "thread",
233
- adapter,
234
- appId: "app123",
235
- conversationRef,
236
- context: createRevokedThreadContext(),
237
- messages: [{ text: "hello" }],
238
- });
239
-
240
- return {
241
- proactiveSent,
242
- reference: capturedReference as { conversation?: { id?: string }; activityId?: string },
243
- };
244
- }
245
-
246
- it("sends thread messages via the provided context", async () => {
247
- const sent: string[] = [];
248
- const ctx = {
249
- sendActivity: createRecordedSendActivity(sent),
250
- updateActivity: noopUpdateActivity,
251
- deleteActivity: noopDeleteActivity,
252
- };
253
- const adapter = createNoopAdapter();
254
-
255
- const ids = await sendMSTeamsMessages({
256
- replyStyle: "thread",
257
- adapter,
258
- appId: "app123",
259
- conversationRef: baseRef,
260
- context: ctx,
261
- messages: [{ text: "one" }, { text: "two" }],
262
- });
263
-
264
- expect(sent).toEqual(["one", "two"]);
265
- expect(ids).toEqual(["id:one", "id:two"]);
266
- });
267
-
268
- it("sends top-level messages via continueConversation and strips activityId", async () => {
269
- const seen: { reference?: unknown; texts: string[] } = { texts: [] };
270
-
271
- const adapter: MSTeamsAdapter = {
272
- continueConversation: async (_appId, reference, logic) => {
273
- seen.reference = reference;
274
- await logic({
275
- sendActivity: createRecordedSendActivity(seen.texts),
276
- updateActivity: noopUpdateActivity,
277
- deleteActivity: noopDeleteActivity,
278
- });
279
- },
280
- process: async () => {},
281
- updateActivity: noopUpdateActivity,
282
- deleteActivity: noopDeleteActivity,
283
- };
284
-
285
- const ids = await sendMSTeamsMessages({
286
- replyStyle: "top-level",
287
- adapter,
288
- appId: "app123",
289
- conversationRef: baseRef,
290
- messages: [{ text: "hello" }],
291
- });
292
-
293
- expect(seen.texts).toEqual(["hello"]);
294
- expect(ids).toEqual(["id:hello"]);
295
-
296
- const ref = seen.reference as {
297
- activityId?: string;
298
- conversation?: { id?: string };
299
- };
300
- expect(ref.activityId).toBeUndefined();
301
- expect(requireConversationId(ref)).toBe("19:abc@thread.tacv2");
302
- });
303
-
304
- it("preserves parsed mentions when appending OneDrive fallback file links", async () => {
305
- const tmpDir = await mkdtemp(path.join(resolvePreferredOpenClawTmpDir(), "msteams-mention-"));
306
- const localFile = path.join(tmpDir, "note.txt");
307
- await writeFile(localFile, "hello");
308
-
309
- try {
310
- const sent: Array<{ text?: string; entities?: unknown[] }> = [];
311
- const ctx = {
312
- sendActivity: async (activity: unknown) => {
313
- sent.push(activity as { text?: string; entities?: unknown[] });
314
- return { id: "id:one" };
315
- },
316
- updateActivity: noopUpdateActivity,
317
- deleteActivity: noopDeleteActivity,
318
- };
319
-
320
- const adapter = createNoopAdapter();
321
-
322
- const ids = await sendMSTeamsMessages({
323
- replyStyle: "thread",
324
- adapter,
325
- appId: "app123",
326
- conversationRef: {
327
- ...baseRef,
328
- conversation: {
329
- ...baseRef.conversation,
330
- conversationType: "channel",
331
- },
332
- },
333
- context: ctx,
334
- messages: [{ text: "Hello @[John](29:08q2j2o3jc09au90eucae)", mediaUrl: localFile }],
335
- tokenProvider: {
336
- getAccessToken: async () => "token",
337
- },
338
- });
339
-
340
- expect(ids).toEqual(["id:one"]);
341
- expect(graphUploadMockState.uploadAndShareOneDrive).toHaveBeenCalledOnce();
342
- expect(sent).toHaveLength(1);
343
- const firstSent = requireSentMessage(sent);
344
- expect(firstSent.text).toContain("Hello <at>John</at>");
345
- expect(firstSent.text).toContain(
346
- "📎 [upload.txt](https://onedrive.example.com/share/item123)",
347
- );
348
- expect(sent[0]?.entities).toEqual(
349
- expect.arrayContaining([
350
- {
351
- type: "mention",
352
- text: "<at>John</at>",
353
- mentioned: {
354
- id: "29:08q2j2o3jc09au90eucae",
355
- name: "John",
356
- },
357
- },
358
- expect.objectContaining({
359
- additionalType: ["AIGeneratedContent"],
360
- }),
361
- ]),
362
- );
363
- } finally {
364
- await rm(tmpDir, { recursive: true, force: true });
365
- }
366
- });
367
-
368
- it("retries thread sends on throttling (429)", async () => {
369
- const attempts: string[] = [];
370
- const retryEvents: Array<{ nextAttempt: number; delayMs: number }> = [];
371
-
372
- const ctx = {
373
- sendActivity: createRecordedSendActivity(attempts, 429),
374
- updateActivity: noopUpdateActivity,
375
- deleteActivity: noopDeleteActivity,
376
- };
377
- const adapter = createNoopAdapter();
378
-
379
- const ids = await sendMSTeamsMessages({
380
- replyStyle: "thread",
381
- adapter,
382
- appId: "app123",
383
- conversationRef: baseRef,
384
- context: ctx,
385
- messages: [{ text: "one" }],
386
- retry: { maxAttempts: 2, baseDelayMs: 0, maxDelayMs: 0 },
387
- onRetry: (e) => retryEvents.push({ nextAttempt: e.nextAttempt, delayMs: e.delayMs }),
388
- });
389
-
390
- expect(attempts).toEqual(["one", "one"]);
391
- expect(ids).toEqual(["id:one"]);
392
- expect(retryEvents).toEqual([{ nextAttempt: 2, delayMs: 0 }]);
393
- });
394
-
395
- it("retries full activity preparation when media upload fails transiently", async () => {
396
- const tmpDir = await mkdtemp(path.join(resolvePreferredOpenClawTmpDir(), "msteams-retry-"));
397
- const localFile = path.join(tmpDir, "retry.txt");
398
- await writeFile(localFile, "hello");
399
-
400
- try {
401
- const attempts: string[] = [];
402
- const retryEvents: Array<{ nextAttempt: number; delayMs: number }> = [];
403
- let uploadAttempts = 0;
404
- graphUploadMockState.uploadAndShareOneDrive.mockImplementation(async () => {
405
- uploadAttempts += 1;
406
- if (uploadAttempts === 1) {
407
- throw Object.assign(new Error("transient upload failure"), { statusCode: 429 });
408
- }
409
- return {
410
- itemId: "item123",
411
- webUrl: "https://onedrive.example.com/item123",
412
- shareUrl: "https://onedrive.example.com/share/item123",
413
- name: "retry.txt",
414
- };
415
- });
416
-
417
- const ctx = {
418
- sendActivity: createRecordedSendActivity(attempts),
419
- updateActivity: noopUpdateActivity,
420
- deleteActivity: noopDeleteActivity,
421
- };
422
- const adapter = createNoopAdapter();
423
-
424
- const ids = await sendMSTeamsMessages({
425
- replyStyle: "thread",
426
- adapter,
427
- appId: "app123",
428
- conversationRef: {
429
- ...baseRef,
430
- conversation: {
431
- ...baseRef.conversation,
432
- conversationType: "channel",
433
- },
434
- },
435
- context: ctx,
436
- messages: [{ text: "one", mediaUrl: localFile }],
437
- tokenProvider: {
438
- getAccessToken: async () => "token",
439
- },
440
- retry: { maxAttempts: 2, baseDelayMs: 0, maxDelayMs: 0 },
441
- onRetry: (e) => retryEvents.push({ nextAttempt: e.nextAttempt, delayMs: e.delayMs }),
442
- });
443
-
444
- expect(uploadAttempts).toBe(2);
445
- expect(attempts).toHaveLength(1);
446
- expect(attempts[0]).toContain("📎 [retry.txt]");
447
- expect(ids).toEqual([`id:${attempts[0]}`]);
448
- expect(retryEvents).toEqual([{ nextAttempt: 2, delayMs: 0 }]);
449
- } finally {
450
- await rm(tmpDir, { recursive: true, force: true });
451
- }
452
- });
453
-
454
- it("does not retry thread sends on client errors (4xx)", async () => {
455
- const ctx = {
456
- sendActivity: async () => {
457
- throw Object.assign(new Error("bad request"), { statusCode: 400 });
458
- },
459
- updateActivity: noopUpdateActivity,
460
- deleteActivity: noopDeleteActivity,
461
- };
462
-
463
- const adapter = createNoopAdapter();
464
-
465
- await expect(
466
- sendMSTeamsMessages({
467
- replyStyle: "thread",
468
- adapter,
469
- appId: "app123",
470
- conversationRef: baseRef,
471
- context: ctx,
472
- messages: [{ text: "one" }],
473
- retry: { maxAttempts: 3, baseDelayMs: 0, maxDelayMs: 0 },
474
- }),
475
- ).rejects.toMatchObject({ statusCode: 400 });
476
- });
477
-
478
- it("falls back to proactive messaging when thread context is revoked", async () => {
479
- const proactiveSent: string[] = [];
480
- const ctx = createRevokedThreadContext();
481
- const adapter = createFallbackAdapter(proactiveSent);
482
-
483
- const ids = await sendMSTeamsMessages({
484
- replyStyle: "thread",
485
- adapter,
486
- appId: "app123",
487
- conversationRef: baseRef,
488
- context: ctx,
489
- messages: [{ text: "hello" }],
490
- });
491
-
492
- // Should have fallen back to proactive messaging
493
- expect(proactiveSent).toEqual(["hello"]);
494
- expect(ids).toEqual(["id:hello"]);
495
- });
496
-
497
- it("falls back only for remaining thread messages after context revocation", async () => {
498
- const threadSent: string[] = [];
499
- const proactiveSent: string[] = [];
500
- const ctx = createRevokedThreadContext({ failAfterAttempt: 2, sent: threadSent });
501
- const adapter = createFallbackAdapter(proactiveSent);
502
-
503
- const ids = await sendMSTeamsMessages({
504
- replyStyle: "thread",
505
- adapter,
506
- appId: "app123",
507
- conversationRef: baseRef,
508
- context: ctx,
509
- messages: [{ text: "one" }, { text: "two" }, { text: "three" }],
510
- });
511
-
512
- expect(threadSent).toEqual(["one"]);
513
- expect(proactiveSent).toEqual(["two", "three"]);
514
- expect(ids).toEqual(["id:one", "id:two", "id:three"]);
515
- });
516
-
517
- it("reconstructs threaded conversation ID for channel revoke fallback", async () => {
518
- const { proactiveSent, reference } = await sendAndCaptureRevokeFallbackReference({
519
- conversation: {
520
- id: "19:abc@thread.tacv2;messageid=deadbeef",
521
- conversationType: "channel",
522
- },
523
- });
524
-
525
- expect(proactiveSent).toEqual(["hello"]);
526
- // Conversation ID should include the thread suffix for channel messages
527
- expect(reference.conversation?.id).toBe("19:abc@thread.tacv2;messageid=activity456");
528
- expect(reference.activityId).toBeUndefined();
529
- });
530
-
531
- it("does not add thread suffix for group chat revoke fallback", async () => {
532
- const { proactiveSent, reference } = await sendAndCaptureRevokeFallbackReference({
533
- conversation: {
534
- id: "19:group123@thread.v2",
535
- conversationType: "groupChat",
536
- },
537
- });
538
-
539
- expect(proactiveSent).toEqual(["hello"]);
540
- // Group chat should NOT have thread suffix — flat conversation
541
- expect(reference.conversation?.id).toBe("19:group123@thread.v2");
542
- expect(reference.activityId).toBeUndefined();
543
- });
544
-
545
- it("uses threadId instead of activityId for channel revoke fallback (#58030)", async () => {
546
- const { proactiveSent, reference } = await sendAndCaptureRevokeFallbackReference({
547
- activityId: "current-message-id",
548
- conversation: {
549
- id: "19:abc@thread.tacv2",
550
- conversationType: "channel",
551
- },
552
- // threadId is the thread root, which differs from activityId (current message)
553
- threadId: "thread-root-msg-id",
554
- });
555
-
556
- expect(proactiveSent).toEqual(["hello"]);
557
- // Should use threadId (thread root), NOT activityId (current message)
558
- expect(reference.conversation?.id).toBe("19:abc@thread.tacv2;messageid=thread-root-msg-id");
559
- expect(reference.activityId).toBeUndefined();
560
- });
561
-
562
- it("falls back to activityId when threadId is not set (backward compat)", async () => {
563
- const { proactiveSent, reference } = await sendAndCaptureRevokeFallbackReference({
564
- activityId: "legacy-activity-id",
565
- conversation: {
566
- id: "19:abc@thread.tacv2",
567
- conversationType: "channel",
568
- },
569
- // No threadId — older stored references may not have it
570
- });
571
-
572
- expect(proactiveSent).toEqual(["hello"]);
573
- // Falls back to activityId when threadId is missing
574
- expect(reference.conversation?.id).toBe("19:abc@thread.tacv2;messageid=legacy-activity-id");
575
- });
576
-
577
- it("does not add thread suffix for top-level replyStyle even with threadId set", async () => {
578
- let capturedReference: unknown;
579
- const sent: string[] = [];
580
-
581
- const channelRef: StoredConversationReference = {
582
- activityId: "current-msg",
583
- user: { id: "user123", name: "User" },
584
- agent: { id: "bot123", name: "Bot" },
585
- conversation: {
586
- id: "19:abc@thread.tacv2",
587
- conversationType: "channel",
588
- },
589
- channelId: "msteams",
590
- serviceUrl: "https://service.example.com",
591
- threadId: "thread-root-msg-id",
592
- };
593
-
594
- const adapter: MSTeamsAdapter = {
595
- continueConversation: async (_appId, reference, logic) => {
596
- capturedReference = reference;
597
- await logic({
598
- sendActivity: createRecordedSendActivity(sent),
599
- updateActivity: noopUpdateActivity,
600
- deleteActivity: noopDeleteActivity,
601
- });
602
- },
603
- process: async () => {},
604
- updateActivity: noopUpdateActivity,
605
- deleteActivity: noopDeleteActivity,
606
- };
607
-
608
- await sendMSTeamsMessages({
609
- replyStyle: "top-level",
610
- adapter,
611
- appId: "app123",
612
- conversationRef: channelRef,
613
- messages: [{ text: "hello" }],
614
- });
615
-
616
- expect(sent).toEqual(["hello"]);
617
- const ref = capturedReference as { conversation?: { id?: string } };
618
- // Top-level sends should NOT include thread suffix
619
- expect(ref.conversation?.id).toBe("19:abc@thread.tacv2");
620
- });
621
-
622
- it("retries top-level sends on transient (5xx)", async () => {
623
- const attempts: string[] = [];
624
-
625
- const adapter: MSTeamsAdapter = {
626
- continueConversation: async (_appId, _reference, logic) => {
627
- await logic({
628
- sendActivity: createRecordedSendActivity(attempts, 503),
629
- updateActivity: noopUpdateActivity,
630
- deleteActivity: noopDeleteActivity,
631
- });
632
- },
633
- process: async () => {},
634
- updateActivity: noopUpdateActivity,
635
- deleteActivity: noopDeleteActivity,
636
- };
637
-
638
- const ids = await sendMSTeamsMessages({
639
- replyStyle: "top-level",
640
- adapter,
641
- appId: "app123",
642
- conversationRef: baseRef,
643
- messages: [{ text: "hello" }],
644
- retry: { maxAttempts: 2, baseDelayMs: 0, maxDelayMs: 0 },
645
- });
646
-
647
- expect(attempts).toEqual(["hello", "hello"]);
648
- expect(ids).toEqual(["id:hello"]);
649
- });
650
-
651
- it("delivers all blocks in a multi-block reply via a single continueConversation call (#29379)", async () => {
652
- // Regression: multiple text blocks (e.g. text -> tool -> text) must all
653
- // reach the user. Previously each deliver() call opened a separate
654
- // continueConversation(); Teams silently drops blocks 2+ in that case.
655
- // The fix batches all rendered messages into one sendMSTeamsMessages call
656
- // so they share a single continueConversation().
657
- const conversationCallTexts: string[][] = [];
658
- const adapter: MSTeamsAdapter = {
659
- continueConversation: async (_appId, _reference, logic) => {
660
- const batchTexts: string[] = [];
661
- await logic({
662
- sendActivity: async (activity: unknown) => {
663
- const { text } = activity as { text?: string };
664
- batchTexts.push(text ?? "");
665
- return { id: `id:${text ?? ""}` };
666
- },
667
- updateActivity: noopUpdateActivity,
668
- deleteActivity: noopDeleteActivity,
669
- });
670
- conversationCallTexts.push(batchTexts);
671
- },
672
- process: async () => {},
673
- updateActivity: noopUpdateActivity,
674
- deleteActivity: noopDeleteActivity,
675
- };
676
-
677
- // Three blocks (text + code + text) sent together in one call.
678
- const ids = await sendMSTeamsMessages({
679
- replyStyle: "top-level",
680
- adapter,
681
- appId: "app123",
682
- conversationRef: baseRef,
683
- messages: [
684
- { text: "Let me look that up..." },
685
- { text: "```\nresult = 42\n```" },
686
- { text: "The answer is 42." },
687
- ],
688
- });
689
-
690
- // All three blocks delivered.
691
- expect(ids).toHaveLength(3);
692
- // All three arrive in a single continueConversation() call, not three.
693
- expect(conversationCallTexts).toHaveLength(1);
694
- expect(conversationCallTexts[0]).toEqual([
695
- "Let me look that up...",
696
- "```\nresult = 42\n```",
697
- "The answer is 42.",
698
- ]);
699
- });
700
- });
701
-
702
- describe("buildActivity AI metadata", () => {
703
- const baseRef: StoredConversationReference = {
704
- activityId: "activity123",
705
- user: { id: "user123", name: "User" },
706
- agent: { id: "bot123", name: "Bot" },
707
- conversation: { id: "conv123", conversationType: "personal" },
708
- channelId: "msteams",
709
- serviceUrl: "https://service.example.com",
710
- };
711
-
712
- it("adds AI-generated entity to text messages", async () => {
713
- const activity = await buildActivity({ text: "hello" }, baseRef);
714
- const entities = activity.entities as Array<Record<string, unknown>>;
715
- expect(entities).toEqual(
716
- expect.arrayContaining([
717
- expect.objectContaining({
718
- type: "https://schema.org/Message",
719
- "@type": "Message",
720
- additionalType: ["AIGeneratedContent"],
721
- }),
722
- ]),
723
- );
724
- });
725
-
726
- it("adds AI-generated entity to media-only messages", async () => {
727
- const activity = await buildActivity({ mediaUrl: "https://example.com/img.png" }, baseRef);
728
- const entities = activity.entities as Array<Record<string, unknown>>;
729
- expect(entities).toEqual(
730
- expect.arrayContaining([
731
- expect.objectContaining({
732
- additionalType: ["AIGeneratedContent"],
733
- }),
734
- ]),
735
- );
736
- });
737
-
738
- it("preserves mention entities alongside AI entity", async () => {
739
- const activity = await buildActivity({ text: "hi <at>@User</at>" }, baseRef);
740
- const entities = activity.entities as Array<Record<string, unknown>>;
741
- // Should have at least the AI entity
742
- expect(entities.length).toBeGreaterThanOrEqual(1);
743
- expect(entities).toEqual(
744
- expect.arrayContaining([
745
- expect.objectContaining({
746
- additionalType: ["AIGeneratedContent"],
747
- }),
748
- ]),
749
- );
750
- });
751
-
752
- it("sets feedbackLoopEnabled in channelData when enabled", async () => {
753
- const activity = await buildActivity(
754
- { text: "hello" },
755
- baseRef,
756
- undefined,
757
- undefined,
758
- undefined,
759
- {
760
- feedbackLoopEnabled: true,
761
- },
762
- );
763
- const channelData = activity.channelData as Record<string, unknown>;
764
- expect(channelData.feedbackLoopEnabled).toBe(true);
765
- });
766
-
767
- it("defaults feedbackLoopEnabled to false", async () => {
768
- const activity = await buildActivity({ text: "hello" }, baseRef);
769
- const channelData = activity.channelData as Record<string, unknown>;
770
- expect(channelData.feedbackLoopEnabled).toBe(false);
771
- });
772
- });
773
-
774
- // Regression coverage for #58774: proactive Teams sends fail with HTTP 403
775
- // when the Bot Framework connector does not see `tenantId` / `aadObjectId`
776
- // on the outbound conversation reference.
777
- describe("buildConversationReference tenant/aad forwarding (#58774)", () => {
778
- const storedWithChannelDataTenant: StoredConversationReference = {
779
- activityId: "activity-1",
780
- user: { id: "user123", name: "User", aadObjectId: "aad-user-123" },
781
- agent: { id: "bot123", name: "Bot" },
782
- conversation: {
783
- id: "19:abc@thread.tacv2",
784
- conversationType: "channel",
785
- },
786
- // Canonical channelData source captured by message-handler inbound code.
787
- tenantId: "tenant-abc",
788
- aadObjectId: "aad-user-123",
789
- channelId: "msteams",
790
- serviceUrl: "https://smba.trafficmanager.net/amer/",
791
- };
792
-
793
- it("forwards top-level tenantId and aadObjectId onto the outbound reference", () => {
794
- const reference = buildConversationReference(storedWithChannelDataTenant);
795
- expect(reference.tenantId).toBe("tenant-abc");
796
- expect(reference.aadObjectId).toBe("aad-user-123");
797
- expect(reference.conversation.tenantId).toBe("tenant-abc");
798
- expect(reference.user?.aadObjectId).toBe("aad-user-123");
799
- });
800
-
801
- it("falls back to conversation.tenantId when no top-level tenantId is stored (legacy ref)", () => {
802
- const legacy: StoredConversationReference = {
803
- activityId: "activity-legacy",
804
- user: { id: "user-legacy", name: "Legacy", aadObjectId: "aad-legacy" },
805
- agent: { id: "bot-legacy", name: "Bot" },
806
- conversation: {
807
- id: "a:personal-chat",
808
- conversationType: "personal",
809
- tenantId: "tenant-legacy",
810
- },
811
- channelId: "msteams",
812
- serviceUrl: "https://smba.trafficmanager.net/amer/",
813
- };
814
- const reference = buildConversationReference(legacy);
815
- expect(reference.tenantId).toBe("tenant-legacy");
816
- expect(reference.aadObjectId).toBe("aad-legacy");
817
- });
818
-
819
- it("omits tenantId and aadObjectId when neither source is available", () => {
820
- const minimal: StoredConversationReference = {
821
- activityId: "activity-2",
822
- user: { id: "user456", name: "User" },
823
- agent: { id: "bot456", name: "Bot" },
824
- conversation: { id: "19:xyz@thread.tacv2", conversationType: "channel" },
825
- channelId: "msteams",
826
- serviceUrl: "https://smba.trafficmanager.net/amer/",
827
- };
828
- const reference = buildConversationReference(minimal);
829
- expect(reference.tenantId).toBeUndefined();
830
- expect(reference.aadObjectId).toBeUndefined();
831
- expect(reference.conversation.tenantId).toBeUndefined();
832
- });
833
-
834
- it("propagates tenantId/aadObjectId through sendMSTeamsMessages proactive path", async () => {
835
- let capturedReference:
836
- | { tenantId?: string; aadObjectId?: string; user?: { aadObjectId?: string } }
837
- | undefined;
838
- const adapter: MSTeamsAdapter = {
839
- continueConversation: async (_appId, reference, logic) => {
840
- capturedReference = reference as typeof capturedReference;
841
- await logic({
842
- sendActivity: async () => ({ id: "ok" }),
843
- updateActivity: noopUpdateActivity,
844
- deleteActivity: noopDeleteActivity,
845
- });
846
- },
847
- process: async () => {},
848
- updateActivity: noopUpdateActivity,
849
- deleteActivity: noopDeleteActivity,
850
- };
851
-
852
- await sendMSTeamsMessages({
853
- replyStyle: "top-level",
854
- adapter,
855
- appId: "app123",
856
- conversationRef: storedWithChannelDataTenant,
857
- messages: [{ text: "hello" }],
858
- });
859
-
860
- expect(capturedReference?.tenantId).toBe("tenant-abc");
861
- expect(capturedReference?.aadObjectId).toBe("aad-user-123");
862
- expect(capturedReference?.user?.aadObjectId).toBe("aad-user-123");
863
- });
864
- });
865
- });