Skip to content

fix(msteams): update FileConsentCard in-place after upload via updateActivity#47270

Closed
cwatts-sage wants to merge 1 commit intoopenclaw:mainfrom
cwatts-sage:fix/msteams-file-consent-card-update
Closed

fix(msteams): update FileConsentCard in-place after upload via updateActivity#47270
cwatts-sage wants to merge 1 commit intoopenclaw:mainfrom
cwatts-sage:fix/msteams-file-consent-card-update

Conversation

@cwatts-sage
Copy link
Copy Markdown

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 the FileInfoCard confirmation 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 original FileConsentCard with a FileInfoCard after successful upload.

Before (broken UX)

1. Bot sends FileConsentCard (Allow / Deny)
2. User clicks Allow
3. File uploads successfully
4. FileInfoCard sent as NEW message
5. Original consent card still shows stale Allow/Deny buttons ← bad

After (fixed UX)

1. Bot sends FileConsentCard (Allow / Deny)
2. User clicks Allow
3. File uploads successfully
4. Original consent card REPLACED with FileInfoCard ← card transitions in-place

Changes

File What changed
extensions/msteams/src/sdk-types.ts Added optional updateActivity to MSTeamsTurnContext type
extensions/msteams/src/monitor-handler.ts After upload, use updateActivity() with the invoke replyToId to replace the consent card in-place; fall back to sendActivity if update fails or is unavailable
extensions/msteams/src/monitor-handler.file-consent.test.ts Added tests for: in-place card update, fallback on updateActivity failure, and fallback when replyToId is absent

Fallback Safety

The fix is fully backward-compatible:

  • If updateActivity is not available on the context → falls back to sendActivity (current behavior)
  • If replyToId is not present on the invoke activity → falls back to sendActivity
  • If updateActivity throws (expired activity, permissions error) → catches error, falls back to sendActivity

In all fallback cases, the user still receives the FileInfoCard — just as a new message (existing behavior).

Testing

  • 3 new test cases covering the happy path, error fallback, and missing replyToId fallback
  • Existing tests updated to expose updateActivity mock and replyToId field
  • All existing tests continue to pass

References

Fixes #47268

…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
@openclaw-barnacle openclaw-barnacle bot added channel: msteams Channel integration: msteams size: S labels Mar 15, 2026
@greptile-apps
Copy link
Copy Markdown
Contributor

greptile-apps bot commented Mar 15, 2026

Greptile Summary

This PR fixes a UX regression in the MS Teams file upload flow by replacing the stale FileConsentCard in-place (via updateActivity) after a successful upload, rather than sending a new FileInfoCard message. The fallback logic is well-designed — it gracefully handles missing replyToId, unavailable updateActivity, and runtime update failures, all falling back to the previous sendActivity behaviour.

Key observations:

  • The core handler change is clean and the three-path fallback (no ID, update throws, update unavailable) is correct.
  • updateActivity is appropriately marked optional in MSTeamsTurnContext to accommodate older SDK versions.
  • Three new tests cover the main paths, but the context.updateActivity === undefined branch (the typeof guard in the production code) has no corresponding test.
  • The "updates consent card in-place" test stores filename: "report.pdf" but asserts name: "secret.txt" in the FileInfoCard — technically correct (the card uses uploadInfo.name from the mock), but the mismatch can mislead future readers.
  • The updateActivity payload omits text: "", which may leave stale text if the original consent card happened to include a text field alongside the attachment.

Confidence Score: 4/5

  • Safe to merge — the fallback logic ensures no regression in the existing message-delivery behaviour, and the core update path is straightforward.
  • The implementation is well-structured with appropriate fallbacks for every failure mode. The only gaps are minor: a missing test for the updateActivity === undefined branch and a test data inconsistency that could confuse maintainers, neither of which affects runtime correctness.
  • No files require special attention; the test file has two small clarity issues noted in inline comments.
Prompt To Fix All 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.

---

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

Comment on lines +175 to +180
const uploadId = storePendingUpload({
buffer: Buffer.from("file content"),
filename: "report.pdf",
contentType: "application/pdf",
conversationId: "19:[email protected]",
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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.

Comment on lines +254 to +286
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",
}),
],
}),
);
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Suggested change
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.

Comment on lines +112 to +116
await context.updateActivity({
id: consentActivityId,
type: "message",
attachments: [fileInfoCard],
});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:

Suggested change
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.

@SidU
Copy link
Copy Markdown
Contributor

SidU commented Mar 17, 2026

Confirming the fix works well. FYI @BradGroux

@BradGroux
Copy link
Copy Markdown
Contributor

Closing as superseded by #49580, which implements the same FileConsentCard updateActivity fix with additional improvements.

@BradGroux BradGroux closed this Mar 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

channel: msteams Channel integration: msteams size: S

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[msteams] FileConsentCard not updated after user accepts — consent card remains frozen

3 participants