@openclaw/msteams 2026.3.12 → 2026.5.1-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/api.ts +3 -0
- package/channel-config-api.ts +1 -0
- package/channel-plugin-api.ts +2 -0
- package/config-api.ts +4 -0
- package/contract-api.ts +4 -0
- package/index.ts +15 -12
- package/openclaw.plugin.json +553 -1
- package/package.json +46 -12
- package/runtime-api.ts +73 -0
- package/secret-contract-api.ts +5 -0
- package/setup-entry.ts +13 -0
- package/setup-plugin-api.ts +3 -0
- package/src/ai-entity.ts +7 -0
- package/src/approval-auth.ts +44 -0
- package/src/attachments/bot-framework.test.ts +461 -0
- package/src/attachments/bot-framework.ts +362 -0
- package/src/attachments/download.ts +63 -19
- package/src/attachments/graph.test.ts +416 -0
- package/src/attachments/graph.ts +163 -72
- package/src/attachments/html.ts +33 -1
- package/src/attachments/payload.ts +1 -1
- package/src/attachments/remote-media.test.ts +137 -0
- package/src/attachments/remote-media.ts +75 -8
- package/src/attachments/shared.test.ts +161 -9
- package/src/attachments/shared.ts +193 -26
- package/src/attachments/types.ts +10 -0
- package/src/attachments.graph.test.ts +342 -0
- package/src/attachments.helpers.test.ts +246 -0
- package/src/attachments.test-helpers.ts +17 -0
- package/src/attachments.test.ts +174 -437
- package/src/attachments.ts +5 -5
- package/src/block-streaming-config.test.ts +61 -0
- package/src/channel-api.ts +1 -0
- package/src/channel.actions.test.ts +742 -0
- package/src/channel.directory.test.ts +148 -14
- package/src/channel.runtime.ts +56 -0
- package/src/channel.setup.ts +77 -0
- package/src/channel.test.ts +128 -0
- package/src/channel.ts +1077 -395
- package/src/config-schema.ts +6 -0
- package/src/config-ui-hints.ts +12 -0
- package/src/conversation-store-fs.test.ts +4 -5
- package/src/conversation-store-fs.ts +35 -51
- package/src/conversation-store-helpers.test.ts +202 -0
- package/src/conversation-store-helpers.ts +105 -0
- package/src/conversation-store-memory.ts +27 -23
- package/src/conversation-store.shared.test.ts +225 -0
- package/src/conversation-store.ts +30 -0
- package/src/directory-live.test.ts +156 -0
- package/src/directory-live.ts +7 -4
- package/src/doctor.ts +27 -0
- package/src/errors.test.ts +64 -1
- package/src/errors.ts +50 -9
- package/src/feedback-reflection-prompt.ts +117 -0
- package/src/feedback-reflection-store.ts +114 -0
- package/src/feedback-reflection.test.ts +237 -0
- package/src/feedback-reflection.ts +283 -0
- package/src/file-consent-helpers.test.ts +83 -0
- package/src/file-consent-helpers.ts +64 -11
- package/src/file-consent-invoke.ts +150 -0
- package/src/file-consent.test.ts +363 -0
- package/src/file-consent.ts +165 -4
- package/src/graph-chat.ts +5 -3
- package/src/graph-group-management.test.ts +318 -0
- package/src/graph-group-management.ts +168 -0
- package/src/graph-members.test.ts +89 -0
- package/src/graph-members.ts +48 -0
- package/src/graph-messages.actions.test.ts +243 -0
- package/src/graph-messages.read.test.ts +391 -0
- package/src/graph-messages.search.test.ts +213 -0
- package/src/graph-messages.test-helpers.ts +50 -0
- package/src/graph-messages.ts +534 -0
- package/src/graph-teams.test.ts +215 -0
- package/src/graph-teams.ts +114 -0
- package/src/graph-thread.test.ts +246 -0
- package/src/graph-thread.ts +146 -0
- package/src/graph-upload.test.ts +258 -0
- package/src/graph-upload.ts +87 -8
- package/src/graph.test.ts +516 -0
- package/src/graph.ts +233 -21
- package/src/inbound.test.ts +156 -1
- package/src/inbound.ts +101 -1
- package/src/media-helpers.ts +1 -1
- package/src/mentions.test.ts +27 -18
- package/src/mentions.ts +2 -2
- package/src/messenger.test.ts +522 -45
- package/src/messenger.ts +133 -52
- package/src/monitor-handler/access.ts +125 -0
- package/src/monitor-handler/inbound-media.test.ts +289 -0
- package/src/monitor-handler/inbound-media.ts +57 -5
- package/src/monitor-handler/message-handler-mock-support.test-support.ts +28 -0
- package/src/monitor-handler/message-handler.authz.test.ts +588 -74
- package/src/monitor-handler/message-handler.dm-media.test.ts +54 -0
- package/src/monitor-handler/message-handler.test-support.ts +100 -0
- package/src/monitor-handler/message-handler.thread-parent.test.ts +223 -0
- package/src/monitor-handler/message-handler.thread-session.test.ts +77 -0
- package/src/monitor-handler/message-handler.ts +477 -174
- package/src/monitor-handler/reaction-handler.test.ts +267 -0
- package/src/monitor-handler/reaction-handler.ts +210 -0
- package/src/monitor-handler/thread-session.ts +17 -0
- package/src/monitor-handler.adaptive-card.test.ts +162 -0
- package/src/monitor-handler.feedback-authz.test.ts +314 -0
- package/src/monitor-handler.file-consent.test.ts +301 -106
- package/src/monitor-handler.sso.test.ts +563 -0
- package/src/monitor-handler.test-helpers.ts +180 -0
- package/src/monitor-handler.ts +459 -115
- package/src/monitor-handler.types.ts +27 -0
- package/src/monitor-types.ts +1 -0
- package/src/monitor.lifecycle.test.ts +74 -10
- package/src/monitor.test.ts +35 -1
- package/src/monitor.ts +143 -46
- package/src/oauth.flow.ts +77 -0
- package/src/oauth.shared.ts +37 -0
- package/src/oauth.test.ts +305 -0
- package/src/oauth.token.ts +158 -0
- package/src/oauth.ts +130 -0
- package/src/outbound.test.ts +10 -11
- package/src/outbound.ts +62 -44
- package/src/pending-uploads-fs.test.ts +246 -0
- package/src/pending-uploads-fs.ts +235 -0
- package/src/pending-uploads.test.ts +173 -0
- package/src/pending-uploads.ts +34 -2
- package/src/policy.test.ts +34 -40
- package/src/policy.ts +5 -5
- package/src/polls.test.ts +106 -5
- package/src/polls.ts +15 -7
- package/src/presentation.ts +68 -0
- package/src/probe.test.ts +27 -8
- package/src/probe.ts +43 -9
- package/src/reply-dispatcher.test.ts +437 -0
- package/src/reply-dispatcher.ts +259 -73
- package/src/reply-stream-controller.test.ts +235 -0
- package/src/reply-stream-controller.ts +147 -0
- package/src/resolve-allowlist.test.ts +105 -1
- package/src/resolve-allowlist.ts +112 -7
- package/src/runtime.ts +6 -3
- package/src/sdk-types.ts +43 -3
- package/src/sdk.test.ts +666 -0
- package/src/sdk.ts +867 -16
- package/src/secret-contract.ts +49 -0
- package/src/secret-input.ts +1 -1
- package/src/send-context.ts +76 -9
- package/src/send.test.ts +389 -5
- package/src/send.ts +140 -32
- package/src/sent-message-cache.ts +30 -18
- package/src/session-route.ts +40 -0
- package/src/setup-core.ts +160 -0
- package/src/setup-surface.test.ts +202 -0
- package/src/setup-surface.ts +320 -0
- package/src/sso-token-store.test.ts +72 -0
- package/src/sso-token-store.ts +166 -0
- package/src/sso.ts +300 -0
- package/src/storage.ts +1 -1
- package/src/store-fs.ts +2 -2
- package/src/streaming-message.test.ts +262 -0
- package/src/streaming-message.ts +297 -0
- package/src/test-runtime.ts +1 -1
- package/src/thread-parent-context.test.ts +224 -0
- package/src/thread-parent-context.ts +159 -0
- package/src/token.test.ts +237 -50
- package/src/token.ts +162 -7
- package/src/user-agent.test.ts +86 -0
- package/src/user-agent.ts +53 -0
- package/src/webhook-timeouts.ts +27 -0
- package/src/welcome-card.test.ts +81 -0
- package/src/welcome-card.ts +57 -0
- package/test-api.ts +1 -0
- package/tsconfig.json +16 -0
- package/CHANGELOG.md +0 -101
- package/src/file-lock.ts +0 -1
- package/src/graph-users.test.ts +0 -66
- package/src/onboarding.ts +0 -381
- package/src/polls-store.test.ts +0 -38
- package/src/revoked-context.test.ts +0 -39
- package/src/token-response.test.ts +0 -23
|
@@ -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,35 +98,58 @@ 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
|
|
|
108
|
+
function createConsentInvokeHarness(params: {
|
|
109
|
+
pendingConversationId?: string;
|
|
110
|
+
invokeConversationId: string;
|
|
111
|
+
action: "accept" | "decline";
|
|
112
|
+
consentCardActivityId?: string;
|
|
113
|
+
}) {
|
|
114
|
+
const uploadId = storePendingUpload({
|
|
115
|
+
buffer: Buffer.from("TOP_SECRET_VICTIM_FILE\n"),
|
|
116
|
+
filename: "secret.txt",
|
|
117
|
+
contentType: "text/plain",
|
|
118
|
+
conversationId: params.pendingConversationId ?? "19:victim@thread.v2",
|
|
119
|
+
consentCardActivityId: params.consentCardActivityId,
|
|
120
|
+
});
|
|
121
|
+
const { context, sendActivity, updateActivity } = createInvokeContext({
|
|
122
|
+
conversationId: params.invokeConversationId,
|
|
123
|
+
uploadId,
|
|
124
|
+
action: params.action,
|
|
125
|
+
});
|
|
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;
|
|
135
|
+
}
|
|
136
|
+
|
|
126
137
|
describe("msteams file consent invoke authz", () => {
|
|
127
138
|
beforeEach(() => {
|
|
128
139
|
setMSTeamsRuntime(runtimeStub);
|
|
129
140
|
clearPendingUploads();
|
|
141
|
+
vi.clearAllMocks();
|
|
130
142
|
fileConsentMockState.uploadToConsentUrl.mockReset();
|
|
131
143
|
fileConsentMockState.uploadToConsentUrl.mockResolvedValue(undefined);
|
|
132
144
|
});
|
|
133
145
|
|
|
134
146
|
it("uploads when invoke conversation matches pending upload conversation", async () => {
|
|
135
|
-
const uploadId =
|
|
136
|
-
|
|
137
|
-
filename: "secret.txt",
|
|
138
|
-
contentType: "text/plain",
|
|
139
|
-
conversationId: "19:victim@thread.v2",
|
|
140
|
-
});
|
|
141
|
-
const deps = createDeps();
|
|
142
|
-
const handler = registerMSTeamsHandlers(createActivityHandler(), deps);
|
|
143
|
-
const { context, sendActivity } = createInvokeContext({
|
|
144
|
-
conversationId: "19:victim@thread.v2;messageid=abc123",
|
|
145
|
-
uploadId,
|
|
147
|
+
const { uploadId, context, sendActivity } = createConsentInvokeHarness({
|
|
148
|
+
invokeConversationId: "19:victim@thread.v2;messageid=abc123",
|
|
146
149
|
action: "accept",
|
|
147
150
|
});
|
|
148
151
|
|
|
149
|
-
await
|
|
152
|
+
await respondToMSTeamsFileConsentInvoke(context, log);
|
|
150
153
|
|
|
151
154
|
// invokeResponse should be sent immediately
|
|
152
155
|
expect(sendActivity).toHaveBeenCalledWith(
|
|
@@ -165,22 +168,97 @@ describe("msteams file consent invoke authz", () => {
|
|
|
165
168
|
expect(getPendingUpload(uploadId)).toBeUndefined();
|
|
166
169
|
});
|
|
167
170
|
|
|
168
|
-
it("
|
|
169
|
-
const
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
conversationId: "19:victim@thread.v2",
|
|
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",
|
|
174
176
|
});
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
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",
|
|
180
201
|
action: "accept",
|
|
202
|
+
consentCardActivityId: "consent-card-activity-id-happy",
|
|
181
203
|
});
|
|
182
204
|
|
|
183
|
-
await
|
|
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
|
+
|
|
255
|
+
it("rejects cross-conversation accept invoke and keeps pending upload", async () => {
|
|
256
|
+
const { uploadId, context, sendActivity } = createConsentInvokeHarness({
|
|
257
|
+
invokeConversationId: "19:attacker@thread.v2",
|
|
258
|
+
action: "accept",
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
await respondToMSTeamsFileConsentInvoke(context, log);
|
|
184
262
|
|
|
185
263
|
// invokeResponse should be sent immediately
|
|
186
264
|
expect(sendActivity).toHaveBeenCalledWith(
|
|
@@ -194,25 +272,20 @@ describe("msteams file consent invoke authz", () => {
|
|
|
194
272
|
);
|
|
195
273
|
|
|
196
274
|
expect(fileConsentMockState.uploadToConsentUrl).not.toHaveBeenCalled();
|
|
197
|
-
expect(
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
it("ignores cross-conversation decline invoke and keeps pending upload", async () => {
|
|
201
|
-
const uploadId = storePendingUpload({
|
|
202
|
-
buffer: Buffer.from("TOP_SECRET_VICTIM_FILE\n"),
|
|
275
|
+
expect(requirePendingUpload(uploadId)).toMatchObject({
|
|
276
|
+
conversationId: "19:victim@thread.v2",
|
|
203
277
|
filename: "secret.txt",
|
|
204
278
|
contentType: "text/plain",
|
|
205
|
-
conversationId: "19:victim@thread.v2",
|
|
206
279
|
});
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
it("ignores cross-conversation decline invoke and keeps pending upload", async () => {
|
|
283
|
+
const { uploadId, context, sendActivity } = createConsentInvokeHarness({
|
|
284
|
+
invokeConversationId: "19:attacker@thread.v2",
|
|
212
285
|
action: "decline",
|
|
213
286
|
});
|
|
214
287
|
|
|
215
|
-
await
|
|
288
|
+
await respondToMSTeamsFileConsentInvoke(context, log);
|
|
216
289
|
|
|
217
290
|
// invokeResponse should be sent immediately
|
|
218
291
|
expect(sendActivity).toHaveBeenCalledWith(
|
|
@@ -222,7 +295,129 @@ describe("msteams file consent invoke authz", () => {
|
|
|
222
295
|
);
|
|
223
296
|
|
|
224
297
|
expect(fileConsentMockState.uploadToConsentUrl).not.toHaveBeenCalled();
|
|
225
|
-
expect(
|
|
298
|
+
expect(requirePendingUpload(uploadId)).toMatchObject({
|
|
299
|
+
conversationId: "19:victim@thread.v2",
|
|
300
|
+
filename: "secret.txt",
|
|
301
|
+
contentType: "text/plain",
|
|
302
|
+
});
|
|
226
303
|
expect(sendActivity).toHaveBeenCalledTimes(1);
|
|
227
304
|
});
|
|
228
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
|
+
});
|