@levino/shipyard-docs 0.6.2 → 0.6.3

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.
@@ -1,5 +1,29 @@
1
1
  import { describe, expect, it } from 'vitest'
2
- import { getDocPath, getRouteParams } from './routeHelpers'
2
+ import type { VersionConfig } from './index'
3
+ import {
4
+ docExistsInVersion,
5
+ filterDocsByVersion,
6
+ findFallbackDoc,
7
+ getDocVersions,
8
+ getVersionFromDocId,
9
+ groupDocsByVersion,
10
+ stripVersionFromDocId,
11
+ } from './index'
12
+ import {
13
+ createDeprecatedVersionSet,
14
+ createVersionPathMap,
15
+ findVersionConfig,
16
+ getAvailableVersions,
17
+ getCurrentVersion,
18
+ getDocPath,
19
+ getRouteParams,
20
+ getStableVersion,
21
+ getVersionedDocPath,
22
+ getVersionedRouteParams,
23
+ getVersionPath,
24
+ isVersionDeprecated,
25
+ switchVersionInPath,
26
+ } from './routeHelpers'
3
27
 
4
28
  describe('getRouteParams', () => {
5
29
  describe('without i18n', () => {
@@ -119,3 +143,635 @@ describe('getDocPath', () => {
119
143
  })
120
144
  })
121
145
  })
