@kodelyth/msteams 2026.5.39 → 2026.5.42

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (208) hide show
  1. package/api.ts +3 -0
  2. package/channel-config-api.ts +1 -0
  3. package/channel-plugin-api.ts +2 -0
  4. package/config-api.ts +4 -0
  5. package/contract-api.ts +4 -0
  6. package/dist/api.js +3 -0
  7. package/dist/channel-BvTXHuGs.js +1161 -0
  8. package/dist/channel-config-api.js +2 -0
  9. package/dist/channel-plugin-api.js +2 -0
  10. package/dist/channel.runtime-NssGKZm5.js +650 -0
  11. package/dist/config-schema-Btk-XCOd.js +43 -0
  12. package/dist/contract-api.js +2 -0
  13. package/dist/graph-users-D-gKCguI.js +1411 -0
  14. package/dist/index.js +22 -0
  15. package/dist/oauth-BUxlphX3.js +114 -0
  16. package/dist/oauth.token-ebId9946.js +116 -0
  17. package/dist/probe-Cj2KsAGF.js +2190 -0
  18. package/dist/runtime-api-BL4DOWXD.js +28 -0
  19. package/dist/runtime-api.js +2 -0
  20. package/dist/secret-contract-Bo7kdUrT.js +35 -0
  21. package/dist/secret-contract-api.js +2 -0
  22. package/dist/setup-entry.js +15 -0
  23. package/dist/setup-plugin-api.js +64 -0
  24. package/dist/setup-surface-COTQDcTQ.js +531 -0
  25. package/dist/src-tvpsGYPV.js +4226 -0
  26. package/dist/test-api.js +2 -0
  27. package/index.ts +20 -0
  28. package/klaw.plugin.json +2 -726
  29. package/package.json +4 -4
  30. package/runtime-api.ts +66 -0
  31. package/secret-contract-api.ts +5 -0
  32. package/setup-entry.ts +13 -0
  33. package/setup-plugin-api.ts +3 -0
  34. package/src/ai-entity.ts +7 -0
  35. package/src/approval-auth.ts +44 -0
  36. package/src/attachments/bot-framework.test.ts +506 -0
  37. package/src/attachments/bot-framework.ts +348 -0
  38. package/src/attachments/download.ts +328 -0
  39. package/src/attachments/graph.test.ts +441 -0
  40. package/src/attachments/graph.ts +489 -0
  41. package/src/attachments/html.ts +122 -0
  42. package/src/attachments/payload.ts +14 -0
  43. package/src/attachments/remote-media.test.ts +187 -0
  44. package/src/attachments/remote-media.ts +86 -0
  45. package/src/attachments/shared.test.ts +547 -0
  46. package/src/attachments/shared.ts +655 -0
  47. package/src/attachments/types.ts +47 -0
  48. package/src/attachments.graph.test.ts +414 -0
  49. package/src/attachments.helpers.test.ts +245 -0
  50. package/src/attachments.test-helpers.ts +17 -0
  51. package/src/attachments.test.ts +754 -0
  52. package/src/attachments.ts +18 -0
  53. package/src/block-streaming-config.test.ts +61 -0
  54. package/src/channel-api.ts +1 -0
  55. package/src/channel.actions.test.ts +797 -0
  56. package/src/channel.directory.test.ts +176 -0
  57. package/src/channel.message-adapter.test.ts +227 -0
  58. package/src/channel.runtime.ts +56 -0
  59. package/src/channel.setup.ts +77 -0
  60. package/src/channel.test.ts +136 -0
  61. package/src/channel.ts +1176 -0
  62. package/src/config-schema.ts +6 -0
  63. package/src/config-ui-hints.ts +40 -0
  64. package/src/conversation-store-fs.test.ts +81 -0
  65. package/src/conversation-store-fs.ts +149 -0
  66. package/src/conversation-store-helpers.test.ts +202 -0
  67. package/src/conversation-store-helpers.ts +105 -0
  68. package/src/conversation-store-memory.ts +51 -0
  69. package/src/conversation-store.shared.test.ts +260 -0
  70. package/src/conversation-store.ts +71 -0
  71. package/src/directory-live.test.ts +156 -0
  72. package/src/directory-live.ts +111 -0
  73. package/src/doctor.ts +27 -0
  74. package/src/errors.test.ts +154 -0
  75. package/src/errors.ts +270 -0
  76. package/src/feedback-reflection-prompt.ts +117 -0
  77. package/src/feedback-reflection-store.ts +113 -0
  78. package/src/feedback-reflection.test.ts +237 -0
  79. package/src/feedback-reflection.ts +268 -0
  80. package/src/file-consent-helpers.test.ts +328 -0
  81. package/src/file-consent-helpers.ts +115 -0
  82. package/src/file-consent-invoke.ts +150 -0
  83. package/src/file-consent.test.ts +378 -0
  84. package/src/file-consent.ts +223 -0
  85. package/src/graph-chat.ts +36 -0
  86. package/src/graph-group-management.test.ts +332 -0
  87. package/src/graph-group-management.ts +168 -0
  88. package/src/graph-members.test.ts +89 -0
  89. package/src/graph-members.ts +48 -0
  90. package/src/graph-messages.actions.test.ts +253 -0
  91. package/src/graph-messages.read.test.ts +391 -0
  92. package/src/graph-messages.search.test.ts +227 -0
  93. package/src/graph-messages.test-helpers.ts +50 -0
  94. package/src/graph-messages.ts +534 -0
  95. package/src/graph-teams.test.ts +222 -0
  96. package/src/graph-teams.ts +114 -0
  97. package/src/graph-thread.test.ts +252 -0
  98. package/src/graph-thread.ts +146 -0
  99. package/src/graph-upload.test.ts +253 -0
  100. package/src/graph-upload.ts +531 -0
  101. package/src/graph-users.ts +29 -0
  102. package/src/graph.test.ts +540 -0
  103. package/src/graph.ts +308 -0
  104. package/src/inbound.test.ts +221 -0
  105. package/src/inbound.ts +148 -0
  106. package/src/index.ts +4 -0
  107. package/src/media-helpers.test.ts +220 -0
  108. package/src/media-helpers.ts +105 -0
  109. package/src/mentions.test.ts +254 -0
  110. package/src/mentions.ts +114 -0
  111. package/src/messenger.test.ts +961 -0
  112. package/src/messenger.ts +608 -0
  113. package/src/monitor-handler/access.ts +136 -0
  114. package/src/monitor-handler/inbound-media.test.ts +314 -0
  115. package/src/monitor-handler/inbound-media.ts +180 -0
  116. package/src/monitor-handler/message-handler-mock-support.test-support.ts +28 -0
  117. package/src/monitor-handler/message-handler.authz.test.ts +739 -0
  118. package/src/monitor-handler/message-handler.dm-media.test.ts +54 -0
  119. package/src/monitor-handler/message-handler.test-support.ts +99 -0
  120. package/src/monitor-handler/message-handler.thread-parent.test.ts +225 -0
  121. package/src/monitor-handler/message-handler.thread-session.test.ts +132 -0
  122. package/src/monitor-handler/message-handler.ts +1003 -0
  123. package/src/monitor-handler/reaction-handler.test.ts +325 -0
  124. package/src/monitor-handler/reaction-handler.ts +122 -0
  125. package/src/monitor-handler/thread-session.ts +30 -0
  126. package/src/monitor-handler.adaptive-card.test.ts +158 -0
  127. package/src/monitor-handler.feedback-authz.test.ts +357 -0
  128. package/src/monitor-handler.file-consent.test.ts +443 -0
  129. package/src/monitor-handler.sso.test.ts +576 -0
  130. package/src/monitor-handler.test-helpers.ts +181 -0
  131. package/src/monitor-handler.ts +538 -0
  132. package/src/monitor-handler.types.ts +27 -0
  133. package/src/monitor-types.ts +6 -0
  134. package/src/monitor.lifecycle.test.ts +457 -0
  135. package/src/monitor.test.ts +119 -0
  136. package/src/monitor.ts +476 -0
  137. package/src/oauth.flow.ts +77 -0
  138. package/src/oauth.shared.ts +37 -0
  139. package/src/oauth.test.ts +350 -0
  140. package/src/oauth.token.ts +162 -0
  141. package/src/oauth.ts +130 -0
  142. package/src/outbound.test.ts +400 -0
  143. package/src/outbound.ts +198 -0
  144. package/src/pending-uploads-fs.test.ts +261 -0
  145. package/src/pending-uploads-fs.ts +235 -0
  146. package/src/pending-uploads.test.ts +186 -0
  147. package/src/pending-uploads.ts +121 -0
  148. package/src/policy.test.ts +156 -0
  149. package/src/policy.ts +245 -0
  150. package/src/polls-store-memory.ts +32 -0
  151. package/src/polls.test.ts +169 -0
  152. package/src/polls.ts +312 -0
  153. package/src/presentation.ts +93 -0
  154. package/src/probe.test.ts +79 -0
  155. package/src/probe.ts +132 -0
  156. package/src/reply-dispatcher.test.ts +543 -0
  157. package/src/reply-dispatcher.ts +523 -0
  158. package/src/reply-stream-controller.test.ts +424 -0
  159. package/src/reply-stream-controller.ts +334 -0
  160. package/src/resolve-allowlist.test.ts +253 -0
  161. package/src/resolve-allowlist.ts +309 -0
  162. package/src/revoked-context.ts +17 -0
  163. package/src/runtime.ts +12 -0
  164. package/src/sdk-types.ts +59 -0
  165. package/src/sdk.test.ts +727 -0
  166. package/src/sdk.ts +916 -0
  167. package/src/secret-contract.ts +49 -0
  168. package/src/secret-input.ts +7 -0
  169. package/src/send-context.test.ts +93 -0
  170. package/src/send-context.ts +269 -0
  171. package/src/send.test.ts +588 -0
  172. package/src/send.ts +697 -0
  173. package/src/sent-message-cache.test.ts +106 -0
  174. package/src/sent-message-cache.ts +174 -0
  175. package/src/session-route.ts +40 -0
  176. package/src/setup-core.ts +162 -0
  177. package/src/setup-surface.test.ts +175 -0
  178. package/src/setup-surface.ts +319 -0
  179. package/src/sso-token-store.test.ts +74 -0
  180. package/src/sso-token-store.ts +166 -0
  181. package/src/sso.ts +300 -0
  182. package/src/storage.ts +25 -0
  183. package/src/store-fs.ts +42 -0
  184. package/src/streaming-message.test.ts +323 -0
  185. package/src/streaming-message.ts +327 -0
  186. package/src/test-runtime.ts +16 -0
  187. package/src/thread-parent-context.test.ts +224 -0
  188. package/src/thread-parent-context.ts +159 -0
  189. package/src/token-response.ts +11 -0
  190. package/src/token.test.ts +268 -0
  191. package/src/token.ts +194 -0
  192. package/src/user-agent.test.ts +121 -0
  193. package/src/user-agent.ts +53 -0
  194. package/src/webhook-timeouts.ts +27 -0
  195. package/src/welcome-card.test.ts +104 -0
  196. package/src/welcome-card.ts +57 -0
  197. package/test-api.ts +1 -0
  198. package/tsconfig.json +16 -0
  199. package/api.js +0 -7
  200. package/channel-config-api.js +0 -7
  201. package/channel-plugin-api.js +0 -7
  202. package/contract-api.js +0 -7
  203. package/index.js +0 -7
  204. package/runtime-api.js +0 -7
  205. package/secret-contract-api.js +0 -7
  206. package/setup-entry.js +0 -7
  207. package/setup-plugin-api.js +0 -7
  208. package/test-api.js +0 -7
