@fern-api/fdr-sdk 1.2.39-cc5ce5c5b6 → 1.2.39-dc3805e7f9

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 (94) hide show
  1. package/dist/js/client/FdrClient.js +1 -0
  2. package/dist/js/client/FdrClient.js.map +1 -1
  3. package/dist/js/client/FdrClient.mjs +1 -0
  4. package/dist/js/client/FdrClient.mjs.map +1 -1
  5. package/dist/js/converters/index.js +1 -6
  6. package/dist/js/converters/index.js.map +1 -1
  7. package/dist/js/converters/index.mjs +1 -6
  8. package/dist/js/converters/index.mjs.map +1 -1
  9. package/dist/js/index.js +2 -6
  10. package/dist/js/index.js.map +1 -1
  11. package/dist/js/index.mjs +2 -6
  12. package/dist/js/index.mjs.map +1 -1
  13. package/dist/js/navigation/index.js +1 -6
  14. package/dist/js/navigation/index.js.map +1 -1
  15. package/dist/js/navigation/index.mjs +1 -6
  16. package/dist/js/navigation/index.mjs.map +1 -1
  17. package/dist/js/navigation/ledger-root-builder.js +228 -58
  18. package/dist/js/navigation/ledger-root-builder.js.map +1 -1
  19. package/dist/js/navigation/ledger-root-builder.mjs +228 -58
  20. package/dist/js/navigation/ledger-root-builder.mjs.map +1 -1
  21. package/dist/js/orpc-client.js +1 -0
  22. package/dist/js/orpc-client.js.map +1 -1
  23. package/dist/js/orpc-client.mjs +1 -0
  24. package/dist/js/orpc-client.mjs.map +1 -1
  25. package/dist/navigation/__test__/ledger-root-builder.apiReferenceChildren.test.d.ts +2 -0
  26. package/dist/navigation/__test__/ledger-root-builder.apiReferenceChildren.test.d.ts.map +1 -0
  27. package/dist/navigation/__test__/ledger-root-builder.apiReferenceChildren.test.js +85 -0
  28. package/dist/navigation/__test__/ledger-root-builder.apiReferenceChildren.test.js.map +1 -0
  29. package/dist/navigation/__test__/ledger-root-builder.collapsedDeterminism.test.d.ts +2 -0
  30. package/dist/navigation/__test__/ledger-root-builder.collapsedDeterminism.test.d.ts.map +1 -0
  31. package/dist/navigation/__test__/ledger-root-builder.collapsedDeterminism.test.js +85 -0
  32. package/dist/navigation/__test__/ledger-root-builder.collapsedDeterminism.test.js.map +1 -0
  33. package/dist/navigation/__test__/ledger-root-builder.landingPage.test.d.ts +2 -0
  34. package/dist/navigation/__test__/ledger-root-builder.landingPage.test.d.ts.map +1 -0
  35. package/dist/navigation/__test__/ledger-root-builder.landingPage.test.js +82 -0
  36. package/dist/navigation/__test__/ledger-root-builder.landingPage.test.js.map +1 -0
  37. package/dist/navigation/__test__/ledger-root-builder.metadataPreservation.test.d.ts +2 -0
  38. package/dist/navigation/__test__/ledger-root-builder.metadataPreservation.test.d.ts.map +1 -0
  39. package/dist/navigation/__test__/ledger-root-builder.metadataPreservation.test.js +1031 -0
  40. package/dist/navigation/__test__/ledger-root-builder.metadataPreservation.test.js.map +1 -0
  41. package/dist/navigation/__test__/ledger-root-builder.rootChildOrder.test.js +22 -9
  42. package/dist/navigation/__test__/ledger-root-builder.rootChildOrder.test.js.map +1 -1
  43. package/dist/navigation/__test__/ledger-root-builder.rootSlug.test.d.ts +2 -0
  44. package/dist/navigation/__test__/ledger-root-builder.rootSlug.test.d.ts.map +1 -0
  45. package/dist/navigation/__test__/ledger-root-builder.rootSlug.test.js +47 -0
  46. package/dist/navigation/__test__/ledger-root-builder.rootSlug.test.js.map +1 -0
  47. package/dist/navigation/__test__/ledger-root-builder.skipSlugTabs.test.d.ts +2 -0
  48. package/dist/navigation/__test__/ledger-root-builder.skipSlugTabs.test.d.ts.map +1 -0
  49. package/dist/navigation/__test__/ledger-root-builder.skipSlugTabs.test.js +284 -0
  50. package/dist/navigation/__test__/ledger-root-builder.skipSlugTabs.test.js.map +1 -0
  51. package/dist/navigation/__test__/ledger-root-builder.variantSharedSegments.test.d.ts +2 -0
  52. package/dist/navigation/__test__/ledger-root-builder.variantSharedSegments.test.d.ts.map +1 -0
  53. package/dist/navigation/__test__/ledger-root-builder.variantSharedSegments.test.js +262 -0
  54. package/dist/navigation/__test__/ledger-root-builder.variantSharedSegments.test.js.map +1 -0
  55. package/dist/navigation/ledger-root-builder.d.ts +12 -0
  56. package/dist/navigation/ledger-root-builder.d.ts.map +1 -1
  57. package/dist/navigation/ledger-root-builder.js +298 -69
  58. package/dist/navigation/ledger-root-builder.js.map +1 -1
  59. package/dist/navigation/utils/findNode.d.ts.map +1 -1
  60. package/dist/navigation/utils/findNode.js +0 -8
  61. package/dist/navigation/utils/findNode.js.map +1 -1
  62. package/dist/navigation/utils/followRedirect.d.ts.map +1 -1
  63. package/dist/navigation/utils/followRedirect.js +2 -0
  64. package/dist/navigation/utils/followRedirect.js.map +1 -1
  65. package/dist/orpc-client/docs-ledger/contract.d.ts +50 -0
  66. package/dist/orpc-client/docs-ledger/contract.d.ts.map +1 -1
  67. package/dist/orpc-client/docs-ledger/contract.js +1 -0
  68. package/dist/orpc-client/docs-ledger/contract.js.map +1 -1
  69. package/dist/orpc-client/docs-ledger/ledger-manifest.d.ts +12 -0
  70. package/dist/orpc-client/docs-ledger/ledger-manifest.d.ts.map +1 -1
  71. package/dist/tsconfig.tsbuildinfo +1 -1
  72. package/dist/types/navigation/__test__/ledger-root-builder.apiReferenceChildren.test.d.ts +2 -0
  73. package/dist/types/navigation/__test__/ledger-root-builder.apiReferenceChildren.test.d.ts.map +1 -0
  74. package/dist/types/navigation/__test__/ledger-root-builder.collapsedDeterminism.test.d.ts +2 -0
  75. package/dist/types/navigation/__test__/ledger-root-builder.collapsedDeterminism.test.d.ts.map +1 -0
  76. package/dist/types/navigation/__test__/ledger-root-builder.landingPage.test.d.ts +2 -0
  77. package/dist/types/navigation/__test__/ledger-root-builder.landingPage.test.d.ts.map +1 -0
  78. package/dist/types/navigation/__test__/ledger-root-builder.metadataPreservation.test.d.ts +2 -0
  79. package/dist/types/navigation/__test__/ledger-root-builder.metadataPreservation.test.d.ts.map +1 -0
  80. package/dist/types/navigation/__test__/ledger-root-builder.rootSlug.test.d.ts +2 -0
  81. package/dist/types/navigation/__test__/ledger-root-builder.rootSlug.test.d.ts.map +1 -0
  82. package/dist/types/navigation/__test__/ledger-root-builder.skipSlugTabs.test.d.ts +2 -0
  83. package/dist/types/navigation/__test__/ledger-root-builder.skipSlugTabs.test.d.ts.map +1 -0
  84. package/dist/types/navigation/__test__/ledger-root-builder.variantSharedSegments.test.d.ts +2 -0
  85. package/dist/types/navigation/__test__/ledger-root-builder.variantSharedSegments.test.d.ts.map +1 -0
  86. package/dist/types/navigation/ledger-root-builder.d.ts +12 -0
  87. package/dist/types/navigation/ledger-root-builder.d.ts.map +1 -1
  88. package/dist/types/navigation/utils/findNode.d.ts.map +1 -1
  89. package/dist/types/navigation/utils/followRedirect.d.ts.map +1 -1
  90. package/dist/types/orpc-client/docs-ledger/contract.d.ts +50 -0
  91. package/dist/types/orpc-client/docs-ledger/contract.d.ts.map +1 -1
  92. package/dist/types/orpc-client/docs-ledger/ledger-manifest.d.ts +12 -0
  93. package/dist/types/orpc-client/docs-ledger/ledger-manifest.d.ts.map +1 -1
  94. package/package.json +1 -1