146
+
147
+ // Sample version config for testing
148
+ const sampleVersionConfig: VersionConfig = {
149
+ current: 'v2.0',
150
+ available: [
151
+ { version: 'v3.0', label: 'Version 3.0 (Preview)', banner: 'unreleased' },
152
+ { version: 'v2.0', label: 'Version 2.0' },
153
+ {
154
+ version: 'v1.0',
155
+ label: 'Version 1.0',
156
+ path: 'v1',
157
+ banner: 'unmaintained',
158
+ },
159
+ ],
160
+ deprecated: ['v1.0'],
161
+ stable: 'v2.0',
162
+ }
163
+
164
+ describe('getVersionPath', () => {
165
+ it('should return version string when path is not defined', () => {
166
+ const path = getVersionPath('v2.0', sampleVersionConfig)
167
+ expect(path).toBe('v2.0')
168
+ })
169
+
170
+ it('should return custom path when defined', () => {
171
+ const path = getVersionPath('v1.0', sampleVersionConfig)
172
+ expect(path).toBe('v1')
173
+ })
174
+
175
+ it('should return undefined for non-existent version', () => {
176
+ const path = getVersionPath('v0.5', sampleVersionConfig)
177
+ expect(path).toBeUndefined()
178
+ })
179
+ })
180
+
181
+ describe('getCurrentVersion', () => {
182
+ it('should return the current version', () => {
183
+ expect(getCurrentVersion(sampleVersionConfig)).toBe('v2.0')
184
+ })
185
+ })
186
+
187
+ describe('getAvailableVersions', () => {
188
+ it('should return all available versions', () => {
189
+ const versions = getAvailableVersions(sampleVersionConfig)
190
+ expect(versions).toHaveLength(3)
191
+ expect(versions[0].version).toBe('v3.0')
192
+ expect(versions[1].version).toBe('v2.0')
193
+ expect(versions[2].version).toBe('v1.0')
194
+ })
195
+ })
196
+
197
+ describe('isVersionDeprecated', () => {
198
+ it('should return true for deprecated versions', () => {
199
+ expect(isVersionDeprecated('v1.0', sampleVersionConfig)).toBe(true)
200
+ })
201
+
202
+ it('should return false for non-deprecated versions', () => {
203
+ expect(isVersionDeprecated('v2.0', sampleVersionConfig)).toBe(false)
204
+ })
205
+
206
+ it('should return false for non-existent versions', () => {
207
+ expect(isVersionDeprecated('v0.5', sampleVersionConfig)).toBe(false)
208
+ })
209
+
210
+ it('should handle config without deprecated array', () => {
211
+ const configWithoutDeprecated: VersionConfig = {
212
+ current: 'v1.0',
213
+ available: [{ version: 'v1.0' }],
214
+ }
215
+ expect(isVersionDeprecated('v1.0', configWithoutDeprecated)).toBe(false)
216
+ })
217
+ })
218
+
219
+ describe('getStableVersion', () => {
220
+ it('should return stable version when defined', () => {
221
+ expect(getStableVersion(sampleVersionConfig)).toBe('v2.0')
222
+ })
223
+
224
+ it('should fall back to current when stable is not defined', () => {
225
+ const configWithoutStable: VersionConfig = {
226
+ current: 'v3.0',
227
+ available: [{ version: 'v3.0' }],
228
+ }
229
+ expect(getStableVersion(configWithoutStable)).toBe('v3.0')
230
+ })
231
+ })
232
+
233
+ describe('findVersionConfig', () => {
234
+ it('should find version by version string', () => {
235
+ const config = findVersionConfig('v2.0', sampleVersionConfig)
236
+ expect(config).toBeDefined()
237
+ expect(config?.version).toBe('v2.0')
238
+ expect(config?.label).toBe('Version 2.0')
239
+ })
240
+
241
+ it('should find version by path', () => {
242
+ const config = findVersionConfig('v1', sampleVersionConfig)
243
+ expect(config).toBeDefined()
244
+ expect(config?.version).toBe('v1.0')
245
+ })
246
+
247
+ it('should return undefined for non-existent version', () => {
248
+ const config = findVersionConfig('v0.5', sampleVersionConfig)
249
+ expect(config).toBeUndefined()
250
+ })
251
+ })
252
+
253
+ describe('getVersionedDocPath', () => {
254
+ describe('without i18n', () => {
255
+ it('should generate versioned path', () => {
256
+ const path = getVersionedDocPath('getting-started', 'docs', false, 'v2.0')
257
+ expect(path).toBe('/docs/v2.0/getting-started')
258
+ })
259
+
260
+ it('should handle nested paths', () => {
261
+ const path = getVersionedDocPath('guides/advanced', 'docs', false, 'v1')
262
+ expect(path).toBe('/docs/v1/guides/advanced')
263
+ })
264
+
265
+ it('should normalize base path', () => {
266
+ const path = getVersionedDocPath('intro', '/docs/', false, 'v2.0')
267
+ expect(path).toBe('/docs/v2.0/intro')
268
+ })
269
+
270
+ it('should normalize version path', () => {
271
+ const path = getVersionedDocPath('intro', 'docs', false, '/v2.0/')
272
+ expect(path).toBe('/docs/v2.0/intro')
273
+ })
274
+ })
275
+
276
+ describe('with i18n', () => {
277
+ it('should generate versioned localized path', () => {
278
+ const path = getVersionedDocPath(
279
+ 'en/getting-started',
280
+ 'docs',
281
+ true,
282
+ 'v2.0',
283
+ 'en',
284
+ )
285
+ expect(path).toBe('/en/docs/v2.0/getting-started')
286
+ })
287
+
288
+ it('should handle nested paths with locale', () => {
289
+ const path = getVersionedDocPath(
290
+ 'de/guides/advanced',
291
+ 'docs',
292
+ true,
293
+ 'v1',
294
+ 'de',
295
+ )
296
+ expect(path).toBe('/de/docs/v1/guides/advanced')
297
+ })
298
+
299
+ it('should handle custom base path', () => {
300
+ const path = getVersionedDocPath(
301
+ 'en/tutorial',
302
+ 'guides',
303
+ true,
304
+ 'v3.0',
305
+ 'en',
306
+ )
307
+ expect(path).toBe('/en/guides/v3.0/tutorial')
308
+ })
309
+ })
310
+ })
311
+
312
+ describe('getVersionedRouteParams', () => {
313
+ it('should include version in params without i18n', () => {
314
+ const params = getVersionedRouteParams('getting-started', false, 'v2.0')
315
+ expect(params).toEqual({
316
+ slug: 'getting-started',
317
+ version: 'v2.0',
318
+ })
319
+ })
320
+
321
+ it('should include version and locale in params with i18n', () => {
322
+ const params = getVersionedRouteParams('en/getting-started', true, 'v2.0')
323
+ expect(params).toEqual({
324
+ locale: 'en',
325
+ slug: 'getting-started',
326
+ version: 'v2.0',
327
+ })
328
+ })
329
+ })
330
+
331
+ describe('switchVersionInPath', () => {
332
+ it('should switch version in simple path', () => {
333
+ const newPath = switchVersionInPath('/docs/v1.0/intro', 'v2.0', 'v1.0')
334
+ expect(newPath).toBe('/docs/v2.0/intro')
335
+ })
336
+
337
+ it('should switch version in localized path', () => {
338
+ const newPath = switchVersionInPath(
339
+ '/en/docs/v1.0/getting-started',
340
+ 'v2.0',
341
+ 'v1.0',
342
+ )
343
+ expect(newPath).toBe('/en/docs/v2.0/getting-started')
344
+ })
345
+
346
+ it('should handle special regex characters in version', () => {
347
+ const newPath = switchVersionInPath(
348
+ '/docs/v1.0-beta/intro',
349
+ 'v2.0',
350
+ 'v1.0-beta',
351
+ )
352
+ expect(newPath).toBe('/docs/v2.0/intro')
353
+ })
354
+
355
+ it('should handle custom path aliases', () => {
356
+ const newPath = switchVersionInPath('/docs/v1/intro', 'v2', 'v1')
357
+ expect(newPath).toBe('/docs/v2/intro')
358
+ })
359
+
360
+ it('should only replace first occurrence', () => {
361
+ // Edge case: if version appears multiple times, only first is replaced
362
+ const newPath = switchVersionInPath('/docs/v1/v1/intro', 'v2', 'v1')
363
+ expect(newPath).toBe('/docs/v2/v1/intro')
364
+ })
365
+ })
366
+
367
+ // Tests for versioned content collection helpers
368
+ describe('getVersionFromDocId', () => {
369
+ describe('valid version patterns', () => {
370
+ it('should extract version from v-prefixed semantic version', () => {
371
+ expect(getVersionFromDocId('v1.0/en/getting-started')).toBe('v1.0')
372
+ expect(getVersionFromDocId('v2.0.0/en/intro')).toBe('v2.0.0')
373
+ expect(getVersionFromDocId('v10.20.30/guide')).toBe('v10.20.30')
374
+ })
375
+
376
+ it('should extract version without v prefix', () => {
377
+ expect(getVersionFromDocId('1.0/en/getting-started')).toBe('1.0')
378
+ expect(getVersionFromDocId('2.0.0/intro')).toBe('2.0.0')
379
+ })
380
+
381
+ it('should extract special version names', () => {
382
+ expect(getVersionFromDocId('latest/en/intro')).toBe('latest')
383
+ expect(getVersionFromDocId('next/guide')).toBe('next')
384
+ expect(getVersionFromDocId('main/docs/index')).toBe('main')
385
+ expect(getVersionFromDocId('master/en/intro')).toBe('master')
386
+ expect(getVersionFromDocId('canary/en/intro')).toBe('canary')
387
+ expect(getVersionFromDocId('beta/en/intro')).toBe('beta')
388
+ expect(getVersionFromDocId('alpha/en/intro')).toBe('alpha')
389
+ expect(getVersionFromDocId('stable/en/intro')).toBe('stable')
390
+ })
391
+
392
+ it('should extract release candidate versions', () => {
393
+ expect(getVersionFromDocId('rc1/en/intro')).toBe('rc1')
394
+ expect(getVersionFromDocId('rc/guide')).toBe('rc')
395
+ })
396
+ })
397
+
398
+ describe('non-versioned paths', () => {
399
+ it('should return undefined for locale-only paths', () => {
400
+ expect(getVersionFromDocId('en/getting-started')).toBeUndefined()
401
+ expect(getVersionFromDocId('de/intro')).toBeUndefined()
402
+ })
403
+
404
+ it('should return undefined for paths not starting with version', () => {
405
+ expect(getVersionFromDocId('docs/v1.0/intro')).toBeUndefined()
406
+ expect(getVersionFromDocId('guide')).toBeUndefined()
407
+ })
408
+
409
+ it('should return undefined for empty paths', () => {
410
+ expect(getVersionFromDocId('')).toBeUndefined()
411
+ })
412
+ })
413
+ })
414
+
415
+ describe('stripVersionFromDocId', () => {
416
+ it('should remove version prefix from doc id', () => {
417
+ expect(stripVersionFromDocId('v1.0/en/getting-started')).toBe(
418
+ 'en/getting-started',
419
+ )
420
+ expect(stripVersionFromDocId('latest/en/index')).toBe('en/index')
421
+ expect(stripVersionFromDocId('v2.0.0/guide/intro')).toBe('guide/intro')
422
+ })
423
+
424
+ it('should return unchanged path for non-versioned docs', () => {
425
+ expect(stripVersionFromDocId('en/getting-started')).toBe(
426
+ 'en/getting-started',
427
+ )
428
+ expect(stripVersionFromDocId('guide')).toBe('guide')
429
+ })
430
+
431
+ it('should handle single segment versioned paths', () => {
432
+ expect(stripVersionFromDocId('v1.0/index')).toBe('index')
433
+ expect(stripVersionFromDocId('latest/readme')).toBe('readme')
434
+ })
435
+ })
436
+
437
+ describe('filterDocsByVersion', () => {
438
+ const mockDocs = [
439
+ { id: 'v1.0/en/intro', data: {} },
440
+ { id: 'v1.0/en/guide', data: {} },
441
+ { id: 'v2.0/en/intro', data: {} },
442
+ { id: 'v2.0/en/guide', data: {} },
443
+ { id: 'v2.0/en/new-feature', data: {} },
444
+ { id: 'latest/en/intro', data: {} },
445
+ ]
446
+
447
+ it('should filter docs by version', () => {
448
+ const v1Docs = filterDocsByVersion(mockDocs, 'v1.0')
449
+ expect(v1Docs).toHaveLength(2)
450
+ expect(v1Docs.map((d) => d.id)).toEqual(['v1.0/en/intro', 'v1.0/en/guide'])
451
+ })
452
+
453
+ it('should return empty array for non-existent version', () => {
454
+ const docs = filterDocsByVersion(mockDocs, 'v3.0')
455
+ expect(docs).toHaveLength(0)
456
+ })
457
+
458
+ it('should handle special version names', () => {
459
+ const latestDocs = filterDocsByVersion(mockDocs, 'latest')
460
+ expect(latestDocs).toHaveLength(1)
461
+ expect(latestDocs[0].id).toBe('latest/en/intro')
462
+ })
463
+
464
+ it('should handle empty docs array', () => {
465
+ const docs = filterDocsByVersion([], 'v1.0')
466
+ expect(docs).toHaveLength(0)
467
+ })
468
+ })
469
+
470
+ describe('groupDocsByVersion', () => {
471
+ const mockDocs = [
472
+ { id: 'v1.0/en/intro', data: {} },
473
+ { id: 'v1.0/de/intro', data: {} },
474
+ { id: 'v2.0/en/intro', data: {} },
475
+ { id: 'v2.0/en/guide', data: {} },
476
+ { id: 'latest/en/intro', data: {} },
477
+ ]
478
+
479
+ it('should group docs by version', () => {
480
+ const groups = groupDocsByVersion(mockDocs)
481
+
482
+ expect(groups.size).toBe(3)
483
+ expect(groups.get('v1.0')).toHaveLength(2)
484
+ expect(groups.get('v2.0')).toHaveLength(2)
485
+ expect(groups.get('latest')).toHaveLength(1)
486
+ })
487
+
488
+ it('should handle empty docs array', () => {
489
+ const groups = groupDocsByVersion([])
490
+ expect(groups.size).toBe(0)
491
+ })
492
+
493
+ it('should handle non-versioned docs', () => {
494
+ const nonVersionedDocs = [
495
+ { id: 'en/intro', data: {} },
496
+ { id: 'de/guide', data: {} },
497
+ ]
498
+ const groups = groupDocsByVersion(nonVersionedDocs)
499
+
500
+ expect(groups.size).toBe(1)
501
+ expect(groups.get(undefined)).toHaveLength(2)
502
+ })
503
+
504
+ it('should handle mixed versioned and non-versioned docs', () => {
505
+ const mixedDocs = [
506
+ { id: 'v1.0/en/intro', data: {} },
507
+ { id: 'en/guide', data: {} },
508
+ ]
509
+ const groups = groupDocsByVersion(mixedDocs)
510
+
511
+ expect(groups.size).toBe(2)
512
+ expect(groups.get('v1.0')).toHaveLength(1)
513
+ expect(groups.get(undefined)).toHaveLength(1)
514
+ })
515
+ })
516
+
517
+ describe('findFallbackDoc', () => {
518
+ const mockDocs = [
519
+ { id: 'v1.0/en/intro', data: { title: 'Intro v1' } },
520
+ { id: 'v1.0/en/guide', data: { title: 'Guide v1' } },
521
+ { id: 'v2.0/en/intro', data: { title: 'Intro v2' } },
522
+ { id: 'v2.0/en/guide', data: { title: 'Guide v2' } },
523
+ { id: 'v2.0/en/new-feature', data: { title: 'New Feature v2' } },
524
+ { id: 'latest/en/intro', data: { title: 'Intro latest' } },
525
+ { id: 'latest/en/guide', data: { title: 'Guide latest' } },
526
+ { id: 'latest/en/new-feature', data: { title: 'New Feature latest' } },
527
+ { id: 'latest/en/bleeding-edge', data: { title: 'Bleeding Edge' } },
528
+ ]
529
+
530
+ it('should find fallback doc in first matching version', () => {
531
+ // new-feature doesn't exist in v1.0, should fall back to v2.0
532
+ const result = findFallbackDoc(mockDocs, 'en/new-feature', 'v1.0', [
533
+ 'v2.0',
534
+ 'latest',
535
+ ])
536
+ expect(result).toBeDefined()
537
+ expect(result?.version).toBe('v2.0')
538
+ expect(result?.doc.id).toBe('v2.0/en/new-feature')
539
+ })
540
+
541
+ it('should return first matching fallback in order', () => {
542
+ // intro exists in all versions, should return first fallback
543
+ const result = findFallbackDoc(mockDocs, 'en/intro', 'v1.0', [
544
+ 'v2.0',
545
+ 'latest',
546
+ ])
547
+ expect(result?.version).toBe('v2.0')
548
+ })
549
+
550
+ it('should skip the requested version even if in fallback list', () => {
551
+ const result = findFallbackDoc(mockDocs, 'en/intro', 'v1.0', [
552
+ 'v1.0',
553
+ 'v2.0',
554
+ ])
555
+ expect(result?.version).toBe('v2.0')
556
+ })
557
+
558
+ it('should return undefined when doc not found in any fallback', () => {
559
+ // bleeding-edge only exists in latest
560
+ const result = findFallbackDoc(mockDocs, 'en/bleeding-edge', 'v1.0', [
561
+ 'v2.0',
562
+ ])
563
+ expect(result).toBeUndefined()
564
+ })
565
+
566
+ it('should return undefined for empty fallback versions', () => {
567
+ const result = findFallbackDoc(mockDocs, 'en/intro', 'v1.0', [])
568
+ expect(result).toBeUndefined()
569
+ })
570
+
571
+ it('should handle empty docs array', () => {
572
+ const result = findFallbackDoc([], 'en/intro', 'v1.0', ['v2.0', 'latest'])
573
+ expect(result).toBeUndefined()
574
+ })
575
+ })
576
+
577
+ describe('docExistsInVersion', () => {
578
+ const mockDocs = [
579
+ { id: 'v1.0/en/intro', data: {} },
580
+ { id: 'v2.0/en/intro', data: {} },
581
+ { id: 'v2.0/en/new-feature', data: {} },
582
+ ]
583
+
584
+ it('should return true when doc exists in version', () => {
585
+ expect(docExistsInVersion(mockDocs, 'en/intro', 'v1.0')).toBe(true)
586
+ expect(docExistsInVersion(mockDocs, 'en/intro', 'v2.0')).toBe(true)
587
+ expect(docExistsInVersion(mockDocs, 'en/new-feature', 'v2.0')).toBe(true)
588
+ })
589
+
590
+ it('should return false when doc does not exist in version', () => {
591
+ expect(docExistsInVersion(mockDocs, 'en/new-feature', 'v1.0')).toBe(false)
592
+ expect(docExistsInVersion(mockDocs, 'en/intro', 'v3.0')).toBe(false)
593
+ expect(docExistsInVersion(mockDocs, 'en/nonexistent', 'v1.0')).toBe(false)
594
+ })
595
+
596
+ it('should handle empty docs array', () => {
597
+ expect(docExistsInVersion([], 'en/intro', 'v1.0')).toBe(false)
598
+ })
599
+ })
600
+
601
+ describe('getDocVersions', () => {
602
+ const mockDocs = [
603
+ { id: 'v1.0/en/intro', data: {} },
604
+ { id: 'v1.0/de/intro', data: {} },
605
+ { id: 'v2.0/en/intro', data: {} },
606
+ { id: 'v2.0/en/new-feature', data: {} },
607
+ { id: 'latest/en/intro', data: {} },
608
+ { id: 'latest/en/new-feature', data: {} },
609
+ { id: 'latest/en/bleeding-edge', data: {} },
610
+ ]
611
+
612
+ it('should return all versions where doc exists', () => {
613
+ const versions = getDocVersions(mockDocs, 'en/intro')
614
+ expect(versions).toHaveLength(3)
615
+ expect(versions).toContain('v1.0')
616
+ expect(versions).toContain('v2.0')
617
+ expect(versions).toContain('latest')
618
+ })
619
+
620
+ it('should return limited versions for version-specific doc', () => {
621
+ const versions = getDocVersions(mockDocs, 'en/new-feature')
622
+ expect(versions).toHaveLength(2)
623
+ expect(versions).toContain('v2.0')
624
+ expect(versions).toContain('latest')
625
+ expect(versions).not.toContain('v1.0')
626
+ })
627
+
628
+ it('should return single version for doc only in one version', () => {
629
+ const versions = getDocVersions(mockDocs, 'en/bleeding-edge')
630
+ expect(versions).toHaveLength(1)
631
+ expect(versions).toContain('latest')
632
+ })
633
+
634
+ it('should return empty array for nonexistent doc', () => {
635
+ const versions = getDocVersions(mockDocs, 'en/nonexistent')
636
+ expect(versions).toHaveLength(0)
637
+ })
638
+
639
+ it('should handle locale-specific docs', () => {
640
+ // de/intro only exists in v1.0
641
+ const versions = getDocVersions(mockDocs, 'de/intro')
642
+ expect(versions).toHaveLength(1)
643
+ expect(versions).toContain('v1.0')
644
+ })
645
+
646
+ it('should not return duplicates', () => {
647
+ // Even if we have multiple docs with same version, should only list once
648
+ const docsWithDuplicates = [
649
+ { id: 'v1.0/en/intro', data: {} },
650
+ { id: 'v1.0/en/intro', data: {} }, // Duplicate
651
+ ]
652
+ const versions = getDocVersions(docsWithDuplicates, 'en/intro')
653
+ expect(versions).toHaveLength(1)
654
+ })
655
+
656
+ it('should handle empty docs array', () => {
657
+ const versions = getDocVersions([], 'en/intro')
658
+ expect(versions).toHaveLength(0)
659
+ })
660
+ })
661
+
662
+ // Performance optimization helpers
663
+ describe('createVersionPathMap', () => {
664
+ const testVersions: VersionConfig = {
665
+ current: 'v2',
666
+ available: [
667
+ { version: 'v2', label: 'Version 2.0' },
668
+ { version: 'v1', label: 'Version 1.0' },
669
+ ],
670
+ deprecated: ['v1'],
671
+ stable: 'v2',
672
+ }
673
+
674
+ it('should create a Map from version to path', () => {
675
+ const map = createVersionPathMap(testVersions)
676
+ expect(map.get('v2')).toBe('v2')
677
+ expect(map.get('v1')).toBe('v1')
678
+ })
679
+
680
+ it('should use custom path when specified', () => {
681
+ const versionsWithCustomPaths: VersionConfig = {
682
+ current: 'v2.0.0',
683
+ available: [
684
+ { version: 'v2.0.0', label: 'Version 2.0', path: 'v2' },
685
+ { version: 'v1.0.0', label: 'Version 1.0', path: 'v1' },
686
+ ],
687
+ stable: 'v2.0.0',
688
+ }
689
+ const map = createVersionPathMap(versionsWithCustomPaths)
690
+ expect(map.get('v2.0.0')).toBe('v2')
691
+ expect(map.get('v1.0.0')).toBe('v1')
692
+ })
693
+
694
+ it('should return undefined for non-existent version', () => {
695
+ const map = createVersionPathMap(testVersions)
696
+ expect(map.get('v3')).toBeUndefined()
697
+ })
698
+
699
+ it('should handle empty available array', () => {
700
+ const emptyVersions: VersionConfig = {
701
+ current: 'v1',
702
+ available: [],
703
+ stable: 'v1',
704
+ }
705
+ const map = createVersionPathMap(emptyVersions)
706
+ expect(map.size).toBe(0)
707
+ })
708
+
709
+ it('should handle many versions efficiently', () => {
710
+ const manyVersions: VersionConfig = {
711
+ current: 'v10',
712
+ available: Array.from({ length: 10 }, (_, i) => ({
713
+ version: `v${i + 1}`,
714
+ label: `Version ${i + 1}`,
715
+ })),
716
+ stable: 'v10',
717
+ }
718
+ const map = createVersionPathMap(manyVersions)
719
+ expect(map.size).toBe(10)
720
+ expect(map.get('v1')).toBe('v1')
721
+ expect(map.get('v10')).toBe('v10')
722
+ })
723
+ })
724
+
725
+ describe('createDeprecatedVersionSet', () => {
726
+ it('should create a Set from deprecated versions array', () => {
727
+ const versions: VersionConfig = {
728
+ current: 'v3',
729
+ available: [{ version: 'v3' }, { version: 'v2' }, { version: 'v1' }],
730
+ deprecated: ['v1', 'v2'],
731
+ stable: 'v3',
732
+ }
733
+ const set = createDeprecatedVersionSet(versions)
734
+ expect(set.has('v1')).toBe(true)
735
+ expect(set.has('v2')).toBe(true)
736
+ expect(set.has('v3')).toBe(false)
737
+ })
738
+
739
+ it('should return empty Set when deprecated is undefined', () => {
740
+ const versions: VersionConfig = {
741
+ current: 'v1',
742
+ available: [{ version: 'v1' }],
743
+ stable: 'v1',
744
+ }
745
+ const set = createDeprecatedVersionSet(versions)
746
+ expect(set.size).toBe(0)
747
+ })
748
+
749
+ it('should return empty Set when deprecated is empty array', () => {
750
+ const versions: VersionConfig = {
751
+ current: 'v1',
752
+ available: [{ version: 'v1' }],
753
+ deprecated: [],
754
+ stable: 'v1',
755
+ }
756
+ const set = createDeprecatedVersionSet(versions)
757
+ expect(set.size).toBe(0)
758
+ })
759
+
760
+ it('should provide O(1) lookup for many deprecated versions', () => {
761
+ const versions: VersionConfig = {
762
+ current: 'v20',
763
+ available: Array.from({ length: 20 }, (_, i) => ({
764
+ version: `v${i + 1}`,
765
+ })),
766
+ deprecated: Array.from({ length: 15 }, (_, i) => `v${i + 1}`),
767
+ stable: 'v20',
768
+ }
769
+ const set = createDeprecatedVersionSet(versions)
770
+ expect(set.size).toBe(15)
771
+ // Check O(1) lookups
772
+ expect(set.has('v1')).toBe(true)
773
+ expect(set.has('v15')).toBe(true)
774
+ expect(set.has('v16')).toBe(false)
775
+ expect(set.has('v20')).toBe(false)
776
+ })
777
+ })