@assistant-ui/react-generative-ui 0.0.12 → 0.0.14

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 (48) hide show
  1. package/dist/JSONGenerativeUI.client.d.ts +5 -0
  2. package/dist/JSONGenerativeUI.client.d.ts.map +1 -1
  3. package/dist/JSONGenerativeUI.client.js +12 -3
  4. package/dist/JSONGenerativeUI.client.js.map +1 -1
  5. package/dist/renderGenerativeUI.js +2 -1
  6. package/dist/renderGenerativeUI.js.map +1 -1
  7. package/dist/slack/constants.d.ts +11 -4
  8. package/dist/slack/constants.d.ts.map +1 -1
  9. package/dist/slack/constants.js +11 -4
  10. package/dist/slack/constants.js.map +1 -1
  11. package/dist/slack/fromSlackBlocks.js +2 -2
  12. package/dist/slack/toSlackBlocks.d.ts.map +1 -1
  13. package/dist/slack/toSlackBlocks.js +114 -19
  14. package/dist/slack/toSlackBlocks.js.map +1 -1
  15. package/dist/slack/types.d.ts +7 -1
  16. package/dist/slack/types.d.ts.map +1 -1
  17. package/dist/teams/constants.d.ts +9 -2
  18. package/dist/teams/constants.d.ts.map +1 -1
  19. package/dist/teams/constants.js +10 -3
  20. package/dist/teams/constants.js.map +1 -1
  21. package/dist/teams/toAdaptiveCard.d.ts +10 -2
  22. package/dist/teams/toAdaptiveCard.d.ts.map +1 -1
  23. package/dist/teams/toAdaptiveCard.js +54 -15
  24. package/dist/teams/toAdaptiveCard.js.map +1 -1
  25. package/dist/teams/toTeamsAttachments.d.ts.map +1 -1
  26. package/dist/teams/toTeamsAttachments.js +21 -13
  27. package/dist/teams/toTeamsAttachments.js.map +1 -1
  28. package/dist/teams/types.d.ts +11 -2
  29. package/dist/teams/types.d.ts.map +1 -1
  30. package/dist/vocabulary/data.js +3 -2
  31. package/dist/vocabulary/data.js.map +1 -1
  32. package/dist/vocabulary/layout.js +2 -1
  33. package/dist/vocabulary/layout.js.map +1 -1
  34. package/package.json +5 -5
  35. package/src/JSONGenerativeUI.client.tsx +13 -5
  36. package/src/JSONGenerativeUI.test.tsx +21 -2
  37. package/src/slack/constants.ts +11 -3
  38. package/src/slack/toSlackBlocks.test.ts +374 -8
  39. package/src/slack/toSlackBlocks.ts +181 -24
  40. package/src/slack/types.ts +7 -1
  41. package/src/teams/constants.ts +10 -2
  42. package/src/teams/toAdaptiveCard.test.ts +204 -10
  43. package/src/teams/toAdaptiveCard.ts +79 -15
  44. package/src/teams/toTeamsAttachments.test.ts +47 -2
  45. package/src/teams/toTeamsAttachments.ts +17 -11
  46. package/src/teams/types.ts +11 -2
  47. package/src/vocabulary/layout.test.tsx +16 -1
  48. package/src/vocabulary/layout.tsx +4 -1
@@ -43,17 +43,25 @@ export class JSONGenerativeUI {
43
43
  this.actions = options.actions;
44
44
  }
45
45
 
46
+ /**
47
+ * The surface a `present` call paints into. It owns the vertical rhythm
48
+ * between top-level blocks, which the host's message container does not
49
+ * provide, and stays out of the way while it has nothing to show.
50
+ */
46
51
  private readonly render = ({
47
52
  args,
48
53
  status,
49
54
  }: {
50
55
  args: unknown;
51
56
  status: { type: string };
52
- }): ReactNode =>
53
- renderGenerativeUI(args, this.library, {
54
- status: uiStatus(status),
55
- ...(this.actions ? { dispatch: this.actions.dispatch } : {}),
56
- });
57
+ }): ReactNode => (
58
+ <div data-aui="root">
59
+ {renderGenerativeUI(args, this.library, {
60
+ status: uiStatus(status),
61
+ ...(this.actions ? { dispatch: this.actions.dispatch } : {}),
62
+ })}
63
+ </div>
64
+ );
57
65
 
