@openclaw/feishu 2026.3.13 → 2026.5.2-beta.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (187) hide show
  1. package/api.ts +31 -0
  2. package/channel-entry.ts +20 -0
  3. package/channel-plugin-api.ts +1 -0
  4. package/contract-api.ts +16 -0
  5. package/index.ts +70 -53
  6. package/openclaw.plugin.json +1827 -4
  7. package/package.json +32 -7
  8. package/runtime-api.ts +55 -0
  9. package/secret-contract-api.ts +5 -0
  10. package/security-contract-api.ts +1 -0
  11. package/session-key-api.ts +1 -0
  12. package/setup-api.ts +3 -0
  13. package/setup-entry.test.ts +14 -0
  14. package/setup-entry.ts +13 -0
  15. package/src/accounts.test.ts +95 -7
  16. package/src/accounts.ts +199 -117
  17. package/src/app-registration.ts +331 -0
  18. package/src/approval-auth.test.ts +24 -0
  19. package/src/approval-auth.ts +25 -0
  20. package/src/async.test.ts +35 -0
  21. package/src/async.ts +43 -1
  22. package/src/audio-preflight.runtime.ts +9 -0
  23. package/src/bitable.test.ts +131 -0
  24. package/src/bitable.ts +59 -22
  25. package/src/bot-content.ts +474 -0
  26. package/src/bot-group-name.test.ts +108 -0
  27. package/src/bot-runtime-api.ts +12 -0
  28. package/src/bot-sender-name.ts +125 -0
  29. package/src/bot.broadcast.test.ts +463 -0
  30. package/src/bot.card-action.test.ts +519 -5
  31. package/src/bot.checkBotMentioned.test.ts +92 -20
  32. package/src/bot.helpers.test.ts +118 -0
  33. package/src/bot.stripBotMention.test.ts +13 -21
  34. package/src/bot.test.ts +1334 -401
  35. package/src/bot.ts +778 -775
  36. package/src/card-action.ts +408 -40
  37. package/src/card-interaction.test.ts +129 -0
  38. package/src/card-interaction.ts +159 -0
  39. package/src/card-test-helpers.ts +47 -0
  40. package/src/card-ux-approval.ts +65 -0
  41. package/src/card-ux-launcher.test.ts +99 -0
  42. package/src/card-ux-launcher.ts +121 -0
  43. package/src/card-ux-shared.ts +33 -0
  44. package/src/channel-runtime-api.ts +16 -0
  45. package/src/channel.runtime.ts +47 -0
  46. package/src/channel.test.ts +914 -3
  47. package/src/channel.ts +1253 -309
  48. package/src/chat-schema.ts +5 -4
  49. package/src/chat.test.ts +135 -28
  50. package/src/chat.ts +68 -10
  51. package/src/client.test.ts +212 -103
  52. package/src/client.ts +115 -21
  53. package/src/comment-dispatcher-runtime-api.ts +6 -0
  54. package/src/comment-dispatcher.test.ts +169 -0
  55. package/src/comment-dispatcher.ts +107 -0
  56. package/src/comment-handler-runtime-api.ts +3 -0
  57. package/src/comment-handler.test.ts +486 -0
  58. package/src/comment-handler.ts +309 -0
  59. package/src/comment-reaction.test.ts +166 -0
  60. package/src/comment-reaction.ts +259 -0
  61. package/src/comment-shared.test.ts +182 -0
  62. package/src/comment-shared.ts +406 -0
  63. package/src/comment-target.ts +44 -0
  64. package/src/config-schema.test.ts +63 -1
  65. package/src/config-schema.ts +31 -4
  66. package/src/conversation-id.test.ts +18 -0
  67. package/src/conversation-id.ts +199 -0
  68. package/src/dedup-runtime-api.ts +1 -0
  69. package/src/dedup.ts +33 -95
  70. package/src/directory.static.ts +61 -0
  71. package/src/directory.test.ts +116 -20
  72. package/src/directory.ts +60 -92
  73. package/src/doc-schema.ts +1 -1
  74. package/src/docx-batch-insert.test.ts +39 -38
  75. package/src/docx-batch-insert.ts +55 -19
  76. package/src/docx-color-text.ts +9 -4
  77. package/src/docx-table-ops.test.ts +53 -0
  78. package/src/docx-table-ops.ts +52 -34
  79. package/src/docx-types.ts +38 -0
  80. package/src/docx.account-selection.test.ts +12 -3
  81. package/src/docx.test.ts +314 -74
  82. package/src/docx.ts +278 -122
  83. package/src/drive-schema.ts +47 -1
  84. package/src/drive.test.ts +1219 -0
  85. package/src/drive.ts +614 -13
  86. package/src/dynamic-agent.ts +10 -4
  87. package/src/event-types.ts +45 -0
  88. package/src/external-keys.ts +1 -1
  89. package/src/lifecycle.test-support.ts +220 -0
  90. package/src/media.test.ts +403 -26
  91. package/src/media.ts +509 -132
  92. package/src/mention-target.types.ts +5 -0
  93. package/src/mention.ts +32 -51
  94. package/src/message-action-contract.ts +13 -0
  95. package/src/monitor-state-runtime-api.ts +7 -0
  96. package/src/monitor-transport-runtime-api.ts +7 -0
  97. package/src/monitor.account.ts +218 -312
  98. package/src/monitor.acp-init-failure.lifecycle.test-support.ts +219 -0
  99. package/src/monitor.bot-identity.ts +86 -0
  100. package/src/monitor.bot-menu-handler.ts +165 -0
  101. package/src/monitor.bot-menu.lifecycle.test-support.ts +224 -0
  102. package/src/monitor.bot-menu.test.ts +178 -0
  103. package/src/monitor.broadcast.reply-once.lifecycle.test-support.ts +264 -0
  104. package/src/monitor.card-action.lifecycle.test-support.ts +373 -0
  105. package/src/monitor.cleanup.test.ts +376 -0
  106. package/src/monitor.comment-notice-handler.ts +105 -0
  107. package/src/monitor.comment.test.ts +937 -0
  108. package/src/monitor.comment.ts +1386 -0
  109. package/src/monitor.lifecycle.test.ts +4 -0
  110. package/src/monitor.message-handler.ts +339 -0
  111. package/src/monitor.reaction.lifecycle.test-support.ts +68 -0
  112. package/src/monitor.reaction.test.ts +108 -48
  113. package/src/monitor.startup.test.ts +11 -9
  114. package/src/monitor.startup.ts +26 -16
  115. package/src/monitor.state.ts +20 -5
  116. package/src/monitor.synthetic-error.ts +18 -0
  117. package/src/monitor.test-mocks.ts +2 -2
  118. package/src/monitor.transport.ts +220 -60
  119. package/src/monitor.ts +15 -10
  120. package/src/monitor.webhook-e2e.test.ts +65 -7
  121. package/src/monitor.webhook-security.test.ts +122 -0
  122. package/src/monitor.webhook.test-helpers.ts +44 -26
  123. package/src/outbound-runtime-api.ts +1 -0
  124. package/src/outbound.test.ts +616 -37
  125. package/src/outbound.ts +623 -81
  126. package/src/perm-schema.ts +1 -1
  127. package/src/perm.ts +1 -7
  128. package/src/pins.ts +108 -0
  129. package/src/policy.test.ts +297 -117
  130. package/src/policy.ts +142 -29
  131. package/src/post.ts +7 -6
  132. package/src/probe.test.ts +14 -9
  133. package/src/probe.ts +26 -16
  134. package/src/processing-claims.ts +59 -0
  135. package/src/qr-terminal.ts +1 -0
  136. package/src/reactions.ts +4 -34
  137. package/src/reasoning-preview.test.ts +59 -0
  138. package/src/reasoning-preview.ts +20 -0
  139. package/src/reply-dispatcher-runtime-api.ts +7 -0
  140. package/src/reply-dispatcher.test.ts +660 -29
  141. package/src/reply-dispatcher.ts +407 -154
  142. package/src/runtime.ts +6 -3
  143. package/src/secret-contract.ts +145 -0
  144. package/src/secret-input.ts +1 -13
  145. package/src/security-audit-shared.ts +69 -0
  146. package/src/security-audit.test.ts +61 -0
  147. package/src/security-audit.ts +1 -0
  148. package/src/send-result.ts +1 -1
  149. package/src/send-target.test.ts +9 -3
  150. package/src/send-target.ts +10 -4
  151. package/src/send.reply-fallback.test.ts +105 -2
  152. package/src/send.test.ts +386 -4
  153. package/src/send.ts +414 -95
  154. package/src/sequential-key.test.ts +72 -0
  155. package/src/sequential-key.ts +28 -0
  156. package/src/sequential-queue.test.ts +92 -0
  157. package/src/sequential-queue.ts +16 -0
  158. package/src/session-conversation.ts +42 -0
  159. package/src/session-route.ts +48 -0
  160. package/src/setup-core.ts +51 -0
  161. package/src/{onboarding.test.ts → setup-surface.test.ts} +52 -21
  162. package/src/setup-surface.ts +581 -0
  163. package/src/streaming-card.test.ts +138 -2
  164. package/src/streaming-card.ts +134 -18
  165. package/src/subagent-hooks.test.ts +603 -0
  166. package/src/subagent-hooks.ts +397 -0
  167. package/src/targets.ts +3 -13
  168. package/src/test-support/lifecycle-test-support.ts +453 -0
  169. package/src/thread-bindings.test.ts +143 -0
  170. package/src/thread-bindings.ts +330 -0
  171. package/src/tool-account-routing.test.ts +66 -8
  172. package/src/tool-account.test.ts +44 -0
  173. package/src/tool-account.ts +40 -17
  174. package/src/tool-factory-test-harness.ts +11 -8
  175. package/src/tool-result.ts +3 -1
  176. package/src/tools-config.ts +1 -1
  177. package/src/types.ts +16 -15
  178. package/src/typing.ts +10 -6
  179. package/src/wiki-schema.ts +1 -1
  180. package/src/wiki.ts +1 -7
  181. package/subagent-hooks-api.ts +31 -0
  182. package/tsconfig.json +16 -0
  183. package/src/feishu-command-handler.ts +0 -59
  184. package/src/onboarding.status.test.ts +0 -25
  185. package/src/onboarding.ts +0 -489
  186. package/src/send-message.ts +0 -71
  187. package/src/targets.test.ts +0 -70
