fix(msteams): update FileConsentCard in-place after upload via updateActivity#47270
fix(msteams): update FileConsentCard in-place after upload via updateActivity#47270cwatts-sage wants to merge 1 commit intoopenclaw:mainfrom
Conversation
…Activity After a user clicks Allow on a FileConsentCard, the card remains frozen in its Allow/Deny state even though the file uploads successfully. This is because the FileInfoCard confirmation is sent as a new message instead of replacing the original consent card. This commit: - Adds optional updateActivity to MSTeamsTurnContext type - Uses context.updateActivity() with the invoke's replyToId to replace the original FileConsentCard with a FileInfoCard in-place - Falls back to sendActivity (new message) if updateActivity is unavailable or fails (e.g. expired activity, permissions) - Adds test coverage for: in-place update, fallback on failure, and fallback when replyToId is absent
Greptile SummaryThis PR fixes a UX regression in the MS Teams file upload flow by replacing the stale Key observations:
Confidence Score: 4/5
Prompt To Fix All With AIThis is a comment left during a code review.
Path: extensions/msteams/src/monitor-handler.file-consent.test.ts
Line: 175-180
Comment:
**Test data mismatch on FileInfoCard name assertion**
`storePendingUpload` registers the pending file as `"report.pdf"`, but the assertion checks that `updateActivity` was called with `name: "secret.txt"`.
The code is actually correct — `buildFileInfoCard` reads from `consentResponse.uploadInfo.name`, which is hardcoded to `"secret.txt"` in the `createInvokeContext` helper — not from `pendingFile.filename`. However, the inconsistency between the registered filename (`"report.pdf"`) and the asserted name (`"secret.txt"`) is misleading for future maintainers. Anyone reading this test would expect the stored filename (`"report.pdf"`) to appear in the confirmation card.
Consider aligning the test data: either change `storePendingUpload`'s filename to `"secret.txt"` to match the hardcoded `uploadInfo.name`, or update `createInvokeContext` to take the upload name as a parameter so the two values can be made consistent.
```suggestion
const uploadId = storePendingUpload({
buffer: Buffer.from("file content"),
filename: "secret.txt",
contentType: "application/pdf",
conversationId: "19:[email protected]",
});
```
How can I resolve this? If you propose a fix, please make it concise.
---
This is a comment left during a code review.
Path: extensions/msteams/src/monitor-handler.file-consent.test.ts
Line: 254-286
Comment:
**Missing test coverage for `updateActivity` not being defined**
The three new test cases cover:
1. `updateActivity` present + `replyToId` present → in-place update ✅
2. `updateActivity` present + `replyToId` present, but update throws → fallback ✅
3. `updateActivity` present + `replyToId` absent → fallback ✅
But there is no test for `context.updateActivity` itself being `undefined` (i.e., the SDK version doesn't expose it). The `typeof context.updateActivity === "function"` guard in `monitor-handler.ts` specifically handles this case, so a test should verify it falls back to `sendActivity` correctly.
```suggestion
it("sends FileInfoCard as new message when updateActivity is not available on context", async () => {
const uploadId = storePendingUpload({
buffer: Buffer.from("file content"),
filename: "secret.txt",
contentType: "application/pdf",
conversationId: "19:[email protected]",
});
const deps = createDeps();
const handler = registerMSTeamsHandlers(createActivityHandler(), deps);
const { context, sendActivity } = createInvokeContext({
conversationId: "19:[email protected];messageid=abc123",
uploadId,
action: "accept",
replyToId: "consent-card-activity-id",
});
// Simulate SDK without updateActivity support
(context as Record<string, unknown>).updateActivity = undefined;
await handler.run?.(context);
expect(sendActivity).toHaveBeenCalledWith(
expect.objectContaining({
type: "message",
attachments: [
expect.objectContaining({
contentType: "application/vnd.microsoft.teams.card.file.info",
}),
],
}),
);
});
```
How can I resolve this? If you propose a fix, please make it concise.
---
This is a comment left during a code review.
Path: extensions/msteams/src/monitor-handler.ts
Line: 112-116
Comment:
**Consider explicitly clearing `text` in the `updateActivity` payload**
The activity sent to `updateActivity` only specifies `id`, `type`, and `attachments`. If the original FileConsentCard message was sent with a `text` field (or if Teams preserves the previous `text` during a partial update), the stale text would remain visible in the updated card alongside the new FileInfoCard attachment.
Bot Framework's update semantics perform a full replacement of the activity, so omitting `text` should effectively leave it empty. However, being explicit makes the intent clear and guards against any SDK version that might merge fields rather than replace them:
```suggestion
await context.updateActivity({
id: consentActivityId,
type: "message",
text: "",
attachments: [fileInfoCard],
});
```
How can I resolve this? If you propose a fix, please make it concise.Last reviewed commit: 9694b44 |
| const uploadId = storePendingUpload({ | ||
| buffer: Buffer.from("file content"), | ||
| filename: "report.pdf", | ||
| contentType: "application/pdf", | ||
| conversationId: "19:[email protected]", | ||
| }); |
There was a problem hiding this comment.
Test data mismatch on FileInfoCard name assertion
storePendingUpload registers the pending file as "report.pdf", but the assertion checks that updateActivity was called with name: "secret.txt".
The code is actually correct — buildFileInfoCard reads from consentResponse.uploadInfo.name, which is hardcoded to "secret.txt" in the createInvokeContext helper — not from pendingFile.filename. However, the inconsistency between the registered filename ("report.pdf") and the asserted name ("secret.txt") is misleading for future maintainers. Anyone reading this test would expect the stored filename ("report.pdf") to appear in the confirmation card.
Consider aligning the test data: either change storePendingUpload's filename to "secret.txt" to match the hardcoded uploadInfo.name, or update createInvokeContext to take the upload name as a parameter so the two values can be made consistent.
| const uploadId = storePendingUpload({ | |
| buffer: Buffer.from("file content"), | |
| filename: "report.pdf", | |
| contentType: "application/pdf", | |
| conversationId: "19:[email protected]", | |
| }); | |
| const uploadId = storePendingUpload({ | |
| buffer: Buffer.from("file content"), | |
| filename: "secret.txt", | |
| contentType: "application/pdf", | |
| conversationId: "19:[email protected]", | |
| }); |
Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/msteams/src/monitor-handler.file-consent.test.ts
Line: 175-180
Comment:
**Test data mismatch on FileInfoCard name assertion**
`storePendingUpload` registers the pending file as `"report.pdf"`, but the assertion checks that `updateActivity` was called with `name: "secret.txt"`.
The code is actually correct — `buildFileInfoCard` reads from `consentResponse.uploadInfo.name`, which is hardcoded to `"secret.txt"` in the `createInvokeContext` helper — not from `pendingFile.filename`. However, the inconsistency between the registered filename (`"report.pdf"`) and the asserted name (`"secret.txt"`) is misleading for future maintainers. Anyone reading this test would expect the stored filename (`"report.pdf"`) to appear in the confirmation card.
Consider aligning the test data: either change `storePendingUpload`'s filename to `"secret.txt"` to match the hardcoded `uploadInfo.name`, or update `createInvokeContext` to take the upload name as a parameter so the two values can be made consistent.
```suggestion
const uploadId = storePendingUpload({
buffer: Buffer.from("file content"),
filename: "secret.txt",
contentType: "application/pdf",
conversationId: "19:[email protected]",
});
```
How can I resolve this? If you propose a fix, please make it concise.| it("sends FileInfoCard as new message when replyToId is not present", async () => { | ||
| const uploadId = storePendingUpload({ | ||
| buffer: Buffer.from("file content"), | ||
| filename: "report.pdf", | ||
| contentType: "application/pdf", | ||
| conversationId: "19:[email protected]", | ||
| }); | ||
| const deps = createDeps(); | ||
| const handler = registerMSTeamsHandlers(createActivityHandler(), deps); | ||
| const { context, sendActivity, updateActivity } = createInvokeContext({ | ||
| conversationId: "19:[email protected];messageid=abc123", | ||
| uploadId, | ||
| action: "accept", | ||
| // No replyToId | ||
| }); | ||
|
|
||
| await handler.run?.(context); | ||
|
|
||
| // Should NOT call updateActivity | ||
| expect(updateActivity).not.toHaveBeenCalled(); | ||
|
|
||
| // Should send FileInfoCard as a new message (fallback path) | ||
| expect(sendActivity).toHaveBeenCalledWith( | ||
| expect.objectContaining({ | ||
| type: "message", | ||
| attachments: [ | ||
| expect.objectContaining({ | ||
| contentType: "application/vnd.microsoft.teams.card.file.info", | ||
| }), | ||
| ], | ||
| }), | ||
| ); | ||
| }); |
There was a problem hiding this comment.
Missing test coverage for updateActivity not being defined
The three new test cases cover:
updateActivitypresent +replyToIdpresent → in-place update ✅updateActivitypresent +replyToIdpresent, but update throws → fallback ✅updateActivitypresent +replyToIdabsent → fallback ✅
But there is no test for context.updateActivity itself being undefined (i.e., the SDK version doesn't expose it). The typeof context.updateActivity === "function" guard in monitor-handler.ts specifically handles this case, so a test should verify it falls back to sendActivity correctly.
| it("sends FileInfoCard as new message when replyToId is not present", async () => { | |
| const uploadId = storePendingUpload({ | |
| buffer: Buffer.from("file content"), | |
| filename: "report.pdf", | |
| contentType: "application/pdf", | |
| conversationId: "19:[email protected]", | |
| }); | |
| const deps = createDeps(); | |
| const handler = registerMSTeamsHandlers(createActivityHandler(), deps); | |
| const { context, sendActivity, updateActivity } = createInvokeContext({ | |
| conversationId: "19:[email protected];messageid=abc123", | |
| uploadId, | |
| action: "accept", | |
| // No replyToId | |
| }); | |
| await handler.run?.(context); | |
| // Should NOT call updateActivity | |
| expect(updateActivity).not.toHaveBeenCalled(); | |
| // Should send FileInfoCard as a new message (fallback path) | |
| expect(sendActivity).toHaveBeenCalledWith( | |
| expect.objectContaining({ | |
| type: "message", | |
| attachments: [ | |
| expect.objectContaining({ | |
| contentType: "application/vnd.microsoft.teams.card.file.info", | |
| }), | |
| ], | |
| }), | |
| ); | |
| }); | |
| it("sends FileInfoCard as new message when updateActivity is not available on context", async () => { | |
| const uploadId = storePendingUpload({ | |
| buffer: Buffer.from("file content"), | |
| filename: "secret.txt", | |
| contentType: "application/pdf", | |
| conversationId: "19:[email protected]", | |
| }); | |
| const deps = createDeps(); | |
| const handler = registerMSTeamsHandlers(createActivityHandler(), deps); | |
| const { context, sendActivity } = createInvokeContext({ | |
| conversationId: "19:[email protected];messageid=abc123", | |
| uploadId, | |
| action: "accept", | |
| replyToId: "consent-card-activity-id", | |
| }); | |
| // Simulate SDK without updateActivity support | |
| (context as Record<string, unknown>).updateActivity = undefined; | |
| await handler.run?.(context); | |
| expect(sendActivity).toHaveBeenCalledWith( | |
| expect.objectContaining({ | |
| type: "message", | |
| attachments: [ | |
| expect.objectContaining({ | |
| contentType: "application/vnd.microsoft.teams.card.file.info", | |
| }), | |
| ], | |
| }), | |
| ); | |
| }); |
Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/msteams/src/monitor-handler.file-consent.test.ts
Line: 254-286
Comment:
**Missing test coverage for `updateActivity` not being defined**
The three new test cases cover:
1. `updateActivity` present + `replyToId` present → in-place update ✅
2. `updateActivity` present + `replyToId` present, but update throws → fallback ✅
3. `updateActivity` present + `replyToId` absent → fallback ✅
But there is no test for `context.updateActivity` itself being `undefined` (i.e., the SDK version doesn't expose it). The `typeof context.updateActivity === "function"` guard in `monitor-handler.ts` specifically handles this case, so a test should verify it falls back to `sendActivity` correctly.
```suggestion
it("sends FileInfoCard as new message when updateActivity is not available on context", async () => {
const uploadId = storePendingUpload({
buffer: Buffer.from("file content"),
filename: "secret.txt",
contentType: "application/pdf",
conversationId: "19:[email protected]",
});
const deps = createDeps();
const handler = registerMSTeamsHandlers(createActivityHandler(), deps);
const { context, sendActivity } = createInvokeContext({
conversationId: "19:[email protected];messageid=abc123",
uploadId,
action: "accept",
replyToId: "consent-card-activity-id",
});
// Simulate SDK without updateActivity support
(context as Record<string, unknown>).updateActivity = undefined;
await handler.run?.(context);
expect(sendActivity).toHaveBeenCalledWith(
expect.objectContaining({
type: "message",
attachments: [
expect.objectContaining({
contentType: "application/vnd.microsoft.teams.card.file.info",
}),
],
}),
);
});
```
How can I resolve this? If you propose a fix, please make it concise.| await context.updateActivity({ | ||
| id: consentActivityId, | ||
| type: "message", | ||
| attachments: [fileInfoCard], | ||
| }); |
There was a problem hiding this comment.
Consider explicitly clearing text in the updateActivity payload
The activity sent to updateActivity only specifies id, type, and attachments. If the original FileConsentCard message was sent with a text field (or if Teams preserves the previous text during a partial update), the stale text would remain visible in the updated card alongside the new FileInfoCard attachment.
Bot Framework's update semantics perform a full replacement of the activity, so omitting text should effectively leave it empty. However, being explicit makes the intent clear and guards against any SDK version that might merge fields rather than replace them:
| await context.updateActivity({ | |
| id: consentActivityId, | |
| type: "message", | |
| attachments: [fileInfoCard], | |
| }); | |
| await context.updateActivity({ | |
| id: consentActivityId, | |
| type: "message", | |
| text: "", | |
| attachments: [fileInfoCard], | |
| }); |
Prompt To Fix With AI
This is a comment left during a code review.
Path: extensions/msteams/src/monitor-handler.ts
Line: 112-116
Comment:
**Consider explicitly clearing `text` in the `updateActivity` payload**
The activity sent to `updateActivity` only specifies `id`, `type`, and `attachments`. If the original FileConsentCard message was sent with a `text` field (or if Teams preserves the previous `text` during a partial update), the stale text would remain visible in the updated card alongside the new FileInfoCard attachment.
Bot Framework's update semantics perform a full replacement of the activity, so omitting `text` should effectively leave it empty. However, being explicit makes the intent clear and guards against any SDK version that might merge fields rather than replace them:
```suggestion
await context.updateActivity({
id: consentActivityId,
type: "message",
text: "",
attachments: [fileInfoCard],
});
```
How can I resolve this? If you propose a fix, please make it concise.|
Confirming the fix works well. FYI @BradGroux |
|
Closing as superseded by #49580, which implements the same FileConsentCard updateActivity fix with additional improvements. |
Summary
After a user clicks "Allow" on a
FileConsentCard, the card remains frozen showing Allow/Deny buttons even though the file uploads successfully. This is because theFileInfoCardconfirmation is sent as a new message instead of replacing the original consent card in-place.This PR fixes the UX by using
context.updateActivity()to replace the originalFileConsentCardwith aFileInfoCardafter successful upload.Before (broken UX)
After (fixed UX)
Changes
extensions/msteams/src/sdk-types.tsupdateActivitytoMSTeamsTurnContexttypeextensions/msteams/src/monitor-handler.tsupdateActivity()with the invokereplyToIdto replace the consent card in-place; fall back tosendActivityif update fails or is unavailableextensions/msteams/src/monitor-handler.file-consent.test.tsupdateActivityfailure, and fallback whenreplyToIdis absentFallback Safety
The fix is fully backward-compatible:
updateActivityis not available on the context → falls back tosendActivity(current behavior)replyToIdis not present on the invoke activity → falls back tosendActivityupdateActivitythrows (expired activity, permissions error) → catches error, falls back tosendActivityIn all fallback cases, the user still receives the
FileInfoCard— just as a new message (existing behavior).Testing
replyToIdfallbackupdateActivitymock andreplyToIdfieldReferences
Fixes #47268