@fragno-dev/cli 0.1.16 → 0.1.18

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.
@@ -0,0 +1,1129 @@
1
+ import { describe, it, expect } from "vitest";
2
+ import type { Subject } from "@fragno-dev/corpus";
3
+ import { getSubjects, getSubject } from "@fragno-dev/corpus";
4
+ import {
5
+ addLineNumbers,
6
+ filterByLineRange,
7
+ buildSubjectsMarkdown,
8
+ extractHeadingsAndBlocks,
9
+ } from "./corpus.js";
10
+
11
+ describe("corpus package integration", () => {
12
+ it("should be able to find and load subjects directory from published package", () => {
13
+ // This test verifies that the subjects directory is correctly included in the
14
+ // published package and can be found when the code runs from dist/
15
+ const subjects = getSubjects();
16
+
17
+ // Should find at least one subject
18
+ expect(subjects.length).toBeGreaterThan(0);
19
+
20
+ // Each subject should have an id and title
21
+ for (const subject of subjects) {
22
+ expect(subject.id).toBeDefined();
23
+ expect(subject.id.length).toBeGreaterThan(0);
24
+ expect(subject.title).toBeDefined();
25
+ expect(subject.title.length).toBeGreaterThan(0);
26
+ }
27
+ });
28
+
29
+ it("should be able to load a specific subject file", () => {
30
+ // Verify we can actually load a subject's content (not just list them)
31
+ const subjects = getSubject("defining-routes");
32
+
33
+ expect(subjects).toHaveLength(1);
34
+ const subject = subjects[0];
35
+
36
+ expect(subject.id).toBe("defining-routes");
37
+ expect(subject.title).toBeDefined();
38
+ expect(subject.title.length).toBeGreaterThan(0);
39
+
40
+ // Subject should have sections (the actual markdown content)
41
+ expect(subject.sections.length).toBeGreaterThan(0);
42
+ });
43
+
44
+ it("should handle loading multiple subjects at once", () => {
45
+ const subjects = getSubject("defining-routes", "client-state-management");
46
+
47
+ expect(subjects).toHaveLength(2);
48
+
49
+ const ids = subjects.map((s) => s.id);
50
+ expect(ids).toContain("defining-routes");
51
+ expect(ids).toContain("client-state-management");
52
+ });
53
+ });
54
+
55
+ describe("addLineNumbers", () => {
56
+ it("should add line numbers with proper padding", () => {
57
+ const content = "line 1\nline 2\nline 3";
58
+ const result = addLineNumbers(content);
59
+
60
+ expect(result).toBe("1│ line 1\n2│ line 2\n3│ line 3");
61
+ });
62
+
63
+ it("should pad line numbers correctly for multi-digit lines", () => {
64
+ const content = Array.from({ length: 100 }, (_, i) => `line ${i + 1}`).join("\n");
65
+ const result = addLineNumbers(content);
66
+
67
+ const lines = result.split("\n");
68
+ // First line should have padding
69
+ expect(lines[0]).toBe(" 1│ line 1");
70
+ // Line 10 should have less padding
71
+ expect(lines[9]).toBe(" 10│ line 10");
72
+ // Line 100 should have no padding
73
+ expect(lines[99]).toBe("100│ line 100");
74
+ });
75
+
76
+ it("should start from custom line number", () => {
77
+ const content = "line 1\nline 2\nline 3";
78
+ const result = addLineNumbers(content, 50);
79
+
80
+ expect(result).toBe("50│ line 1\n51│ line 2\n52│ line 3");
81
+ });
82
+
83
+ it("should handle empty lines", () => {
84
+ const content = "line 1\n\nline 3";
85
+ const result = addLineNumbers(content);
86
+
87
+ expect(result).toBe("1│ line 1\n2│ \n3│ line 3");
88
+ });
89
+
90
+ it("should handle single line", () => {
91
+ const content = "single line";
92
+ const result = addLineNumbers(content);
93
+
94
+ expect(result).toBe("1│ single line");
95
+ });
96
+
97
+ it("should handle empty content", () => {
98
+ const content = "";
99
+ const result = addLineNumbers(content);
100
+
101
+ expect(result).toBe("1│ ");
102
+ });
103
+
104
+ it("should pad correctly when starting from high numbers", () => {
105
+ const content = "line 1\nline 2";
106
+ const result = addLineNumbers(content, 998);
107
+
108
+ expect(result).toBe("998│ line 1\n999│ line 2");
109
+ });
110
+ });
111
+
112
+ describe("filterByLineRange", () => {
113
+ it("should filter lines within range", () => {
114
+ const content = "line 1\nline 2\nline 3\nline 4\nline 5";
115
+ const result = filterByLineRange(content, 2, 4);
116
+
117
+ expect(result).toBe("line 2\nline 3\nline 4");
118
+ });
119
+
120
+ it("should handle start line 1", () => {
121
+ const content = "line 1\nline 2\nline 3";
122
+ const result = filterByLineRange(content, 1, 2);
123
+
124
+ expect(result).toBe("line 1\nline 2");
125
+ });
126
+
127
+ it("should handle range extending to end", () => {
128
+ const content = "line 1\nline 2\nline 3";
129
+ const result = filterByLineRange(content, 2, 5);
130
+
131
+ expect(result).toBe("line 2\nline 3");
132
+ });
133
+
134
+ it("should handle range starting beyond content length", () => {
135
+ const content = "line 1\nline 2\nline 3";
136
+ const result = filterByLineRange(content, 10, 20);
137
+
138
+ expect(result).toBe("");
139
+ });
140
+
141
+ it("should handle negative start line (clamps to 0)", () => {
142
+ const content = "line 1\nline 2\nline 3";
143
+ const result = filterByLineRange(content, -5, 2);
144
+
145
+ expect(result).toBe("line 1\nline 2");
146
+ });
147
+
148
+ it("should return single line when start equals end", () => {
149
+ const content = "line 1\nline 2\nline 3";
150
+ const result = filterByLineRange(content, 2, 2);
151
+
152
+ expect(result).toBe("line 2");
153
+ });
154
+
155
+ it("should handle entire content", () => {
156
+ const content = "line 1\nline 2\nline 3";
157
+ const result = filterByLineRange(content, 1, 3);
158
+
159
+ expect(result).toBe("line 1\nline 2\nline 3");
160
+ });
161
+ });
162
+
163
+ describe("buildSubjectsMarkdown", () => {
164
+ it("should build markdown for subject with title only", () => {
165
+ const subjects: Subject[] = [
166
+ {
167
+ id: "test-subject",
168
+ title: "Test Subject",
169
+ description: "",
170
+ imports: "",
171
+ prelude: [],
172
+ testInit: [],
173
+ examples: [],
174
+ sections: [],
175
+ },
176
+ ];
177
+
178
+ const result = buildSubjectsMarkdown(subjects);
179
+ expect(result).toBe("# Test Subject\n\n");
180
+ });
181
+
182
+ it("should include description when present", () => {
183
+ const subjects: Subject[] = [
184
+ {
185
+ id: "test-subject",
186
+ title: "Test Subject",
187
+ description: "This is a test description.",
188
+ imports: "",
189
+ prelude: [],
190
+ testInit: [],
191
+ examples: [],
192
+ sections: [],
193
+ },
194
+ ];
195
+
196
+ const result = buildSubjectsMarkdown(subjects);
197
+ expect(result).toContain("# Test Subject");
198
+ expect(result).toContain("This is a test description.");
199
+ });
200
+
201
+ it("should include imports block when present", () => {
202
+ const subjects: Subject[] = [
203
+ {
204
+ id: "test-subject",
205
+ title: "Test Subject",
206
+ description: "",
207
+ imports: 'import { x } from "y";',
208
+ prelude: [],
209
+ testInit: [],
210
+ examples: [],
211
+ sections: [],
212
+ },
213
+ ];
214
+
215
+ const result = buildSubjectsMarkdown(subjects);
216
+ expect(result).toContain("### Imports");
217
+ expect(result).toContain("```typescript");
218
+ expect(result).toContain('import { x } from "y";');
219
+ expect(result).toContain("```");
220
+ });
221
+
222
+ it("should include prelude blocks when present", () => {
223
+ const subjects: Subject[] = [
224
+ {
225
+ id: "test-subject",
226
+ title: "Test Subject",
227
+ description: "",
228
+ imports: "",
229
+ prelude: [
230
+ { code: "const schema = {};", id: "schema" },
231
+ { code: "const config = {};", id: "config" },
232
+ ],
233
+ testInit: [],
234
+ examples: [],
235
+ sections: [],
236
+ },
237
+ ];
238
+
239
+ const result = buildSubjectsMarkdown(subjects);
240
+ expect(result).toContain("### Prelude");
241
+ expect(result).toContain("const schema = {};");
242
+ expect(result).toContain("const config = {};");
243
+ // Should have multiple code blocks
244
+ expect(result.split("```typescript").length).toBe(3); // One opening for each block
245
+ });
246
+
247
+ it("should include sections with content", () => {
248
+ const subjects: Subject[] = [
249
+ {
250
+ id: "test-subject",
251
+ title: "Test Subject",
252
+ description: "",
253
+ imports: "",
254
+ prelude: [],
255
+ testInit: [],
256
+ examples: [],
257
+ sections: [
258
+ {
259
+ heading: "Basic Usage",
260
+ content: "This is how you use it.\n\n```typescript\nconst x = 1;\n```",
261
+ },
262
+ {
263
+ heading: "Advanced Usage",
264
+ content: "This is advanced usage.",
265
+ },
266
+ ],
267
+ },
268
+ ];
269
+
270
+ const result = buildSubjectsMarkdown(subjects);
271
+ expect(result).toContain("## Basic Usage");
272
+ expect(result).toContain("This is how you use it.");
273
+ expect(result).toContain("## Advanced Usage");
274
+ expect(result).toContain("This is advanced usage.");
275
+ });
276
+
277
+ it("should build markdown for multiple subjects", () => {
278
+ const subjects: Subject[] = [
279
+ {
280
+ id: "subject-1",
281
+ title: "Subject 1",
282
+ description: "First subject",
283
+ imports: "",
284
+ prelude: [],
285
+ testInit: [],
286
+ examples: [],
287
+ sections: [],
288
+ },
289
+ {
290
+ id: "subject-2",
291
+ title: "Subject 2",
292
+ description: "Second subject",
293
+ imports: "",
294
+ prelude: [],
295
+ testInit: [],
296
+ examples: [],
297
+ sections: [],
298
+ },
299
+ ];
300
+
301
+ const result = buildSubjectsMarkdown(subjects);
302
+ expect(result).toContain("# Subject 1");
303
+ expect(result).toContain("First subject");
304
+ expect(result).toContain("# Subject 2");
305
+ expect(result).toContain("Second subject");
306
+ });
307
+
308
+ it("should build complete markdown with all components", () => {
309
+ const subjects: Subject[] = [
310
+ {
311
+ id: "complete-subject",
312
+ title: "Complete Subject",
313
+ description: "A complete example",
314
+ imports: 'import { defineRoute } from "@fragno-dev/core";',
315
+ prelude: [{ code: "const schema = {};", id: "schema" }],
316
+ testInit: [],
317
+ examples: [
318
+ {
319
+ code: "const example = 1;",
320
+ explanation: "Example explanation",
321
+ id: "example-1",
322
+ },
323
+ ],
324
+ sections: [
325
+ {
326
+ heading: "Usage",
327
+ content: "Usage information here",
328
+ },
329
+ ],
330
+ },
331
+ ];
332
+
333
+ const result = buildSubjectsMarkdown(subjects);
334
+ expect(result).toContain("# Complete Subject");
335
+ expect(result).toContain("A complete example");
336
+ expect(result).toContain("### Imports");
337
+ expect(result).toContain('import { defineRoute } from "@fragno-dev/core";');
338
+ expect(result).toContain("### Prelude");
339
+ expect(result).toContain("const schema = {};");
340
+ expect(result).toContain("## Usage");
341
+ expect(result).toContain("Usage information here");
342
+ });
343
+ });
344
+
345
+ describe("buildSubjectsMarkdown - code blocks", () => {
346
+ it("should properly format code blocks in sections", () => {
347
+ const subjects: Subject[] = [
348
+ {
349
+ id: "test-subject",
350
+ title: "Test Subject",
351
+ description: "",
352
+ imports: "",
353
+ prelude: [],
354
+ testInit: [],
355
+ examples: [
356
+ {
357
+ code: "const x = 1;\nconst y = 2;",
358
+ explanation: "Example with multiline code",
359
+ id: "example-1",
360
+ },
361
+ ],
362
+ sections: [
363
+ {
364
+ heading: "Examples",
365
+ content:
366
+ "Here's an example:\n\n```typescript\nconst x = 1;\nconst y = 2;\n```\n\nThat's the example.",
367
+ },
368
+ ],
369
+ },
370
+ ];
371
+
372
+ const result = buildSubjectsMarkdown(subjects);
373
+ expect(result).toContain("## Examples");
374
+ expect(result).toContain("```typescript");
375
+ expect(result).toContain("const x = 1;");
376
+ expect(result).toContain("const y = 2;");
377
+ });
378
+
379
+ it("should handle sections with special markdown characters", () => {
380
+ const subjects: Subject[] = [
381
+ {
382
+ id: "test-subject",
383
+ title: "Test Subject",
384
+ description: "",
385
+ imports: "",
386
+ prelude: [],
387
+ testInit: [],
388
+ examples: [],
389
+ sections: [
390
+ {
391
+ heading: "Special Characters",
392
+ content: "Text with **bold** and *italic* and `code` inline.",
393
+ },
394
+ ],
395
+ },
396
+ ];
397
+
398
+ const result = buildSubjectsMarkdown(subjects);
399
+ expect(result).toContain("**bold**");
400
+ expect(result).toContain("*italic*");
401
+ expect(result).toContain("`code`");
402
+ });
403
+
404
+ it("should handle nested lists in sections", () => {
405
+ const subjects: Subject[] = [
406
+ {
407
+ id: "test-subject",
408
+ title: "Test Subject",
409
+ description: "",
410
+ imports: "",
411
+ prelude: [],
412
+ testInit: [],
413
+ examples: [],
414
+ sections: [
415
+ {
416
+ heading: "Lists",
417
+ content: "- Item 1\n- Item 2\n - Nested item\n- Item 3",
418
+ },
419
+ ],
420
+ },
421
+ ];
422
+
423
+ const result = buildSubjectsMarkdown(subjects);
424
+ expect(result).toContain("- Item 1");
425
+ expect(result).toContain("- Item 2");
426
+ expect(result).toContain(" - Nested item");
427
+ expect(result).toContain("- Item 3");
428
+ });
429
+
430
+ it("should handle code blocks with different languages", () => {
431
+ const subjects: Subject[] = [
432
+ {
433
+ id: "test-subject",
434
+ title: "Test Subject",
435
+ description: "",
436
+ imports: "",
437
+ prelude: [],
438
+ testInit: [],
439
+ examples: [],
440
+ sections: [
441
+ {
442
+ heading: "Multi-language Examples",
443
+ content: "TypeScript:\n\n```typescript\nconst x = 1;\n```\n\nJSON:\n\n```json\n{}\n```",
444
+ },
445
+ ],
446
+ },
447
+ ];
448
+
449
+ const result = buildSubjectsMarkdown(subjects);
450
+ expect(result).toContain("```typescript");
451
+ expect(result).toContain("```json");
452
+ });
453
+
454
+ it("should preserve code block indentation", () => {
455
+ const subjects: Subject[] = [
456
+ {
457
+ id: "test-subject",
458
+ title: "Test Subject",
459
+ description: "",
460
+ imports: "",
461
+ prelude: [
462
+ {
463
+ code: "function test() {\n const x = 1;\n return x;\n}",
464
+ id: "test-fn",
465
+ },
466
+ ],
467
+ testInit: [],
468
+ examples: [],
469
+ sections: [],
470
+ },
471
+ ];
472
+
473
+ const result = buildSubjectsMarkdown(subjects);
474
+ expect(result).toContain(" const x = 1;");
475
+ expect(result).toContain(" return x;");
476
+ });
477
+ });
478
+
479
+ describe("addLineNumbers - edge cases", () => {
480
+ it("should handle very long lines without breaking", () => {
481
+ const longLine = "a".repeat(1000);
482
+ const content = `short line\n${longLine}\nshort line`;
483
+ const result = addLineNumbers(content);
484
+
485
+ const lines = result.split("\n");
486
+ expect(lines).toHaveLength(3);
487
+ expect(lines[1]).toContain(longLine);
488
+ expect(lines[1]).toMatch(/^2│/);
489
+ });
490
+
491
+ it("should handle lines with tabs", () => {
492
+ const content = "line 1\n\tindented line\n\t\tdoubly indented";
493
+ const result = addLineNumbers(content);
494
+
495
+ expect(result).toContain("1│ line 1");
496
+ expect(result).toContain("2│ \tindented line");
497
+ expect(result).toContain("3│ \t\tdoubly indented");
498
+ });
499
+
500
+ it("should handle unicode characters", () => {
501
+ const content = "Hello 世界\nمرحبا\n🎉";
502
+ const result = addLineNumbers(content);
503
+
504
+ expect(result).toContain("1│ Hello 世界");
505
+ expect(result).toContain("2│ مرحبا");
506
+ expect(result).toContain("3│ 🎉");
507
+ });
508
+
509
+ it("should handle content with only newlines", () => {
510
+ const content = "\n\n\n";
511
+ const result = addLineNumbers(content);
512
+
513
+ const lines = result.split("\n");
514
+ expect(lines).toHaveLength(4);
515
+ expect(lines[0]).toBe("1│ ");
516
+ expect(lines[1]).toBe("2│ ");
517
+ expect(lines[2]).toBe("3│ ");
518
+ expect(lines[3]).toBe("4│ ");
519
+ });
520
+
521
+ it("should pad consistently for very large line numbers", () => {
522
+ const content = Array.from({ length: 1000 }, (_, i) => `line ${i + 1}`).join("\n");
523
+ const result = addLineNumbers(content);
524
+
525
+ const lines = result.split("\n");
526
+ // All lines should start with 4 digits + │
527
+ for (const line of lines) {
528
+ expect(line).toMatch(/^\s{0,3}\d{1,4}│/);
529
+ }
530
+
531
+ // First line should have 3 spaces of padding
532
+ expect(lines[0]).toMatch(/^\s{3}1│/);
533
+ // Line 1000 should have no padding
534
+ expect(lines[999]).toMatch(/^1000│/);
535
+ });
536
+ });
537
+
538
+ describe("filterByLineRange - edge cases", () => {
539
+ it("should handle Windows-style line endings", () => {
540
+ const content = "line 1\r\nline 2\r\nline 3";
541
+ const result = filterByLineRange(content, 2, 2);
542
+
543
+ // The split will include the \r
544
+ expect(result).toContain("line 2");
545
+ });
546
+
547
+ it("should handle empty lines at boundaries", () => {
548
+ const content = "\nline 2\n\nline 4\n";
549
+ const result = filterByLineRange(content, 2, 4);
550
+
551
+ expect(result).toBe("line 2\n\nline 4");
552
+ });
553
+
554
+ it("should handle single character per line", () => {
555
+ const content = "a\nb\nc\nd";
556
+ const result = filterByLineRange(content, 2, 3);
557
+
558
+ expect(result).toBe("b\nc");
559
+ });
560
+
561
+ it("should preserve whitespace-only lines", () => {
562
+ const content = "line 1\n \nline 3";
563
+ const result = filterByLineRange(content, 1, 3);
564
+
565
+ expect(result).toBe("line 1\n \nline 3");
566
+ });
567
+ });
568
+
569
+ describe("extractHeadingsAndBlocks", () => {
570
+ it("should extract title with line number", () => {
571
+ const subjects: Subject[] = [
572
+ {
573
+ id: "test-subject",
574
+ title: "Test Subject",
575
+ description: "",
576
+ imports: "",
577
+ prelude: [],
578
+ testInit: [],
579
+ examples: [],
580
+ sections: [],
581
+ },
582
+ ];
583
+
584
+ const result = extractHeadingsAndBlocks(subjects);
585
+ expect(result).toContain("1│ # Test Subject");
586
+ });
587
+
588
+ it("should show instruction header", () => {
589
+ const subjects: Subject[] = [
590
+ {
591
+ id: "test-subject",
592
+ title: "Test Subject",
593
+ description: "",
594
+ imports: "",
595
+ prelude: [],
596
+ testInit: [],
597
+ examples: [],
598
+ sections: [],
599
+ },
600
+ ];
601
+
602
+ const result = extractHeadingsAndBlocks(subjects);
603
+ expect(result).toContain("Use --start N --end N flags to show specific line ranges");
604
+ });
605
+
606
+ it("should include description with line numbers", () => {
607
+ const subjects: Subject[] = [
608
+ {
609
+ id: "test-subject",
610
+ title: "Test Subject",
611
+ description: "This is a description.",
612
+ imports: "",
613
+ prelude: [],
614
+ testInit: [],
615
+ examples: [],
616
+ sections: [],
617
+ },
618
+ ];
619
+
620
+ const result = extractHeadingsAndBlocks(subjects);
621
+ expect(result).toContain("This is a description.");
622
+ // Should have line numbers
623
+ expect(result).toMatch(/\d+│ This is a description\./);
624
+ });
625
+
626
+ it("should show imports heading with line number", () => {
627
+ const subjects: Subject[] = [
628
+ {
629
+ id: "test-subject",
630
+ title: "Test Subject",
631
+ description: "",
632
+ imports: 'import { x } from "y";',
633
+ prelude: [],
634
+ testInit: [],
635
+ examples: [],
636
+ sections: [],
637
+ },
638
+ ];
639
+
640
+ const result = extractHeadingsAndBlocks(subjects);
641
+ expect(result).toContain("### Imports");
642
+ expect(result).toMatch(/\d+│ ### Imports/);
643
+ });
644
+
645
+ it("should list prelude blocks with IDs and line ranges", () => {
646
+ const subjects: Subject[] = [
647
+ {
648
+ id: "test-subject",
649
+ title: "Test Subject",
650
+ description: "",
651
+ imports: "",
652
+ prelude: [
653
+ { code: "const schema = {};", id: "schema" },
654
+ { code: "const config = {\n key: 'value'\n};", id: "config" },
655
+ ],
656
+ testInit: [],
657
+ examples: [],
658
+ sections: [],
659
+ },
660
+ ];
661
+
662
+ const result = extractHeadingsAndBlocks(subjects);
663
+ expect(result).toContain("### Prelude");
664
+ expect(result).toContain("- id: `schema`");
665
+ expect(result).toContain("- id: `config`");
666
+ // Should show line ranges
667
+ expect(result).toMatch(/- id: `schema`, L\d+-\d+/);
668
+ expect(result).toMatch(/- id: `config`, L\d+-\d+/);
669
+ });
670
+
671
+ it("should extract section headings with line numbers", () => {
672
+ const subjects: Subject[] = [
673
+ {
674
+ id: "test-subject",
675
+ title: "Test Subject",
676
+ description: "",
677
+ imports: "",
678
+ prelude: [],
679
+ testInit: [],
680
+ examples: [],
681
+ sections: [
682
+ {
683
+ heading: "Basic Usage",
684
+ content: "Content here",
685
+ },
686
+ {
687
+ heading: "Advanced Usage",
688
+ content: "More content",
689
+ },
690
+ ],
691
+ },
692
+ ];
693
+
694
+ const result = extractHeadingsAndBlocks(subjects);
695
+ expect(result).toMatch(/\d+│ ## Basic Usage/);
696
+ expect(result).toMatch(/\d+│ ## Advanced Usage/);
697
+ });
698
+
699
+ it("should handle multiline descriptions", () => {
700
+ const subjects: Subject[] = [
701
+ {
702
+ id: "test-subject",
703
+ title: "Test Subject",
704
+ description: "Line 1 of description\nLine 2 of description\nLine 3 of description",
705
+ imports: "",
706
+ prelude: [],
707
+ testInit: [],
708
+ examples: [],
709
+ sections: [],
710
+ },
711
+ ];
712
+
713
+ const result = extractHeadingsAndBlocks(subjects);
714
+ expect(result).toContain("Line 1 of description");
715
+ expect(result).toContain("Line 2 of description");
716
+ expect(result).toContain("Line 3 of description");
717
+ // Each line should have a line number
718
+ const lines = result.split("\n").filter((line) => line.includes("of description"));
719
+ expect(lines).toHaveLength(3);
720
+ for (const line of lines) {
721
+ expect(line).toMatch(/\d+│/);
722
+ }
723
+ });
724
+
725
+ it("should show empty lines with line numbers", () => {
726
+ const subjects: Subject[] = [
727
+ {
728
+ id: "test-subject",
729
+ title: "Test Subject",
730
+ description: "",
731
+ imports: "",
732
+ prelude: [],
733
+ testInit: [],
734
+ examples: [],
735
+ sections: [],
736
+ },
737
+ ];
738
+
739
+ const result = extractHeadingsAndBlocks(subjects);
740
+ // There should be an empty line after title with line number
741
+ expect(result).toMatch(/2│\s*$/m);
742
+ });
743
+
744
+ it("should handle subjects without prelude or imports", () => {
745
+ const subjects: Subject[] = [
746
+ {
747
+ id: "test-subject",
748
+ title: "Test Subject",
749
+ description: "Simple description",
750
+ imports: "",
751
+ prelude: [],
752
+ testInit: [],
753
+ examples: [],
754
+ sections: [
755
+ {
756
+ heading: "Section 1",
757
+ content: "Content",
758
+ },
759
+ ],
760
+ },
761
+ ];
762
+
763
+ const result = extractHeadingsAndBlocks(subjects);
764
+ expect(result).toContain("# Test Subject");
765
+ expect(result).toContain("Simple description");
766
+ expect(result).toContain("## Section 1");
767
+ expect(result).not.toContain("### Imports");
768
+ expect(result).not.toContain("### Prelude");
769
+ });
770
+
771
+ it("should handle multiple subjects", () => {
772
+ const subjects: Subject[] = [
773
+ {
774
+ id: "subject-1",
775
+ title: "Subject 1",
776
+ description: "",
777
+ imports: "",
778
+ prelude: [],
779
+ testInit: [],
780
+ examples: [],
781
+ sections: [],
782
+ },
783
+ {
784
+ id: "subject-2",
785
+ title: "Subject 2",
786
+ description: "",
787
+ imports: "",
788
+ prelude: [],
789
+ testInit: [],
790
+ examples: [],
791
+ sections: [],
792
+ },
793
+ ];
794
+
795
+ const result = extractHeadingsAndBlocks(subjects);
796
+ expect(result).toContain("# Subject 1");
797
+ expect(result).toContain("# Subject 2");
798
+ });
799
+
800
+ it("should pad line numbers consistently", () => {
801
+ const subjects: Subject[] = [
802
+ {
803
+ id: "test-subject",
804
+ title: "Test Subject",
805
+ description: "",
806
+ imports: "",
807
+ prelude: [],
808
+ testInit: [],
809
+ examples: [],
810
+ sections: Array.from({ length: 20 }, (_, i) => ({
811
+ heading: `Section ${i + 1}`,
812
+ content: "Some content",
813
+ })),
814
+ },
815
+ ];
816
+
817
+ const result = extractHeadingsAndBlocks(subjects);
818
+ const lines = result.split("\n");
819
+ // Find lines with content (not just the header)
820
+ const numberedLines = lines.filter((line) => /^\s*\d+│/.test(line));
821
+
822
+ // Check that all line numbers have consistent padding
823
+ const lineNumbers = numberedLines.map((line) => {
824
+ const match = line.match(/^(\s*\d+)│/);
825
+ return match ? match[1] : "";
826
+ });
827
+
828
+ // All line number parts should have the same length
829
+ const lengths = lineNumbers.map((num) => num.length);
830
+ const maxLength = Math.max(...lengths);
831
+ for (const length of lengths) {
832
+ expect(length).toBe(maxLength);
833
+ }
834
+ });
835
+
836
+ it("should handle prelude blocks without IDs", () => {
837
+ const subjects: Subject[] = [
838
+ {
839
+ id: "test-subject",
840
+ title: "Test Subject",
841
+ description: "",
842
+ imports: "",
843
+ prelude: [
844
+ { code: "const x = 1;", id: undefined },
845
+ { code: "const y = 2;", id: "with-id" },
846
+ ],
847
+ testInit: [],
848
+ examples: [],
849
+ sections: [],
850
+ },
851
+ ];
852
+
853
+ const result = extractHeadingsAndBlocks(subjects);
854
+ expect(result).toContain("- id: `(no-id)`");
855
+ expect(result).toContain("- id: `with-id`");
856
+ });
857
+
858
+ it("should calculate correct line ranges for imports code", () => {
859
+ const subjects: Subject[] = [
860
+ {
861
+ id: "test-subject",
862
+ title: "Test Subject",
863
+ description: "",
864
+ imports: 'import { a } from "a";\nimport { b } from "b";',
865
+ prelude: [],
866
+ testInit: [],
867
+ examples: [],
868
+ sections: [],
869
+ },
870
+ ];
871
+
872
+ const result = extractHeadingsAndBlocks(subjects);
873
+ // Should show imports section
874
+ expect(result).toContain("### Imports");
875
+ // Should show the code fence line
876
+ expect(result).toContain("```typescript");
877
+ });
878
+ });
879
+
880
+ describe("integration - line numbers with filtering", () => {
881
+ it("should correctly filter and then add line numbers", () => {
882
+ const content = "line 1\nline 2\nline 3\nline 4\nline 5";
883
+
884
+ // Filter to lines 2-4
885
+ const filtered = filterByLineRange(content, 2, 4);
886
+ expect(filtered).toBe("line 2\nline 3\nline 4");
887
+
888
+ // Add line numbers starting from 2
889
+ const numbered = addLineNumbers(filtered, 2);
890
+ expect(numbered).toBe("2│ line 2\n3│ line 3\n4│ line 4");
891
+ });
892
+
893
+ it("should handle filtering single line then adding line numbers", () => {
894
+ const content = "line 1\nline 2\nline 3";
895
+
896
+ const filtered = filterByLineRange(content, 2, 2);
897
+ const numbered = addLineNumbers(filtered, 2);
898
+
899
+ expect(numbered).toBe("2│ line 2");
900
+ });
901
+
902
+ it("should preserve content integrity through filter and number pipeline", () => {
903
+ const content = Array.from({ length: 50 }, (_, i) => `content line ${i + 1}`).join("\n");
904
+
905
+ // Filter middle section
906
+ const filtered = filterByLineRange(content, 20, 30);
907
+ const lines = filtered.split("\n");
908
+ expect(lines).toHaveLength(11); // 20 through 30 inclusive
909
+
910
+ // Add line numbers
911
+ const numbered = addLineNumbers(filtered, 20);
912
+ const numberedLines = numbered.split("\n");
913
+
914
+ // Verify first and last lines
915
+ expect(numberedLines[0]).toContain("20│ content line 20");
916
+ expect(numberedLines[10]).toContain("30│ content line 30");
917
+ });
918
+ });
919
+
920
+ describe("integration - markdown building with special content", () => {
921
+ it("should handle subjects with all possible components", () => {
922
+ const subjects: Subject[] = [
923
+ {
924
+ id: "full-subject",
925
+ title: "Complete Test Subject",
926
+ description: "First line\nSecond line\nThird line",
927
+ imports: 'import { x } from "x";\nimport { y } from "y";',
928
+ prelude: [
929
+ { code: "const schema = {};", id: "schema" },
930
+ { code: "const config = {\n key: 'value'\n};", id: "config" },
931
+ ],
932
+ testInit: [{ code: "setup();" }],
933
+ examples: [
934
+ {
935
+ code: "const example1 = 1;",
936
+ explanation: "First example",
937
+ id: "ex1",
938
+ },
939
+ {
940
+ code: "const example2 = 2;",
941
+ explanation: "Second example",
942
+ id: "ex2",
943
+ },
944
+ ],
945
+ sections: [
946
+ {
947
+ heading: "Getting Started",
948
+ content: "Introduction paragraph.\n\nMore details here.",
949
+ },
950
+ {
951
+ heading: "Advanced Usage",
952
+ content: "Advanced content with code:\n\n```typescript\nadvanced code;\n```",
953
+ },
954
+ ],
955
+ },
956
+ ];
957
+
958
+ const markdown = buildSubjectsMarkdown(subjects);
959
+
960
+ // Verify all components are present
961
+ expect(markdown).toContain("# Complete Test Subject");
962
+ expect(markdown).toContain("First line\nSecond line\nThird line");
963
+ expect(markdown).toContain("### Imports");
964
+ expect(markdown).toContain('import { x } from "x";');
965
+ expect(markdown).toContain("### Prelude");
966
+ expect(markdown).toContain("const schema = {};");
967
+ expect(markdown).toContain("const config");
968
+ expect(markdown).toContain("## Getting Started");
969
+ expect(markdown).toContain("## Advanced Usage");
970
+
971
+ // Verify structure - title should come before sections
972
+ const titleIndex = markdown.indexOf("# Complete Test Subject");
973
+ const sectionIndex = markdown.indexOf("## Getting Started");
974
+ expect(titleIndex).toBeLessThan(sectionIndex);
975
+ });
976
+
977
+ it("should build consistent markdown for empty subjects", () => {
978
+ const subjects: Subject[] = [
979
+ {
980
+ id: "empty",
981
+ title: "Empty Subject",
982
+ description: "",
983
+ imports: "",
984
+ prelude: [],
985
+ testInit: [],
986
+ examples: [],
987
+ sections: [],
988
+ },
989
+ ];
990
+
991
+ const markdown = buildSubjectsMarkdown(subjects);
992
+ expect(markdown).toBe("# Empty Subject\n\n");
993
+ });
994
+
995
+ it("should properly separate multiple subjects in markdown", () => {
996
+ const subjects: Subject[] = [
997
+ {
998
+ id: "first",
999
+ title: "First Subject",
1000
+ description: "First description",
1001
+ imports: "",
1002
+ prelude: [],
1003
+ testInit: [],
1004
+ examples: [],
1005
+ sections: [],
1006
+ },
1007
+ {
1008
+ id: "second",
1009
+ title: "Second Subject",
1010
+ description: "Second description",
1011
+ imports: "",
1012
+ prelude: [],
1013
+ testInit: [],
1014
+ examples: [],
1015
+ sections: [],
1016
+ },
1017
+ ];
1018
+
1019
+ const markdown = buildSubjectsMarkdown(subjects);
1020
+
1021
+ // Both subjects should be present
1022
+ expect(markdown).toContain("# First Subject");
1023
+ expect(markdown).toContain("# Second Subject");
1024
+
1025
+ // They should be in order
1026
+ const firstIndex = markdown.indexOf("# First Subject");
1027
+ const secondIndex = markdown.indexOf("# Second Subject");
1028
+ expect(firstIndex).toBeLessThan(secondIndex);
1029
+ });
1030
+ });
1031
+
1032
+ describe("edge cases - line number display width", () => {
1033
+ it("should handle transition from 9 to 10 lines correctly", () => {
1034
+ const content = Array.from({ length: 10 }, (_, i) => `line ${i + 1}`).join("\n");
1035
+ const result = addLineNumbers(content);
1036
+
1037
+ const lines = result.split("\n");
1038
+ // Lines 1-9 should have 1 space padding
1039
+ expect(lines[0]).toMatch(/^ 1│/);
1040
+ expect(lines[8]).toMatch(/^ 9│/);
1041
+ // Line 10 should have no padding
1042
+ expect(lines[9]).toMatch(/^10│/);
1043
+ });
1044
+
1045
+ it("should handle transition from 99 to 100 lines correctly", () => {
1046
+ const content = Array.from({ length: 100 }, (_, i) => `line ${i + 1}`).join("\n");
1047
+ const result = addLineNumbers(content);
1048
+
1049
+ const lines = result.split("\n");
1050
+ // Lines 1-9 should have 2 spaces
1051
+ expect(lines[0]).toMatch(/^ {2}1│/);
1052
+ // Lines 10-99 should have 1 space
1053
+ expect(lines[9]).toMatch(/^ 10│/);
1054
+ expect(lines[98]).toMatch(/^ 99│/);
1055
+ // Line 100 should have no padding
1056
+ expect(lines[99]).toMatch(/^100│/);
1057
+ });
1058
+
1059
+ it("should handle transition from 999 to 1000 lines correctly", () => {
1060
+ const content = Array.from({ length: 1000 }, (_, i) => `line ${i + 1}`).join("\n");
1061
+ const result = addLineNumbers(content);
1062
+
1063
+ const lines = result.split("\n");
1064
+ // Line 1 should have 3 spaces
1065
+ expect(lines[0]).toMatch(/^ {3}1│/);
1066
+ // Line 999 should have no padding
1067
+ expect(lines[998]).toMatch(/^ 999│/);
1068
+ // Line 1000 should have no padding
1069
+ expect(lines[999]).toMatch(/^1000│/);
1070
+ });
1071
+ });
1072
+
1073
+ describe("extractHeadingsAndBlocks - complex scenarios", () => {
1074
+ it("should handle examples that match sections", () => {
1075
+ const subjects: Subject[] = [
1076
+ {
1077
+ id: "test",
1078
+ title: "Test",
1079
+ description: "",
1080
+ imports: "",
1081
+ prelude: [],
1082
+ testInit: [],
1083
+ examples: [
1084
+ {
1085
+ code: "const user = { name: 'John' };",
1086
+ explanation: "Create a user",
1087
+ id: "create-user",
1088
+ },
1089
+ ],
1090
+ sections: [
1091
+ {
1092
+ heading: "User Management",
1093
+ content:
1094
+ "Here's how to create a user:\n\n```typescript\nconst user = { name: 'John' };\n```",
1095
+ },
1096
+ ],
1097
+ },
1098
+ ];
1099
+
1100
+ const result = extractHeadingsAndBlocks(subjects);
1101
+ expect(result).toContain("## User Management");
1102
+ expect(result).toContain("- id: `create-user`");
1103
+ });
1104
+
1105
+ it("should handle sections with no matching examples", () => {
1106
+ const subjects: Subject[] = [
1107
+ {
1108
+ id: "test",
1109
+ title: "Test",
1110
+ description: "",
1111
+ imports: "",
1112
+ prelude: [],
1113
+ testInit: [],
1114
+ examples: [],
1115
+ sections: [
1116
+ {
1117
+ heading: "Overview",
1118
+ content: "This is just text, no code examples.",
1119
+ },
1120
+ ],
1121
+ },
1122
+ ];
1123
+
1124
+ const result = extractHeadingsAndBlocks(subjects);
1125
+ expect(result).toContain("## Overview");
1126
+ // Should not have any example IDs listed
1127
+ expect(result).not.toContain("- id:");
1128
+ });
1129
+ });