@openclaw/msteams 2026.3.13 → 2026.5.1-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/api.ts +3 -0
- package/channel-config-api.ts +1 -0
- package/channel-plugin-api.ts +2 -0
- package/config-api.ts +4 -0
- package/contract-api.ts +4 -0
- package/index.ts +15 -12
- package/openclaw.plugin.json +553 -1
- package/package.json +46 -12
- package/runtime-api.ts +73 -0
- package/secret-contract-api.ts +5 -0
- package/setup-entry.ts +13 -0
- package/setup-plugin-api.ts +3 -0
- package/src/ai-entity.ts +7 -0
- package/src/approval-auth.ts +44 -0
- package/src/attachments/bot-framework.test.ts +461 -0
- package/src/attachments/bot-framework.ts +362 -0
- package/src/attachments/download.ts +63 -19
- package/src/attachments/graph.test.ts +416 -0
- package/src/attachments/graph.ts +163 -72
- package/src/attachments/html.ts +33 -1
- package/src/attachments/payload.ts +1 -1
- package/src/attachments/remote-media.test.ts +137 -0
- package/src/attachments/remote-media.ts +75 -8
- package/src/attachments/shared.test.ts +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
|
@@ -1,14 +1,11 @@
|
|
|
1
|
-
import
|
|
2
|
-
import
|
|
3
|
-
import
|
|
4
|
-
import
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
registerMSTeamsHandlers,
|
|
9
|
-
} from "./monitor-handler.js";
|
|
1
|
+
import fs from "node:fs";
|
|
2
|
+
import os from "node:os";
|
|
3
|
+
import path from "node:path";
|
|
4
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
5
|
+
import type { PluginRuntime } from "../runtime-api.js";
|
|
6
|
+
import { respondToMSTeamsFileConsentInvoke } from "./file-consent-invoke.js";
|
|
7
|
+
import { getPendingUploadFs, storePendingUploadFs } from "./pending-uploads-fs.js";
|
|
10
8
|
import { clearPendingUploads, getPendingUpload, storePendingUpload } from "./pending-uploads.js";
|
|
11
|
-
import type { MSTeamsPollStore } from "./polls.js";
|
|
12
9
|
import { setMSTeamsRuntime } from "./runtime.js";
|
|
13
10
|
import type { MSTeamsTurnContext } from "./sdk-types.js";
|
|
14
11
|
|
|
@@ -16,6 +13,14 @@ const fileConsentMockState = vi.hoisted(() => ({
|
|
|
16
13
|
uploadToConsentUrl: vi.fn(),
|
|
17
14
|
}));
|
|
18
15
|
|
|
16
|
+
vi.mock("./monitor-handler/message-handler.js", () => ({
|
|
17
|
+
createMSTeamsMessageHandler: () => async () => {},
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
vi.mock("./monitor-handler/reaction-handler.js", () => ({
|
|
21
|
+
createMSTeamsReactionHandler: () => async () => {},
|
|
22
|
+
}));
|
|
23
|
+
|
|
19
24
|
vi.mock("./file-consent.js", async () => {
|
|
20
25
|
const actual = await vi.importActual<typeof import("./file-consent.js")>("./file-consent.js");
|
|
21
26
|
return {
|
|
@@ -24,75 +29,50 @@ vi.mock("./file-consent.js", async () => {
|
|
|
24
29
|
};
|
|
25
30
|
});
|
|
26
31
|
|
|
27
|
-
|
|
28
|
-
logging: {
|
|
29
|
-
shouldLogVerbose: () => false,
|
|
30
|
-
},
|
|
31
|
-
channel: {
|
|
32
|
-
debounce: {
|
|
33
|
-
resolveInboundDebounceMs: () => 0,
|
|
34
|
-
createInboundDebouncer: () => ({
|
|
35
|
-
enqueue: async () => {},
|
|
36
|
-
}),
|
|
37
|
-
},
|
|
38
|
-
},
|
|
39
|
-
} as unknown as PluginRuntime;
|
|
40
|
-
|
|
41
|
-
function createDeps(): MSTeamsMessageHandlerDeps {
|
|
42
|
-
const adapter: MSTeamsAdapter = {
|
|
43
|
-
continueConversation: async () => {},
|
|
44
|
-
process: async () => {},
|
|
45
|
-
};
|
|
46
|
-
const conversationStore: MSTeamsConversationStore = {
|
|
47
|
-
upsert: async () => {},
|
|
48
|
-
get: async () => null,
|
|
49
|
-
list: async () => [],
|
|
50
|
-
remove: async () => false,
|
|
51
|
-
findByUserId: async () => null,
|
|
52
|
-
};
|
|
53
|
-
const pollStore: MSTeamsPollStore = {
|
|
54
|
-
createPoll: async () => {},
|
|
55
|
-
getPoll: async () => null,
|
|
56
|
-
recordVote: async () => null,
|
|
57
|
-
};
|
|
32
|
+
function createRuntimeStub(stateDir?: string): PluginRuntime {
|
|
58
33
|
return {
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
error: vi.fn(),
|
|
62
|
-
} as unknown as RuntimeEnv,
|
|
63
|
-
appId: "test-app-id",
|
|
64
|
-
adapter,
|
|
65
|
-
tokenProvider: {
|
|
66
|
-
getAccessToken: async () => "token",
|
|
34
|
+
logging: {
|
|
35
|
+
shouldLogVerbose: () => false,
|
|
67
36
|
},
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
debug: vi.fn(),
|
|
37
|
+
channel: {
|
|
38
|
+
debounce: {
|
|
39
|
+
resolveInboundDebounceMs: () => 0,
|
|
40
|
+
createInboundDebouncer: () => ({
|
|
41
|
+
enqueue: async () => {},
|
|
42
|
+
}),
|
|
43
|
+
},
|
|
76
44
|
},
|
|
77
|
-
|
|
45
|
+
state: {
|
|
46
|
+
resolveStateDir: (env?: NodeJS.ProcessEnv) => {
|
|
47
|
+
const override = env?.OPENCLAW_STATE_DIR?.trim();
|
|
48
|
+
if (override) {
|
|
49
|
+
return override;
|
|
50
|
+
}
|
|
51
|
+
return stateDir ?? path.join(os.homedir(), ".openclaw");
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
} as unknown as PluginRuntime;
|
|
78
55
|
}
|
|
79
56
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
return handler;
|
|
88
|
-
}
|
|
57
|
+
const runtimeStub: PluginRuntime = createRuntimeStub();
|
|
58
|
+
|
|
59
|
+
const log = {
|
|
60
|
+
debug: vi.fn(),
|
|
61
|
+
info: vi.fn(),
|
|
62
|
+
error: vi.fn(),
|
|
63
|
+
};
|
|
89
64
|
|
|
90
65
|
function createInvokeContext(params: {
|
|
91
66
|
conversationId: string;
|
|
92
67
|
uploadId: string;
|
|
93
68
|
action: "accept" | "decline";
|
|
94
|
-
}): {
|
|
69
|
+
}): {
|
|
70
|
+
context: MSTeamsTurnContext;
|
|
71
|
+
sendActivity: ReturnType<typeof vi.fn>;
|
|
72
|
+
updateActivity: ReturnType<typeof vi.fn>;
|
|
73
|
+
} {
|
|
95
74
|
const sendActivity = vi.fn(async () => ({ id: "activity-id" }));
|
|
75
|
+
const updateActivity = vi.fn(async () => ({ id: "activity-id" }));
|
|
96
76
|
const uploadInfo =
|
|
97
77
|
params.action === "accept"
|
|
98
78
|
? {
|
|
@@ -118,8 +98,10 @@ function createInvokeContext(params: {
|
|
|
118
98
|
},
|
|
119
99
|
sendActivity,
|
|
120
100
|
sendActivities: async () => [],
|
|
101
|
+
updateActivity,
|
|
121
102
|
} as unknown as MSTeamsTurnContext,
|
|
122
103
|
sendActivity,
|
|
104
|
+
updateActivity,
|
|
123
105
|
};
|
|
124
106
|
}
|
|
125
107
|
|
|
@@ -127,37 +109,47 @@ function createConsentInvokeHarness(params: {
|
|
|
127
109
|
pendingConversationId?: string;
|
|
128
110
|
invokeConversationId: string;
|
|
129
111
|
action: "accept" | "decline";
|
|
112
|
+
consentCardActivityId?: string;
|
|
130
113
|
}) {
|
|
131
114
|
const uploadId = storePendingUpload({
|
|
132
115
|
buffer: Buffer.from("TOP_SECRET_VICTIM_FILE\n"),
|
|
133
116
|
filename: "secret.txt",
|
|
134
117
|
contentType: "text/plain",
|
|
135
118
|
conversationId: params.pendingConversationId ?? "19:victim@thread.v2",
|
|
119
|
+
consentCardActivityId: params.consentCardActivityId,
|
|
136
120
|
});
|
|
137
|
-
const
|
|
138
|
-
const { context, sendActivity } = createInvokeContext({
|
|
121
|
+
const { context, sendActivity, updateActivity } = createInvokeContext({
|
|
139
122
|
conversationId: params.invokeConversationId,
|
|
140
123
|
uploadId,
|
|
141
124
|
action: params.action,
|
|
142
125
|
});
|
|
143
|
-
return { uploadId,
|
|
126
|
+
return { uploadId, context, sendActivity, updateActivity };
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function requirePendingUpload(uploadId: string) {
|
|
130
|
+
const upload = getPendingUpload(uploadId);
|
|
131
|
+
if (!upload) {
|
|
132
|
+
throw new Error(`expected pending upload ${uploadId}`);
|
|
133
|
+
}
|
|
134
|
+
return upload;
|
|
144
135
|
}
|
|
145
136
|
|
|
146
137
|
describe("msteams file consent invoke authz", () => {
|
|
147
138
|
beforeEach(() => {
|
|
148
139
|
setMSTeamsRuntime(runtimeStub);
|
|
149
140
|
clearPendingUploads();
|
|
141
|
+
vi.clearAllMocks();
|
|
150
142
|
fileConsentMockState.uploadToConsentUrl.mockReset();
|
|
151
143
|
fileConsentMockState.uploadToConsentUrl.mockResolvedValue(undefined);
|
|
152
144
|
});
|
|
153
145
|
|
|
154
146
|
it("uploads when invoke conversation matches pending upload conversation", async () => {
|
|
155
|
-
const { uploadId,
|
|
147
|
+
const { uploadId, context, sendActivity } = createConsentInvokeHarness({
|
|
156
148
|
invokeConversationId: "19:victim@thread.v2;messageid=abc123",
|
|
157
149
|
action: "accept",
|
|
158
150
|
});
|
|
159
151
|
|
|
160
|
-
await
|
|
152
|
+
await respondToMSTeamsFileConsentInvoke(context, log);
|
|
161
153
|
|
|
162
154
|
// invokeResponse should be sent immediately
|
|
163
155
|
expect(sendActivity).toHaveBeenCalledWith(
|
|
@@ -176,13 +168,97 @@ describe("msteams file consent invoke authz", () => {
|
|
|
176
168
|
expect(getPendingUpload(uploadId)).toBeUndefined();
|
|
177
169
|
});
|
|
178
170
|
|
|
171
|
+
it("calls updateActivity to replace the consent card when consentCardActivityId is set", async () => {
|
|
172
|
+
const { context, sendActivity, updateActivity } = createConsentInvokeHarness({
|
|
173
|
+
invokeConversationId: "19:victim@thread.v2;messageid=abc123",
|
|
174
|
+
action: "accept",
|
|
175
|
+
consentCardActivityId: "consent-card-activity-id-123",
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
await respondToMSTeamsFileConsentInvoke(context, log);
|
|
179
|
+
|
|
180
|
+
expect(sendActivity).toHaveBeenCalledWith(expect.objectContaining({ type: "invokeResponse" }));
|
|
181
|
+
expect(fileConsentMockState.uploadToConsentUrl).toHaveBeenCalledTimes(1);
|
|
182
|
+
|
|
183
|
+
// Should replace the original consent card with the file info card
|
|
184
|
+
expect(updateActivity).toHaveBeenCalledTimes(1);
|
|
185
|
+
expect(updateActivity).toHaveBeenCalledWith(
|
|
186
|
+
expect.objectContaining({
|
|
187
|
+
id: "consent-card-activity-id-123",
|
|
188
|
+
type: "message",
|
|
189
|
+
attachments: expect.arrayContaining([
|
|
190
|
+
expect.objectContaining({
|
|
191
|
+
contentType: "application/vnd.microsoft.teams.card.file.info",
|
|
192
|
+
}),
|
|
193
|
+
]),
|
|
194
|
+
}),
|
|
195
|
+
);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it("does not send file info card via sendActivity when updateActivity succeeds", async () => {
|
|
199
|
+
const { context, sendActivity, updateActivity } = createConsentInvokeHarness({
|
|
200
|
+
invokeConversationId: "19:victim@thread.v2;messageid=abc123",
|
|
201
|
+
action: "accept",
|
|
202
|
+
consentCardActivityId: "consent-card-activity-id-happy",
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
await respondToMSTeamsFileConsentInvoke(context, log);
|
|
206
|
+
|
|
207
|
+
// updateActivity should replace the consent card in-place
|
|
208
|
+
expect(updateActivity).toHaveBeenCalledTimes(1);
|
|
209
|
+
|
|
210
|
+
// sendActivity should only be called once for the invokeResponse, NOT for the file info card
|
|
211
|
+
expect(sendActivity).toHaveBeenCalledTimes(1);
|
|
212
|
+
expect(sendActivity).toHaveBeenCalledWith(expect.objectContaining({ type: "invokeResponse" }));
|
|
213
|
+
|
|
214
|
+
// Explicitly verify no file info card was sent via sendActivity
|
|
215
|
+
for (const call of sendActivity.mock.calls) {
|
|
216
|
+
const arg = call[0] as Record<string, unknown>;
|
|
217
|
+
if (typeof arg === "object" && arg !== null && "attachments" in arg) {
|
|
218
|
+
const attachments = arg.attachments as Array<{ contentType?: string }>;
|
|
219
|
+
for (const att of attachments) {
|
|
220
|
+
expect(att.contentType).not.toBe("application/vnd.microsoft.teams.card.file.info");
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it("does not call updateActivity when no consentCardActivityId is stored", async () => {
|
|
227
|
+
const { context, updateActivity } = createConsentInvokeHarness({
|
|
228
|
+
invokeConversationId: "19:victim@thread.v2;messageid=abc123",
|
|
229
|
+
action: "accept",
|
|
230
|
+
// no consentCardActivityId
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
await respondToMSTeamsFileConsentInvoke(context, log);
|
|
234
|
+
|
|
235
|
+
expect(fileConsentMockState.uploadToConsentUrl).toHaveBeenCalledTimes(1);
|
|
236
|
+
expect(updateActivity).not.toHaveBeenCalled();
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it("still completes upload if updateActivity throws", async () => {
|
|
240
|
+
const { uploadId, context, updateActivity } = createConsentInvokeHarness({
|
|
241
|
+
invokeConversationId: "19:victim@thread.v2;messageid=abc123",
|
|
242
|
+
action: "accept",
|
|
243
|
+
consentCardActivityId: "consent-card-activity-id-fail",
|
|
244
|
+
});
|
|
245
|
+
updateActivity.mockRejectedValueOnce(new Error("Teams API error"));
|
|
246
|
+
|
|
247
|
+
await respondToMSTeamsFileConsentInvoke(context, log);
|
|
248
|
+
|
|
249
|
+
// Upload should have completed despite updateActivity failure
|
|
250
|
+
expect(fileConsentMockState.uploadToConsentUrl).toHaveBeenCalledTimes(1);
|
|
251
|
+
expect(getPendingUpload(uploadId)).toBeUndefined();
|
|
252
|
+
expect(updateActivity).toHaveBeenCalledTimes(1);
|
|
253
|
+
});
|
|
254
|
+
|
|
179
255
|
it("rejects cross-conversation accept invoke and keeps pending upload", async () => {
|
|
180
|
-
const { uploadId,
|
|
256
|
+
const { uploadId, context, sendActivity } = createConsentInvokeHarness({
|
|
181
257
|
invokeConversationId: "19:attacker@thread.v2",
|
|
182
258
|
action: "accept",
|
|
183
259
|
});
|
|
184
260
|
|
|
185
|
-
await
|
|
261
|
+
await respondToMSTeamsFileConsentInvoke(context, log);
|
|
186
262
|
|
|
187
263
|
// invokeResponse should be sent immediately
|
|
188
264
|
expect(sendActivity).toHaveBeenCalledWith(
|
|
@@ -196,16 +272,20 @@ describe("msteams file consent invoke authz", () => {
|
|
|
196
272
|
);
|
|
197
273
|
|
|
198
274
|
expect(fileConsentMockState.uploadToConsentUrl).not.toHaveBeenCalled();
|
|
199
|
-
expect(
|
|
275
|
+
expect(requirePendingUpload(uploadId)).toMatchObject({
|
|
276
|
+
conversationId: "19:victim@thread.v2",
|
|
277
|
+
filename: "secret.txt",
|
|
278
|
+
contentType: "text/plain",
|
|
279
|
+
});
|
|
200
280
|
});
|
|
201
281
|
|
|
202
282
|
it("ignores cross-conversation decline invoke and keeps pending upload", async () => {
|
|
203
|
-
const { uploadId,
|
|
283
|
+
const { uploadId, context, sendActivity } = createConsentInvokeHarness({
|
|
204
284
|
invokeConversationId: "19:attacker@thread.v2",
|
|
205
285
|
action: "decline",
|
|
206
286
|
});
|
|
207
287
|
|
|
208
|
-
await
|
|
288
|
+
await respondToMSTeamsFileConsentInvoke(context, log);
|
|
209
289
|
|
|
210
290
|
// invokeResponse should be sent immediately
|
|
211
291
|
expect(sendActivity).toHaveBeenCalledWith(
|
|
@@ -215,7 +295,129 @@ describe("msteams file consent invoke authz", () => {
|
|
|
215
295
|
);
|
|
216
296
|
|
|
217
297
|
expect(fileConsentMockState.uploadToConsentUrl).not.toHaveBeenCalled();
|
|
218
|
-
expect(
|
|
298
|
+
expect(requirePendingUpload(uploadId)).toMatchObject({
|
|
299
|
+
conversationId: "19:victim@thread.v2",
|
|
300
|
+
filename: "secret.txt",
|
|
301
|
+
contentType: "text/plain",
|
|
302
|
+
});
|
|
219
303
|
expect(sendActivity).toHaveBeenCalledTimes(1);
|
|
220
304
|
});
|
|
221
305
|
});
|
|
306
|
+
|
|
307
|
+
describe("msteams file consent invoke FS fallback", () => {
|
|
308
|
+
let tmpDir: string;
|
|
309
|
+
let originalStateDir: string | undefined;
|
|
310
|
+
|
|
311
|
+
beforeEach(async () => {
|
|
312
|
+
originalStateDir = process.env.OPENCLAW_STATE_DIR;
|
|
313
|
+
tmpDir = await fs.promises.mkdtemp(path.join(os.tmpdir(), "openclaw-msteams-invoke-"));
|
|
314
|
+
process.env.OPENCLAW_STATE_DIR = tmpDir;
|
|
315
|
+
setMSTeamsRuntime(createRuntimeStub(tmpDir));
|
|
316
|
+
clearPendingUploads();
|
|
317
|
+
vi.clearAllMocks();
|
|
318
|
+
fileConsentMockState.uploadToConsentUrl.mockReset();
|
|
319
|
+
fileConsentMockState.uploadToConsentUrl.mockResolvedValue(undefined);
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
afterEach(async () => {
|
|
323
|
+
if (originalStateDir === undefined) {
|
|
324
|
+
delete process.env.OPENCLAW_STATE_DIR;
|
|
325
|
+
} else {
|
|
326
|
+
process.env.OPENCLAW_STATE_DIR = originalStateDir;
|
|
327
|
+
}
|
|
328
|
+
try {
|
|
329
|
+
await fs.promises.rm(tmpDir, { recursive: true, force: true });
|
|
330
|
+
} catch {
|
|
331
|
+
// tmp dir may already be gone
|
|
332
|
+
}
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
it("reads pending upload from FS store when in-memory store is empty (cross-process CLI path)", async () => {
|
|
336
|
+
// Simulate the CLI process writing to the FS store before exiting; the
|
|
337
|
+
// in-memory store in this (monitor) process is empty.
|
|
338
|
+
const uploadId = "cli-upload-id-123";
|
|
339
|
+
const conversationId = "19:victim@thread.v2";
|
|
340
|
+
await storePendingUploadFs({
|
|
341
|
+
id: uploadId,
|
|
342
|
+
buffer: Buffer.from("CLI PAYLOAD"),
|
|
343
|
+
filename: "cli.bin",
|
|
344
|
+
contentType: "application/octet-stream",
|
|
345
|
+
conversationId,
|
|
346
|
+
});
|
|
347
|
+
|
|
348
|
+
expect(getPendingUpload(uploadId)).toBeUndefined();
|
|
349
|
+
|
|
350
|
+
const sendActivity = vi.fn(async () => ({ id: "activity-id" }));
|
|
351
|
+
const updateActivity = vi.fn(async () => ({ id: "activity-id" }));
|
|
352
|
+
const context = {
|
|
353
|
+
activity: {
|
|
354
|
+
type: "invoke",
|
|
355
|
+
name: "fileConsent/invoke",
|
|
356
|
+
conversation: { id: `${conversationId};messageid=abc123` },
|
|
357
|
+
value: {
|
|
358
|
+
type: "fileUpload",
|
|
359
|
+
action: "accept",
|
|
360
|
+
uploadInfo: {
|
|
361
|
+
name: "cli.bin",
|
|
362
|
+
uploadUrl: "https://upload.example.com/put",
|
|
363
|
+
contentUrl: "https://content.example.com/cli.bin",
|
|
364
|
+
uniqueId: "unique-cli",
|
|
365
|
+
fileType: "bin",
|
|
366
|
+
},
|
|
367
|
+
context: { uploadId },
|
|
368
|
+
},
|
|
369
|
+
},
|
|
370
|
+
sendActivity,
|
|
371
|
+
sendActivities: async () => [],
|
|
372
|
+
updateActivity,
|
|
373
|
+
} as unknown as MSTeamsTurnContext;
|
|
374
|
+
|
|
375
|
+
await respondToMSTeamsFileConsentInvoke(context, log);
|
|
376
|
+
|
|
377
|
+
// The upload should have run using the FS-loaded buffer
|
|
378
|
+
expect(fileConsentMockState.uploadToConsentUrl).toHaveBeenCalledTimes(1);
|
|
379
|
+
expect(fileConsentMockState.uploadToConsentUrl).toHaveBeenCalledWith(
|
|
380
|
+
expect.objectContaining({
|
|
381
|
+
url: "https://upload.example.com/put",
|
|
382
|
+
}),
|
|
383
|
+
);
|
|
384
|
+
|
|
385
|
+
// FS entry should have been cleaned up after successful upload
|
|
386
|
+
expect(await getPendingUploadFs(uploadId)).toBeUndefined();
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
it("cleans up FS entry on decline even when in-memory store is empty", async () => {
|
|
390
|
+
const uploadId = "cli-decline-id";
|
|
391
|
+
const conversationId = "19:victim@thread.v2";
|
|
392
|
+
await storePendingUploadFs({
|
|
393
|
+
id: uploadId,
|
|
394
|
+
buffer: Buffer.from("DECLINED"),
|
|
395
|
+
filename: "decline.txt",
|
|
396
|
+
contentType: "text/plain",
|
|
397
|
+
conversationId,
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
const sendActivity = vi.fn(async () => ({ id: "activity-id" }));
|
|
401
|
+
const updateActivity = vi.fn(async () => ({ id: "activity-id" }));
|
|
402
|
+
const context = {
|
|
403
|
+
activity: {
|
|
404
|
+
type: "invoke",
|
|
405
|
+
name: "fileConsent/invoke",
|
|
406
|
+
conversation: { id: `${conversationId};messageid=abc123` },
|
|
407
|
+
value: {
|
|
408
|
+
type: "fileUpload",
|
|
409
|
+
action: "decline",
|
|
410
|
+
context: { uploadId },
|
|
411
|
+
},
|
|
412
|
+
},
|
|
413
|
+
sendActivity,
|
|
414
|
+
sendActivities: async () => [],
|
|
415
|
+
updateActivity,
|
|
416
|
+
} as unknown as MSTeamsTurnContext;
|
|
417
|
+
|
|
418
|
+
await respondToMSTeamsFileConsentInvoke(context, log);
|
|
419
|
+
|
|
420
|
+
expect(fileConsentMockState.uploadToConsentUrl).not.toHaveBeenCalled();
|
|
421
|
+
expect(await getPendingUploadFs(uploadId)).toBeUndefined();
|
|
422
|
+
});
|
|
423
|
+
});
|