58
66
  present(options?: PresentToolOptions): PresentTool {
59
67
  return {
@@ -43,7 +43,26 @@ describe("JSONGenerativeUI — client build", () => {
43
43
 
44
44
  it("present renders the model's tree against the library", () => {
45
45
  const html = renderTool(ui.present(), { $type: "Card", title: "Hi" });
46
- expect(html).toBe('<section data-title="Hi"></section>');
46
+ expect(html).toContain('<section data-title="Hi"></section>');
47
+ });
48
+
49
+ it("wraps the tree so top-level siblings are spaced by the surface", () => {
50
+ const html = renderTool(ui.present(), [
51
+ { $type: "Card", title: "One" },
52
+ { $type: "Card", title: "Two" },
53
+ ]);
54
+ expect(html.match(/data-aui="root"/g)).toHaveLength(1);
55
+ expect(html.indexOf('data-title="One"')).toBeLessThan(
56
+ html.indexOf('data-title="Two"'),
57
+ );
58
+ });
59
+
60
+ it("leaves the surface childless until the stream produces a node", () => {
61
+ // `:empty` is what hides it, so the contract is that it has no children.
62
+ expect(renderTool(ui.present(), undefined)).toBe(
63
+ '<div data-aui="root"></div>',
64
+ );
65
+ expect(renderTool(ui.present(), [])).toBe('<div data-aui="root"></div>');
47
66
  });
48
67
 
49
68
  it("prompt_user is a human tool that renders the tree (no execute)", () => {
@@ -52,7 +71,7 @@ describe("JSONGenerativeUI — client build", () => {
52
71
  expect(tool.unstable_backendDefault).toEqual({ parameters: true });
53
72
  expect((tool as any).execute).toBeUndefined();
54
73
  const html = renderTool(tool, { $type: "Button", label: "ok" });
55
- expect(html).toBe("<button>ok</button>");
74
+ expect(html).toContain("<button>ok</button>");
56
75
  });
57
76
  });
58
77
 
@@ -11,9 +11,17 @@ import type {
11
11
  SlackTextObject,
12
12
  } from "./types";
13
13
 
14
- /** The maximum normalized traversal depth. */
14
+ /** The maximum traversal depth, counted in visited values, not tree levels. */
15
15
  export const MAX_TRAVERSAL_DEPTH = 64;
16
16
 
17
+ /**
18
+ * The element-nesting ceiling a caller observes. `boundNode` visits an element
19
+ * and its `children` array separately, so one level of nesting spends two of
20
+ * {@link MAX_TRAVERSAL_DEPTH}; it runs before the conversion walk and is never
21
+ * the looser of the two. A root array costs one more level than a root object.
22
+ */
23
+ export const MAX_ELEMENT_DEPTH = MAX_TRAVERSAL_DEPTH / 2;
24
+
17
25
  /**
18
26
  * The maximum number of entries kept at any single level (root, or a
19
27
  * `children` array at any depth) of the raw spec, before it ever reaches
@@ -112,7 +120,7 @@ export const ALERT_TEXT_CAP = 200;
112
120
  * The maximum number of data rows in a data-table block; the emitted `rows`
113
121
  * array additionally carries one header row.
114
122
  */
115
- export const DATA_TABLE_ROW_CAP = 100;
123
+ export const DATA_TABLE_ROW_CAP = 200;
116
124
 
117
125
  /** The maximum number of columns in a data-table block. */
118
126
  export const DATA_TABLE_COLUMN_CAP = 20;
@@ -121,7 +129,7 @@ export const DATA_TABLE_COLUMN_CAP = 20;
121
129
  * The cumulative character limit (summed across every cell's text) shared by
122
130
  * every data-table block in one payload.
123
131
  */
124
- export const DATA_TABLE_CHAR_BUDGET = 10000;
132
+ export const DATA_TABLE_CHAR_BUDGET = 20000;
125
133
 
126
134
  /** The caption emitted on every native data-table block. */
127
135
  export const TABLE_CAPTION = "Table";
@@ -328,6 +328,21 @@ describe("toSlackBlocks", () => {
328
328
  });
329
329
 
330
330
  describe("Select", () => {
331
+ it("reports options dropped for want of a string label and value", () => {
332
+ const { blocks, warnings } = toSlackBlocks({
333
+ $type: "Select",
334
+ options: [{ label: "ok", value: "a" }, { label: "bad" }, "nope"],
335
+ });
336
+ const select = (blocks[0] as SlackActionsBlock)
337
+ .elements[0] as SlackStaticSelectElement;
338
+ expect(select.options).toHaveLength(1);
339
+ expect(warnings).toContainEqual({
340
+ code: "dropped",
341
+ component: "Select",
342
+ detail: "2 options were dropped for want of a string label and value.",
343
+ });
344
+ });
345
+
331
346
  it("converts options and placeholder", () => {
332
347
  const { blocks } = toSlackBlocks({
333
348
  $type: "Select",
@@ -579,6 +594,18 @@ describe("toSlackBlocks", () => {
579
594
  });
580
595
 
581
596
  describe("RadioGroup", () => {
597
+ it("names RadioGroup when the same option loss happens there", () => {
598
+ const { warnings } = toSlackBlocks({
599
+ $type: "RadioGroup",
600
+ options: [{ label: "ok", value: "a" }, { label: "bad" }],
601
+ });
602
+ expect(warnings).toContainEqual({
603
+ code: "dropped",
604
+ component: "RadioGroup",
605
+ detail: "1 option was dropped for want of a string label and value.",
606
+ });
607
+ });
608
+
582
609
  const options = [
583
610
  { label: "Small", value: "s" },
584
611
  { label: "Large", value: "l" },
@@ -1118,7 +1145,7 @@ describe("toSlackBlocks", () => {
1118
1145
  expect(warnings).toContainEqual({
1119
1146
  code: "dropped",
1120
1147
  component: "Carousel",
1121
- detail: "A non-card child was dropped.",
1148
+ detail: "1 non-card child was dropped.",
1122
1149
  });
1123
1150
  });
1124
1151
 
@@ -1148,10 +1175,215 @@ describe("toSlackBlocks", () => {
1148
1175
  ],
1149
1176
  },
1150
1177
  ]);
1178
+ expect(warnings).toContainEqual({
1179
+ code: "fallback",
1180
+ component: "Card",
1181
+ detail: "A card inside a carousel was reshaped to title and body.",
1182
+ });
1183
+ expect(warnings.some((warning) => warning.code === "dropped")).toBe(
1184
+ false,
1185
+ );
1186
+ });
1187
+
1188
+ it("does not report a Caption as lost, since its text reaches the body", () => {
1189
+ const { blocks, warnings } = toSlackBlocks({
1190
+ $type: "Carousel",
1191
+ children: [
1192
+ {
1193
+ $type: "Card",
1194
+ title: "Plan",
1195
+ children: [
1196
+ { $type: "Text", value: "body" },
1197
+ { $type: "Caption", value: "fine print" },
1198
+ { $type: "Divider" },
1199
+ ],
1200
+ },
1201
+ ],
1202
+ });
1203
+ const card = (blocks[0] as SlackCarouselBlock).elements[0];
1204
+ expect(card?.body).toEqual({ type: "mrkdwn", text: "body\nfine print" });
1205
+ expect(warnings.some((warning) => warning.code === "dropped")).toBe(
1206
+ false,
1207
+ );
1208
+ });
1209
+
1210
+ it("reports the images and controls a reshaped card really loses", () => {
1211
+ const { blocks, warnings } = toSlackBlocks({
1212
+ $type: "Carousel",
1213
+ children: [
1214
+ {
1215
+ $type: "Card",
1216
+ title: "Plan",
1217
+ confirm: { label: "Buy", $action: { type: "buy" } },
1218
+ children: [
1219
+ { $type: "Image", src: "hero.png", alt: "hero" },
1220
+ { $type: "Text", value: "body" },
1221
+ { $type: "Caption", value: "fine print" },
1222
+ { $type: "Divider" },
1223
+ ],
1224
+ },
1225
+ ],
1226
+ });
1227
+ const card = (blocks[0] as SlackCarouselBlock).elements[0];
1228
+ expect(card?.hero_image).toBeUndefined();
1229
+ expect(card?.actions).toBeUndefined();
1230
+ expect(card?.body).toEqual({ type: "mrkdwn", text: "body\nfine print" });
1231
+ expect(warnings).toContainEqual({
1232
+ code: "fallback",
1233
+ component: "Card",
1234
+ detail: "A card inside a carousel was reshaped to title and body.",
1235
+ });
1236
+ expect(warnings).toContainEqual({
1237
+ code: "dropped",
1238
+ component: "Card",
1239
+ detail: "A reshaped carousel card's images and controls were dropped.",
1240
+ });
1241
+ });
1242
+
1243
+ it.each([
1244
+ ["tables", { $type: "Table", columns: [{ label: "A" }], rows: [["1"]] }],
1245
+ ["charts", { $type: "Chart", series: [] }],
1246
+ [
1247
+ "images",
1248
+ { $type: "ListViewItem", children: [{ $type: "Image", src: "x.png" }] },
1249
+ ],
1250
+ [
1251
+ "controls",
1252
+ {
1253
+ $type: "ListViewItem",
1254
+ children: [{ $type: "Button", label: "Go", $action: { type: "go" } }],
1255
+ },
1256
+ ],
1257
+ ["controls", { $type: "Select", options: [{ label: "A", value: "a" }] }],
1258
+ ["controls", { $type: "Input", name: "email", label: "Email" }],
1259
+ [
1260
+ "controls",
1261
+ { $type: "Form", children: [{ $type: "Text", value: "inner" }] },
1262
+ ],
1263
+ [
1264
+ "controls",
1265
+ {
1266
+ $type: "Card",
1267
+ title: "nested",
1268
+ confirm: { label: "Buy", $action: { type: "buy" } },
1269
+ },
1270
+ ],
1271
+ ])(
1272
+ "reports %s that a reshape cannot carry, however deep",
1273
+ (kind, child) => {
1274
+ const { warnings } = toSlackBlocks({
1275
+ $type: "Carousel",
1276
+ children: [
1277
+ {
1278
+ $type: "Card",
1279
+ title: "Plan",
1280
+ children: [
1281
+ { $type: "Text", value: "b" },
1282
+ child,
1283
+ { $type: "Divider" },
1284
+ ],
1285
+ },
1286
+ ],
1287
+ });
1288
+ expect(warnings).toContainEqual({
1289
+ code: "dropped",
1290
+ component: "Card",
1291
+ detail: `A reshaped carousel card's ${kind} were dropped.`,
1292
+ });
1293
+ },
1294
+ );
1295
+
1296
+ it("scans below a child whose text the reshape consumes", () => {
1297
+ const { warnings } = toSlackBlocks({
1298
+ $type: "Carousel",
1299
+ children: [
1300
+ {
1301
+ $type: "Card",
1302
+ title: "Plan",
1303
+ children: [
1304
+ {
1305
+ $type: "Text",
1306
+ value: "body",
1307
+ children: [{ $type: "Image", src: "x.png" }],
1308
+ },
1309
+ { $type: "Divider" },
1310
+ ],
1311
+ },
1312
+ ],
1313
+ });
1314
+ expect(warnings).toContainEqual({
1315
+ code: "dropped",
1316
+ component: "Card",
1317
+ detail: "A reshaped carousel card's images were dropped.",
1318
+ });
1319
+ });
1320
+
1321
+ it("does not report a ListViewItem whose action renders no accessory", () => {
1322
+ const { warnings } = toSlackBlocks({
1323
+ $type: "Carousel",
1324
+ children: [
1325
+ {
1326
+ $type: "Card",
1327
+ title: "Plan",
1328
+ children: [
1329
+ { $type: "Text", value: "body" },
1330
+ { $type: "ListViewItem", title: "row", $action: "open" },
1331
+ { $type: "Divider" },
1332
+ ],
1333
+ },
1334
+ ],
1335
+ });
1336
+ expect(warnings.some((warning) => warning.code === "dropped")).toBe(
1337
+ false,
1338
+ );
1339
+ });
1340
+
1341
+ it("does not report an action on a node that renders no control", () => {
1342
+ const { warnings } = toSlackBlocks({
1343
+ $type: "Carousel",
1344
+ children: [
1345
+ {
1346
+ $type: "Card",
1347
+ title: "Plan",
1348
+ children: [
1349
+ {
1350
+ $type: "Box",
1351
+ $action: { type: "open" },
1352
+ children: [{ $type: "Text", value: "body" }],
1353
+ },
1354
+ { $type: "Divider" },
1355
+ ],
1356
+ },
1357
+ ],
1358
+ });
1359
+ expect(warnings.some((warning) => warning.code === "dropped")).toBe(
1360
+ false,
1361
+ );
1362
+ });
1363
+
1364
+ it("clamps a reshaped card's title and body instead of slicing them silently", () => {
1365
+ const { warnings } = toSlackBlocks({
1366
+ $type: "Carousel",
1367
+ children: [
1368
+ {
1369
+ $type: "Card",
1370
+ title: "t".repeat(CARD_TITLE_CAP + 10),
1371
+ children: [
1372
+ { $type: "Text", value: "b".repeat(CARD_BODY_CAP + 10) },
1373
+ { $type: "Fact", label: "L", value: "V" },
1374
+ ],
1375
+ },
1376
+ ],
1377
+ });
1151
1378
  expect(warnings).toContainEqual({
1152
1379
  code: "clamped",
1153
1380
  component: "Card",
1154
- detail: "A card inside a carousel was degraded to title and body.",
1381
+ detail: `title was clamped to ${CARD_TITLE_CAP} characters.`,
1382
+ });
1383
+ expect(warnings).toContainEqual({
1384
+ code: "clamped",
1385
+ component: "Card",
1386
+ detail: `body was clamped to ${CARD_BODY_CAP} characters.`,
1155
1387
  });
1156
1388
  });
1157
1389
 
@@ -1360,6 +1592,27 @@ describe("toSlackBlocks", () => {
1360
1592
  });
1361
1593
 
1362
1594
  describe("Table", () => {
1595
+ it.each([["A"], [{}], [{ label: 42 }]])(
1596
+ "keeps an unlabeled column's position in the header and reports it: %j",
1597
+ (column) => {
1598
+ const { blocks, warnings } = toSlackBlocks({
1599
+ $type: "Table",
1600
+ columns: [column, { label: "B" }],
1601
+ rows: [["1", "2"]],
1602
+ });
1603
+ const table = blocks[0] as SlackDataTableBlock;
1604
+ expect(table.rows[0]).toEqual([
1605
+ { type: "raw_text", text: "" },
1606
+ { type: "raw_text", text: "B" },
1607
+ ]);
1608
+ expect(warnings).toContainEqual({
1609
+ code: "dropped",
1610
+ component: "Table",
1611
+ detail: "1 column header was left blank for want of a string label.",
1612
+ });
1613
+ },
1614
+ );
1615
+
1363
1616
  it("converts columns and rows into a header-first rows array with typed cells", () => {
1364
1617
  const { blocks } = toSlackBlocks({
1365
1618
  $type: "Table",
@@ -1433,7 +1686,7 @@ describe("toSlackBlocks", () => {
1433
1686
  });
1434
1687
 
1435
1688
  it(`clamps rows to fit the ${DATA_TABLE_CHAR_BUDGET}-character table budget, always keeping the header row`, () => {
1436
- const bigCell = "z".repeat(2000);
1689
+ const bigCell = "z".repeat(Math.ceil(DATA_TABLE_CHAR_BUDGET / 5));
1437
1690
  const rows = Array.from({ length: 6 }, (_, r) => [`row${r}`, bigCell]);
1438
1691
  const { blocks, warnings } = toSlackBlocks({
1439
1692
  $type: "Table",
@@ -1454,10 +1707,11 @@ describe("toSlackBlocks", () => {
1454
1707
  });
1455
1708
 
1456
1709
  it("shares the character budget across multiple data-table blocks in one payload", () => {
1457
- const filler = "f".repeat(9994);
1710
+ const secondValue = "second";
1711
+ const filler = "f".repeat(DATA_TABLE_CHAR_BUDGET - secondValue.length);
1458
1712
  const { blocks, warnings } = toSlackBlocks([
1459
1713
  { $type: "Table", columns: [{ label: "A" }], rows: [[filler]] },
1460
- { $type: "Table", columns: [{ label: "B" }], rows: [["second"]] },
1714
+ { $type: "Table", columns: [{ label: "B" }], rows: [[secondValue]] },
1461
1715
  ]);
1462
1716
  const first = blocks[0] as SlackDataTableBlock;
1463
1717
  const second = blocks[1] as SlackDataTableBlock;
@@ -1510,6 +1764,41 @@ describe("toSlackBlocks", () => {
1510
1764
  const table = blocks[0] as SlackDataTableBlock;
1511
1765
  expect(table.rows).toHaveLength(2);
1512
1766
  });
1767
+
1768
+ it("emits Slack's documented ceiling of 200 data rows unclamped", () => {
1769
+ const { blocks, warnings } = toSlackBlocks({
1770
+ $type: "Table",
1771
+ columns: [{ label: "n" }],
1772
+ rows: Array.from({ length: 200 }, (_, r) => [String(r)]),
1773
+ });
1774
+ const table = blocks[0] as SlackDataTableBlock;
1775
+ expect(table.rows).toHaveLength(201);
1776
+ expect(warnings).toEqual([]);
1777
+ });
1778
+
1779
+ it("emits Slack's documented ceiling of 20,000 characters and clamps at 20,001", () => {
1780
+ const atBudget = toSlackBlocks({
1781
+ $type: "Table",
1782
+ columns: [{ label: "h" }],
1783
+ rows: [["c".repeat(19999)]],
1784
+ });
1785
+ expect((atBudget.blocks[0] as SlackDataTableBlock).rows).toHaveLength(2);
1786
+ expect(atBudget.warnings).toEqual([]);
1787
+
1788
+ const overBudget = toSlackBlocks({
1789
+ $type: "Table",
1790
+ columns: [{ label: "h" }],
1791
+ rows: [["c".repeat(20000)]],
1792
+ });
1793
+ expect((overBudget.blocks[0] as SlackDataTableBlock).rows).toHaveLength(
1794
+ 1,
1795
+ );
1796
+ expect(overBudget.warnings).toContainEqual({
1797
+ code: "clamped",
1798
+ component: "Table",
1799
+ detail: "rows were clamped to fit the 20000-character table budget.",
1800
+ });
1801
+ });
1513
1802
  });
1514
1803
 
1515
1804
  describe("Markdown", () => {
@@ -1575,6 +1864,65 @@ describe("toSlackBlocks", () => {
1575
1864
  });
1576
1865
 
1577
1866
  describe("ListView and ListViewItem", () => {
1867
+ it("forwards a discarded child's own dropped warning and counts it", () => {
1868
+ const { warnings } = toSlackBlocks({
1869
+ $type: "ListView",
1870
+ children: [
1871
+ { $type: "Mystery" },
1872
+ { $type: "ListViewItem", children: { $type: "Text", value: "row" } },
1873
+ ],
1874
+ });
1875
+ expect(warnings).toContainEqual({
1876
+ code: "dropped",
1877
+ component: "Mystery",
1878
+ detail: "Unknown component type was dropped.",
1879
+ });
1880
+ expect(warnings).toContainEqual({
1881
+ code: "dropped",
1882
+ component: "ListView",
1883
+ detail: "1 non-item child was dropped.",
1884
+ });
1885
+ });
1886
+
1887
+ it("does not let a discarded child spend the markdown budget", () => {
1888
+ const { blocks, warnings } = toSlackBlocks([
1889
+ {
1890
+ $type: "ListView",
1891
+ children: [
1892
+ { $type: "Markdown", value: "x".repeat(MARKDOWN_TEXT_BUDGET) },
1893
+ {
1894
+ $type: "ListViewItem",
1895
+ children: { $type: "Text", value: "row" },
1896
+ },
1897
+ ],
1898
+ },
1899
+ { $type: "Markdown", value: "**real**" },
1900
+ ]);
1901
+ expect(blocks[blocks.length - 1]).toEqual({
1902
+ type: "markdown",
1903
+ text: "**real**",
1904
+ });
1905
+ expect(warnings.some((warning) => warning.component === "Markdown")).toBe(
1906
+ false,
1907
+ );
1908
+ });
1909
+
1910
+ it("reports a non-item child it filters out", () => {
1911
+ const { blocks, warnings } = toSlackBlocks({
1912
+ $type: "ListView",
1913
+ children: [
1914
+ { $type: "Text", value: "stray" },
1915
+ { $type: "ListViewItem", children: { $type: "Text", value: "Kept" } },
1916
+ ],
1917
+ });
1918
+ expect(blocks).toHaveLength(1);
1919
+ expect(warnings).toContainEqual({
1920
+ code: "dropped",
1921
+ component: "ListView",
1922
+ detail: "1 non-item child was dropped.",
1923
+ });
1924
+ });
1925
+
1578
1926
  it("renders each item as a section, separated by dividers", () => {
1579
1927
  const { blocks } = toSlackBlocks({
1580
1928
  $type: "ListView",
@@ -1887,8 +2235,26 @@ describe("toSlackBlocks", () => {
1887
2235
  expect(warnings).toContainEqual({
1888
2236
  code: "clamped",
1889
2237
  component: "Root",
1890
- detail: "nodes deeper than 64 levels were dropped.",
1891
- });
2238
+ detail: "nodes deeper than 32 levels were dropped.",
2239
+ });
2240
+ });
2241
+
2242
+ it("reports the depth detail at the level it actually starts dropping", () => {
2243
+ const chain = (levels: number) => {
2244
+ let node: unknown = { $type: "Caption", value: "x" };
2245
+ for (let i = 0; i < levels; i++) {
2246
+ node = { $type: "Card", children: [node] };
2247
+ }
2248
+ return node;
2249
+ };
2250
+ const dropped = (node: unknown) =>
2251
+ toSlackBlocks(node).warnings.some((warning) =>
2252
+ warning.detail.includes("deeper than"),
2253
+ );
2254
+ expect(dropped(chain(32))).toBe(false);
2255
+ expect(dropped(chain(33))).toBe(true);
2256
+ expect(dropped([chain(31)])).toBe(false);
2257
+ expect(dropped([chain(32)])).toBe(true);
1892
2258
  });
1893
2259
  });
1894
2260
  });
@@ -1922,7 +2288,7 @@ describe("toSlackBlocks data_table integrity", () => {
1922
2288
  it("drops a table whose header row alone exceeds the character budget", () => {
1923
2289
  const { blocks, warnings } = toSlackBlocks({
1924
2290
  $type: "Table",
1925
- columns: [{ label: "h".repeat(10001) }],
2291
+ columns: [{ label: "h".repeat(DATA_TABLE_CHAR_BUDGET + 1) }],
1926
2292
  rows: [["x"]],
1927
2293
  });
1928
2294
  expect(blocks).toEqual([]);