@openclaw/msteams 2026.3.12 → 2026.5.1-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 +161 -9
- 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 +174 -437
- 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 +148 -14
- 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 +258 -0
- package/src/graph-upload.ts +87 -8
- 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 +522 -45
- 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 +477 -174
- 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 +301 -106
- 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 +34 -40
- 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 -101
- 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",
|
|
@@ -139,6 +171,24 @@ describe("msteams messenger", () => {
|
|
|
139
171
|
});
|
|
140
172
|
|
|
141
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
|
+
|
|
142
192
|
const baseRef: StoredConversationReference = {
|
|
143
193
|
activityId: "activity123",
|
|
144
194
|
user: { id: "user123", name: "User" },
|
|
@@ -148,10 +198,57 @@ describe("msteams messenger", () => {
|
|
|
148
198
|
serviceUrl: "https://service.example.com",
|
|
149
199
|
};
|
|
150
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
|
+
|
|
151
246
|
it("sends thread messages via the provided context", async () => {
|
|
152
247
|
const sent: string[] = [];
|
|
153
248
|
const ctx = {
|
|
154
249
|
sendActivity: createRecordedSendActivity(sent),
|
|
250
|
+
updateActivity: noopUpdateActivity,
|
|
251
|
+
deleteActivity: noopDeleteActivity,
|
|
155
252
|
};
|
|
156
253
|
const adapter = createNoopAdapter();
|
|
157
254
|
|
|
@@ -176,9 +273,13 @@ describe("msteams messenger", () => {
|
|
|
176
273
|
seen.reference = reference;
|
|
177
274
|
await logic({
|
|
178
275
|
sendActivity: createRecordedSendActivity(seen.texts),
|
|
276
|
+
updateActivity: noopUpdateActivity,
|
|
277
|
+
deleteActivity: noopDeleteActivity,
|
|
179
278
|
});
|
|
180
279
|
},
|
|
181
280
|
process: async () => {},
|
|
281
|
+
updateActivity: noopUpdateActivity,
|
|
282
|
+
deleteActivity: noopDeleteActivity,
|
|
182
283
|
};
|
|
183
284
|
|
|
184
285
|
const ids = await sendMSTeamsMessages({
|
|
@@ -197,7 +298,7 @@ describe("msteams messenger", () => {
|
|
|
197
298
|
conversation?: { id?: string };
|
|
198
299
|
};
|
|
199
300
|
expect(ref.activityId).toBeUndefined();
|
|
200
|
-
expect(ref
|
|
301
|
+
expect(requireConversationId(ref)).toBe("19:abc@thread.tacv2");
|
|
201
302
|
});
|
|
202
303
|
|
|
203
304
|
it("preserves parsed mentions when appending OneDrive fallback file links", async () => {
|
|
@@ -212,6 +313,8 @@ describe("msteams messenger", () => {
|
|
|
212
313
|
sent.push(activity as { text?: string; entities?: unknown[] });
|
|
213
314
|
return { id: "id:one" };
|
|
214
315
|
},
|
|
316
|
+
updateActivity: noopUpdateActivity,
|
|
317
|
+
deleteActivity: noopDeleteActivity,
|
|
215
318
|
};
|
|
216
319
|
|
|
217
320
|
const adapter = createNoopAdapter();
|
|
@@ -237,20 +340,26 @@ describe("msteams messenger", () => {
|
|
|
237
340
|
expect(ids).toEqual(["id:one"]);
|
|
238
341
|
expect(graphUploadMockState.uploadAndShareOneDrive).toHaveBeenCalledOnce();
|
|
239
342
|
expect(sent).toHaveLength(1);
|
|
240
|
-
|
|
241
|
-
expect(
|
|
343
|
+
const firstSent = requireSentMessage(sent);
|
|
344
|
+
expect(firstSent.text).toContain("Hello <at>John</at>");
|
|
345
|
+
expect(firstSent.text).toContain(
|
|
242
346
|
"📎 [upload.txt](https://onedrive.example.com/share/item123)",
|
|
243
347
|
);
|
|
244
|
-
expect(sent[0]?.entities).toEqual(
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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
|
+
},
|
|
251
357
|
},
|
|
252
|
-
|
|
253
|
-
|
|
358
|
+
expect.objectContaining({
|
|
359
|
+
additionalType: ["AIGeneratedContent"],
|
|
360
|
+
}),
|
|
361
|
+
]),
|
|
362
|
+
);
|
|
254
363
|
} finally {
|
|
255
364
|
await rm(tmpDir, { recursive: true, force: true });
|
|
256
365
|
}
|
|
@@ -262,6 +371,8 @@ describe("msteams messenger", () => {
|
|
|
262
371
|
|
|
263
372
|
const ctx = {
|
|
264
373
|
sendActivity: createRecordedSendActivity(attempts, 429),
|
|
374
|
+
updateActivity: noopUpdateActivity,
|
|
375
|
+
deleteActivity: noopDeleteActivity,
|
|
265
376
|
};
|
|
266
377
|
const adapter = createNoopAdapter();
|
|
267
378
|
|
|
@@ -281,11 +392,72 @@ describe("msteams messenger", () => {
|
|
|
281
392
|
expect(retryEvents).toEqual([{ nextAttempt: 2, delayMs: 0 }]);
|
|
282
393
|
});
|
|
283
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
|
+
|
|
284
454
|
it("does not retry thread sends on client errors (4xx)", async () => {
|
|
285
455
|
const ctx = {
|
|
286
456
|
sendActivity: async () => {
|
|
287
457
|
throw Object.assign(new Error("bad request"), { statusCode: 400 });
|
|
288
458
|
},
|
|
459
|
+
updateActivity: noopUpdateActivity,
|
|
460
|
+
deleteActivity: noopDeleteActivity,
|
|
289
461
|
};
|
|
290
462
|
|
|
291
463
|
const adapter = createNoopAdapter();
|
|
@@ -305,13 +477,7 @@ describe("msteams messenger", () => {
|
|
|
305
477
|
|
|
306
478
|
it("falls back to proactive messaging when thread context is revoked", async () => {
|
|
307
479
|
const proactiveSent: string[] = [];
|
|
308
|
-
|
|
309
|
-
const ctx = {
|
|
310
|
-
sendActivity: async () => {
|
|
311
|
-
throw new TypeError(REVOCATION_ERROR);
|
|
312
|
-
},
|
|
313
|
-
};
|
|
314
|
-
|
|
480
|
+
const ctx = createRevokedThreadContext();
|
|
315
481
|
const adapter = createFallbackAdapter(proactiveSent);
|
|
316
482
|
|
|
317
483
|
const ids = await sendMSTeamsMessages({
|
|
@@ -331,21 +497,7 @@ describe("msteams messenger", () => {
|
|
|
331
497
|
it("falls back only for remaining thread messages after context revocation", async () => {
|
|
332
498
|
const threadSent: string[] = [];
|
|
333
499
|
const proactiveSent: string[] = [];
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
const ctx = {
|
|
337
|
-
sendActivity: async (activity: unknown) => {
|
|
338
|
-
const { text } = activity as { text?: string };
|
|
339
|
-
const content = text ?? "";
|
|
340
|
-
attempt += 1;
|
|
341
|
-
if (attempt === 1) {
|
|
342
|
-
threadSent.push(content);
|
|
343
|
-
return { id: `id:${content}` };
|
|
344
|
-
}
|
|
345
|
-
throw new TypeError(REVOCATION_ERROR);
|
|
346
|
-
},
|
|
347
|
-
};
|
|
348
|
-
|
|
500
|
+
const ctx = createRevokedThreadContext({ failAfterAttempt: 2, sent: threadSent });
|
|
349
501
|
const adapter = createFallbackAdapter(proactiveSent);
|
|
350
502
|
|
|
351
503
|
const ids = await sendMSTeamsMessages({
|
|
@@ -362,14 +514,125 @@ describe("msteams messenger", () => {
|
|
|
362
514
|
expect(ids).toEqual(["id:one", "id:two", "id:three"]);
|
|
363
515
|
});
|
|
364
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
|
+
|
|
365
622
|
it("retries top-level sends on transient (5xx)", async () => {
|
|
366
623
|
const attempts: string[] = [];
|
|
367
624
|
|
|
368
625
|
const adapter: MSTeamsAdapter = {
|
|
369
626
|
continueConversation: async (_appId, _reference, logic) => {
|
|
370
|
-
await logic({
|
|
627
|
+
await logic({
|
|
628
|
+
sendActivity: createRecordedSendActivity(attempts, 503),
|
|
629
|
+
updateActivity: noopUpdateActivity,
|
|
630
|
+
deleteActivity: noopDeleteActivity,
|
|
631
|
+
});
|
|
371
632
|
},
|
|
372
633
|
process: async () => {},
|
|
634
|
+
updateActivity: noopUpdateActivity,
|
|
635
|
+
deleteActivity: noopDeleteActivity,
|
|
373
636
|
};
|
|
374
637
|
|
|
375
638
|
const ids = await sendMSTeamsMessages({
|
|
@@ -384,5 +647,219 @@ describe("msteams messenger", () => {
|
|
|
384
647
|
expect(attempts).toEqual(["hello", "hello"]);
|
|
385
648
|
expect(ids).toEqual(["id:hello"]);
|
|
386
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
|
+
});
|
|
387
864
|
});
|
|
388
865
|
});
|