Skip to content

Commit de43266

Browse files
fix(teams): scope file delivery to post_message only (upstream fidelity)
The initial port wired filesToAttachments into both post_message AND edit_message. Upstream vercel/chat wires it into postMessage + postChannelMessage only — editMessage never carries files. And chinchill delivers execution artifacts via a fresh post(), never by editing files into an existing message. Carrying files in edit_message was an unrequested divergence (and an edit_message+files test has no upstream counterpart, which the repo's verify_test_fidelity gate would flag). Revert edit_message to file-free, mirroring upstream. Replace the edit_message+file test with a fidelity guard asserting edit_message carries no file attachments. post_message file delivery (the actual parity fix) is unchanged. Full suite: 4052 passed. pyrefly clean. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 981d940 commit de43266

3 files changed

Lines changed: 23 additions & 16 deletions

File tree

CHANGELOG.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,11 @@ Python-only feature port. No upstream version change.
66

77
### Features
88

9-
- **Teams now delivers outbound files via base64 data-URI activity attachments** — the Teams adapter previously dropped a `Postable`'s `.files` entirely: `post_message`/`edit_message` never called `extract_files`, so execution artifacts (CSVs, charts, etc.) silently vanished. This ports upstream `vercel/chat`'s `filesToAttachments` (`packages/adapter-teams/src/index.ts` lines ~1006-1035) to Python. A new `TeamsAdapter._files_to_attachments` resolves each `FileUpload`'s bytes via `to_buffer(..., throw_on_unsupported=False)`, base64-encodes them, and builds a Bot Framework attachment `{"contentType", "contentUrl": "data:<mime>;base64,<b64>", "name"}` (mime defaults to `application/octet-stream`); files whose data can't be resolved to bytes are skipped with a debug log (mirrors upstream's `if (!buffer) continue`). `post_message` and `edit_message` now call `extract_files` and attach the results: the adaptive-card branch appends file attachments after the card attachment, and the text branch sets `attachments` to the file attachments when present. This is the Teams half of outbound artifact parity and unblocks chinchill routing execution artifacts to Teams. **Caveat (inherited from upstream's design):** data-URI attachments inline the full file payload base64-encoded in the activity, so large files inflate the Bot Framework payload — the same size constraint upstream carries.
9+
- **Teams now delivers outbound files via base64 data-URI activity attachments** — the Teams adapter previously dropped a `Postable`'s `.files` entirely: `post_message`/`edit_message` never called `extract_files`, so execution artifacts (CSVs, charts, etc.) silently vanished. This ports upstream `vercel/chat`'s `filesToAttachments` (`packages/adapter-teams/src/index.ts` lines ~1006-1035) to Python. A new `TeamsAdapter._files_to_attachments` resolves each `FileUpload`'s bytes via `to_buffer(..., throw_on_unsupported=False)`, base64-encodes them, and builds a Bot Framework attachment `{"contentType", "contentUrl": "data:<mime>;base64,<b64>", "name"}` (mime defaults to `application/octet-stream`); files whose data can't be resolved to bytes are skipped with a debug log (mirrors upstream's `if (!buffer) continue`). `post_message` now calls `extract_files` and attaches the results: the adaptive-card branch appends file attachments after the card attachment, and the text branch sets `attachments` to the file attachments when present. **Scoped to `post_message` only** — upstream wires `filesToAttachments` into `postMessage`/`postChannelMessage`, NOT `editMessage`, so `edit_message` stays file-free for fidelity (and chinchill delivers artifacts via a fresh `post`, never an edit). This is the Teams half of outbound artifact parity and unblocks chinchill routing execution artifacts to Teams. **Caveat (inherited from upstream's design):** data-URI attachments inline the full file payload base64-encoded in the activity, so large files inflate the Bot Framework payload — the same size constraint upstream carries.
1010

1111
### Test quality
1212

13-
- Added 5 tests in `tests/test_teams_adapter.py` (`TestFileAttachments`): text+file, adaptive-card+file (both attachments present), `edit_message`+file, mime-type default to `application/octet-stream`, and the skip-unresolvable-bytes branch. Each delivery test round-trips or asserts the `data:<mime>;base64,...` `contentUrl`, `contentType`, and `name`.
13+
- Added 5 tests in `tests/test_teams_adapter.py` (`TestFileAttachments`): text+file, adaptive-card+file (both attachments present), mime-type default to `application/octet-stream`, the skip-unresolvable-bytes branch, and a fidelity guard that `edit_message` carries NO file attachments. Each delivery test asserts the `data:<mime>;base64,...` `contentUrl`, `contentType`, and `name`.
1414

1515
## 0.4.29a2 (2026-05-28)
1616

src/chat_sdk/adapters/teams/adapter.py

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1171,14 +1171,18 @@ async def edit_message(
11711171
message_id: str,
11721172
message: AdapterPostableMessage,
11731173
) -> RawMessage:
1174-
"""Edit an existing Teams message."""
1174+
"""Edit an existing Teams message.
1175+
1176+
Note: file delivery is intentionally NOT wired here. Upstream
1177+
``vercel/chat`` ports ``filesToAttachments`` into ``postMessage`` and
1178+
``postChannelMessage`` only — ``editMessage`` does not carry files — and
1179+
chinchill delivers execution artifacts via a fresh ``post`` (never by
1180+
editing files into an existing message). Keeping ``edit_message``
1181+
file-free preserves upstream fidelity.
1182+
"""
11751183
decoded = self.decode_thread_id(thread_id)
11761184

1177-
files = extract_files(message)
1178-
file_attachments = await self._files_to_attachments(files) if files else []
1179-
11801185
card = extract_card(message)
1181-
activity_payload: dict[str, Any]
11821186
if card:
11831187
adaptive_card = card_to_adaptive_card(card)
11841188
activity_payload = {
@@ -1188,7 +1192,6 @@ async def edit_message(
11881192
"contentType": "application/vnd.microsoft.card.adaptive",
11891193
"content": adaptive_card,
11901194
},
1191-
*file_attachments,
11921195
],
11931196
}
11941197
else:
@@ -1201,8 +1204,6 @@ async def edit_message(
12011204
"text": text,
12021205
"textFormat": "markdown",
12031206
}
1204-
if file_attachments:
1205-
activity_payload["attachments"] = file_attachments
12061207

