@openclaw/msteams 2026.3.13 → 2026.5.2-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/api.ts +3 -0
- package/channel-config-api.ts +1 -0
- package/channel-plugin-api.ts +2 -0
- package/config-api.ts +4 -0
- package/contract-api.ts +4 -0
- package/index.ts +15 -12
- package/openclaw.plugin.json +553 -1
- package/package.json +46 -12
- package/runtime-api.ts +73 -0
- package/secret-contract-api.ts +5 -0
- package/setup-entry.ts +13 -0
- package/setup-plugin-api.ts +3 -0
- package/src/ai-entity.ts +7 -0
- package/src/approval-auth.ts +44 -0
- package/src/attachments/bot-framework.test.ts +461 -0
- package/src/attachments/bot-framework.ts +362 -0
- package/src/attachments/download.ts +63 -19
- package/src/attachments/graph.test.ts +416 -0
- package/src/attachments/graph.ts +163 -72
- package/src/attachments/html.ts +33 -1
- package/src/attachments/payload.ts +1 -1
- package/src/attachments/remote-media.test.ts +137 -0
- package/src/attachments/remote-media.ts +75 -8
- package/src/attachments/shared.test.ts +138 -1
- package/src/attachments/shared.ts +193 -26
- package/src/attachments/types.ts +10 -0
- package/src/attachments.graph.test.ts +342 -0
- package/src/attachments.helpers.test.ts +246 -0
- package/src/attachments.test-helpers.ts +17 -0
- package/src/attachments.test.ts +163 -418
- package/src/attachments.ts +5 -5
- package/src/block-streaming-config.test.ts +61 -0
- package/src/channel-api.ts +1 -0
- package/src/channel.actions.test.ts +742 -0
- package/src/channel.directory.test.ts +145 -4
- package/src/channel.runtime.ts +56 -0
- package/src/channel.setup.ts +77 -0
- package/src/channel.test.ts +128 -0
- package/src/channel.ts +1077 -395
- package/src/config-schema.ts +6 -0
- package/src/config-ui-hints.ts +12 -0
- package/src/conversation-store-fs.test.ts +4 -5
- package/src/conversation-store-fs.ts +35 -51
- package/src/conversation-store-helpers.test.ts +202 -0
- package/src/conversation-store-helpers.ts +105 -0
- package/src/conversation-store-memory.ts +27 -23
- package/src/conversation-store.shared.test.ts +225 -0
- package/src/conversation-store.ts +30 -0
- package/src/directory-live.test.ts +156 -0
- package/src/directory-live.ts +7 -4
- package/src/doctor.ts +27 -0
- package/src/errors.test.ts +64 -1
- package/src/errors.ts +50 -9
- package/src/feedback-reflection-prompt.ts +117 -0
- package/src/feedback-reflection-store.ts +114 -0
- package/src/feedback-reflection.test.ts +237 -0
- package/src/feedback-reflection.ts +283 -0
- package/src/file-consent-helpers.test.ts +83 -0
- package/src/file-consent-helpers.ts +64 -11
- package/src/file-consent-invoke.ts +150 -0
- package/src/file-consent.test.ts +363 -0
- package/src/file-consent.ts +165 -4
- package/src/graph-chat.ts +5 -3
- package/src/graph-group-management.test.ts +318 -0
- package/src/graph-group-management.ts +168 -0
- package/src/graph-members.test.ts +89 -0
- package/src/graph-members.ts +48 -0
- package/src/graph-messages.actions.test.ts +243 -0
- package/src/graph-messages.read.test.ts +391 -0
- package/src/graph-messages.search.test.ts +213 -0
- package/src/graph-messages.test-helpers.ts +50 -0
- package/src/graph-messages.ts +534 -0
- package/src/graph-teams.test.ts +215 -0
- package/src/graph-teams.ts +114 -0
- package/src/graph-thread.test.ts +246 -0
- package/src/graph-thread.ts +146 -0
- package/src/graph-upload.test.ts +161 -4
- package/src/graph-upload.ts +147 -56
- package/src/graph.test.ts +516 -0
- package/src/graph.ts +233 -21
- package/src/inbound.test.ts +156 -1
- package/src/inbound.ts +101 -1
- package/src/media-helpers.ts +1 -1
- package/src/mentions.test.ts +27 -18
- package/src/mentions.ts +2 -2
- package/src/messenger.test.ts +504 -23
- package/src/messenger.ts +133 -52
- package/src/monitor-handler/access.ts +125 -0
- package/src/monitor-handler/inbound-media.test.ts +289 -0
- package/src/monitor-handler/inbound-media.ts +57 -5
- package/src/monitor-handler/message-handler-mock-support.test-support.ts +28 -0
- package/src/monitor-handler/message-handler.authz.test.ts +588 -74
- package/src/monitor-handler/message-handler.dm-media.test.ts +54 -0
- package/src/monitor-handler/message-handler.test-support.ts +100 -0
- package/src/monitor-handler/message-handler.thread-parent.test.ts +223 -0
- package/src/monitor-handler/message-handler.thread-session.test.ts +77 -0
- package/src/monitor-handler/message-handler.ts +470 -164
- package/src/monitor-handler/reaction-handler.test.ts +267 -0
- package/src/monitor-handler/reaction-handler.ts +210 -0
- package/src/monitor-handler/thread-session.ts +17 -0
- package/src/monitor-handler.adaptive-card.test.ts +162 -0
- package/src/monitor-handler.feedback-authz.test.ts +314 -0
- package/src/monitor-handler.file-consent.test.ts +281 -79
- package/src/monitor-handler.sso.test.ts +563 -0
- package/src/monitor-handler.test-helpers.ts +180 -0
- package/src/monitor-handler.ts +459 -115
- package/src/monitor-handler.types.ts +27 -0
- package/src/monitor-types.ts +1 -0
- package/src/monitor.lifecycle.test.ts +74 -10
- package/src/monitor.test.ts +35 -1
- package/src/monitor.ts +143 -46
- package/src/oauth.flow.ts +77 -0
- package/src/oauth.shared.ts +37 -0
- package/src/oauth.test.ts +305 -0
- package/src/oauth.token.ts +158 -0
- package/src/oauth.ts +130 -0
- package/src/outbound.test.ts +10 -11
- package/src/outbound.ts +62 -44
- package/src/pending-uploads-fs.test.ts +246 -0
- package/src/pending-uploads-fs.ts +235 -0
- package/src/pending-uploads.test.ts +173 -0
- package/src/pending-uploads.ts +34 -2
- package/src/policy.test.ts +11 -5
- package/src/policy.ts +5 -5
- package/src/polls.test.ts +106 -5
- package/src/polls.ts +15 -7
- package/src/presentation.ts +68 -0
- package/src/probe.test.ts +27 -8
- package/src/probe.ts +43 -9
- package/src/reply-dispatcher.test.ts +437 -0
- package/src/reply-dispatcher.ts +259 -73
- package/src/reply-stream-controller.test.ts +235 -0
- package/src/reply-stream-controller.ts +147 -0
- package/src/resolve-allowlist.test.ts +105 -1
- package/src/resolve-allowlist.ts +112 -7
- package/src/runtime.ts +6 -3
- package/src/sdk-types.ts +43 -3
- package/src/sdk.test.ts +666 -0
- package/src/sdk.ts +867 -16
- package/src/secret-contract.ts +49 -0
- package/src/secret-input.ts +1 -1
- package/src/send-context.ts +76 -9
- package/src/send.test.ts +389 -5
- package/src/send.ts +140 -32
- package/src/sent-message-cache.ts +30 -18
- package/src/session-route.ts +40 -0
- package/src/setup-core.ts +160 -0
- package/src/setup-surface.test.ts +202 -0
- package/src/setup-surface.ts +320 -0
- package/src/sso-token-store.test.ts +72 -0
- package/src/sso-token-store.ts +166 -0
- package/src/sso.ts +300 -0
- package/src/storage.ts +1 -1
- package/src/store-fs.ts +2 -2
- package/src/streaming-message.test.ts +262 -0
- package/src/streaming-message.ts +297 -0
- package/src/test-runtime.ts +1 -1
- package/src/thread-parent-context.test.ts +224 -0
- package/src/thread-parent-context.ts +159 -0
- package/src/token.test.ts +237 -50
- package/src/token.ts +162 -7
- package/src/user-agent.test.ts +86 -0
- package/src/user-agent.ts +53 -0
- package/src/webhook-timeouts.ts +27 -0
- package/src/welcome-card.test.ts +81 -0
- package/src/welcome-card.ts +57 -0
- package/test-api.ts +1 -0
- package/tsconfig.json +16 -0
- package/CHANGELOG.md +0 -107
- package/src/file-lock.ts +0 -1
- package/src/graph-users.test.ts +0 -66
- package/src/onboarding.ts +0 -381
- package/src/polls-store.test.ts +0 -38
- package/src/revoked-context.test.ts +0 -39
- package/src/token-response.test.ts +0 -23
|
@@ -0,0 +1,243 @@
|
|
|
1
|
+
import { beforeAll, describe, expect, it } from "vitest";
|
|
2
|
+
import type { OpenClawConfig } from "../runtime-api.js";
|
|
3
|
+
import {
|
|
4
|
+
CHANNEL_TO,
|
|
5
|
+
CHAT_ID,
|
|
6
|
+
TOKEN,
|
|
7
|
+
type GraphMessagesTestModule,
|
|
8
|
+
getGraphMessagesMockState,
|
|
9
|
+
installGraphMessagesMockDefaults,
|
|
10
|
+
loadGraphMessagesTestModule,
|
|
11
|
+
} from "./graph-messages.test-helpers.js";
|
|
12
|
+
|
|
13
|
+
const mockState = getGraphMessagesMockState();
|
|
14
|
+
installGraphMessagesMockDefaults();
|
|
15
|
+
let pinMessageMSTeams: GraphMessagesTestModule["pinMessageMSTeams"];
|
|
16
|
+
let reactMessageMSTeams: GraphMessagesTestModule["reactMessageMSTeams"];
|
|
17
|
+
let unpinMessageMSTeams: GraphMessagesTestModule["unpinMessageMSTeams"];
|
|
18
|
+
let unreactMessageMSTeams: GraphMessagesTestModule["unreactMessageMSTeams"];
|
|
19
|
+
|
|
20
|
+
beforeAll(async () => {
|
|
21
|
+
({ pinMessageMSTeams, reactMessageMSTeams, unpinMessageMSTeams, unreactMessageMSTeams } =
|
|
22
|
+
await loadGraphMessagesTestModule());
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
describe("pinMessageMSTeams", () => {
|
|
26
|
+
it("pins a message in a chat via message@odata.bind body", async () => {
|
|
27
|
+
mockState.postGraphJson.mockResolvedValue({ id: "pinned-1" });
|
|
28
|
+
|
|
29
|
+
const result = await pinMessageMSTeams({
|
|
30
|
+
cfg: {} as OpenClawConfig,
|
|
31
|
+
to: CHAT_ID,
|
|
32
|
+
messageId: "msg-1",
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
expect(result).toEqual({ ok: true, pinnedMessageId: "pinned-1" });
|
|
36
|
+
expect(mockState.postGraphJson).toHaveBeenCalledWith({
|
|
37
|
+
token: TOKEN,
|
|
38
|
+
path: `/chats/${encodeURIComponent(CHAT_ID)}/pinnedMessages`,
|
|
39
|
+
body: {
|
|
40
|
+
"message@odata.bind": `https://graph.microsoft.com/v1.0/chats/${encodeURIComponent(
|
|
41
|
+
CHAT_ID,
|
|
42
|
+
)}/messages/${encodeURIComponent("msg-1")}`,
|
|
43
|
+
},
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("rejects pinning a message in a channel on Graph v1.0", async () => {
|
|
48
|
+
await expect(
|
|
49
|
+
pinMessageMSTeams({
|
|
50
|
+
cfg: {} as OpenClawConfig,
|
|
51
|
+
to: CHANNEL_TO,
|
|
52
|
+
messageId: "msg-2",
|
|
53
|
+
}),
|
|
54
|
+
).rejects.toThrow(/Pin\/unpin is not supported for channel messages/);
|
|
55
|
+
expect(mockState.postGraphJson).not.toHaveBeenCalled();
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
describe("unpinMessageMSTeams", () => {
|
|
60
|
+
it("unpins a message from a chat", async () => {
|
|
61
|
+
mockState.deleteGraphRequest.mockResolvedValue(undefined);
|
|
62
|
+
|
|
63
|
+
const result = await unpinMessageMSTeams({
|
|
64
|
+
cfg: {} as OpenClawConfig,
|
|
65
|
+
to: CHAT_ID,
|
|
66
|
+
pinnedMessageId: "pinned-1",
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
expect(result).toEqual({ ok: true });
|
|
70
|
+
expect(mockState.deleteGraphRequest).toHaveBeenCalledWith({
|
|
71
|
+
token: TOKEN,
|
|
72
|
+
path: `/chats/${encodeURIComponent(CHAT_ID)}/pinnedMessages/${encodeURIComponent("pinned-1")}`,
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("rejects unpinning a message from a channel on Graph v1.0", async () => {
|
|
77
|
+
await expect(
|
|
78
|
+
unpinMessageMSTeams({
|
|
79
|
+
cfg: {} as OpenClawConfig,
|
|
80
|
+
to: CHANNEL_TO,
|
|
81
|
+
pinnedMessageId: "pinned-2",
|
|
82
|
+
}),
|
|
83
|
+
).rejects.toThrow(/Pin\/unpin is not supported for channel messages/);
|
|
84
|
+
expect(mockState.deleteGraphRequest).not.toHaveBeenCalled();
|
|
85
|
+
});
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
describe("reactMessageMSTeams", () => {
|
|
89
|
+
it("sets a like reaction on a chat message", async () => {
|
|
90
|
+
mockState.postGraphBetaJson.mockResolvedValue(undefined);
|
|
91
|
+
|
|
92
|
+
const result = await reactMessageMSTeams({
|
|
93
|
+
cfg: {} as OpenClawConfig,
|
|
94
|
+
to: CHAT_ID,
|
|
95
|
+
messageId: "msg-1",
|
|
96
|
+
reactionType: "like",
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
expect(result).toEqual({ ok: true });
|
|
100
|
+
expect(mockState.postGraphBetaJson).toHaveBeenCalledWith({
|
|
101
|
+
token: TOKEN,
|
|
102
|
+
path: `/chats/${encodeURIComponent(CHAT_ID)}/messages/msg-1/setReaction`,
|
|
103
|
+
body: { reactionType: "like" },
|
|
104
|
+
});
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it("sets a reaction on a channel message", async () => {
|
|
108
|
+
mockState.postGraphBetaJson.mockResolvedValue(undefined);
|
|
109
|
+
|
|
110
|
+
const result = await reactMessageMSTeams({
|
|
111
|
+
cfg: {} as OpenClawConfig,
|
|
112
|
+
to: CHANNEL_TO,
|
|
113
|
+
messageId: "msg-2",
|
|
114
|
+
reactionType: "heart",
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
expect(result).toEqual({ ok: true });
|
|
118
|
+
expect(mockState.postGraphBetaJson).toHaveBeenCalledWith({
|
|
119
|
+
token: TOKEN,
|
|
120
|
+
path: "/teams/team-id-1/channels/channel-id-1/messages/msg-2/setReaction",
|
|
121
|
+
body: { reactionType: "heart" },
|
|
122
|
+
});
|
|
123
|
+
});
|
|
124
|
+
|
|
125
|
+
it("normalizes reaction type to lowercase", async () => {
|
|
126
|
+
mockState.postGraphBetaJson.mockResolvedValue(undefined);
|
|
127
|
+
|
|
128
|
+
await reactMessageMSTeams({
|
|
129
|
+
cfg: {} as OpenClawConfig,
|
|
130
|
+
to: CHAT_ID,
|
|
131
|
+
messageId: "msg-1",
|
|
132
|
+
reactionType: "LAUGH",
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
expect(mockState.postGraphBetaJson).toHaveBeenCalledWith({
|
|
136
|
+
token: TOKEN,
|
|
137
|
+
path: `/chats/${encodeURIComponent(CHAT_ID)}/messages/msg-1/setReaction`,
|
|
138
|
+
body: { reactionType: "laugh" },
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
it("passes through non-well-known reaction types (e.g. Unicode emoji)", async () => {
|
|
143
|
+
// Graph setReaction accepts arbitrary Unicode emoji plus the legacy
|
|
144
|
+
// well-known types; normalizeReactionType only lowercases the legacy set
|
|
145
|
+
// and lets any other non-empty value through unchanged.
|
|
146
|
+
mockState.postGraphBetaJson.mockResolvedValue(undefined);
|
|
147
|
+
|
|
148
|
+
await reactMessageMSTeams({
|
|
149
|
+
cfg: {} as OpenClawConfig,
|
|
150
|
+
to: CHAT_ID,
|
|
151
|
+
messageId: "msg-1",
|
|
152
|
+
reactionType: "🎉",
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
expect(mockState.postGraphBetaJson).toHaveBeenCalledWith({
|
|
156
|
+
token: TOKEN,
|
|
157
|
+
path: `/chats/${encodeURIComponent(CHAT_ID)}/messages/msg-1/setReaction`,
|
|
158
|
+
body: { reactionType: "🎉" },
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
it("rejects empty reaction type", async () => {
|
|
163
|
+
await expect(
|
|
164
|
+
reactMessageMSTeams({
|
|
165
|
+
cfg: {} as OpenClawConfig,
|
|
166
|
+
to: CHAT_ID,
|
|
167
|
+
messageId: "msg-1",
|
|
168
|
+
reactionType: " ",
|
|
169
|
+
}),
|
|
170
|
+
).rejects.toThrow(/Reaction type is required/);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it("resolves user: target through conversation store", async () => {
|
|
174
|
+
mockState.findPreferredDmByUserId.mockResolvedValue({
|
|
175
|
+
conversationId: "a:bot-id",
|
|
176
|
+
reference: { graphChatId: "19:dm-chat@thread.tacv2" },
|
|
177
|
+
});
|
|
178
|
+
mockState.postGraphBetaJson.mockResolvedValue(undefined);
|
|
179
|
+
|
|
180
|
+
await reactMessageMSTeams({
|
|
181
|
+
cfg: {} as OpenClawConfig,
|
|
182
|
+
to: "user:aad-user-1",
|
|
183
|
+
messageId: "msg-1",
|
|
184
|
+
reactionType: "like",
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
expect(mockState.findPreferredDmByUserId).toHaveBeenCalledWith("aad-user-1");
|
|
188
|
+
expect(mockState.postGraphBetaJson).toHaveBeenCalledWith({
|
|
189
|
+
token: TOKEN,
|
|
190
|
+
path: `/chats/${encodeURIComponent("19:dm-chat@thread.tacv2")}/messages/msg-1/setReaction`,
|
|
191
|
+
body: { reactionType: "like" },
|
|
192
|
+
});
|
|
193
|
+
});
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
describe("unreactMessageMSTeams", () => {
|
|
197
|
+
it("removes a reaction from a chat message", async () => {
|
|
198
|
+
mockState.postGraphBetaJson.mockResolvedValue(undefined);
|
|
199
|
+
|
|
200
|
+
const result = await unreactMessageMSTeams({
|
|
201
|
+
cfg: {} as OpenClawConfig,
|
|
202
|
+
to: CHAT_ID,
|
|
203
|
+
messageId: "msg-1",
|
|
204
|
+
reactionType: "sad",
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
expect(result).toEqual({ ok: true });
|
|
208
|
+
expect(mockState.postGraphBetaJson).toHaveBeenCalledWith({
|
|
209
|
+
token: TOKEN,
|
|
210
|
+
path: `/chats/${encodeURIComponent(CHAT_ID)}/messages/msg-1/unsetReaction`,
|
|
211
|
+
body: { reactionType: "sad" },
|
|
212
|
+
});
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it("removes a reaction from a channel message", async () => {
|
|
216
|
+
mockState.postGraphBetaJson.mockResolvedValue(undefined);
|
|
217
|
+
|
|
218
|
+
const result = await unreactMessageMSTeams({
|
|
219
|
+
cfg: {} as OpenClawConfig,
|
|
220
|
+
to: CHANNEL_TO,
|
|
221
|
+
messageId: "msg-2",
|
|
222
|
+
reactionType: "angry",
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
expect(result).toEqual({ ok: true });
|
|
226
|
+
expect(mockState.postGraphBetaJson).toHaveBeenCalledWith({
|
|
227
|
+
token: TOKEN,
|
|
228
|
+
path: "/teams/team-id-1/channels/channel-id-1/messages/msg-2/unsetReaction",
|
|
229
|
+
body: { reactionType: "angry" },
|
|
230
|
+
});
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it("rejects empty reaction type", async () => {
|
|
234
|
+
await expect(
|
|
235
|
+
unreactMessageMSTeams({
|
|
236
|
+
cfg: {} as OpenClawConfig,
|
|
237
|
+
to: CHAT_ID,
|
|
238
|
+
messageId: "msg-1",
|
|
239
|
+
reactionType: "",
|
|
240
|
+
}),
|
|
241
|
+
).rejects.toThrow(/Reaction type is required/);
|
|
242
|
+
});
|
|
243
|
+
});
|
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
import { beforeAll, describe, expect, it } from "vitest";
|
|
2
|
+
import type { OpenClawConfig } from "../runtime-api.js";
|
|
3
|
+
import {
|
|
4
|
+
CHANNEL_TO,
|
|
5
|
+
CHAT_ID,
|
|
6
|
+
TOKEN,
|
|
7
|
+
type GraphMessagesTestModule,
|
|
8
|
+
getGraphMessagesMockState,
|
|
9
|
+
installGraphMessagesMockDefaults,
|
|
10
|
+
loadGraphMessagesTestModule,
|
|
11
|
+
} from "./graph-messages.test-helpers.js";
|
|
12
|
+
|
|
13
|
+
const mockState = getGraphMessagesMockState();
|
|
14
|
+
installGraphMessagesMockDefaults();
|
|
15
|
+
let getMessageMSTeams: GraphMessagesTestModule["getMessageMSTeams"];
|
|
16
|
+
let listPinsMSTeams: GraphMessagesTestModule["listPinsMSTeams"];
|
|
17
|
+
let listReactionsMSTeams: GraphMessagesTestModule["listReactionsMSTeams"];
|
|
18
|
+
|
|
19
|
+
beforeAll(async () => {
|
|
20
|
+
({ getMessageMSTeams, listPinsMSTeams, listReactionsMSTeams } =
|
|
21
|
+
await loadGraphMessagesTestModule());
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
describe("getMessageMSTeams", () => {
|
|
25
|
+
it("resolves user: target using graphChatId from store", async () => {
|
|
26
|
+
mockState.findPreferredDmByUserId.mockResolvedValue({
|
|
27
|
+
conversationId: "a:bot-framework-dm-id",
|
|
28
|
+
reference: { graphChatId: "19:graph-native-chat@thread.tacv2" },
|
|
29
|
+
});
|
|
30
|
+
mockState.fetchGraphJson.mockResolvedValue({
|
|
31
|
+
id: "msg-1",
|
|
32
|
+
body: { content: "From user DM" },
|
|
33
|
+
createdDateTime: "2026-03-23T12:00:00Z",
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
await getMessageMSTeams({
|
|
37
|
+
cfg: {} as OpenClawConfig,
|
|
38
|
+
to: "user:aad-object-id-123",
|
|
39
|
+
messageId: "msg-1",
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
expect(mockState.findPreferredDmByUserId).toHaveBeenCalledWith("aad-object-id-123");
|
|
43
|
+
expect(mockState.fetchGraphJson).toHaveBeenCalledWith({
|
|
44
|
+
token: TOKEN,
|
|
45
|
+
path: `/chats/${encodeURIComponent("19:graph-native-chat@thread.tacv2")}/messages/msg-1`,
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it("falls back to conversationId when it starts with 19:", async () => {
|
|
50
|
+
mockState.findPreferredDmByUserId.mockResolvedValue({
|
|
51
|
+
conversationId: "19:resolved-chat@thread.tacv2",
|
|
52
|
+
reference: {},
|
|
53
|
+
});
|
|
54
|
+
mockState.fetchGraphJson.mockResolvedValue({
|
|
55
|
+
id: "msg-1",
|
|
56
|
+
body: { content: "Hello" },
|
|
57
|
+
createdDateTime: "2026-03-23T10:00:00Z",
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
await getMessageMSTeams({
|
|
61
|
+
cfg: {} as OpenClawConfig,
|
|
62
|
+
to: "user:aad-id",
|
|
63
|
+
messageId: "msg-1",
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
expect(mockState.fetchGraphJson).toHaveBeenCalledWith({
|
|
67
|
+
token: TOKEN,
|
|
68
|
+
path: `/chats/${encodeURIComponent("19:resolved-chat@thread.tacv2")}/messages/msg-1`,
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("throws when user: target has no stored conversation", async () => {
|
|
73
|
+
mockState.findPreferredDmByUserId.mockResolvedValue(null);
|
|
74
|
+
|
|
75
|
+
await expect(
|
|
76
|
+
getMessageMSTeams({
|
|
77
|
+
cfg: {} as OpenClawConfig,
|
|
78
|
+
to: "user:unknown-user",
|
|
79
|
+
messageId: "msg-1",
|
|
80
|
+
}),
|
|
81
|
+
).rejects.toThrow("No conversation found for user:unknown-user");
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
it("throws when user: target has Bot Framework ID and no graphChatId", async () => {
|
|
85
|
+
mockState.findPreferredDmByUserId.mockResolvedValue({
|
|
86
|
+
conversationId: "a:bot-framework-dm-id",
|
|
87
|
+
reference: {},
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
await expect(
|
|
91
|
+
getMessageMSTeams({
|
|
92
|
+
cfg: {} as OpenClawConfig,
|
|
93
|
+
to: "user:some-user",
|
|
94
|
+
messageId: "msg-1",
|
|
95
|
+
}),
|
|
96
|
+
).rejects.toThrow("Bot Framework ID");
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("strips conversation: prefix from target", async () => {
|
|
100
|
+
mockState.fetchGraphJson.mockResolvedValue({
|
|
101
|
+
id: "msg-1",
|
|
102
|
+
body: { content: "Hello" },
|
|
103
|
+
from: undefined,
|
|
104
|
+
createdDateTime: "2026-03-23T10:00:00Z",
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
await getMessageMSTeams({
|
|
108
|
+
cfg: {} as OpenClawConfig,
|
|
109
|
+
to: `conversation:${CHAT_ID}`,
|
|
110
|
+
messageId: "msg-1",
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
expect(mockState.fetchGraphJson).toHaveBeenCalledWith({
|
|
114
|
+
token: TOKEN,
|
|
115
|
+
path: `/chats/${encodeURIComponent(CHAT_ID)}/messages/msg-1`,
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("reads a message from a chat conversation", async () => {
|
|
120
|
+
mockState.fetchGraphJson.mockResolvedValue({
|
|
121
|
+
id: "msg-1",
|
|
122
|
+
body: { content: "Hello world", contentType: "text" },
|
|
123
|
+
from: { user: { id: "user-1", displayName: "Alice" } },
|
|
124
|
+
createdDateTime: "2026-03-23T10:00:00Z",
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
const result = await getMessageMSTeams({
|
|
128
|
+
cfg: {} as OpenClawConfig,
|
|
129
|
+
to: CHAT_ID,
|
|
130
|
+
messageId: "msg-1",
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
expect(result).toEqual({
|
|
134
|
+
id: "msg-1",
|
|
135
|
+
text: "Hello world",
|
|
136
|
+
from: { user: { id: "user-1", displayName: "Alice" } },
|
|
137
|
+
createdAt: "2026-03-23T10:00:00Z",
|
|
138
|
+
});
|
|
139
|
+
expect(mockState.fetchGraphJson).toHaveBeenCalledWith({
|
|
140
|
+
token: TOKEN,
|
|
141
|
+
path: `/chats/${encodeURIComponent(CHAT_ID)}/messages/msg-1`,
|
|
142
|
+
});
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("reads a message from a channel conversation", async () => {
|
|
146
|
+
mockState.fetchGraphJson.mockResolvedValue({
|
|
147
|
+
id: "msg-2",
|
|
148
|
+
body: { content: "Channel message" },
|
|
149
|
+
from: { application: { id: "app-1", displayName: "Bot" } },
|
|
150
|
+
createdDateTime: "2026-03-23T11:00:00Z",
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
const result = await getMessageMSTeams({
|
|
154
|
+
cfg: {} as OpenClawConfig,
|
|
155
|
+
to: CHANNEL_TO,
|
|
156
|
+
messageId: "msg-2",
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
expect(result).toEqual({
|
|
160
|
+
id: "msg-2",
|
|
161
|
+
text: "Channel message",
|
|
162
|
+
from: { application: { id: "app-1", displayName: "Bot" } },
|
|
163
|
+
createdAt: "2026-03-23T11:00:00Z",
|
|
164
|
+
});
|
|
165
|
+
expect(mockState.fetchGraphJson).toHaveBeenCalledWith({
|
|
166
|
+
token: TOKEN,
|
|
167
|
+
path: "/teams/team-id-1/channels/channel-id-1/messages/msg-2",
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
describe("listPinsMSTeams", () => {
|
|
173
|
+
it("lists pinned messages in a chat", async () => {
|
|
174
|
+
mockState.fetchGraphJson.mockResolvedValue({
|
|
175
|
+
value: [
|
|
176
|
+
{
|
|
177
|
+
id: "pinned-1",
|
|
178
|
+
message: { id: "msg-1", body: { content: "Pinned msg" } },
|
|
179
|
+
},
|
|
180
|
+
{
|
|
181
|
+
id: "pinned-2",
|
|
182
|
+
message: { id: "msg-2", body: { content: "Another pin" } },
|
|
183
|
+
},
|
|
184
|
+
],
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
const result = await listPinsMSTeams({
|
|
188
|
+
cfg: {} as OpenClawConfig,
|
|
189
|
+
to: CHAT_ID,
|
|
190
|
+
});
|
|
191
|
+
|
|
192
|
+
expect(result.pins).toEqual([
|
|
193
|
+
{ id: "pinned-1", pinnedMessageId: "pinned-1", messageId: "msg-1", text: "Pinned msg" },
|
|
194
|
+
{ id: "pinned-2", pinnedMessageId: "pinned-2", messageId: "msg-2", text: "Another pin" },
|
|
195
|
+
]);
|
|
196
|
+
expect(mockState.fetchGraphJson).toHaveBeenCalledWith({
|
|
197
|
+
token: TOKEN,
|
|
198
|
+
path: `/chats/${encodeURIComponent(CHAT_ID)}/pinnedMessages?$expand=message`,
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
|
|
202
|
+
it("returns empty array when no pins exist", async () => {
|
|
203
|
+
mockState.fetchGraphJson.mockResolvedValue({ value: [] });
|
|
204
|
+
|
|
205
|
+
const result = await listPinsMSTeams({
|
|
206
|
+
cfg: {} as OpenClawConfig,
|
|
207
|
+
to: CHAT_ID,
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
expect(result.pins).toEqual([]);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it("follows @odata.nextLink pagination", async () => {
|
|
214
|
+
mockState.fetchGraphJson.mockResolvedValue({
|
|
215
|
+
value: [{ id: "pinned-1", message: { id: "msg-1", body: { content: "First page" } } }],
|
|
216
|
+
"@odata.nextLink":
|
|
217
|
+
"https://graph.microsoft.com/v1.0/chats/19%3Aabc%40thread.tacv2/pinnedMessages?$expand=message&$skiptoken=page2",
|
|
218
|
+
});
|
|
219
|
+
mockState.fetchGraphAbsoluteUrl.mockResolvedValue({
|
|
220
|
+
value: [{ id: "pinned-2", message: { id: "msg-2", body: { content: "Second page" } } }],
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
const result = await listPinsMSTeams({
|
|
224
|
+
cfg: {} as OpenClawConfig,
|
|
225
|
+
to: CHAT_ID,
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
expect(result.pins).toEqual([
|
|
229
|
+
{ id: "pinned-1", pinnedMessageId: "pinned-1", messageId: "msg-1", text: "First page" },
|
|
230
|
+
{ id: "pinned-2", pinnedMessageId: "pinned-2", messageId: "msg-2", text: "Second page" },
|
|
231
|
+
]);
|
|
232
|
+
expect(mockState.fetchGraphAbsoluteUrl).toHaveBeenCalledWith({
|
|
233
|
+
token: TOKEN,
|
|
234
|
+
url: "https://graph.microsoft.com/v1.0/chats/19%3Aabc%40thread.tacv2/pinnedMessages?$expand=message&$skiptoken=page2",
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
|
|
238
|
+
it("stops paginating after max pages", async () => {
|
|
239
|
+
const makePageResponse = (pageNum: number) => ({
|
|
240
|
+
value: [
|
|
241
|
+
{
|
|
242
|
+
id: `pinned-${pageNum}`,
|
|
243
|
+
message: { id: `msg-${pageNum}`, body: { content: `Page ${pageNum}` } },
|
|
244
|
+
},
|
|
245
|
+
],
|
|
246
|
+
"@odata.nextLink": `https://graph.microsoft.com/v1.0/next?page=${pageNum + 1}`,
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
mockState.fetchGraphJson.mockResolvedValue(makePageResponse(1));
|
|
250
|
+
for (let i = 2; i <= 10; i++) {
|
|
251
|
+
mockState.fetchGraphAbsoluteUrl.mockResolvedValueOnce(makePageResponse(i));
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const result = await listPinsMSTeams({
|
|
255
|
+
cfg: {} as OpenClawConfig,
|
|
256
|
+
to: CHAT_ID,
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
expect(result.pins).toHaveLength(10);
|
|
260
|
+
expect(mockState.fetchGraphAbsoluteUrl).toHaveBeenCalledTimes(9);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it("throws for channel list-pins (not supported on Graph v1.0)", async () => {
|
|
264
|
+
await expect(
|
|
265
|
+
listPinsMSTeams({
|
|
266
|
+
cfg: {} as OpenClawConfig,
|
|
267
|
+
to: CHANNEL_TO,
|
|
268
|
+
}),
|
|
269
|
+
).rejects.toThrow("not supported for channels");
|
|
270
|
+
});
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
describe("listReactionsMSTeams", () => {
|
|
274
|
+
it("lists reactions grouped by type with user details", async () => {
|
|
275
|
+
mockState.fetchGraphJson.mockResolvedValue({
|
|
276
|
+
id: "msg-1",
|
|
277
|
+
body: { content: "Hello" },
|
|
278
|
+
reactions: [
|
|
279
|
+
{ reactionType: "like", user: { id: "u1", displayName: "Alice" } },
|
|
280
|
+
{ reactionType: "like", user: { id: "u2", displayName: "Bob" } },
|
|
281
|
+
{ reactionType: "heart", user: { id: "u1", displayName: "Alice" } },
|
|
282
|
+
],
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
const result = await listReactionsMSTeams({
|
|
286
|
+
cfg: {} as OpenClawConfig,
|
|
287
|
+
to: CHAT_ID,
|
|
288
|
+
messageId: "msg-1",
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
expect(result.reactions).toEqual([
|
|
292
|
+
{
|
|
293
|
+
reactionType: "like",
|
|
294
|
+
name: "like",
|
|
295
|
+
emoji: "\u{1F44D}",
|
|
296
|
+
count: 2,
|
|
297
|
+
users: [
|
|
298
|
+
{ id: "u1", displayName: "Alice" },
|
|
299
|
+
{ id: "u2", displayName: "Bob" },
|
|
300
|
+
],
|
|
301
|
+
},
|
|
302
|
+
{
|
|
303
|
+
reactionType: "heart",
|
|
304
|
+
name: "heart",
|
|
305
|
+
emoji: "\u2764\uFE0F",
|
|
306
|
+
count: 1,
|
|
307
|
+
users: [{ id: "u1", displayName: "Alice" }],
|
|
308
|
+
},
|
|
309
|
+
]);
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it("returns empty array when message has no reactions", async () => {
|
|
313
|
+
mockState.fetchGraphJson.mockResolvedValue({
|
|
314
|
+
id: "msg-1",
|
|
315
|
+
body: { content: "No reactions" },
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
const result = await listReactionsMSTeams({
|
|
319
|
+
cfg: {} as OpenClawConfig,
|
|
320
|
+
to: CHAT_ID,
|
|
321
|
+
messageId: "msg-1",
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
expect(result.reactions).toEqual([]);
|
|
325
|
+
});
|
|
326
|
+
|
|
327
|
+
it("counts reactions from users without an ID", async () => {
|
|
328
|
+
mockState.fetchGraphJson.mockResolvedValue({
|
|
329
|
+
id: "msg-1",
|
|
330
|
+
body: { content: "Hello" },
|
|
331
|
+
reactions: [
|
|
332
|
+
{ reactionType: "like", user: { id: "u1", displayName: "Alice" } },
|
|
333
|
+
{ reactionType: "like", user: { displayName: "Deleted User" } },
|
|
334
|
+
{ reactionType: "like", user: undefined },
|
|
335
|
+
{ reactionType: "like" },
|
|
336
|
+
{ reactionType: "heart", user: { id: "u2", displayName: "Bob" } },
|
|
337
|
+
],
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
const result = await listReactionsMSTeams({
|
|
341
|
+
cfg: {} as OpenClawConfig,
|
|
342
|
+
to: CHAT_ID,
|
|
343
|
+
messageId: "msg-1",
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
expect(result.reactions).toEqual([
|
|
347
|
+
{
|
|
348
|
+
reactionType: "like",
|
|
349
|
+
name: "like",
|
|
350
|
+
emoji: "\u{1F44D}",
|
|
351
|
+
count: 4,
|
|
352
|
+
users: [{ id: "u1", displayName: "Alice" }],
|
|
353
|
+
},
|
|
354
|
+
{
|
|
355
|
+
reactionType: "heart",
|
|
356
|
+
name: "heart",
|
|
357
|
+
emoji: "\u2764\uFE0F",
|
|
358
|
+
count: 1,
|
|
359
|
+
users: [{ id: "u2", displayName: "Bob" }],
|
|
360
|
+
},
|
|
361
|
+
]);
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
it("fetches from channel path for channel targets", async () => {
|
|
365
|
+
mockState.fetchGraphJson.mockResolvedValue({
|
|
366
|
+
id: "msg-2",
|
|
367
|
+
body: { content: "Channel msg" },
|
|
368
|
+
reactions: [{ reactionType: "surprised", user: { id: "u3", displayName: "Carol" } }],
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
const result = await listReactionsMSTeams({
|
|
372
|
+
cfg: {} as OpenClawConfig,
|
|
373
|
+
to: CHANNEL_TO,
|
|
374
|
+
messageId: "msg-2",
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
expect(result.reactions).toEqual([
|
|
378
|
+
{
|
|
379
|
+
reactionType: "surprised",
|
|
380
|
+
name: "surprised",
|
|
381
|
+
emoji: "\u{1F62E}",
|
|
382
|
+
count: 1,
|
|
383
|
+
users: [{ id: "u3", displayName: "Carol" }],
|
|
384
|
+
},
|
|
385
|
+
]);
|
|
386
|
+
expect(mockState.fetchGraphJson).toHaveBeenCalledWith({
|
|
387
|
+
token: TOKEN,
|
|
388
|
+
path: "/teams/team-id-1/channels/channel-id-1/messages/msg-2",
|
|
389
|
+
});
|
|
390
|
+
});
|
|
391
|
+
});
|