@@ -0,0 +1,1031 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { buildFullRootFromSegments } from "../ledger-root-builder.js";
3
+ /**
4
+ * Verify that metadata fields on ledger nodes are correctly read from segment
5
+ * and route metadata rather than hardcoded to undefined. Covers: availability,
6
+ * noindex, target, image, authed, orphaned, featureFlags, and changelog tags.
7
+ */
8
+ function segment(opts) {
9
+ return {
10
+ segmentId: opts.segmentHash,
11
+ segmentHash: opts.segmentHash,
12
+ section: opts.section,
13
+ locale: "en",
14
+ sortOrder: opts.sortOrder,
15
+ hidden: false,
16
+ metadata: opts.metadata,
17
+ product: opts.product ?? null,
18
+ version: opts.version ?? null,
19
+ variant: opts.variant ?? null,
20
+ tab: opts.tab ?? null
21
+ };
22
+ }
23
+ function restRoute(opts) {
24
+ return {
25
+ type: "rest",
26
+ fullPath: opts.fullPath,
27
+ hidden: false,
28
+ metadata: {
29
+ title: opts.title,
30
+ endpointId: opts.endpointId,
31
+ method: opts.method,
32
+ apiDefinitionId: "api-def-1",
33
+ ...(opts.availability != null ? { availability: opts.availability } : {}),
34
+ ...(opts.authed != null ? { authed: opts.authed } : {})
35
+ }
36
+ };
37
+ }
38
+ function webhookRoute(opts) {
39
+ return {
40
+ type: "webhook",
41
+ fullPath: opts.fullPath,
42
+ hidden: false,
43
+ metadata: {
44
+ title: opts.title,
45
+ webhookId: opts.webhookId,
46
+ method: opts.method,
47
+ apiDefinitionId: "api-def-1",
48
+ ...(opts.availability != null ? { availability: opts.availability } : {})
49
+ }
50
+ };
51
+ }
52
+ function linkRoute(opts) {
53
+ return {
54
+ type: "link",
55
+ fullPath: null,
56
+ hidden: false,
57
+ metadata: {
58
+ title: opts.title,
59
+ url: opts.url,
60
+ ...(opts.target != null ? { target: opts.target } : {})
61
+ }
62
+ };
63
+ }
64
+ function pageRoute(opts) {
65
+ return {
66
+ type: "markdown",
67
+ fullPath: opts.fullPath,
68
+ hidden: false,
69
+ metadata: {
70
+ title: opts.title,
71
+ pageId: `${opts.fullPath}.mdx`,
72
+ ...(opts.authed != null ? { authed: opts.authed } : {}),
73
+ ...(opts.orphaned != null ? { orphaned: opts.orphaned } : {}),
74
+ ...(opts.featureFlags != null ? { featureFlags: opts.featureFlags } : {}),
75
+ ...(opts.noindex != null ? { noindex: opts.noindex } : {}),
76
+ ...(opts.tags != null ? { tags: opts.tags } : {}),
77
+ ...(opts.date != null ? { date: opts.date } : {})
78
+ }
79
+ };
80
+ }
81
+ function changelogRoute(opts) {
82
+ return {
83
+ type: "changelog",
84
+ fullPath: opts.fullPath,
85
+ hidden: false,
86
+ metadata: {
87
+ title: opts.title,
88
+ pageId: `${opts.fullPath}.mdx`,
89
+ date: opts.date,
90
+ ...(opts.tags != null ? { tags: opts.tags } : {})
91
+ }
92
+ };
93
+ }
94
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
95
+ function collectByType(node, targetType) {
96
+ const results = [];
97
+ if (node == null || typeof node !== "object") {
98
+ return results;
99
+ }
100
+ if (node.type === targetType) {
101
+ results.push(node);
102
+ }
103
+ for (const child of node.children ?? []) {
104
+ results.push(...collectByType(child, targetType));
105
+ }
106
+ if (node.child != null) {
107
+ results.push(...collectByType(node.child, targetType));
108
+ }
109
+ return results;
110
+ }
111
+ describe("metadata preservation on API leaf nodes", () => {
112
+ it("reads availability from endpoint metadata", () => {
113
+ const root = buildFullRootFromSegments({
114
+ basePath: "",
115
+ title: "Test",
116
+ segments: [
117
+ segment({
118
+ segmentHash: "seg-api",
119
+ section: "api",
120
+ sortOrder: 0,
121
+ metadata: { type: "apiReference", title: "API", apiDefinitionId: "api-def-1" }
122
+ })
123
+ ],
124
+ navBySegment: new Map([
125
+ [
126
+ "seg-api",
127
+ [
128
+ restRoute({
129
+ fullPath: "api/create",
130
+ title: "Create",
131
+ endpointId: "create",
132
+ method: "POST",
133
+ availability: "Beta"
134
+ })
135
+ ]
136
+ ]
137
+ ])
138
+ });
139
+ const endpoints = collectByType(root, "endpoint");
140
+ expect(endpoints).toHaveLength(1);
141
+ expect(endpoints[0].availability).toBe("Beta");
142
+ });
143
+ it("reads availability from webhook metadata", () => {
144
+ const root = buildFullRootFromSegments({
145
+ basePath: "",
146
+ title: "Test",
147
+ segments: [
148
+ segment({
149
+ segmentHash: "seg-api",
150
+ section: "api",
151
+ sortOrder: 0,
152
+ metadata: { type: "apiReference", title: "API", apiDefinitionId: "api-def-1" }
153
+ })
154
+ ],
155
+ navBySegment: new Map([
156
+ [
157
+ "seg-api",
158
+ [
159
+ webhookRoute({
160
+ fullPath: "api/on-created",
161
+ title: "On Created",
162
+ webhookId: "on-created",
163
+ method: "POST",
164
+ availability: "Deprecated"
165
+ })
166
+ ]
167
+ ]
168
+ ])
169
+ });
170
+ const webhooks = collectByType(root, "webhook");
171
+ expect(webhooks).toHaveLength(1);
172
+ expect(webhooks[0].availability).toBe("Deprecated");
173
+ });
174
+ it("reads authed from endpoint metadata via withMetadataDefaults", () => {
175
+ const root = buildFullRootFromSegments({
176
+ basePath: "",
177
+ title: "Test",
178
+ segments: [
179
+ segment({
180
+ segmentHash: "seg-api",
181
+ section: "api",
182
+ sortOrder: 0,
183
+ metadata: { type: "apiReference", title: "API", apiDefinitionId: "api-def-1" }
184
+ })
185
+ ],
186
+ navBySegment: new Map([
187
+ [
188
+ "seg-api",
189
+ [
190
+ restRoute({
191
+ fullPath: "api/secure",
192
+ title: "Secure",
193
+ endpointId: "secure",
194
+ method: "GET",
195
+ authed: true
196
+ })
197
+ ]
198
+ ]
199
+ ])
200
+ });
201
+ const endpoints = collectByType(root, "endpoint");
202
+ expect(endpoints).toHaveLength(1);
203
+ expect(endpoints[0].authed).toBe(true);
204
+ });
205
+ });
206
+ describe("metadata preservation on section nodes", () => {
207
+ it("reads availability from section segment metadata", () => {
208
+ const root = buildFullRootFromSegments({
209
+ basePath: "",
210
+ title: "Test",
211
+ segments: [
212
+ segment({
213
+ segmentHash: "seg-section",
214
+ section: "guides",
215
+ sortOrder: 0,
216
+ metadata: { type: "section", title: "Guides", availability: "Beta" }
217
+ })
218
+ ],
219
+ navBySegment: new Map([["seg-section", [pageRoute({ fullPath: "guides/intro", title: "Intro" })]]])
220
+ });
221
+ const sections = collectByType(root, "section");
222
+ expect(sections).toHaveLength(1);
223
+ expect(sections[0].availability).toBe("Beta");
224
+ });
225
+ it("reads noindex from section segment metadata", () => {
226
+ const root = buildFullRootFromSegments({
227
+ basePath: "",
228
+ title: "Test",
229
+ segments: [
230
+ segment({
231
+ segmentHash: "seg-section",
232
+ section: "guides",
233
+ sortOrder: 0,
234
+ metadata: { type: "section", title: "Guides", noindex: true }
235
+ })
236
+ ],
237
+ navBySegment: new Map([["seg-section", [pageRoute({ fullPath: "guides/intro", title: "Intro" })]]])
238
+ });
239
+ const sections = collectByType(root, "section");
240
+ expect(sections).toHaveLength(1);
241
+ expect(sections[0].noindex).toBe(true);
242
+ });
243
+ });
244
+ describe("metadata preservation on apiReference nodes", () => {
245
+ it("reads availability and noindex from apiReference segment metadata", () => {
246
+ const root = buildFullRootFromSegments({
247
+ basePath: "",
248
+ title: "Test",
249
+ segments: [
250
+ segment({
251
+ segmentHash: "seg-api",
252
+ section: "api",
253
+ sortOrder: 0,
254
+ metadata: {
255
+ type: "apiReference",
256
+ title: "API",
257
+ apiDefinitionId: "api-def-1",
258
+ availability: "Generally Available",
259
+ noindex: true
260
+ }
261
+ })
262
+ ],
263
+ navBySegment: new Map([
264
+ [
265
+ "seg-api",
266
+ [
267
+ restRoute({
268
+ fullPath: "api/list",
269
+ title: "List",
270
+ endpointId: "list",
271
+ method: "GET"
272
+ })
273
+ ]
274
+ ]
275
+ ])
276
+ });
277
+ const apis = collectByType(root, "apiReference");
278
+ expect(apis).toHaveLength(1);
279
+ expect(apis[0].availability).toBe("Generally Available");
280
+ expect(apis[0].noindex).toBe(true);
281
+ });
282
+ });
283
+ describe("metadata preservation on link nodes", () => {
284
+ it("reads target from link artifact metadata", () => {
285
+ const root = buildFullRootFromSegments({
286
+ basePath: "",
287
+ title: "Test",
288
+ segments: [
289
+ segment({
290
+ segmentHash: "seg-root",
291
+ section: "",
292
+ sortOrder: 0,
293
+ metadata: { type: "tabRoot" }
294
+ })
295
+ ],
296
+ navBySegment: new Map([
297
+ ["seg-root", [linkRoute({ title: "External", url: "https://example.com", target: "_blank" })]]
298
+ ])
299
+ });
300
+ const links = collectByType(root, "link");
301
+ expect(links).toHaveLength(1);
302
+ expect(links[0].target).toBe("_blank");
303
+ });
304
+ it("reads target from link tab metadata", () => {
305
+ const tab = {
306
+ id: "link-tab",
307
+ displayName: "External",
308
+ slug: "external",
309
+ icon: null,
310
+ sortOrder: 1,
311
+ hidden: false,
312
+ viewers: [],
313
+ orphaned: false,
314
+ featureFlags: [],
315
+ metadata: { tab_type: "link", url: "https://example.com", target: "_blank" }
316
+ };
317
+ const root = buildFullRootFromSegments({
318
+ basePath: "",
319
+ title: "Test",
320
+ segments: [
321
+ segment({
322
+ segmentHash: "seg-root",
323
+ section: "",
324
+ sortOrder: 0,
325
+ metadata: { type: "tabRoot" },
326
+ tab: {
327
+ id: "docs-tab",
328
+ displayName: "Docs",
329
+ slug: "docs",
330
+ icon: null,
331
+ sortOrder: 0,
332
+ hidden: false,
333
+ viewers: [],
334
+ orphaned: false,
335
+ featureFlags: [],
336
+ metadata: {}
337
+ }
338
+ }),
339
+ segment({
340
+ segmentHash: "seg-link",
341
+ section: "",
342
+ sortOrder: 1,
343
+ metadata: { type: "tabRoot" },
344
+ tab
345
+ })
346
+ ],
347
+ navBySegment: new Map([
348
+ ["seg-root", [pageRoute({ fullPath: "docs/intro", title: "Intro" })]],
349
+ ["seg-link", []]
350
+ ])
351
+ });
352
+ const links = collectByType(root, "link");
353
+ expect(links.length).toBeGreaterThanOrEqual(1);
354
+ const externalLink = links.find((l) => l.title === "External");
355
+ expect(externalLink).toBeDefined();
356
+ expect(externalLink.target).toBe("_blank");
357
+ });
358
+ });
359
+ describe("metadata preservation on page nodes", () => {
360
+ it("reads authed, orphaned, and featureFlags from page metadata", () => {
361
+ const root = buildFullRootFromSegments({
362
+ basePath: "",
363
+ title: "Test",
364
+ segments: [
365
+ segment({
366
+ segmentHash: "seg-section",
367
+ section: "guides",
368
+ sortOrder: 0,
369
+ metadata: { type: "section", title: "Guides" }
370
+ })
371
+ ],
372
+ navBySegment: new Map([
373
+ [
374
+ "seg-section",
375
+ [
376
+ pageRoute({
377
+ fullPath: "guides/protected",
378
+ title: "Protected",
379
+ authed: true,
380
+ orphaned: true,
381
+ featureFlags: ["beta-feature"]
382
+ })
383
+ ]
384
+ ]
385
+ ])
386
+ });
387
+ const pages = collectByType(root, "page");
388
+ expect(pages).toHaveLength(1);
389
+ expect(pages[0].authed).toBe(true);
390
+ expect(pages[0].orphaned).toBe(true);
391
+ expect(pages[0].featureFlags).toEqual(["beta-feature"]);
392
+ });
393
+ });
394
+ describe("changelog tags preservation", () => {
395
+ it("reads tags from changelog entry metadata", () => {
396
+ const root = buildFullRootFromSegments({
397
+ basePath: "",
398
+ title: "Test",
399
+ segments: [
400
+ segment({
401
+ segmentHash: "seg-changelog",
402
+ section: "changelog",
403
+ sortOrder: 0,
404
+ metadata: { type: "changelog", title: "Changelog" }
405
+ })
406
+ ],
407
+ navBySegment: new Map([
408
+ [
409
+ "seg-changelog",
410
+ [
411
+ changelogRoute({
412
+ fullPath: "changelog/2026-01-15",
413
+ title: "January Release",
414
+ tags: ["feature", "bugfix"],
415
+ date: "2026-01-15"
416
+ }),
417
+ changelogRoute({
418
+ fullPath: "changelog/2026-02-01",
419
+ title: "February Release",
420
+ tags: ["security"],
421
+ date: "2026-02-01"
422
+ })
423
+ ]
424
+ ]
425
+ ])
426
+ });
427
+ const entries = collectByType(root, "changelogEntry");
428
+ expect(entries).toHaveLength(2);
429
+ const jan = entries.find((e) => e.title === "January Release");
430
+ expect(jan).toBeDefined();
431
+ expect(jan.tags).toEqual(["feature", "bugfix"]);
432
+ const feb = entries.find((e) => e.title === "February Release");
433
+ expect(feb).toBeDefined();
434
+ expect(feb.tags).toEqual(["security"]);
435
+ });
436
+ it("returns undefined tags when metadata has no tags", () => {
437
+ const root = buildFullRootFromSegments({
438
+ basePath: "",
439
+ title: "Test",
440
+ segments: [
441
+ segment({
442
+ segmentHash: "seg-changelog",
443
+ section: "changelog",
444
+ sortOrder: 0,
445
+ metadata: { type: "changelog", title: "Changelog" }
446
+ })
447
+ ],
448
+ navBySegment: new Map([
449
+ [
450
+ "seg-changelog",
451
+ [
452
+ changelogRoute({
453
+ fullPath: "changelog/2026-03-01",
454
+ title: "March Release",
455
+ date: "2026-03-01"
456
+ })
457
+ ]
458
+ ]
459
+ ])
460
+ });
461
+ const entries = collectByType(root, "changelogEntry");
462
+ expect(entries).toHaveLength(1);
463
+ expect(entries[0].tags).toBeUndefined();
464
+ });
465
+ it("returns undefined for empty tags array in metadata", () => {
466
+ const root = buildFullRootFromSegments({
467
+ basePath: "",
468
+ title: "Test",
469
+ segments: [
470
+ segment({
471
+ segmentHash: "seg-changelog",
472
+ section: "changelog",
473
+ sortOrder: 0,
474
+ metadata: { type: "changelog", title: "Changelog" }
475
+ })
476
+ ],
477
+ navBySegment: new Map([
478
+ [
479
+ "seg-changelog",
480
+ [
481
+ changelogRoute({
482
+ fullPath: "changelog/2026-04-01",
483
+ title: "April Release",
484
+ tags: [],
485
+ date: "2026-04-01"
486
+ })
487
+ ]
488
+ ]
489
+ ])
490
+ });
491
+ const entries = collectByType(root, "changelogEntry");
492
+ expect(entries).toHaveLength(1);
493
+ // metaStrArr returns undefined for empty arrays
494
+ expect(entries[0].tags == null || entries[0].tags.length === 0).toBe(true);
495
+ });
496
+ it("preserves multi-word tags with spaces", () => {
497
+ const root = buildFullRootFromSegments({
498
+ basePath: "",
499
+ title: "Test",
500
+ segments: [
501
+ segment({
502
+ segmentHash: "seg-changelog",
503
+ section: "changelog",
504
+ sortOrder: 0,
505
+ metadata: { type: "changelog", title: "Changelog" }
506
+ })
507
+ ],
508
+ navBySegment: new Map([
509
+ [
510
+ "seg-changelog",
511
+ [
512
+ changelogRoute({
513
+ fullPath: "changelog/2026-05-01",
514
+ title: "May Release",
515
+ tags: ["Breaking Change", "New Feature", "Bug Fix"],
516
+ date: "2026-05-01"
517
+ })
518
+ ]
519
+ ]
520
+ ])
521
+ });
522
+ const entries = collectByType(root, "changelogEntry");
523
+ expect(entries).toHaveLength(1);
524
+ expect(entries[0].tags).toEqual(["Breaking Change", "New Feature", "Bug Fix"]);
525
+ });
526
+ it("preserves tags independently per entry (mixed tagged and untagged)", () => {
527
+ const root = buildFullRootFromSegments({
528
+ basePath: "",
529
+ title: "Test",
530
+ segments: [
531
+ segment({
532
+ segmentHash: "seg-changelog",
533
+ section: "changelog",
534
+ sortOrder: 0,
535
+ metadata: { type: "changelog", title: "Changelog" }
536
+ })
537
+ ],
538
+ navBySegment: new Map([
539
+ [
540
+ "seg-changelog",
541
+ [
542
+ changelogRoute({
543
+ fullPath: "changelog/2026-06-15",
544
+ title: "Tagged Entry",
545
+ tags: ["feature", "api"],
546
+ date: "2026-06-15"
547
+ }),
548
+ changelogRoute({
549
+ fullPath: "changelog/2026-06-10",
550
+ title: "Untagged Entry",
551
+ date: "2026-06-10"
552
+ }),
553
+ changelogRoute({
554
+ fullPath: "changelog/2026-06-05",
555
+ title: "Another Tagged",
556
+ tags: ["deprecation"],
557
+ date: "2026-06-05"
558
+ })
559
+ ]
560
+ ]
561
+ ])
562
+ });
563
+ const entries = collectByType(root, "changelogEntry");
564
+ expect(entries).toHaveLength(3);
565
+ const tagged = entries.find((e) => e.title === "Tagged Entry");
566
+ expect(tagged.tags).toEqual(["feature", "api"]);
567
+ const untagged = entries.find((e) => e.title === "Untagged Entry");
568
+ expect(untagged.tags).toBeUndefined();
569
+ const another = entries.find((e) => e.title === "Another Tagged");
570
+ expect(another.tags).toEqual(["deprecation"]);
571
+ });
572
+ it("preserves tags across year/month boundaries", () => {
573
+ const root = buildFullRootFromSegments({
574
+ basePath: "",
575
+ title: "Test",
576
+ segments: [
577
+ segment({
578
+ segmentHash: "seg-changelog",
579
+ section: "changelog",
580
+ sortOrder: 0,
581
+ metadata: { type: "changelog", title: "Changelog" }
582
+ })
583
+ ],
584
+ navBySegment: new Map([
585
+ [
586
+ "seg-changelog",
587
+ [
588
+ changelogRoute({
589
+ fullPath: "changelog/2026-01-10",
590
+ title: "Jan 2026",
591
+ tags: ["feature"],
592
+ date: "2026-01-10"
593
+ }),
594
+ changelogRoute({
595
+ fullPath: "changelog/2025-12-20",
596
+ title: "Dec 2025",
597
+ tags: ["bugfix", "security"],
598
+ date: "2025-12-20"
599
+ }),
600
+ changelogRoute({
601
+ fullPath: "changelog/2025-06-01",
602
+ title: "Jun 2025",
603
+ tags: ["deprecation"],
604
+ date: "2025-06-01"
605
+ })
606
+ ]
607
+ ]
608
+ ])
609
+ });
610
+ const entries = collectByType(root, "changelogEntry");
611
+ expect(entries).toHaveLength(3);
612
+ const jan = entries.find((e) => e.title === "Jan 2026");
613
+ expect(jan.tags).toEqual(["feature"]);
614
+ const dec = entries.find((e) => e.title === "Dec 2025");
615
+ expect(dec.tags).toEqual(["bugfix", "security"]);
616
+ const jun = entries.find((e) => e.title === "Jun 2025");
617
+ expect(jun.tags).toEqual(["deprecation"]);
618
+ });
619
+ });
620
+ describe("metadata preservation on additional API leaf types", () => {
621
+ it("reads availability from websocket (asyncapi) metadata", () => {
622
+ const root = buildFullRootFromSegments({
623
+ basePath: "",
624
+ title: "Test",
625
+ segments: [
626
+ segment({
627
+ segmentHash: "seg-api",
628
+ section: "api",
629
+ sortOrder: 0,
630
+ metadata: { type: "apiReference", title: "API", apiDefinitionId: "api-def-1" }
631
+ })
632
+ ],
633
+ navBySegment: new Map([
634
+ [
635
+ "seg-api",
636
+ [
637
+ {
638
+ type: "asyncapi",
639
+ fullPath: "api/ws-stream",
640
+ hidden: false,
641
+ metadata: {
642
+ title: "Stream",
643
+ webSocketId: "ws-stream",
644
+ apiDefinitionId: "api-def-1",
645
+ availability: "Beta"
646
+ }
647
+ }
648
+ ]
649
+ ]
650
+ ])
651
+ });
652
+ const wsNodes = collectByType(root, "webSocket");
653
+ expect(wsNodes).toHaveLength(1);
654
+ expect(wsNodes[0].availability).toBe("Beta");
655
+ });
656
+ it("reads availability from grpc metadata", () => {
657
+ const root = buildFullRootFromSegments({
658
+ basePath: "",
659
+ title: "Test",
660
+ segments: [
661
+ segment({
662
+ segmentHash: "seg-api",
663
+ section: "api",
664
+ sortOrder: 0,
665
+ metadata: { type: "apiReference", title: "API", apiDefinitionId: "api-def-1" }
666
+ })
667
+ ],
668
+ navBySegment: new Map([
669
+ [
670
+ "seg-api",
671
+ [
672
+ {
673
+ type: "grpc",
674
+ fullPath: "api/grpc-call",
675
+ hidden: false,
676
+ metadata: {
677
+ title: "Call",
678
+ grpcId: "grpc-call",
679
+ method: "POST",
680
+ apiDefinitionId: "api-def-1",
681
+ availability: "Generally Available"
682
+ }
683
+ }
684
+ ]
685
+ ]
686
+ ])
687
+ });
688
+ const grpcNodes = collectByType(root, "grpc");
689
+ expect(grpcNodes).toHaveLength(1);
690
+ expect(grpcNodes[0].availability).toBe("Generally Available");
691
+ });
692
+ it("reads availability from graphql metadata", () => {
693
+ const root = buildFullRootFromSegments({
694
+ basePath: "",
695
+ title: "Test",
696
+ segments: [
697
+ segment({
698
+ segmentHash: "seg-api",
699
+ section: "api",
700
+ sortOrder: 0,
701
+ metadata: { type: "apiReference", title: "API", apiDefinitionId: "api-def-1" }
702
+ })
703
+ ],
704
+ navBySegment: new Map([
705
+ [
706
+ "seg-api",
707
+ [
708
+ {
709
+ type: "graphql",
710
+ fullPath: "api/gql-query",
711
+ hidden: false,
712
+ metadata: {
713
+ title: "Query",
714
+ graphqlOperationId: "gql-query",
715
+ operationType: "query",
716
+ apiDefinitionId: "api-def-1",
717
+ availability: "Deprecated"
718
+ }
719
+ }
720
+ ]
721
+ ]
722
+ ])
723
+ });
724
+ const gqlNodes = collectByType(root, "graphql");
725
+ expect(gqlNodes).toHaveLength(1);
726
+ expect(gqlNodes[0].availability).toBe("Deprecated");
727
+ });
728
+ });
729
+ describe("metadata preservation on variant and product nodes", () => {
730
+ it("reads image from variant metadata", () => {
731
+ const variantA = {
732
+ id: "variant-a",
733
+ displayName: "Variant A",
734
+ slug: "variant-a",
735
+ icon: null,
736
+ sortOrder: 0,
737
+ hidden: false,
738
+ viewers: [],
739
+ orphaned: false,
740
+ featureFlags: [],
741
+ metadata: { default: true }
742
+ };
743
+ const variantB = {
744
+ id: "variant-b",
745
+ displayName: "Variant B",
746
+ slug: "variant-b",
747
+ icon: null,
748
+ sortOrder: 1,
749
+ hidden: false,
750
+ viewers: [],
751
+ orphaned: false,
752
+ featureFlags: [],
753
+ metadata: { image: "img-variant-b.png" }
754
+ };
755
+ const root = buildFullRootFromSegments({
756
+ basePath: "",
757
+ title: "Test",
758
+ segments: [
759
+ segment({
760
+ segmentHash: "seg-a",
761
+ section: "variant-a",
762
+ sortOrder: 0,
763
+ metadata: { type: "section", title: "Section A" },
764
+ variant: variantA
765
+ }),
766
+ segment({
767
+ segmentHash: "seg-b",
768
+ section: "variant-b",
769
+ sortOrder: 1,
770
+ metadata: { type: "section", title: "Section B" },
771
+ variant: variantB
772
+ })
773
+ ],
774
+ navBySegment: new Map([
775
+ ["seg-a", [pageRoute({ fullPath: "variant-a/page", title: "Page A" })]],
776
+ ["seg-b", [pageRoute({ fullPath: "variant-b/page", title: "Page B" })]]
777
+ ])
778
+ });
779
+ const variants = collectByType(root, "variant");
780
+ expect(variants).toHaveLength(2);
781
+ const vb = variants.find((v) => v.variantId === "variant-b");
782
+ expect(vb).toBeDefined();
783
+ expect(vb.image).toBe("img-variant-b.png");
784
+ const va = variants.find((v) => v.variantId === "variant-a");
785
+ expect(va).toBeDefined();
786
+ expect(va.image).toBeUndefined();
787
+ });
788
+ it("reads image from product metadata", () => {
789
+ const productA = {
790
+ id: "prod-a",
791
+ displayName: "Product A",
792
+ slug: "product-a",
793
+ icon: null,
794
+ sortOrder: 0,
795
+ hidden: false,
796
+ viewers: [],
797
+ orphaned: false,
798
+ featureFlags: [],
799
+ metadata: { default: true, image: "img-prod-a.png" }
800
+ };
801
+ const productB = {
802
+ id: "prod-b",
803
+ displayName: "Product B",
804
+ slug: "product-b",
805
+ icon: null,
806
+ sortOrder: 1,
807
+ hidden: false,
808
+ viewers: [],
809
+ orphaned: false,
810
+ featureFlags: [],
811
+ metadata: {}
812
+ };
813
+ const root = buildFullRootFromSegments({
814
+ basePath: "",
815
+ title: "Test",
816
+ segments: [
817
+ segment({
818
+ segmentHash: "seg-pa",
819
+ section: "product-a",
820
+ sortOrder: 0,
821
+ metadata: { type: "section", title: "Section A" },
822
+ product: productA
823
+ }),
824
+ segment({
825
+ segmentHash: "seg-pb",
826
+ section: "product-b",
827
+ sortOrder: 1,
828
+ metadata: { type: "section", title: "Section B" },
829
+ product: productB
830
+ })
831
+ ],
832
+ navBySegment: new Map([
833
+ ["seg-pa", [pageRoute({ fullPath: "product-a/page", title: "Page A" })]],
834
+ ["seg-pb", [pageRoute({ fullPath: "product-b/page", title: "Page B" })]]
835
+ ])
836
+ });
837
+ const products = collectByType(root, "product");
838
+ expect(products).toHaveLength(2);
839
+ const pa = products.find((p) => p.productId === "Product A");
840
+ expect(pa).toBeDefined();
841
+ expect(pa.image).toBe("img-prod-a.png");
842
+ const pb = products.find((p) => p.productId === "Product B");
843
+ expect(pb).toBeDefined();
844
+ expect(pb.image).toBeUndefined();
845
+ });
846
+ });
847
+ describe("metadata preservation on apiPackage nodes", () => {
848
+ it("reads availability and noindex from apiPackage segment metadata", () => {
849
+ const root = buildFullRootFromSegments({
850
+ basePath: "",
851
+ title: "Test",
852
+ segments: [
853
+ segment({
854
+ segmentHash: "seg-api",
855
+ section: "api",
856
+ sortOrder: 0,
857
+ metadata: { type: "apiReference", title: "API", apiDefinitionId: "api-def-1" }
858
+ }),
859
+ segment({
860
+ segmentHash: "seg-pkg",
861
+ section: "api/users",
862
+ sortOrder: 1,
863
+ metadata: {
864
+ type: "apiPackage",
865
+ title: "Users",
866
+ apiDefinitionId: "api-def-1",
867
+ availability: "Beta",
868
+ noindex: true
869
+ }
870
+ })
871
+ ],
872
+ navBySegment: new Map([
873
+ ["seg-api", []],
874
+ [
875
+ "seg-pkg",
876
+ [
877
+ restRoute({
878
+ fullPath: "api/users/list",
879
+ title: "List Users",
880
+ endpointId: "list-users",
881
+ method: "GET"
882
+ })
883
+ ]
884
+ ]
885
+ ])
886
+ });
887
+ const packages = collectByType(root, "apiPackage");
888
+ expect(packages).toHaveLength(1);
889
+ expect(packages[0].availability).toBe("Beta");
890
+ expect(packages[0].noindex).toBe(true);
891
+ });
892
+ });
893
+ describe("skipSharedSectionMerge option", () => {
894
+ const defaultVersion = {
895
+ id: "v-2026-02",
896
+ displayName: "2026-02-09",
897
+ slug: "2026-02-09",
898
+ icon: null,
899
+ sortOrder: 0,
900
+ hidden: false,
901
+ viewers: [],
902
+ orphaned: false,
903
+ featureFlags: [],
904
+ metadata: { default: true }
905
+ };
906
+ const olderVersion = {
907
+ id: "v-2024-12",
908
+ displayName: "2024-12-23",
909
+ slug: "2024-12-23",
910
+ icon: null,
911
+ sortOrder: 1,
912
+ hidden: false,
913
+ viewers: [],
914
+ orphaned: false,
915
+ featureFlags: [],
916
+ metadata: {}
917
+ };
918
+ const docsTab = {
919
+ id: "docs-tab",
920
+ displayName: "Documentation",
921
+ slug: "docs",
922
+ icon: null,
923
+ sortOrder: 0,
924
+ hidden: false,
925
+ viewers: [],
926
+ orphaned: false,
927
+ featureFlags: [],
928
+ metadata: {}
929
+ };
930
+ function buildVersionedRoot(skipMerge) {
931
+ return buildFullRootFromSegments({
932
+ basePath: "",
933
+ title: "Test",
934
+ segments: [
935
+ // Default version tab root
936
+ segment({
937
+ segmentHash: "seg-default-root",
938
+ section: "",
939
+ sortOrder: 0,
940
+ metadata: { type: "tabRoot" },
941
+ version: defaultVersion,
942
+ tab: docsTab
943
+ }),
944
+ // Default version section
945
+ segment({
946
+ segmentHash: "seg-default-guides",
947
+ section: "2026-02-09",
948
+ sortOrder: 1,
949
+ metadata: { type: "section", title: "Getting Started" },
950
+ version: defaultVersion,
951
+ tab: docsTab
952
+ }),
953
+ // Older version tab root
954
+ segment({
955
+ segmentHash: "seg-older-root",
956
+ section: "",
957
+ sortOrder: 2,
958
+ metadata: { type: "tabRoot" },
959
+ version: olderVersion,
960
+ tab: docsTab
961
+ }),
962
+ // Older version section
963
+ segment({
964
+ segmentHash: "seg-older-guides",
965
+ section: "2024-12-23",
966
+ sortOrder: 3,
967
+ metadata: { type: "section", title: "Getting Started" },
968
+ version: olderVersion,
969
+ tab: docsTab
970
+ }),
971
+ // Shared section (no version prefix path) — assigned to older version only
972
+ segment({
973
+ segmentHash: "seg-shared-workflows",
974
+ section: "workflows",
975
+ sortOrder: 4,
976
+ metadata: { type: "section", title: "Workflows" },
977
+ version: olderVersion,
978
+ tab: docsTab
979
+ })
980
+ ],
981
+ navBySegment: new Map([
982
+ ["seg-default-root", [pageRoute({ fullPath: "overview", title: "Overview" })]],
983
+ [
984
+ "seg-default-guides",
985
+ [pageRoute({ fullPath: "2026-02-09/getting-started", title: "Getting Started" })]
986
+ ],
987
+ ["seg-older-root", [pageRoute({ fullPath: "2024-12-23/overview", title: "Overview" })]],
988
+ [
989
+ "seg-older-guides",
990
+ [
991
+ pageRoute({
992
+ fullPath: "2024-12-23/getting-started",
993
+ title: "Getting Started"
994
+ })
995
+ ]
996
+ ],
997
+ [
998
+ "seg-shared-workflows",
999
+ [
1000
+ pageRoute({
1001
+ fullPath: "2024-12-23/workflows/create",
1002
+ title: "Create Workflow"
1003
+ })
1004
+ ]
1005
+ ]
1006
+ ]),
1007
+ skipSharedSectionMerge: skipMerge
1008
+ });
1009
+ }
1010
+ it("merges shared sections into default version when skipSharedSectionMerge is false", () => {
1011
+ const root = buildVersionedRoot(false);
1012
+ const versions = collectByType(root, "version");
1013
+ const defaultVer = versions.find((v) => v.default);
1014
+ expect(defaultVer).toBeDefined();
1015
+ // The default version should have the "Workflows" section merged in.
1016
+ const sections = collectByType(defaultVer, "section");
1017
+ const sectionTitles = sections.map((s) => s.title);
1018
+ expect(sectionTitles).toContain("Workflows");
1019
+ });
1020
+ it("does NOT merge shared sections when skipSharedSectionMerge is true", () => {
1021
+ const root = buildVersionedRoot(true);
1022
+ const versions = collectByType(root, "version");
1023
+ const defaultVer = versions.find((v) => v.default);
1024
+ expect(defaultVer).toBeDefined();
1025
+ // The default version should NOT have the "Workflows" section.
1026
+ const sections = collectByType(defaultVer, "section");
1027
+ const sectionTitles = sections.map((s) => s.title);
1028
+ expect(sectionTitles).not.toContain("Workflows");
1029
+ });
1030
+ });
1031
+ //# sourceMappingURL=ledger-root-builder.metadataPreservation.test.js.map