@openclaw/msteams 2026.3.13 → 2026.5.2-beta.1
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.
- package/api.ts +3 -0
- package/channel-config-api.ts +1 -0
- package/channel-plugin-api.ts +2 -0
- package/config-api.ts +4 -0
- package/contract-api.ts +4 -0
- package/index.ts +15 -12
- package/openclaw.plugin.json +553 -1
- package/package.json +46 -12
- package/runtime-api.ts +73 -0
- package/secret-contract-api.ts +5 -0
- package/setup-entry.ts +13 -0
- package/setup-plugin-api.ts +3 -0
- package/src/ai-entity.ts +7 -0
- package/src/approval-auth.ts +44 -0
- package/src/attachments/bot-framework.test.ts +461 -0
- package/src/attachments/bot-framework.ts +362 -0
- package/src/attachments/download.ts +63 -19
- package/src/attachments/graph.test.ts +416 -0
- package/src/attachments/graph.ts +163 -72
- package/src/attachments/html.ts +33 -1
- package/src/attachments/payload.ts +1 -1
- package/src/attachments/remote-media.test.ts +137 -0
- package/src/attachments/remote-media.ts +75 -8
- package/src/attachments/shared.test.ts +138 -1
- package/src/attachments/shared.ts +193 -26
- package/src/attachments/types.ts +10 -0
- package/src/attachments.graph.test.ts +342 -0
- package/src/attachments.helpers.test.ts +246 -0
- package/src/attachments.test-helpers.ts +17 -0
- package/src/attachments.test.ts +163 -418
- package/src/attachments.ts +5 -5
- package/src/block-streaming-config.test.ts +61 -0
- package/src/channel-api.ts +1 -0
- package/src/channel.actions.test.ts +742 -0
- package/src/channel.directory.test.ts +145 -4
- package/src/channel.runtime.ts +56 -0
- package/src/channel.setup.ts +77 -0
- package/src/channel.test.ts +128 -0
- package/src/channel.ts +1077 -395
- package/src/config-schema.ts +6 -0
- package/src/config-ui-hints.ts +12 -0
- package/src/conversation-store-fs.test.ts +4 -5
- package/src/conversation-store-fs.ts +35 -51
- package/src/conversation-store-helpers.test.ts +202 -0
- package/src/conversation-store-helpers.ts +105 -0
- package/src/conversation-store-memory.ts +27 -23
- package/src/conversation-store.shared.test.ts +225 -0
- package/src/conversation-store.ts +30 -0
- package/src/directory-live.test.ts +156 -0
- package/src/directory-live.ts +7 -4
- package/src/doctor.ts +27 -0
- package/src/errors.test.ts +64 -1
- package/src/errors.ts +50 -9
- package/src/feedback-reflection-prompt.ts +117 -0
- package/src/feedback-reflection-store.ts +114 -0
- package/src/feedback-reflection.test.ts +237 -0
- package/src/feedback-reflection.ts +283 -0
- package/src/file-consent-helpers.test.ts +83 -0
- package/src/file-consent-helpers.ts +64 -11
- package/src/file-consent-invoke.ts +150 -0
- package/src/file-consent.test.ts +363 -0
- package/src/file-consent.ts +165 -4
- package/src/graph-chat.ts +5 -3
- package/src/graph-group-management.test.ts +318 -0
- package/src/graph-group-management.ts +168 -0
- package/src/graph-members.test.ts +89 -0
- package/src/graph-members.ts +48 -0
- package/src/graph-messages.actions.test.ts +243 -0
- package/src/graph-messages.read.test.ts +391 -0
- package/src/graph-messages.search.test.ts +213 -0
- package/src/graph-messages.test-helpers.ts +50 -0
- package/src/graph-messages.ts +534 -0
- package/src/graph-teams.test.ts +215 -0
- package/src/graph-teams.ts +114 -0
- package/src/graph-thread.test.ts +246 -0
- package/src/graph-thread.ts +146 -0
- package/src/graph-upload.test.ts +161 -4
- package/src/graph-upload.ts +147 -56
- package/src/graph.test.ts +516 -0
- package/src/graph.ts +233 -21
- package/src/inbound.test.ts +156 -1
- package/src/inbound.ts +101 -1
- package/src/media-helpers.ts +1 -1
- package/src/mentions.test.ts +27 -18
- package/src/mentions.ts +2 -2
- package/src/messenger.test.ts +504 -23
- package/src/messenger.ts +133 -52
- package/src/monitor-handler/access.ts +125 -0
- package/src/monitor-handler/inbound-media.test.ts +289 -0
- package/src/monitor-handler/inbound-media.ts +57 -5
- package/src/monitor-handler/message-handler-mock-support.test-support.ts +28 -0
- package/src/monitor-handler/message-handler.authz.test.ts +588 -74
- package/src/monitor-handler/message-handler.dm-media.test.ts +54 -0
- package/src/monitor-handler/message-handler.test-support.ts +100 -0
- package/src/monitor-handler/message-handler.thread-parent.test.ts +223 -0
- package/src/monitor-handler/message-handler.thread-session.test.ts +77 -0
- package/src/monitor-handler/message-handler.ts +470 -164
- package/src/monitor-handler/reaction-handler.test.ts +267 -0
- package/src/monitor-handler/reaction-handler.ts +210 -0
- package/src/monitor-handler/thread-session.ts +17 -0
- package/src/monitor-handler.adaptive-card.test.ts +162 -0
- package/src/monitor-handler.feedback-authz.test.ts +314 -0
- package/src/monitor-handler.file-consent.test.ts +281 -79
- package/src/monitor-handler.sso.test.ts +563 -0
- package/src/monitor-handler.test-helpers.ts +180 -0
- package/src/monitor-handler.ts +459 -115
- package/src/monitor-handler.types.ts +27 -0
- package/src/monitor-types.ts +1 -0
- package/src/monitor.lifecycle.test.ts +74 -10
- package/src/monitor.test.ts +35 -1
- package/src/monitor.ts +143 -46
- package/src/oauth.flow.ts +77 -0
- package/src/oauth.shared.ts +37 -0
- package/src/oauth.test.ts +305 -0
- package/src/oauth.token.ts +158 -0
- package/src/oauth.ts +130 -0
- package/src/outbound.test.ts +10 -11
- package/src/outbound.ts +62 -44
- package/src/pending-uploads-fs.test.ts +246 -0
- package/src/pending-uploads-fs.ts +235 -0
- package/src/pending-uploads.test.ts +173 -0
- package/src/pending-uploads.ts +34 -2
- package/src/policy.test.ts +11 -5
- package/src/policy.ts +5 -5
- package/src/polls.test.ts +106 -5
- package/src/polls.ts +15 -7
- package/src/presentation.ts +68 -0
- package/src/probe.test.ts +27 -8
- package/src/probe.ts +43 -9
- package/src/reply-dispatcher.test.ts +437 -0
- package/src/reply-dispatcher.ts +259 -73
- package/src/reply-stream-controller.test.ts +235 -0
- package/src/reply-stream-controller.ts +147 -0
- package/src/resolve-allowlist.test.ts +105 -1
- package/src/resolve-allowlist.ts +112 -7
- package/src/runtime.ts +6 -3
- package/src/sdk-types.ts +43 -3
- package/src/sdk.test.ts +666 -0
- package/src/sdk.ts +867 -16
- package/src/secret-contract.ts +49 -0
- package/src/secret-input.ts +1 -1
- package/src/send-context.ts +76 -9
- package/src/send.test.ts +389 -5
- package/src/send.ts +140 -32
- package/src/sent-message-cache.ts +30 -18
- package/src/session-route.ts +40 -0
- package/src/setup-core.ts +160 -0
- package/src/setup-surface.test.ts +202 -0
- package/src/setup-surface.ts +320 -0
- package/src/sso-token-store.test.ts +72 -0
- package/src/sso-token-store.ts +166 -0
- package/src/sso.ts +300 -0
- package/src/storage.ts +1 -1
- package/src/store-fs.ts +2 -2
- package/src/streaming-message.test.ts +262 -0
- package/src/streaming-message.ts +297 -0
- package/src/test-runtime.ts +1 -1
- package/src/thread-parent-context.test.ts +224 -0
- package/src/thread-parent-context.ts +159 -0
- package/src/token.test.ts +237 -50
- package/src/token.ts +162 -7
- package/src/user-agent.test.ts +86 -0
- package/src/user-agent.ts +53 -0
- package/src/webhook-timeouts.ts +27 -0
- package/src/welcome-card.test.ts +81 -0
- package/src/welcome-card.ts +57 -0
- package/test-api.ts +1 -0
- package/tsconfig.json +16 -0
- package/CHANGELOG.md +0 -107
- package/src/file-lock.ts +0 -1
- package/src/graph-users.test.ts +0 -66
- package/src/onboarding.ts +0 -381
- package/src/polls-store.test.ts +0 -38
- package/src/revoked-context.test.ts +0 -39
- package/src/token-response.test.ts +0 -23
package/src/messenger.test.ts
CHANGED
|
@@ -1,27 +1,30 @@
|
|
|
1
1
|
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
2
|
-
import os from "node:os";
|
|
3
2
|
import path from "node:path";
|
|
4
|
-
import { SILENT_REPLY_TOKEN
|
|
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";
|
|
5
6
|
import { beforeEach, describe, expect, it, vi } from "vitest";
|
|
6
|
-
import { createPluginRuntimeMock } from "../../test-utils/plugin-runtime-mock.js";
|
|
7
7
|
import type { StoredConversationReference } from "./conversation-store.js";
|
|
8
8
|
const graphUploadMockState = vi.hoisted(() => ({
|
|
9
9
|
uploadAndShareOneDrive: vi.fn(),
|
|
10
|
+
uploadAndShareSharePoint: vi.fn(),
|
|
11
|
+
getDriveItemProperties: vi.fn(),
|
|
10
12
|
}));
|
|
11
13
|
|
|
12
|
-
vi.mock("./graph-upload.js",
|
|
13
|
-
const actual = await vi.importActual<typeof import("./graph-upload.js")>("./graph-upload.js");
|
|
14
|
+
vi.mock("./graph-upload.js", () => {
|
|
14
15
|
return {
|
|
15
|
-
...actual,
|
|
16
16
|
uploadAndShareOneDrive: graphUploadMockState.uploadAndShareOneDrive,
|
|
17
|
+
uploadAndShareSharePoint: graphUploadMockState.uploadAndShareSharePoint,
|
|
18
|
+
getDriveItemProperties: graphUploadMockState.getDriveItemProperties,
|
|
17
19
|
};
|
|
18
20
|
});
|
|
19
21
|
|
|
20
|
-
import { resolvePreferredOpenClawTmpDir } from "../../../src/infra/tmp-openclaw-dir.js";
|
|
21
22
|
import {
|
|
22
|
-
|
|
23
|
+
buildActivity,
|
|
24
|
+
buildConversationReference,
|
|
23
25
|
renderReplyPayloadsToMessages,
|
|
24
26
|
sendMSTeamsMessages,
|
|
27
|
+
type MSTeamsAdapter,
|
|
25
28
|
} from "./messenger.js";
|
|
26
29
|
import { setMSTeamsRuntime } from "./runtime.js";
|
|
27
30
|
|
|
@@ -39,7 +42,10 @@ const chunkMarkdownText = (text: string, limit: number) => {
|
|
|
39
42
|
return chunks;
|
|
40
43
|
};
|
|
41
44
|
|
|
42
|
-
const runtimeStub
|
|
45
|
+
const runtimeStub = {
|
|
46
|
+
config: {
|
|
47
|
+
loadConfig: () => ({}),
|
|
48
|
+
},
|
|
43
49
|
channel: {
|
|
44
50
|
text: {
|
|
45
51
|
chunkMarkdownText,
|
|
@@ -48,11 +54,16 @@ const runtimeStub: PluginRuntime = createPluginRuntimeMock({
|
|
|
48
54
|
convertMarkdownTables: (text: string) => text,
|
|
49
55
|
},
|
|
50
56
|
},
|
|
51
|
-
}
|
|
57
|
+
} as unknown as PluginRuntime;
|
|
58
|
+
|
|
59
|
+
const noopUpdateActivity = async () => {};
|
|
60
|
+
const noopDeleteActivity = async () => {};
|
|
52
61
|
|
|
53
62
|
const createNoopAdapter = (): MSTeamsAdapter => ({
|
|
54
63
|
continueConversation: async () => {},
|
|
55
64
|
process: async () => {},
|
|
65
|
+
updateActivity: noopUpdateActivity,
|
|
66
|
+
deleteActivity: noopDeleteActivity,
|
|
56
67
|
});
|
|
57
68
|
|
|
58
69
|
const createRecordedSendActivity = (
|
|
@@ -74,19 +85,40 @@ const createRecordedSendActivity = (
|
|
|
74
85
|
|
|
75
86
|
const REVOCATION_ERROR = "Cannot perform 'set' on a proxy that has been revoked";
|
|
76
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
|
+
|
|
77
103
|
const createFallbackAdapter = (proactiveSent: string[]): MSTeamsAdapter => ({
|
|
78
104
|
continueConversation: async (_appId, _reference, logic) => {
|
|
79
105
|
await logic({
|
|
80
106
|
sendActivity: createRecordedSendActivity(proactiveSent),
|
|
107
|
+
updateActivity: noopUpdateActivity,
|
|
108
|
+
deleteActivity: noopDeleteActivity,
|
|
81
109
|
});
|
|
82
110
|
},
|
|
83
111
|
process: async () => {},
|
|
112
|
+
updateActivity: noopUpdateActivity,
|
|
113
|
+
deleteActivity: noopDeleteActivity,
|
|
84
114
|
});
|
|
85
115
|
|
|
86
116
|
describe("msteams messenger", () => {
|
|
87
117
|
beforeEach(() => {
|
|
88
118
|
setMSTeamsRuntime(runtimeStub);
|
|
89
119
|
graphUploadMockState.uploadAndShareOneDrive.mockReset();
|
|
120
|
+
graphUploadMockState.uploadAndShareSharePoint.mockReset();
|
|
121
|
+
graphUploadMockState.getDriveItemProperties.mockReset();
|
|
90
122
|
graphUploadMockState.uploadAndShareOneDrive.mockResolvedValue({
|
|
91
123
|
itemId: "item123",
|
|
92
124
|
webUrl: "https://onedrive.example.com/item123",
|
|
@@ -152,6 +184,8 @@ describe("msteams messenger", () => {
|
|
|
152
184
|
}
|
|
153
185
|
throw new TypeError(REVOCATION_ERROR);
|
|
154
186
|
},
|
|
187
|
+
updateActivity: noopUpdateActivity,
|
|
188
|
+
deleteActivity: noopDeleteActivity,
|
|
155
189
|
};
|
|
156
190
|
}
|
|
157
191
|
|
|
@@ -164,10 +198,57 @@ describe("msteams messenger", () => {
|
|
|
164
198
|
serviceUrl: "https://service.example.com",
|
|
165
199
|
};
|
|
166
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
|
+
|
|
167
246
|
it("sends thread messages via the provided context", async () => {
|
|
168
247
|
const sent: string[] = [];
|
|
169
248
|
const ctx = {
|
|
170
249
|
sendActivity: createRecordedSendActivity(sent),
|
|
250
|
+
updateActivity: noopUpdateActivity,
|
|
251
|
+
deleteActivity: noopDeleteActivity,
|
|
171
252
|
};
|
|
172
253
|
const adapter = createNoopAdapter();
|
|
173
254
|
|
|
@@ -192,9 +273,13 @@ describe("msteams messenger", () => {
|
|
|
192
273
|
seen.reference = reference;
|
|
193
274
|
await logic({
|
|
194
275
|
sendActivity: createRecordedSendActivity(seen.texts),
|
|
276
|
+
updateActivity: noopUpdateActivity,
|
|
277
|
+
deleteActivity: noopDeleteActivity,
|
|
195
278
|
});
|
|
196
279
|
},
|
|
197
280
|
process: async () => {},
|
|
281
|
+
updateActivity: noopUpdateActivity,
|
|
282
|
+
deleteActivity: noopDeleteActivity,
|
|
198
283
|
};
|
|
199
284
|
|
|
200
285
|
const ids = await sendMSTeamsMessages({
|
|
@@ -213,7 +298,7 @@ describe("msteams messenger", () => {
|
|
|
213
298
|
conversation?: { id?: string };
|
|
214
299
|
};
|
|
215
300
|
expect(ref.activityId).toBeUndefined();
|
|
216
|
-
expect(ref
|
|
301
|
+
expect(requireConversationId(ref)).toBe("19:abc@thread.tacv2");
|
|
217
302
|
});
|
|
218
303
|
|
|
219
304
|
it("preserves parsed mentions when appending OneDrive fallback file links", async () => {
|
|
@@ -228,6 +313,8 @@ describe("msteams messenger", () => {
|
|
|
228
313
|
sent.push(activity as { text?: string; entities?: unknown[] });
|
|
229
314
|
return { id: "id:one" };
|
|
230
315
|
},
|
|
316
|
+
updateActivity: noopUpdateActivity,
|
|
317
|
+
deleteActivity: noopDeleteActivity,
|
|
231
318
|
};
|
|
232
319
|
|
|
233
320
|
const adapter = createNoopAdapter();
|
|
@@ -253,20 +340,26 @@ describe("msteams messenger", () => {
|
|
|
253
340
|
expect(ids).toEqual(["id:one"]);
|
|
254
341
|
expect(graphUploadMockState.uploadAndShareOneDrive).toHaveBeenCalledOnce();
|
|
255
342
|
expect(sent).toHaveLength(1);
|
|
256
|
-
|
|
257
|
-
expect(
|
|
343
|
+
const firstSent = requireSentMessage(sent);
|
|
344
|
+
expect(firstSent.text).toContain("Hello <at>John</at>");
|
|
345
|
+
expect(firstSent.text).toContain(
|
|
258
346
|
"📎 [upload.txt](https://onedrive.example.com/share/item123)",
|
|
259
347
|
);
|
|
260
|
-
expect(sent[0]?.entities).toEqual(
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
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
|
+
},
|
|
267
357
|
},
|
|
268
|
-
|
|
269
|
-
|
|
358
|
+
expect.objectContaining({
|
|
359
|
+
additionalType: ["AIGeneratedContent"],
|
|
360
|
+
}),
|
|
361
|
+
]),
|
|
362
|
+
);
|
|
270
363
|
} finally {
|
|
271
364
|
await rm(tmpDir, { recursive: true, force: true });
|
|
272
365
|
}
|
|
@@ -278,6 +371,8 @@ describe("msteams messenger", () => {
|
|
|
278
371
|
|
|
279
372
|
const ctx = {
|
|
280
373
|
sendActivity: createRecordedSendActivity(attempts, 429),
|
|
374
|
+
updateActivity: noopUpdateActivity,
|
|
375
|
+
deleteActivity: noopDeleteActivity,
|
|
281
376
|
};
|
|
282
377
|
const adapter = createNoopAdapter();
|
|
283
378
|
|
|
@@ -297,11 +392,72 @@ describe("msteams messenger", () => {
|
|
|
297
392
|
expect(retryEvents).toEqual([{ nextAttempt: 2, delayMs: 0 }]);
|
|
298
393
|
});
|
|
299
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
|
+
|
|
300
454
|
it("does not retry thread sends on client errors (4xx)", async () => {
|
|
301
455
|
const ctx = {
|
|
302
456
|
sendActivity: async () => {
|
|
303
457
|
throw Object.assign(new Error("bad request"), { statusCode: 400 });
|
|
304
458
|
},
|
|
459
|
+
updateActivity: noopUpdateActivity,
|
|
460
|
+
deleteActivity: noopDeleteActivity,
|
|
305
461
|
};
|
|
306
462
|
|
|
307
463
|
const adapter = createNoopAdapter();
|
|
@@ -358,14 +514,125 @@ describe("msteams messenger", () => {
|
|
|
358
514
|
expect(ids).toEqual(["id:one", "id:two", "id:three"]);
|
|
359
515
|
});
|
|
360
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
|
+
|
|
361
622
|
it("retries top-level sends on transient (5xx)", async () => {
|
|
362
623
|
const attempts: string[] = [];
|
|
363
624
|
|
|
364
625
|
const adapter: MSTeamsAdapter = {
|
|
365
626
|
continueConversation: async (_appId, _reference, logic) => {
|
|
366
|
-
await logic({
|
|
627
|
+
await logic({
|
|
628
|
+
sendActivity: createRecordedSendActivity(attempts, 503),
|
|
629
|
+
updateActivity: noopUpdateActivity,
|
|
630
|
+
deleteActivity: noopDeleteActivity,
|
|
631
|
+
});
|
|
367
632
|
},
|
|
368
633
|
process: async () => {},
|
|
634
|
+
updateActivity: noopUpdateActivity,
|
|
635
|
+
deleteActivity: noopDeleteActivity,
|
|
369
636
|
};
|
|
370
637
|
|
|
371
638
|
const ids = await sendMSTeamsMessages({
|
|
@@ -380,5 +647,219 @@ describe("msteams messenger", () => {
|
|
|
380
647
|
expect(attempts).toEqual(["hello", "hello"]);
|
|
381
648
|
expect(ids).toEqual(["id:hello"]);
|
|
382
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
|
+
});
|
|
383
864
|
});
|
|
384
865
|
});
|