Skip to content

Commit 8506c0a

Browse files
rrajpcursoragent
andcommitted
IDR-msteams-adaptive-card-tables: fix(msteams): address adaptive table review findings
Keep adaptive Teams table rendering opt-in, preserve adaptive-card-only sends, and record all split-send platform IDs. Add focused regression coverage and document the new table mode. Co-authored-by: Cursor <[email protected]>
1 parent 48b5418 commit 8506c0a

12 files changed

Lines changed: 84 additions & 6 deletions

File tree

docs/concepts/markdown-formatting.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -66,15 +66,18 @@ Markdown tables are not consistently supported across chat clients. Use
6666

6767
- `code`: render tables as code blocks (default for most channels).
6868
- `bullets`: convert each row into bullet points (default for Matrix, Signal, and WhatsApp).
69+
- `adaptive`: render tables as Microsoft Teams Adaptive Card tables. This is
70+
opt-in for Teams because Adaptive Card table support depends on the Teams
71+
client and tenant.
6972
- `off`: disable table parsing and conversion; raw table text passes through.
7073

7174
Config keys:
7275

7376
```yaml
7477
channels:
75-
discord:
78+
msteams:
7679
markdown:
77-
tables: code
80+
tables: adaptive
7881
accounts:
7982
work:
8083
markdown:

extensions/msteams/src/adaptive-card-table.test.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,10 +108,11 @@ describe("splitTextAndTables", () => {
108108
];
109109
const result = splitTextAndTables(text, tables);
110110

111-
expect(result).toHaveLength(4);
111+
expect(result).toHaveLength(5);
112112
expect(result[0]).toEqual({ kind: "text", text: "Intro" });
113113
expect(result[1]).toEqual({ kind: "table", table: tables[0] });
114114
expect(result[2]).toEqual({ kind: "text", text: "Middle" });
115115
expect(result[3]).toEqual({ kind: "table", table: tables[1] });
116+
expect(result[4]).toEqual({ kind: "text", text: "End" });
116117
});
117118
});

extensions/msteams/src/channel.test.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,6 +54,10 @@ describe("msteamsPlugin", () => {
5454
expect(looksLikeId?.("a:1bfPersonalChat")).toBe(true);
5555
expect(looksLikeId?.("user:Jane Doe")).toBe(false);
5656
});
57+
58+
it("keeps adaptive markdown tables opt-in", () => {
59+
expect(msteamsPlugin.messaging?.defaultMarkdownTableMode).toBeUndefined();
60+
});
5761
});
5862

5963
describe("msteams config schema", () => {

extensions/msteams/src/channel.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -507,7 +507,6 @@ export const msteamsPlugin: ChannelPlugin<ResolvedMSTeamsAccount, ProbeMSTeamsRe
507507
setup: msteamsSetupAdapter,
508508
messaging: {
509509
targetPrefixes: ["msteams", "teams"],
510-
defaultMarkdownTableMode: "adaptive",
511510
normalizeTarget: normalizeMSTeamsMessagingTarget,
512511
resolveOutboundSessionRoute: (params) => resolveMSTeamsOutboundSessionRoute(params),
513512
targetResolver: {

extensions/msteams/src/messenger.test.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -369,6 +369,33 @@ describe("msteams messenger", () => {
369369
expect(capturedConversationId).toBe("19:[email protected]");
370370
});
371371

372+
it("keeps adaptive-card-only messages in proactive send batches", async () => {
373+
const sent: Array<Record<string, unknown>> = [];
374+
const card = { type: "AdaptiveCard", version: "1.5", body: [{ type: "Table" }] };
375+
376+
const ids = await sendMSTeamsMessages({
377+
replyStyle: "top-level",
378+
app: createMockApp({
379+
createFn: async (activity: unknown) => {
380+
sent.push(activity as Record<string, unknown>);
381+
return { id: "id:card" };
382+
},
383+
}),
384+
appId: "app123",
385+
conversationRef: baseRef,
386+
messages: [{ adaptiveCard: card }],
387+
});
388+
389+
expect(ids).toEqual(["id:card"]);
390+
expect(sent).toHaveLength(1);
391+
expect(sent[0]?.attachments).toEqual([
392+
{
393+
contentType: "application/vnd.microsoft.card.adaptive",
394+
content: card,
395+
},
396+
]);
397+
});
398+
372399
it("preserves parsed mentions when appending OneDrive fallback file links", async () => {
373400
const tmpDir = await mkdtemp(path.join(resolvePreferredOpenClawTmpDir(), "msteams-mention-"));
374401
const localFile = path.join(tmpDir, "note.txt");

extensions/msteams/src/messenger.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -482,7 +482,7 @@ export async function sendMSTeamsMessages(params: {
482482
serviceUrlBoundary?: MSTeamsSdkCloudOptions;
483483
}): Promise<string[]> {
484484
const messages = params.messages.filter(
485-
(m) => (m.text && m.text.trim().length > 0) || m.mediaUrl,
485+
(m) => (m.text && m.text.trim().length > 0) || m.mediaUrl || m.adaptiveCard,
486486
);
487487
if (messages.length === 0) {
488488
return [];

extensions/msteams/src/send.test.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -381,6 +381,34 @@ describe("sendMessageMSTeams", () => {
381381
expect(firstObjectArg(mockState.sendMSTeamsMessages).replyStyle).toBe("top-level");
382382
});
383383

384+
it("sends adaptive table segments and preserves every receipt id", async () => {
385+
mockState.resolveMarkdownTableMode.mockReturnValue("adaptive");
386+
mockState.sendMSTeamsMessages.mockResolvedValue(["text-1", "table-1", "text-2"]);
387+
388+
const result = await sendMessageMSTeams({
389+
cfg: { channels: { msteams: { markdown: { tables: "adaptive" } } } } as OpenClawConfig,
390+
to: "conversation:19:[email protected]",
391+
text: "Before\n\n| Name | Age |\n|---|---|\n| Alice | 30 |\n\nAfter",
392+
});
393+
394+
const sendPayload = firstObjectArg(mockState.sendMSTeamsMessages);
395+
const messages = sendPayload.messages as Array<Record<string, unknown>>;
396+
expect(messages).toHaveLength(3);
397+
expect(messages[0]).toEqual({ text: "Before" });
398+
expect(messages[1]?.adaptiveCard).toMatchObject({
399+
type: "AdaptiveCard",
400+
version: "1.5",
401+
});
402+
expect(messages[2]).toEqual({ text: "After" });
403+
expect(result.messageId).toBe("text-1");
404+
expect(result.receipt?.platformMessageIds).toEqual(["text-1", "table-1", "text-2"]);
405+
expect(result.receipt?.parts.map((part) => part.platformMessageId)).toEqual([
406+
"text-1",
407+
"table-1",
408+
"text-2",
409+
]);
410+
});
411+
384412
it("uses graphChatId instead of conversationId when uploading to SharePoint", async () => {
385413
// Simulates a group chat where Bot Framework conversationId is valid but we have
386414
// a resolved Graph chat ID cached from a prior send.

extensions/msteams/src/send.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -437,6 +437,7 @@ async function sendAdaptiveTableSegments(
437437

438438
return createMSTeamsSendResult({
439439
messageId: platformMessageIds[0] ?? "unknown",
440+
platformMessageIds,
440441
conversationId,
441442
kind: "text",
442443
});

packages/markdown-core/src/tables.test.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,4 +7,10 @@ describe("convertMarkdownTables", () => {
77

88
expect(rendered).toBe("```\n| A | B |\n| --- | --- |\n| 1 | 2 |\n```");
99
});
10+
11+
it("leaves markdown unchanged for adaptive mode", () => {
12+
const markdown = "| A | B |\n|---|---|\n| 1 | 2 |";
13+
14+
expect(convertMarkdownTables(markdown, "adaptive")).toBe(markdown);
15+
});
1016
});

src/config/markdown-tables.test.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { describe, expect, it, vi } from "vitest";
44
const listChannelPluginsMock = vi.hoisted(() =>
55
vi.fn(() => [
66
{ id: "mattermost", messaging: { defaultMarkdownTableMode: "off" as const } },
7+
{ id: "msteams", messaging: {} },
78
{ id: "signal", messaging: { defaultMarkdownTableMode: "bullets" as const } },
89
{ id: "whatsapp", messaging: { defaultMarkdownTableMode: "bullets" as const } },
910
]),
@@ -47,6 +48,10 @@ describe("DEFAULT_TABLE_MODES", () => {
4748
it("slack has no special default in this seam-only slice", () => {
4849
expect(DEFAULT_TABLE_MODES.get("slack")).toBeUndefined();
4950
});
51+
52+
it("msteams has no adaptive default", () => {
53+
expect(DEFAULT_TABLE_MODES.get("msteams")).toBeUndefined();
54+
});
5055
});
5156

5257
describe("resolveMarkdownTableMode", () => {

0 commit comments

Comments
 (0)