Skip to content

Commit 3ec3e9a

Browse files
packwoodclaude
andcommitted
fix(msteams): gate graphMediaFallback on a group-chat allowlist, not not-channel
The prior scope fix excluded channel conversations with a denylist, but personal DMs also arrive as conversationType 'personal' and can carry Graph-compatible 19:[email protected] chat IDs that isBotFrameworkPersonalChatId does not catch — so a documented group-chat option silently changed DM Graph-lookup behavior. Gate on conversationType === 'groupChat' (normalized) instead. Adds a regression: personal + Graph-compatible 19: ID + flag on does not call Graph. Addresses ClawSweeper P1 on #101096. Co-Authored-By: Claude Fable 5 <[email protected]>
1 parent b57bc80 commit 3ec3e9a

3 files changed

Lines changed: 41 additions & 10 deletions

File tree

extensions/msteams/src/monitor-handler/inbound-media.test.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -177,6 +177,33 @@ describe("resolveMSTeamsInboundMedia graph fallback trigger", () => {
177177
expect(buildMSTeamsGraphMessageUrls).not.toHaveBeenCalled();
178178
});
179179

180+
it("does NOT apply graphMediaFallback to personal chats with Graph-compatible 19: IDs", async () => {
181+
vi.mocked(downloadMSTeamsAttachments).mockResolvedValue([]);
182+
// A personal DM can carry a `19:[email protected]` chat ID that is NOT a
183+
// Bot Framework personal ID, so isBotFrameworkPersonalChatId does not catch
184+
// it. The trigger must be a group-chat allowlist, not "not channel", or the
185+
// documented group-chat option would silently change DM Graph behavior.
186+
vi.mocked(extractMSTeamsHtmlAttachmentIds).mockReturnValueOnce([]);
187+
vi.mocked(downloadMSTeamsGraphMedia).mockClear();
188+
vi.mocked(buildMSTeamsGraphMessageUrls).mockClear();
189+
190+
await resolveMSTeamsInboundMedia({
191+
...baseParams,
192+
conversationType: "personal",
193+
conversationId: "19:[email protected]",
194+
graphMediaFallback: true,
195+
attachments: [
196+
{
197+
contentType: "text/html",
198+
content: "<div>please read the attached file</div>",
199+
},
200+
],
201+
});
202+
203+
expect(downloadMSTeamsGraphMedia).not.toHaveBeenCalled();
204+
expect(buildMSTeamsGraphMessageUrls).not.toHaveBeenCalled();
205+
});
206+
180207
it("does NOT apply graphMediaFallback to channel conversations", async () => {
181208
vi.mocked(downloadMSTeamsAttachments).mockResolvedValue([]);
182209
// Channel Graph URL building still has known team-GUID and reply-URL

extensions/msteams/src/monitor-handler/inbound-media.ts

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -136,19 +136,22 @@ export async function resolveMSTeamsInboundMedia(params: {
136136
// Teams strips `<attachment id>` tags from the HTML delivered to bots in
137137
// group chats, so real file attachments can arrive with an empty extracted
138138
// ID list (#89594). With `channels.msteams.graphMediaFallback` enabled,
139-
// attempt the Graph fetch for any group-chat message that carried an HTML
139+
// attempt the Graph fetch for a group-chat message that carried an HTML
140140
// attachment and produced no media — at the cost of one Graph message
141141
// lookup per such message. The default stays off so mention-only messages
142-
// don't generate spurious lookups (#58617). Channel conversations are
143-
// excluded: the channel Graph URL builder still has known team-GUID and
144-
// reply-URL defects, so widening the trigger there would route channel
145-
// traffic into a path that can 400/404 (see the channel attachment repair
146-
// tracked alongside #89594).
147-
const isChannelConversation = conversationType.trim().toLowerCase() === "channel";
142+
// don't generate spurious lookups (#58617).
143+
//
144+
// The widened trigger is an allowlist on the group-chat conversation type,
145+
// not "everything except channel": personal DMs also arrive as `personal`
146+
// and can carry Graph-compatible `19:...` chat IDs that are not caught by
147+
// `isBotFrameworkPersonalChatId`, so a denylist would change DM behavior
148+
// too. Channel conversations stay out because their Graph URL builder has
149+
// separate known team-GUID and reply-URL defects (broader channel repair).
150+
const isGroupChatConversation = conversationType.trim().toLowerCase() === "groupchat";
148151
const hasGraphFallbackCandidate =
149152
hasHtmlFileAttachment ||
150153
(params.graphMediaFallback === true &&
151-
!isChannelConversation &&
154+
isGroupChatConversation &&
152155
attachments.some(
153156
(att) =>
154157
typeof att.contentType === "string" &&

src/config/types.msteams.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -165,8 +165,9 @@ export type MSTeamsConfig = {
165165
* Fetch group-chat message media via Microsoft Graph even when the Bot
166166
* Framework payload contains no `<attachment id>` references. Teams strips
167167
* those tags from group-chat HTML, so file attachments are otherwise
168-
* invisible to the bot. Applies to group chats only; channel (team-scope)
169-
* messages keep the tag-gated fallback. Requires `Chat.Read.All` plus
168+
* invisible to the bot. Applies to group chats only (conversationType
169+
* `groupChat`); personal DMs and channel messages keep the tag-gated
170+
* fallback. Requires `Chat.Read.All` plus
170171
* `Files.Read.All` Graph Application permissions with admin consent. Adds
171172
* one Graph message lookup per group-chat message that yields no media.
172173
* Default: false.

0 commit comments

Comments
 (0)