@@ -0,0 +1,588 @@
1
+ import { beforeEach, describe, expect, it, vi } from "vitest";
2
+ import type { KlawConfig } from "../runtime-api.js";
3
+ import { deleteMessageMSTeams, editMessageMSTeams, sendMessageMSTeams } from "./send.js";
4
+
5
+ const mockState = vi.hoisted(() => ({
6
+ loadOutboundMediaFromUrl: vi.fn(),
7
+ resolveMSTeamsSendContext: vi.fn(),
8
+ resolveMarkdownTableMode: vi.fn(() => "off"),
9
+ convertMarkdownTables: vi.fn((text: string) => text),
10
+ runtimeResolveMarkdownTableMode: vi.fn(() => "off"),
11
+ runtimeConvertMarkdownTables: vi.fn((text: string) => text),
12
+ requiresFileConsent: vi.fn(),
13
+ prepareFileConsentActivity: vi.fn(),
14
+ prepareFileConsentActivityFs: vi.fn(),
15
+ extractFilename: vi.fn(async () => "fallback.bin"),
16
+ sendMSTeamsMessages: vi.fn(),
17
+ uploadAndShareSharePoint: vi.fn(),
18
+ getDriveItemProperties: vi.fn(),
19
+ buildTeamsFileInfoCard: vi.fn(),
20
+ }));
21
+
22
+ vi.mock("klaw/plugin-sdk/outbound-media", () => ({
23
+ loadOutboundMediaFromUrl: mockState.loadOutboundMediaFromUrl,
24
+ }));
25
+
26
+ vi.mock("klaw/plugin-sdk/markdown-table-runtime", () => ({
27
+ resolveMarkdownTableMode: mockState.resolveMarkdownTableMode,
28
+ }));
29
+
30
+ vi.mock("klaw/plugin-sdk/text-chunking", async (importOriginal) => {
31
+ const actual = await importOriginal<typeof import("klaw/plugin-sdk/text-chunking")>();
32
+ return {
33
+ ...actual,
34
+ convertMarkdownTables: mockState.convertMarkdownTables,
35
+ };
36
+ });
37
+
38
+ vi.mock("./send-context.js", () => ({
39
+ resolveMSTeamsSendContext: mockState.resolveMSTeamsSendContext,
40
+ }));
41
+
42
+ vi.mock("./file-consent-helpers.js", () => ({
43
+ requiresFileConsent: mockState.requiresFileConsent,
44
+ prepareFileConsentActivity: mockState.prepareFileConsentActivity,
45
+ prepareFileConsentActivityFs: mockState.prepareFileConsentActivityFs,
46
+ }));
47
+
48
+ vi.mock("./media-helpers.js", () => ({
49
+ extractFilename: mockState.extractFilename,
50
+ extractMessageId: () => "message-1",
51
+ }));
52
+
53
+ vi.mock("./messenger.js", () => ({
54
+ sendMSTeamsMessages: mockState.sendMSTeamsMessages,
55
+ buildConversationReference: () => ({}),
56
+ }));
57
+
58
+ vi.mock("./runtime.js", () => ({
59
+ getMSTeamsRuntime: () => ({
60
+ channel: {
61
+ text: {
62
+ resolveMarkdownTableMode: mockState.runtimeResolveMarkdownTableMode,
63
+ convertMarkdownTables: mockState.runtimeConvertMarkdownTables,
64
+ },
65
+ },
66
+ }),
67
+ }));
68
+
69
+ vi.mock("./graph-upload.js", () => ({
70
+ uploadAndShareSharePoint: mockState.uploadAndShareSharePoint,
71
+ getDriveItemProperties: mockState.getDriveItemProperties,
72
+ uploadAndShareOneDrive: vi.fn(),
73
+ }));
74
+
75
+ vi.mock("./graph-chat.js", () => ({
76
+ buildTeamsFileInfoCard: mockState.buildTeamsFileInfoCard,
77
+ }));
78
+
79
+ function mockContinueConversationFailure(error: string) {
80
+ const mockContinueConversation = vi.fn().mockRejectedValue(new Error(error));
81
+ mockState.resolveMSTeamsSendContext.mockResolvedValue({
82
+ adapter: { continueConversation: mockContinueConversation },
83
+ appId: "app-id",
84
+ conversationId: "19:conversation@thread.tacv2",
85
+ ref: {
86
+ user: { id: "user-1" },
87
+ agent: { id: "agent-1" },
88
+ conversation: { id: "19:conversation@thread.tacv2" },
89
+ channelId: "msteams",
90
+ },
91
+ log: { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() },
92
+ conversationType: "personal",
93
+ tokenProvider: {},
94
+ });
95
+ return mockContinueConversation;
96
+ }
97
+
98
+ const continueConversationFailureCases = [
99
+ {
100
+ name: "editMessageMSTeams",
101
+ error: "Service unavailable",
102
+ expected: "msteams edit failed",
103
+ invoke: () =>
104
+ editMessageMSTeams({
105
+ cfg: {} as KlawConfig,
106
+ to: "conversation:19:conversation@thread.tacv2",
107
+ activityId: "activity-123",
108
+ text: "Updated text",
109
+ }),
110
+ },
111
+ {
112
+ name: "deleteMessageMSTeams",
113
+ error: "Not found",
114
+ expected: "msteams delete failed",
115
+ invoke: () =>
116
+ deleteMessageMSTeams({
117
+ cfg: {} as KlawConfig,
118
+ to: "conversation:19:conversation@thread.tacv2",
119
+ activityId: "activity-456",
120
+ }),
121
+ },
122
+ ];
123
+
124
+ function createSharePointSendContext(params: {
125
+ conversationId: string;
126
+ graphChatId: string | null;
127
+ siteId: string;
128
+ }) {
129
+ return {
130
+ adapter: {
131
+ continueConversation: vi.fn(
132
+ async (
133
+ _id: string,
134
+ _ref: unknown,
135
+ fn: (ctx: { sendActivity: () => { id: "msg-1" } }) => Promise<void>,
136
+ ) => fn({ sendActivity: () => ({ id: "msg-1" }) }),
137
+ ),
138
+ },
139
+ appId: "app-id",
140
+ conversationId: params.conversationId,
141
+ graphChatId: params.graphChatId,
142
+ ref: {},
143
+ log: { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() },
144
+ conversationType: "groupChat" as const,
145
+ replyStyle: "top-level" as const,
146
+ tokenProvider: { getAccessToken: vi.fn(async () => "token") },
147
+ mediaMaxBytes: 8 * 1024 * 1024,
148
+ sharePointSiteId: params.siteId,
149
+ };
150
+ }
151
+
152
+ function mockSharePointPdfUpload(params: {
153
+ bufferSize: number;
154
+ fileName: string;
155
+ itemId: string;
156
+ uniqueId: string;
157
+ }) {
158
+ mockState.loadOutboundMediaFromUrl.mockResolvedValueOnce({
159
+ buffer: Buffer.alloc(params.bufferSize, "pdf"),
160
+ contentType: "application/pdf",
161
+ fileName: params.fileName,
162
+ kind: "file",
163
+ });
164
+ mockState.requiresFileConsent.mockReturnValue(false);
165
+ mockState.uploadAndShareSharePoint.mockResolvedValue({
166
+ itemId: params.itemId,
167
+ webUrl: `https://sp.example.com/${params.fileName}`,
168
+ shareUrl: `https://sp.example.com/share/${params.fileName}`,
169
+ name: params.fileName,
170
+ });
171
+ mockState.getDriveItemProperties.mockResolvedValue({
172
+ eTag: `"${params.uniqueId},1"`,
173
+ webDavUrl: `https://sp.example.com/dav/${params.fileName}`,
174
+ name: params.fileName,
175
+ });
176
+ mockState.buildTeamsFileInfoCard.mockReturnValue({
177
+ contentType: "application/vnd.microsoft.teams.card.file.info",
178
+ contentUrl: `https://sp.example.com/dav/${params.fileName}`,
179
+ name: params.fileName,
180
+ content: { uniqueId: params.uniqueId, fileType: "pdf" },
181
+ });
182
+ }
183
+
184
+ type MockWithCalls = {
185
+ mock: { calls: unknown[][] };
186
+ };
187
+
188
+ function mockCallAt(mock: MockWithCalls, index = 0): unknown[] {
189
+ const call = mock.mock.calls[index];
190
+ if (!call) {
191
+ throw new Error(`expected mock call ${index}`);
192
+ }
193
+ return call;
194
+ }
195
+
196
+ function firstObjectArg(mock: MockWithCalls): Record<string, unknown> {
197
+ const value = mockCallAt(mock)[0];
198
+ if (value === undefined || value === null || typeof value !== "object" || Array.isArray(value)) {
199
+ throw new Error("expected first mock call to receive an object argument");
200
+ }
201
+ return value as Record<string, unknown>;
202
+ }
203
+
204
+ function continueConversationCall(mock: MockWithCalls): unknown[] {
205
+ return mockCallAt(mock);
206
+ }
207
+
208
+ function continueConversationRef(mock: MockWithCalls): Record<string, unknown> {
209
+ const ref = continueConversationCall(mock)[1];
210
+ if (ref === undefined || ref === null || typeof ref !== "object" || Array.isArray(ref)) {
211
+ throw new Error("expected continueConversation ref object");
212
+ }
213
+ return ref as Record<string, unknown>;
214
+ }
215
+
216
+ describe("sendMessageMSTeams", () => {
217
+ beforeEach(() => {
218
+ mockState.loadOutboundMediaFromUrl.mockReset();
219
+ mockState.resolveMSTeamsSendContext.mockReset();
220
+ mockState.resolveMarkdownTableMode.mockReset();
221
+ mockState.resolveMarkdownTableMode.mockReturnValue("off");
222
+ mockState.convertMarkdownTables.mockReset();
223
+ mockState.convertMarkdownTables.mockImplementation((text: string) => text);
224
+ mockState.runtimeResolveMarkdownTableMode.mockReset();
225
+ mockState.runtimeResolveMarkdownTableMode.mockReturnValue("off");
226
+ mockState.runtimeConvertMarkdownTables.mockReset();
227
+ mockState.runtimeConvertMarkdownTables.mockImplementation((text: string) => text);
228
+ mockState.requiresFileConsent.mockReset();
229
+ mockState.prepareFileConsentActivity.mockReset();
230
+ mockState.prepareFileConsentActivityFs.mockReset();
231
+ mockState.extractFilename.mockReset();
232
+ mockState.sendMSTeamsMessages.mockReset();
233
+ mockState.uploadAndShareSharePoint.mockReset();
234
+ mockState.getDriveItemProperties.mockReset();
235
+ mockState.buildTeamsFileInfoCard.mockReset();
236
+
237
+ mockState.extractFilename.mockResolvedValue("fallback.bin");
238
+ mockState.requiresFileConsent.mockReturnValue(false);
239
+ mockState.resolveMSTeamsSendContext.mockResolvedValue({
240
+ adapter: {},
241
+ appId: "app-id",
242
+ conversationId: "19:conversation@thread.tacv2",
243
+ ref: {},
244
+ log: { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() },
245
+ conversationType: "personal",
246
+ replyStyle: "top-level",
247
+ tokenProvider: { getAccessToken: vi.fn(async () => "token") },
248
+ mediaMaxBytes: 8 * 1024,
249
+ sharePointSiteId: undefined,
250
+ });
251
+ mockState.sendMSTeamsMessages.mockResolvedValue(["message-1"]);
252
+ });
253
+
254
+ it("loads media through shared helper and forwards mediaLocalRoots", async () => {
255
+ const mediaBuffer = Buffer.from("tiny-image");
256
+ mockState.loadOutboundMediaFromUrl.mockResolvedValueOnce({
257
+ buffer: mediaBuffer,
258
+ contentType: "image/png",
259
+ fileName: "inline.png",
260
+ kind: "image",
261
+ });
262
+
263
+ const result = await sendMessageMSTeams({
264
+ cfg: {} as KlawConfig,
265
+ to: "conversation:19:conversation@thread.tacv2",
266
+ text: "hello",
267
+ mediaUrl: "file:///tmp/agent-workspace/inline.png",
268
+ mediaLocalRoots: ["/tmp/agent-workspace"],
269
+ });
270
+
271
+ expect(mockState.loadOutboundMediaFromUrl).toHaveBeenCalledWith(
272
+ "file:///tmp/agent-workspace/inline.png",
273
+ {
274
+ maxBytes: 8 * 1024,
275
+ mediaLocalRoots: ["/tmp/agent-workspace"],
276
+ },
277
+ );
278
+
279
+ const sendPayload = firstObjectArg(mockState.sendMSTeamsMessages);
280
+ const messages = sendPayload.messages as Array<Record<string, unknown>>;
281
+ expect(messages).toHaveLength(1);
282
+ expect(messages[0]?.text).toBe("hello");
283
+ expect(messages[0]?.mediaUrl).toBe(`data:image/png;base64,${mediaBuffer.toString("base64")}`);
284
+ expect(result.receipt?.primaryPlatformMessageId).toBe("message-1");
285
+ expect(result.receipt?.platformMessageIds).toEqual(["message-1"]);
286
+ expect(result.receipt?.parts).toHaveLength(1);
287
+ expect(result.receipt?.parts[0]?.platformMessageId).toBe("message-1");
288
+ expect(result.receipt?.parts[0]?.kind).toBe("media");
289
+ });
290
+
291
+ it("sends with provided cfg even when Teams runtime text helpers are unavailable", async () => {
292
+ mockState.runtimeResolveMarkdownTableMode.mockImplementation(() => {
293
+ throw new Error("MSTeams runtime not initialized");
294
+ });
295
+ mockState.runtimeConvertMarkdownTables.mockImplementation(() => {
296
+ throw new Error("MSTeams runtime not initialized");
297
+ });
298
+ mockState.resolveMarkdownTableMode.mockReturnValue("off");
299
+ mockState.convertMarkdownTables.mockReturnValue("hello");
300
+
301
+ const result = await sendMessageMSTeams({
302
+ cfg: {} as KlawConfig,
303
+ to: "conversation:19:conversation@thread.tacv2",
304
+ text: "hello",
305
+ });
306
+
307
+ expect(result.messageId).toBe("message-1");
308
+ expect(result.conversationId).toBe("19:conversation@thread.tacv2");
309
+ expect(result.receipt?.primaryPlatformMessageId).toBe("message-1");
310
+ expect(result.receipt?.platformMessageIds).toEqual(["message-1"]);
311
+ expect(result.receipt?.parts).toHaveLength(1);
312
+ expect(result.receipt?.parts[0]?.platformMessageId).toBe("message-1");
313
+ expect(result.receipt?.parts[0]?.kind).toBe("text");
314
+
315
+ expect(mockState.resolveMarkdownTableMode).toHaveBeenCalledWith({
316
+ cfg: {},
317
+ channel: "msteams",
318
+ });
319
+ expect(mockState.convertMarkdownTables).toHaveBeenCalledWith("hello", "off");
320
+ });
321
+
322
+ it("passes the resolved proactive replyStyle to text sends", async () => {
323
+ mockState.resolveMSTeamsSendContext.mockResolvedValue({
324
+ adapter: {},
325
+ appId: "app-id",
326
+ conversationId: "19:channel@thread.tacv2",
327
+ ref: {
328
+ threadId: "thread-root-1",
329
+ conversation: { id: "19:channel@thread.tacv2", conversationType: "channel" },
330
+ },
331
+ log: { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() },
332
+ conversationType: "channel",
333
+ replyStyle: "thread",
334
+ tokenProvider: { getAccessToken: vi.fn(async () => "token") },
335
+ mediaMaxBytes: 8 * 1024,
336
+ sharePointSiteId: undefined,
337
+ });
338
+
339
+ await sendMessageMSTeams({
340
+ cfg: {} as KlawConfig,
341
+ to: "conversation:19:channel@thread.tacv2",
342
+ text: "threaded reply",
343
+ });
344
+
345
+ expect(firstObjectArg(mockState.sendMSTeamsMessages).replyStyle).toBe("thread");
346
+ });
347
+
348
+ it("keeps top-level proactive replyStyle when resolved for a channel", async () => {
349
+ mockState.resolveMSTeamsSendContext.mockResolvedValue({
350
+ adapter: {},
351
+ appId: "app-id",
352
+ conversationId: "19:channel@thread.tacv2",
353
+ ref: {
354
+ threadId: "thread-root-1",
355
+ conversation: { id: "19:channel@thread.tacv2", conversationType: "channel" },
356
+ },
357
+ log: { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() },
358
+ conversationType: "channel",
359
+ replyStyle: "top-level",
360
+ tokenProvider: { getAccessToken: vi.fn(async () => "token") },
361
+ mediaMaxBytes: 8 * 1024,
362
+ sharePointSiteId: undefined,
363
+ });
364
+
365
+ await sendMessageMSTeams({
366
+ cfg: {} as KlawConfig,
367
+ to: "conversation:19:channel@thread.tacv2",
368
+ text: "top-level reply",
369
+ });
370
+
371
+ expect(firstObjectArg(mockState.sendMSTeamsMessages).replyStyle).toBe("top-level");
372
+ });
373
+
374
+ it("uses graphChatId instead of conversationId when uploading to SharePoint", async () => {
375
+ // Simulates a group chat where Bot Framework conversationId is valid but we have
376
+ // a resolved Graph chat ID cached from a prior send.
377
+ const graphChatId = "19:graph-native-chat-id@thread.tacv2";
378
+ const botFrameworkConversationId = "19:bot-framework-id@thread.tacv2";
379
+
380
+ mockState.resolveMSTeamsSendContext.mockResolvedValue(
381
+ createSharePointSendContext({
382
+ conversationId: botFrameworkConversationId,
383
+ graphChatId,
384
+ siteId: "site-123",
385
+ }),
386
+ );
387
+ mockSharePointPdfUpload({
388
+ bufferSize: 100,
389
+ fileName: "doc.pdf",
390
+ itemId: "item-1",
391
+ uniqueId: "{GUID-123}",
392
+ });
393
+
394
+ await sendMessageMSTeams({
395
+ cfg: {} as KlawConfig,
396
+ to: "conversation:19:bot-framework-id@thread.tacv2",
397
+ text: "here is a file",
398
+ mediaUrl: "https://example.com/doc.pdf",
399
+ });
400
+
401
+ // The Graph-native chatId must be passed to SharePoint upload, not the Bot Framework ID
402
+ const uploadPayload = firstObjectArg(mockState.uploadAndShareSharePoint);
403
+ expect(uploadPayload.chatId).toBe(graphChatId);
404
+ expect(uploadPayload.siteId).toBe("site-123");
405
+ });
406
+
407
+ it("falls back to conversationId when graphChatId is not available", async () => {
408
+ const botFrameworkConversationId = "19:fallback-id@thread.tacv2";
409
+
410
+ mockState.resolveMSTeamsSendContext.mockResolvedValue(
411
+ createSharePointSendContext({
412
+ conversationId: botFrameworkConversationId,
413
+ graphChatId: null,
414
+ siteId: "site-456",
415
+ }),
416
+ );
417
+ mockSharePointPdfUpload({
418
+ bufferSize: 50,
419
+ fileName: "report.pdf",
420
+ itemId: "item-2",
421
+ uniqueId: "{GUID-456}",
422
+ });
423
+
424
+ await sendMessageMSTeams({
425
+ cfg: {} as KlawConfig,
426
+ to: "conversation:19:fallback-id@thread.tacv2",
427
+ text: "report",
428
+ mediaUrl: "https://example.com/report.pdf",
429
+ });
430
+
431
+ // Falls back to conversationId when graphChatId is null
432
+ const uploadPayload = firstObjectArg(mockState.uploadAndShareSharePoint);
433
+ expect(uploadPayload.chatId).toBe(botFrameworkConversationId);
434
+ expect(uploadPayload.siteId).toBe("site-456");
435
+ });
436
+ });
437
+
438
+ describe("MSTeams continueConversation failure handling", () => {
439
+ beforeEach(() => {
440
+ mockState.resolveMSTeamsSendContext.mockReset();
441
+ });
442
+
443
+ it.each(continueConversationFailureCases)(
444
+ "$name throws a descriptive error when continueConversation fails",
445
+ async ({ error, expected, invoke }) => {
446
+ mockContinueConversationFailure(error);
447
+
448
+ await expect(invoke()).rejects.toThrow(expected);
449
+ },
450
+ );
451
+ });
452
+
453
+ describe("editMessageMSTeams", () => {
454
+ beforeEach(() => {
455
+ mockState.resolveMSTeamsSendContext.mockReset();
456
+ });
457
+
458
+ it("calls continueConversation and updateActivity with correct params", async () => {
459
+ const mockUpdateActivity = vi.fn();
460
+ const mockContinueConversation = vi.fn(
461
+ async (_appId: string, _ref: unknown, logic: (ctx: unknown) => Promise<void>) => {
462
+ await logic({
463
+ sendActivity: vi.fn(),
464
+ updateActivity: mockUpdateActivity,
465
+ deleteActivity: vi.fn(),
466
+ });
467
+ },
468
+ );
469
+ mockState.resolveMSTeamsSendContext.mockResolvedValue({
470
+ adapter: { continueConversation: mockContinueConversation },
471
+ appId: "app-id",
472
+ conversationId: "19:conversation@thread.tacv2",
473
+ ref: {
474
+ user: { id: "user-1" },
475
+ agent: { id: "agent-1" },
476
+ conversation: { id: "19:conversation@thread.tacv2", conversationType: "personal" },
477
+ channelId: "msteams",
478
+ },
479
+ log: { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() },
480
+ conversationType: "personal",
481
+ tokenProvider: {},
482
+ });
483
+
484
+ const result = await editMessageMSTeams({
485
+ cfg: {} as KlawConfig,
486
+ to: "conversation:19:conversation@thread.tacv2",
487
+ activityId: "activity-123",
488
+ text: "Updated message text",
489
+ });
490
+
491
+ expect(result.conversationId).toBe("19:conversation@thread.tacv2");
492
+ expect(mockContinueConversation).toHaveBeenCalledTimes(1);
493
+ const call = continueConversationCall(mockContinueConversation);
494
+ expect(call[0]).toBe("app-id");
495
+ expect(continueConversationRef(mockContinueConversation).activityId).toBeUndefined();
496
+ expect(typeof call[2]).toBe("function");
497
+ expect(mockUpdateActivity).toHaveBeenCalledWith({
498
+ type: "message",
499
+ id: "activity-123",
500
+ text: "Updated message text",
501
+ });
502
+ });
503
+ });
504
+
505
+ describe("deleteMessageMSTeams", () => {
506
+ beforeEach(() => {
507
+ mockState.resolveMSTeamsSendContext.mockReset();
508
+ });
509
+
510
+ it("calls continueConversation and deleteActivity with correct activityId", async () => {
511
+ const mockDeleteActivity = vi.fn();
512
+ const mockContinueConversation = vi.fn(
513
+ async (_appId: string, _ref: unknown, logic: (ctx: unknown) => Promise<void>) => {
514
+ await logic({
515
+ sendActivity: vi.fn(),
516
+ updateActivity: vi.fn(),
517
+ deleteActivity: mockDeleteActivity,
518
+ });
519
+ },
520
+ );
521
+ mockState.resolveMSTeamsSendContext.mockResolvedValue({
522
+ adapter: { continueConversation: mockContinueConversation },
523
+ appId: "app-id",
524
+ conversationId: "19:conversation@thread.tacv2",
525
+ ref: {
526
+ user: { id: "user-1" },
527
+ agent: { id: "agent-1" },
528
+ conversation: { id: "19:conversation@thread.tacv2", conversationType: "groupChat" },
529
+ channelId: "msteams",
530
+ },
531
+ log: { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() },
532
+ conversationType: "groupChat",
533
+ tokenProvider: {},
534
+ });
535
+
536
+ const result = await deleteMessageMSTeams({
537
+ cfg: {} as KlawConfig,
538
+ to: "conversation:19:conversation@thread.tacv2",
539
+ activityId: "activity-456",
540
+ });
541
+
542
+ expect(result.conversationId).toBe("19:conversation@thread.tacv2");
543
+ expect(mockContinueConversation).toHaveBeenCalledTimes(1);
544
+ const call = continueConversationCall(mockContinueConversation);
545
+ expect(call[0]).toBe("app-id");
546
+ expect(continueConversationRef(mockContinueConversation).activityId).toBeUndefined();
547
+ expect(typeof call[2]).toBe("function");
548
+ expect(mockDeleteActivity).toHaveBeenCalledWith("activity-456");
549
+ });
550
+
551
+ it("passes the appId and proactive ref to continueConversation", async () => {
552
+ const mockContinueConversation = vi.fn(
553
+ async (_appId: string, _ref: unknown, logic: (ctx: unknown) => Promise<void>) => {
554
+ await logic({
555
+ sendActivity: vi.fn(),
556
+ updateActivity: vi.fn(),
557
+ deleteActivity: vi.fn(),
558
+ });
559
+ },
560
+ );
561
+ mockState.resolveMSTeamsSendContext.mockResolvedValue({
562
+ adapter: { continueConversation: mockContinueConversation },
563
+ appId: "my-app-id",
564
+ conversationId: "19:conv@thread.tacv2",
565
+ ref: {
566
+ activityId: "original-activity",
567
+ user: { id: "user-1" },
568
+ agent: { id: "agent-1" },
569
+ conversation: { id: "19:conv@thread.tacv2" },
570
+ channelId: "msteams",
571
+ },
572
+ log: { debug: vi.fn(), info: vi.fn(), warn: vi.fn(), error: vi.fn() },
573
+ conversationType: "personal",
574
+ tokenProvider: {},
575
+ });
576
+
577
+ await deleteMessageMSTeams({
578
+ cfg: {} as KlawConfig,
579
+ to: "conversation:19:conv@thread.tacv2",
580
+ activityId: "activity-789",
581
+ });
582
+
583
+ // appId should be forwarded correctly
584
+ expect(continueConversationCall(mockContinueConversation)[0]).toBe("my-app-id");
585
+ // activityId on the proactive ref should be cleared (undefined) — proactive pattern
586
+ expect(continueConversationRef(mockContinueConversation).activityId).toBeUndefined();
587
+ });
588
+ });