12071208
self._logger.debug(
12081209
"Teams API: updateActivity",

tests/test_teams_adapter.py

Lines changed: 12 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -764,7 +764,13 @@ async def test_card_message_with_file(self):
764764
assert file_att["contentUrl"].startswith("data:image/png;base64,")
765765

766766
@pytest.mark.asyncio
767-
async def test_edit_message_with_file(self):
767+
async def test_edit_message_does_not_carry_files(self):
768+
"""Upstream fidelity: ``editMessage`` never delivers files (upstream wires
769+
``filesToAttachments`` into ``postMessage``/``postChannelMessage`` only), and
770+
chinchill delivers execution artifacts via a fresh ``post`` — never by editing
771+
files into an existing message. A ``PostableMarkdown`` carrying files must edit
772+
the text only, with no file attachments on the activity.
773+
"""
768774
from chat_sdk.types import FileUpload, PostableMarkdown
769775

770776
adapter = _make_adapter(app_id="test-app-id", logger=_make_logger())
@@ -777,11 +783,11 @@ async def test_edit_message_with_file(self):
777783
result = await adapter.edit_message(self._thread_id(adapter), "edit-1", message)
778784

779785
payload = adapter._teams_update.call_args.args[2]
780-
attachments = payload["attachments"]
781-
assert len(attachments) == 1
782-
assert attachments[0]["contentType"] == "text/plain"
783-
assert attachments[0]["name"] == "note.txt"
784-
assert attachments[0]["contentUrl"].startswith("data:text/plain;base64,")
786+
assert "attachments" not in payload, (
787+
"edit_message must not carry file attachments — outbound file delivery is "
788+
f"post_message-only (upstream fidelity); got attachments={payload.get('attachments')!r}"
789+
)
790+
assert payload["text"] == "updated"
785791
assert result.id == "edit-1"
786792

787793
@pytest.mark.asyncio

0 commit comments

Comments
 (0)