@openclaw/msteams 2026.5.2-beta.2 → 2026.5.3-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/dist/api.js +3 -0
- package/dist/channel-D7hdreTh.js +984 -0
- package/dist/channel-config-api.js +2 -0
- package/dist/channel-plugin-api.js +2 -0
- package/dist/channel.runtime-BC1ruIfN.js +573 -0
- package/dist/config-schema-B8QezH6t.js +15 -0
- package/dist/contract-api.js +2 -0
- package/dist/graph-users-9uQJepqr.js +1354 -0
- package/dist/index.js +22 -0
- package/dist/oauth-BWJyilR1.js +114 -0
- package/dist/oauth.token-xxpoLWy5.js +115 -0
- package/dist/policy-DTnU2GR7.js +142 -0
- package/dist/probe-D_H8yFps.js +2194 -0
- package/dist/resolve-allowlist-D41JSziq.js +219 -0
- package/dist/runtime-api-DV1iVMn1.js +28 -0
- package/dist/runtime-api.js +2 -0
- package/dist/secret-contract-BuoEXmPS.js +35 -0
- package/dist/secret-contract-api.js +2 -0
- package/dist/setup-entry.js +15 -0
- package/dist/setup-plugin-api.js +64 -0
- package/dist/setup-surface-BLkFQYIQ.js +313 -0
- package/dist/src-CFp1QpFd.js +4064 -0
- package/dist/test-api.js +2 -0
- package/package.json +14 -6
- package/api.ts +0 -3
- package/channel-config-api.ts +0 -1
- package/channel-plugin-api.ts +0 -2
- package/config-api.ts +0 -4
- package/contract-api.ts +0 -4
- package/index.ts +0 -20
- package/runtime-api.ts +0 -73
- package/secret-contract-api.ts +0 -5
- package/setup-entry.ts +0 -13
- package/setup-plugin-api.ts +0 -3
- package/src/ai-entity.ts +0 -7
- package/src/approval-auth.ts +0 -44
- package/src/attachments/bot-framework.test.ts +0 -461
- package/src/attachments/bot-framework.ts +0 -362
- package/src/attachments/download.ts +0 -311
- package/src/attachments/graph.test.ts +0 -416
- package/src/attachments/graph.ts +0 -484
- package/src/attachments/html.ts +0 -122
- package/src/attachments/payload.ts +0 -14
- package/src/attachments/remote-media.test.ts +0 -137
- package/src/attachments/remote-media.ts +0 -112
- package/src/attachments/shared.test.ts +0 -530
- package/src/attachments/shared.ts +0 -626
- package/src/attachments/types.ts +0 -47
- package/src/attachments.graph.test.ts +0 -342
- package/src/attachments.helpers.test.ts +0 -246
- package/src/attachments.test-helpers.ts +0 -17
- package/src/attachments.test.ts +0 -687
- package/src/attachments.ts +0 -18
- package/src/block-streaming-config.test.ts +0 -61
- package/src/channel-api.ts +0 -1
- package/src/channel.actions.test.ts +0 -742
- package/src/channel.directory.test.ts +0 -200
- package/src/channel.runtime.ts +0 -56
- package/src/channel.setup.ts +0 -77
- package/src/channel.test.ts +0 -128
- package/src/channel.ts +0 -1136
- package/src/config-schema.ts +0 -6
- package/src/config-ui-hints.ts +0 -12
- package/src/conversation-store-fs.test.ts +0 -74
- package/src/conversation-store-fs.ts +0 -149
- package/src/conversation-store-helpers.test.ts +0 -202
- package/src/conversation-store-helpers.ts +0 -105
- package/src/conversation-store-memory.ts +0 -51
- package/src/conversation-store.shared.test.ts +0 -225
- package/src/conversation-store.ts +0 -71
- package/src/directory-live.test.ts +0 -156
- package/src/directory-live.ts +0 -111
- package/src/doctor.ts +0 -27
- package/src/errors.test.ts +0 -133
- package/src/errors.ts +0 -246
- package/src/feedback-reflection-prompt.ts +0 -117
- package/src/feedback-reflection-store.ts +0 -114
- package/src/feedback-reflection.test.ts +0 -237
- package/src/feedback-reflection.ts +0 -283
- package/src/file-consent-helpers.test.ts +0 -326
- package/src/file-consent-helpers.ts +0 -126
- package/src/file-consent-invoke.ts +0 -150
- package/src/file-consent.test.ts +0 -363
- package/src/file-consent.ts +0 -287
- package/src/graph-chat.ts +0 -55
- package/src/graph-group-management.test.ts +0 -318
- package/src/graph-group-management.ts +0 -168
- package/src/graph-members.test.ts +0 -89
- package/src/graph-members.ts +0 -48
- package/src/graph-messages.actions.test.ts +0 -243
- package/src/graph-messages.read.test.ts +0 -391
- package/src/graph-messages.search.test.ts +0 -213
- package/src/graph-messages.test-helpers.ts +0 -50
- package/src/graph-messages.ts +0 -534
- package/src/graph-teams.test.ts +0 -215
- package/src/graph-teams.ts +0 -114
- package/src/graph-thread.test.ts +0 -246
- package/src/graph-thread.ts +0 -146
- package/src/graph-upload.test.ts +0 -258
- package/src/graph-upload.ts +0 -531
- package/src/graph-users.ts +0 -29
- package/src/graph.test.ts +0 -516
- package/src/graph.ts +0 -293
- package/src/inbound.test.ts +0 -221
- package/src/inbound.ts +0 -148
- package/src/index.ts +0 -4
- package/src/media-helpers.test.ts +0 -202
- package/src/media-helpers.ts +0 -105
- package/src/mentions.test.ts +0 -244
- package/src/mentions.ts +0 -114
- package/src/messenger.test.ts +0 -865
- package/src/messenger.ts +0 -605
- package/src/monitor-handler/access.ts +0 -125
- package/src/monitor-handler/inbound-media.test.ts +0 -289
- package/src/monitor-handler/inbound-media.ts +0 -180
- package/src/monitor-handler/message-handler-mock-support.test-support.ts +0 -28
- package/src/monitor-handler/message-handler.authz.test.ts +0 -669
- package/src/monitor-handler/message-handler.dm-media.test.ts +0 -54
- package/src/monitor-handler/message-handler.test-support.ts +0 -100
- package/src/monitor-handler/message-handler.thread-parent.test.ts +0 -223
- package/src/monitor-handler/message-handler.thread-session.test.ts +0 -77
- package/src/monitor-handler/message-handler.ts +0 -1000
- package/src/monitor-handler/reaction-handler.test.ts +0 -267
- package/src/monitor-handler/reaction-handler.ts +0 -210
- package/src/monitor-handler/thread-session.ts +0 -17
- package/src/monitor-handler.adaptive-card.test.ts +0 -162
- package/src/monitor-handler.feedback-authz.test.ts +0 -314
- package/src/monitor-handler.file-consent.test.ts +0 -423
- package/src/monitor-handler.sso.test.ts +0 -563
- package/src/monitor-handler.test-helpers.ts +0 -180
- package/src/monitor-handler.ts +0 -534
- package/src/monitor-handler.types.ts +0 -27
- package/src/monitor-types.ts +0 -6
- package/src/monitor.lifecycle.test.ts +0 -278
- package/src/monitor.test.ts +0 -119
- package/src/monitor.ts +0 -442
- package/src/oauth.flow.ts +0 -77
- package/src/oauth.shared.ts +0 -37
- package/src/oauth.test.ts +0 -305
- package/src/oauth.token.ts +0 -158
- package/src/oauth.ts +0 -130
- package/src/outbound.test.ts +0 -130
- package/src/outbound.ts +0 -71
- package/src/pending-uploads-fs.test.ts +0 -246
- package/src/pending-uploads-fs.ts +0 -235
- package/src/pending-uploads.test.ts +0 -173
- package/src/pending-uploads.ts +0 -121
- package/src/policy.test.ts +0 -240
- package/src/policy.ts +0 -262
- package/src/polls-store-memory.ts +0 -32
- package/src/polls.test.ts +0 -160
- package/src/polls.ts +0 -323
- package/src/presentation.ts +0 -68
- package/src/probe.test.ts +0 -77
- package/src/probe.ts +0 -132
- package/src/reply-dispatcher.test.ts +0 -437
- package/src/reply-dispatcher.ts +0 -346
- package/src/reply-stream-controller.test.ts +0 -235
- package/src/reply-stream-controller.ts +0 -147
- package/src/resolve-allowlist.test.ts +0 -250
- package/src/resolve-allowlist.ts +0 -309
- package/src/revoked-context.ts +0 -17
- package/src/runtime.ts +0 -9
- package/src/sdk-types.ts +0 -59
- package/src/sdk.test.ts +0 -666
- package/src/sdk.ts +0 -884
- package/src/secret-contract.ts +0 -49
- package/src/secret-input.ts +0 -7
- package/src/send-context.ts +0 -231
- package/src/send.test.ts +0 -493
- package/src/send.ts +0 -637
- package/src/sent-message-cache.test.ts +0 -15
- package/src/sent-message-cache.ts +0 -56
- package/src/session-route.ts +0 -40
- package/src/setup-core.ts +0 -160
- package/src/setup-surface.test.ts +0 -202
- package/src/setup-surface.ts +0 -320
- package/src/sso-token-store.test.ts +0 -72
- package/src/sso-token-store.ts +0 -166
- package/src/sso.ts +0 -300
- package/src/storage.ts +0 -25
- package/src/store-fs.ts +0 -44
- package/src/streaming-message.test.ts +0 -262
- package/src/streaming-message.ts +0 -297
- package/src/test-runtime.ts +0 -16
- package/src/thread-parent-context.test.ts +0 -224
- package/src/thread-parent-context.ts +0 -159
- package/src/token-response.ts +0 -11
- package/src/token.test.ts +0 -259
- package/src/token.ts +0 -195
- package/src/user-agent.test.ts +0 -86
- package/src/user-agent.ts +0 -53
- package/src/webhook-timeouts.ts +0 -27
- package/src/welcome-card.test.ts +0 -81
- package/src/welcome-card.ts +0 -57
- package/test-api.ts +0 -1
- package/tsconfig.json +0 -16
package/src/messenger.test.ts
DELETED
|
@@ -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
|
-
});
|