package/src/send.test.ts CHANGED
@@ -1,24 +1,86 @@
1
- import type { ClawdbotConfig } from "openclaw/plugin-sdk/feishu";
2
- import { beforeEach, describe, expect, it, vi } from "vitest";
3
- import { getMessageFeishu } from "./send.js";
1
+ import { beforeAll, beforeEach, describe, expect, it, vi } from "vitest";
2
+ import type { ClawdbotConfig } from "../runtime-api.js";
3
+ import { buildMarkdownCard } from "./send.js";
4
4
 
5
- const { mockClientGet, mockCreateFeishuClient, mockResolveFeishuAccount } = vi.hoisted(() => ({
5
+ const {
6
+ mockConvertMarkdownTables,
7
+ mockClientGet,
8
+ mockClientList,
9
+ mockClientPatch,
10
+ mockCreateFeishuClient,
11
+ mockResolveMarkdownTableMode,
12
+ mockResolveFeishuAccount,
13
+ mockRuntimeConvertMarkdownTables,
14
+ mockRuntimeResolveMarkdownTableMode,
15
+ } = vi.hoisted(() => ({
16
+ mockConvertMarkdownTables: vi.fn((text: string) => text),
6
17
  mockClientGet: vi.fn(),
18
+ mockClientList: vi.fn(),
19
+ mockClientPatch: vi.fn(),
7
20
  mockCreateFeishuClient: vi.fn(),
21
+ mockResolveMarkdownTableMode: vi.fn(() => "preserve"),
8
22
  mockResolveFeishuAccount: vi.fn(),
23
+ mockRuntimeConvertMarkdownTables: vi.fn((text: string) => text),
24
+ mockRuntimeResolveMarkdownTableMode: vi.fn(() => "preserve"),
9
25
  }));
10
26
 
27
+ vi.mock("openclaw/plugin-sdk/markdown-table-runtime", () => ({
28
+ resolveMarkdownTableMode: mockResolveMarkdownTableMode,
29
+ }));
30
+
31
+ vi.mock("openclaw/plugin-sdk/text-runtime", async (importOriginal) => {
32
+ const actual = await importOriginal<typeof import("openclaw/plugin-sdk/text-runtime")>();
33
+ return {
34
+ ...actual,
35
+ convertMarkdownTables: mockConvertMarkdownTables,
36
+ };
37
+ });
38
+
11
39
  vi.mock("./client.js", () => ({
12
40
  createFeishuClient: mockCreateFeishuClient,
13
41
  }));
14
42
 
15
43
  vi.mock("./accounts.js", () => ({
16
44
  resolveFeishuAccount: mockResolveFeishuAccount,
45
+ resolveFeishuRuntimeAccount: mockResolveFeishuAccount,
46
+ }));
47
+
48
+ vi.mock("./runtime.js", () => ({
49
+ getFeishuRuntime: () => ({
50
+ channel: {
51
+ text: {
52
+ resolveMarkdownTableMode: mockRuntimeResolveMarkdownTableMode,
53
+ convertMarkdownTables: mockRuntimeConvertMarkdownTables,
54
+ },
55
+ },
56
+ }),
17
57
  }));
18
58
 
59
+ let buildStructuredCard: typeof import("./send.js").buildStructuredCard;
60
+ let editMessageFeishu: typeof import("./send.js").editMessageFeishu;
61
+ let getMessageFeishu: typeof import("./send.js").getMessageFeishu;
62
+ let listFeishuThreadMessages: typeof import("./send.js").listFeishuThreadMessages;
63
+ let resolveFeishuCardTemplate: typeof import("./send.js").resolveFeishuCardTemplate;
64
+ let sendMessageFeishu: typeof import("./send.js").sendMessageFeishu;
65
+
19
66
  describe("getMessageFeishu", () => {
67
+ beforeAll(async () => {
68
+ ({
69
+ buildStructuredCard,
70
+ editMessageFeishu,
71
+ getMessageFeishu,
72
+ listFeishuThreadMessages,
73
+ resolveFeishuCardTemplate,
74
+ sendMessageFeishu,
75
+ } = await import("./send.js"));
76
+ });
77
+
20
78
  beforeEach(() => {
21
79
  vi.clearAllMocks();
80
+ mockResolveMarkdownTableMode.mockReturnValue("preserve");
81
+ mockConvertMarkdownTables.mockImplementation((text: string) => text);
82
+ mockRuntimeResolveMarkdownTableMode.mockReturnValue("preserve");
83
+ mockRuntimeConvertMarkdownTables.mockImplementation((text: string) => text);
22
84
  mockResolveFeishuAccount.mockReturnValue({
23
85
  accountId: "default",
24
86
  configured: true,
@@ -26,12 +88,49 @@ describe("getMessageFeishu", () => {
26
88
  mockCreateFeishuClient.mockReturnValue({
27
89
  im: {
28
90
  message: {
91
+ create: vi.fn(),
29
92
  get: mockClientGet,
93
+ list: mockClientList,
94
+ patch: mockClientPatch,
30
95
  },
31
96
  },
32
97
  });
33
98
  });
34
99
 
100
+ it("sends text without requiring Feishu runtime text helpers", async () => {
101
+ mockRuntimeResolveMarkdownTableMode.mockImplementation(() => {
102
+ throw new Error("Feishu runtime not initialized");
103
+ });
104
+ mockRuntimeConvertMarkdownTables.mockImplementation(() => {
105
+ throw new Error("Feishu runtime not initialized");
106
+ });
107
+ mockClientPatch.mockResolvedValueOnce({ code: 0 });
108
+ mockCreateFeishuClient.mockReturnValue({
109
+ im: {
110
+ message: {
111
+ create: vi.fn().mockResolvedValue({ code: 0, data: { message_id: "om_send" } }),
112
+ reply: vi.fn(),
113
+ get: mockClientGet,
114
+ list: mockClientList,
115
+ patch: mockClientPatch,
116
+ },
117
+ },
118
+ });
119
+
120
+ const result = await sendMessageFeishu({
121
+ cfg: {} as ClawdbotConfig,
122
+ to: "oc_send",
123
+ text: "hello",
124
+ });
125
+
126
+ expect(mockResolveMarkdownTableMode).toHaveBeenCalledWith({
127
+ cfg: {},
128
+ channel: "feishu",
129
+ });
130
+ expect(mockConvertMarkdownTables).toHaveBeenCalledWith("hello", "preserve");
131
+ expect(result).toEqual({ messageId: "om_send", chatId: "oc_send" });
132
+ });
133
+
35
134
  it("extracts text content from interactive card elements", async () => {
36
135
  mockClientGet.mockResolvedValueOnce({
37
136
  code: 0,
@@ -69,6 +168,95 @@ describe("getMessageFeishu", () => {
69
168
  );
70
169
  });
71
170
 
171
+ it("falls through empty interactive card element arrays and locale variants", async () => {
172
+ mockClientGet.mockResolvedValueOnce({
173
+ code: 0,
174
+ data: {
175
+ items: [
176
+ {
177
+ message_id: "om_i18n_card",
178
+ chat_id: "oc_i18n_card",
179
+ msg_type: "interactive",
180
+ body: {
181
+ content: JSON.stringify({
182
+ elements: [],
183
+ body: { elements: [] },
184
+ i18n_elements: {
185
+ zh_cn: [],
186
+ en_us: [
187
+ {
188
+ tag: "markdown",
189
+ content: "hello ${count} {{label}} {{metadata}}",
190
+ },
191
+ ],
192
+ },
193
+ template_variable: {
194
+ count: 2,
195
+ label: "tasks",
196
+ metadata: { ignored: true },
197
+ },
198
+ }),
199
+ },
200
+ },
201
+ ],
202
+ },
203
+ });
204
+
205
+ const result = await getMessageFeishu({
206
+ cfg: {} as ClawdbotConfig,
207
+ messageId: "om_i18n_card",
208
+ });
209
+
210
+ expect(result).toEqual(
211
+ expect.objectContaining({
212
+ messageId: "om_i18n_card",
213
+ chatId: "oc_i18n_card",
214
+ contentType: "interactive",
215
+ content: "hello 2 tasks {{metadata}}",
216
+ }),
217
+ );
218
+ });
219
+
220
+ it("falls back to post-format content when interactive card elements are empty", async () => {
221
+ mockClientGet.mockResolvedValueOnce({
222
+ code: 0,
223
+ data: {
224
+ items: [
225
+ {
226
+ message_id: "om_post_card",
227
+ chat_id: "oc_post_card",
228
+ msg_type: "interactive",
229
+ body: {
230
+ content: JSON.stringify({
231
+ elements: [],
232
+ post: {
233
+ zh_cn: {
234
+ title: "Card summary",
235
+ content: [[{ tag: "md", text: "**fallback** body" }]],
236
+ },
237
+ },
238
+ }),
239
+ },
240
+ },
241
+ ],
242
+ },
243
+ });
244
+
245
+ const result = await getMessageFeishu({
246
+ cfg: {} as ClawdbotConfig,
247
+ messageId: "om_post_card",
248
+ });
249
+
250
+ expect(result).toEqual(
251
+ expect.objectContaining({
252
+ messageId: "om_post_card",
253
+ chatId: "oc_post_card",
254
+ contentType: "interactive",
255
+ content: "Card summary\n\n**fallback** body",
256
+ }),
257
+ );
258
+ });
259
+
72
260
  it("extracts text content from post messages", async () => {
73
261
  mockClientGet.mockResolvedValueOnce({
74
262
  code: 0,
@@ -165,4 +353,198 @@ describe("getMessageFeishu", () => {
165
353
  }),
166
354
  );
167
355
  });
356
+
357
+ it("reuses the same content parsing for thread history messages", async () => {
358
+ mockClientList.mockResolvedValueOnce({
359
+ code: 0,
360
+ data: {
361
+ items: [
362
+ {
363
+ message_id: "om_root",
364
+ msg_type: "text",
365
+ body: {
366
+ content: JSON.stringify({ text: "root starter" }),
367
+ },
368
+ },
369
+ {
370
+ message_id: "om_card",
371
+ msg_type: "interactive",
372
+ body: {
373
+ content: JSON.stringify({
374
+ body: {
375
+ elements: [{ tag: "markdown", content: "hello from card 2.0" }],
376
+ },
377
+ }),
378
+ },
379
+ sender: {
380
+ id: "app_1",
381
+ sender_type: "app",
382
+ },
383
+ create_time: "1710000000000",
384
+ },
385
+ {
386
+ message_id: "om_file",
387
+ msg_type: "file",
388
+ body: {
389
+ content: JSON.stringify({ file_key: "file_v3_123" }),
390
+ },
391
+ sender: {
392
+ id: "ou_1",
393
+ sender_type: "user",
394
+ },
395
+ create_time: "1710000001000",
396
+ },
397
+ ],
398
+ },
399
+ });
400
+
401
+ const result = await listFeishuThreadMessages({
402
+ cfg: {} as ClawdbotConfig,
403
+ threadId: "omt_1",
404
+ rootMessageId: "om_root",
405
+ });
406
+
407
+ expect(result).toEqual([
408
+ expect.objectContaining({
409
+ messageId: "om_file",
410
+ contentType: "file",
411
+ content: "[file message]",
412
+ }),
413
+ expect.objectContaining({
414
+ messageId: "om_card",
415
+ contentType: "interactive",
416
+ content: "hello from card 2.0",
417
+ }),
418
+ ]);
419
+ });
420
+ });
421
+
422
+ describe("editMessageFeishu", () => {
423
+ beforeEach(() => {
424
+ vi.clearAllMocks();
425
+ mockResolveFeishuAccount.mockReturnValue({
426
+ accountId: "default",
427
+ configured: true,
428
+ });
429
+ mockCreateFeishuClient.mockReturnValue({
430
+ im: {
431
+ message: {
432
+ patch: mockClientPatch,
433
+ },
434
+ },
435
+ });
436
+ });
437
+
438
+ it("patches post content for text edits", async () => {
439
+ mockRuntimeResolveMarkdownTableMode.mockImplementation(() => {
440
+ throw new Error("Feishu runtime not initialized");
441
+ });
442
+ mockRuntimeConvertMarkdownTables.mockImplementation(() => {
443
+ throw new Error("Feishu runtime not initialized");
444
+ });
445
+ mockClientPatch.mockResolvedValueOnce({ code: 0 });
446
+
447
+ const result = await editMessageFeishu({
448
+ cfg: {} as ClawdbotConfig,
449
+ messageId: "om_edit",
450
+ text: "updated body",
451
+ });
452
+
453
+ expect(mockClientPatch).toHaveBeenCalledWith({
454
+ path: { message_id: "om_edit" },
455
+ data: {
456
+ content: JSON.stringify({
457
+ zh_cn: {
458
+ content: [
459
+ [
460
+ {
461
+ tag: "md",
462
+ text: "updated body",
463
+ },
464
+ ],
465
+ ],
466
+ },
467
+ }),
468
+ },
469
+ });
470
+ expect(result).toEqual({ messageId: "om_edit", contentType: "post" });
471
+ });
472
+
473
+ it("patches interactive content for card edits", async () => {
474
+ mockClientPatch.mockResolvedValueOnce({ code: 0 });
475
+
476
+ const result = await editMessageFeishu({
477
+ cfg: {} as ClawdbotConfig,
478
+ messageId: "om_card",
479
+ card: { schema: "2.0" },
480
+ });
481
+
482
+ expect(mockClientPatch).toHaveBeenCalledWith({
483
+ path: { message_id: "om_card" },
484
+ data: {
485
+ content: JSON.stringify({ schema: "2.0" }),
486
+ },
487
+ });
488
+ expect(result).toEqual({ messageId: "om_card", contentType: "interactive" });
489
+ });
490
+ });
491
+
492
+ describe("resolveFeishuCardTemplate", () => {
493
+ it("accepts supported Feishu templates", () => {
494
+ expect(resolveFeishuCardTemplate(" purple ")).toBe("purple");
495
+ });
496
+
497
+ it("drops unsupported free-form identity themes", () => {
498
+ expect(resolveFeishuCardTemplate("space lobster")).toBeUndefined();
499
+ });
500
+ });
501
+
502
+ describe("buildStructuredCard", () => {
503
+ it("uses schema-2.0 width config instead of legacy wide screen mode", () => {
504
+ const card = buildStructuredCard("hello") as {
505
+ config: {
506
+ width_mode?: string;
507
+ enable_forward?: boolean;
508
+ wide_screen_mode?: boolean;
509
+ };
510
+ };
511
+
512
+ expect(card.config.width_mode).toBe("fill");
513
+ expect(card.config.enable_forward).toBeUndefined();
514
+ expect(card.config.wide_screen_mode).toBeUndefined();
515
+ });
516
+
517
+ it("falls back to blue when the header template is unsupported", () => {
518
+ const card = buildStructuredCard("hello", {
519
+ header: {
520
+ title: "Agent",
521
+ template: "space lobster",
522
+ },
523
+ });
524
+
525
+ expect(card).toEqual(
526
+ expect.objectContaining({
527
+ header: {
528
+ title: { tag: "plain_text", content: "Agent" },
529
+ template: "blue",
530
+ },
531
+ }),
532
+ );
533
+ });
534
+ });
535
+
536
+ describe("buildMarkdownCard", () => {
537
+ it("uses schema-2.0 width config instead of legacy wide screen mode", () => {
538
+ const card = buildMarkdownCard("hello") as {
539
+ config: {
540
+ width_mode?: string;
541
+ enable_forward?: boolean;
542
+ wide_screen_mode?: boolean;
543
+ };
544
+ };
545
+
546
+ expect(card.config.width_mode).toBe("fill");
547
+ expect(card.config.enable_forward).toBeUndefined();
548
+ expect(card.config.wide_screen_mode).toBeUndefined();
549
+ });
168
550
  });