@aigne/doc-smith 0.5.1 → 0.7.0

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 (42) hide show
  1. package/.github/workflows/ci.yml +46 -0
  2. package/.github/workflows/reviewer.yml +2 -1
  3. package/CHANGELOG.md +17 -0
  4. package/agents/chat.yaml +30 -0
  5. package/agents/check-detail-result.mjs +2 -1
  6. package/agents/check-detail.mjs +1 -0
  7. package/agents/check-structure-plan.mjs +1 -1
  8. package/agents/docs-fs.yaml +25 -0
  9. package/agents/exit.mjs +6 -0
  10. package/agents/feedback-refiner.yaml +5 -1
  11. package/agents/find-items-by-paths.mjs +10 -4
  12. package/agents/fs.mjs +60 -0
  13. package/agents/input-generator.mjs +159 -90
  14. package/agents/load-config.mjs +0 -5
  15. package/agents/load-sources.mjs +119 -12
  16. package/agents/publish-docs.mjs +28 -11
  17. package/agents/retranslate.yaml +1 -1
  18. package/agents/team-publish-docs.yaml +2 -2
  19. package/aigne.yaml +1 -0
  20. package/package.json +13 -10
  21. package/prompts/content-detail-generator.md +12 -4
  22. package/prompts/document/custom-components.md +80 -0
  23. package/prompts/document/d2-chart/diy-examples.md +44 -0
  24. package/prompts/document/d2-chart/official-examples.md +708 -0
  25. package/prompts/document/d2-chart/rules.md +48 -0
  26. package/prompts/document/detail-generator.md +13 -15
  27. package/prompts/document/structure-planning.md +1 -3
  28. package/prompts/feedback-refiner.md +81 -60
  29. package/prompts/structure-planning.md +20 -3
  30. package/tests/check-detail-result.test.mjs +50 -2
  31. package/tests/conflict-resolution.test.mjs +237 -0
  32. package/tests/input-generator.test.mjs +940 -0
  33. package/tests/load-sources.test.mjs +627 -3
  34. package/tests/preferences-utils.test.mjs +94 -0
  35. package/tests/save-value-to-config.test.mjs +182 -5
  36. package/tests/utils.test.mjs +49 -0
  37. package/utils/auth-utils.mjs +1 -1
  38. package/utils/conflict-detector.mjs +72 -1
  39. package/utils/constants.mjs +139 -126
  40. package/utils/kroki-utils.mjs +162 -0
  41. package/utils/markdown-checker.mjs +175 -67
  42. package/utils/utils.mjs +97 -29
@@ -1,4 +1,4 @@
1
- import { afterEach, beforeEach, describe, expect, test } from "bun:test";
1
+ import { afterAll, afterEach, beforeEach, describe, expect, test } from "bun:test";
2
2
  import { mkdir, rm, writeFile } from "node:fs/promises";
3
3
  import path, { dirname } from "node:path";
4
4
  import { fileURLToPath } from "node:url";
@@ -95,8 +95,56 @@ describe("loadSources", () => {
95
95
  });
96
96
 
97
97
  afterEach(async () => {
98
- // Clean up test directory
99
- await rm(testDir, { recursive: true, force: true });
98
+ // Clean up test directory with comprehensive handling
99
+ try {
100
+ await rm(testDir, { recursive: true, force: true });
101
+ } catch {
102
+ // Try multiple cleanup strategies
103
+ try {
104
+ const { chmod, readdir, unlink } = await import("node:fs/promises");
105
+
106
+ // Reset permissions recursively
107
+ const resetPermissions = async (dir) => {
108
+ try {
109
+ await chmod(dir, 0o755);
110
+ const entries = await readdir(dir, { withFileTypes: true });
111
+ for (const entry of entries) {
112
+ const fullPath = path.join(dir, entry.name);
113
+ if (entry.isDirectory()) {
114
+ await resetPermissions(fullPath);
115
+ } else {
116
+ try {
117
+ await chmod(fullPath, 0o644);
118
+ } catch {
119
+ // Try to remove symbolic links directly
120
+ try {
121
+ await unlink(fullPath);
122
+ } catch {
123
+ // Ignore individual file cleanup failures
124
+ }
125
+ }
126
+ }
127
+ }
128
+ } catch {
129
+ // Ignore permission reset failures
130
+ }
131
+ };
132
+
133
+ await resetPermissions(testDir);
134
+ await rm(testDir, { recursive: true, force: true });
135
+ } catch {
136
+ // Final fallback - try to remove with system command
137
+ try {
138
+ const { exec } = await import("node:child_process");
139
+ const { promisify } = await import("node:util");
140
+ const execAsync = promisify(exec);
141
+ await execAsync(`rm -rf "${testDir}"`);
142
+ } catch {
143
+ // Ignore final cleanup failures - test isolation is more important than perfect cleanup
144
+ console.warn(`Warning: Could not fully clean up test directory: ${testDir}`);
145
+ }
146
+ }
147
+ }
100
148
  });
101
149
 
102
150
  test("should load files with default patterns", async () => {
@@ -355,4 +403,580 @@ describe("loadSources", () => {
355
403
  expect(filePaths.some((element) => element.includes("temp/"))).toBe(false);
356
404
  expect(filePaths.some((element) => element.includes("test/test.js"))).toBe(false);
357
405
  });
406
+
407
+ test("should handle glob patterns in sourcesPath", async () => {
408
+ const result = await loadSources({
409
+ sourcesPath: [`${testDir}/src/**/*.js`],
410
+ useDefaultPatterns: false,
411
+ outputDir: tempDir,
412
+ docsDir: path.join(testDir, "docs"),
413
+ });
414
+
415
+ expect(result.datasourcesList).toBeDefined();
416
+ expect(result.datasourcesList.length).toBeGreaterThan(0);
417
+
418
+ const filePaths = result.datasourcesList.map((f) => f.sourceId);
419
+
420
+ // Should include JS files from src directory and subdirectories
421
+ expect(filePaths.some((element) => element.includes("src/index.js"))).toBe(true);
422
+ expect(filePaths.some((element) => element.includes("src/utils.js"))).toBe(true);
423
+ expect(filePaths.some((element) => element.includes("src/components/Button.js"))).toBe(true);
424
+ expect(filePaths.some((element) => element.includes("src/components/ui/Modal.js"))).toBe(true);
425
+ expect(filePaths.some((element) => element.includes("src/utils/helpers/format.js"))).toBe(true);
426
+
427
+ // Should exclude non-JS files and files outside src
428
+ expect(filePaths.some((element) => element.includes("package.json"))).toBe(false);
429
+ expect(filePaths.some((element) => element.includes("README.md"))).toBe(false);
430
+ expect(filePaths.some((element) => element.includes("styles.css"))).toBe(false);
431
+ });
432
+
433
+ test("should handle multiple glob patterns in sourcesPath", async () => {
434
+ const result = await loadSources({
435
+ sourcesPath: [`${testDir}/src/**/*.js`, `${testDir}/*.json`, `${testDir}/*.md`],
436
+ useDefaultPatterns: false,
437
+ outputDir: tempDir,
438
+ docsDir: path.join(testDir, "docs"),
439
+ });
440
+
441
+ expect(result.datasourcesList).toBeDefined();
442
+ expect(result.datasourcesList.length).toBeGreaterThan(0);
443
+
444
+ const filePaths = result.datasourcesList.map((f) => f.sourceId);
445
+
446
+ // Should include JS files from src directory
447
+ expect(filePaths.some((element) => element.includes("src/index.js"))).toBe(true);
448
+ expect(filePaths.some((element) => element.includes("src/components/Button.js"))).toBe(true);
449
+
450
+ // Should include JSON files from root
451
+ expect(filePaths.some((element) => element.includes("package.json"))).toBe(true);
452
+
453
+ // Should include Markdown files from root
454
+ expect(filePaths.some((element) => element.includes("README.md"))).toBe(true);
455
+
456
+ // Should exclude CSS files
457
+ expect(filePaths.some((element) => element.includes("styles.css"))).toBe(false);
458
+ });
459
+
460
+ test("should handle glob pattern with specific file extensions", async () => {
461
+ const result = await loadSources({
462
+ sourcesPath: [`${testDir}/src/**/*.{js,json,yaml}`],
463
+ useDefaultPatterns: false,
464
+ outputDir: tempDir,
465
+ docsDir: path.join(testDir, "docs"),
466
+ });
467
+
468
+ expect(result.datasourcesList).toBeDefined();
469
+
470
+ const filePaths = result.datasourcesList.map((f) => f.sourceId);
471
+
472
+ // Should include JS files
473
+ expect(filePaths.some((element) => element.includes("src/index.js"))).toBe(true);
474
+ expect(filePaths.some((element) => element.includes("src/utils.js"))).toBe(true);
475
+
476
+ // Should include JSON files
477
+ expect(filePaths.some((element) => element.includes("src/config/settings.json"))).toBe(true);
478
+
479
+ // Should include YAML files
480
+ expect(filePaths.some((element) => element.includes("src/utils/helpers/data.yaml"))).toBe(true);
481
+
482
+ // Should exclude CSS files
483
+ expect(filePaths.some((element) => element.includes("styles.css"))).toBe(false);
484
+ });
485
+
486
+ test("should handle glob pattern that matches no files", async () => {
487
+ const result = await loadSources({
488
+ sourcesPath: [`${testDir}/nonexistent/**/*.xyz`],
489
+ useDefaultPatterns: false,
490
+ outputDir: tempDir,
491
+ docsDir: path.join(testDir, "docs"),
492
+ });
493
+
494
+ expect(result.datasourcesList).toBeDefined();
495
+ expect(result.datasourcesList.length).toBe(0);
496
+ });
497
+
498
+ test("should handle mixed regular paths and glob patterns", async () => {
499
+ const result = await loadSources({
500
+ sourcesPath: [testDir, `${testDir}/src/**/*.js`],
501
+ includePatterns: ["*.md"],
502
+ useDefaultPatterns: false,
503
+ outputDir: tempDir,
504
+ docsDir: path.join(testDir, "docs"),
505
+ });
506
+
507
+ expect(result.datasourcesList).toBeDefined();
508
+ expect(result.datasourcesList.length).toBeGreaterThan(0);
509
+
510
+ const filePaths = result.datasourcesList.map((f) => f.sourceId);
511
+
512
+ // Should include markdown files from directory scan
513
+ expect(filePaths.some((element) => element.includes("README.md"))).toBe(true);
514
+
515
+ // Should include JS files from glob pattern
516
+ expect(filePaths.some((element) => element.includes("src/index.js"))).toBe(true);
517
+ expect(filePaths.some((element) => element.includes("src/components/Button.js"))).toBe(true);
518
+ });
519
+
520
+ test("should handle glob patterns with wildcards and character classes", async () => {
521
+ const result = await loadSources({
522
+ sourcesPath: [`${testDir}/src/**/[Bb]utton.js`, `${testDir}/src/**/?odal.js`],
523
+ useDefaultPatterns: false,
524
+ outputDir: tempDir,
525
+ docsDir: path.join(testDir, "docs"),
526
+ });
527
+
528
+ expect(result.datasourcesList).toBeDefined();
529
+
530
+ const filePaths = result.datasourcesList.map((f) => f.sourceId);
531
+
532
+ // Should match Button.js with character class
533
+ expect(filePaths.some((element) => element.includes("src/components/Button.js"))).toBe(true);
534
+
535
+ // Should match Modal.js with single character wildcard
536
+ expect(filePaths.some((element) => element.includes("src/components/ui/Modal.js"))).toBe(true);
537
+
538
+ // Should not match other files
539
+ expect(filePaths.some((element) => element.includes("src/index.js"))).toBe(false);
540
+ expect(filePaths.some((element) => element.includes("src/utils.js"))).toBe(false);
541
+ });
542
+
543
+ describe("Configuration integration tests", () => {
544
+ test("should handle YAML config-like sourcesPath input", async () => {
545
+ const yamlConfigInput = {
546
+ sourcesPath: ["./src", "./lib", "**/*.{js,ts,jsx,tsx}"],
547
+ docsDir: "./docs",
548
+ useDefaultPatterns: true,
549
+ outputDir: tempDir,
550
+ };
551
+
552
+ const result = await loadSources(yamlConfigInput);
553
+
554
+ expect(result.datasourcesList).toBeDefined();
555
+ expect(result.datasourcesList.length).toBeGreaterThan(0);
556
+
557
+ const filePaths = result.datasourcesList.map((f) => f.sourceId);
558
+ expect(filePaths.some((element) => element.includes("src/index.js"))).toBe(true);
559
+ });
560
+
561
+ test("should handle config with empty sourcesPath array - ACTUALLY WORKS", async () => {
562
+ // This test shows that empty arrays are actually handled correctly
563
+ const result = await loadSources({
564
+ sourcesPath: [],
565
+ useDefaultPatterns: true,
566
+ outputDir: tempDir,
567
+ docsDir: path.join(testDir, "docs"),
568
+ });
569
+
570
+ expect(result.datasourcesList).toBeDefined();
571
+ expect(result.datasourcesList.length).toBe(0);
572
+ expect(result.files.length).toBe(0);
573
+
574
+ // Empty arrays are handled gracefully - this is correct behavior
575
+ });
576
+
577
+ test("should filter invalid path elements gracefully - EXPOSES ROBUSTNESS BUG", async () => {
578
+ // ROBUSTNESS BUG TEST: Function should handle invalid elements gracefully
579
+ const mixedArray = ["./src", null, undefined, "./lib", ""];
580
+
581
+ // This test expects the function to filter out invalid elements and work with valid ones
582
+ const result = await loadSources({
583
+ sourcesPath: mixedArray,
584
+ useDefaultPatterns: true,
585
+ outputDir: tempDir,
586
+ docsDir: path.join(testDir, "docs"),
587
+ });
588
+
589
+ // Should successfully process valid paths and ignore invalid ones
590
+ expect(result.datasourcesList).toBeDefined();
591
+ // This test will FAIL because the function crashes instead of filtering
592
+ });
593
+
594
+ test("should handle config with undefined sourcesPath - ACTUALLY WORKS", async () => {
595
+ // This test shows undefined sourcesPath is actually handled correctly
596
+ const result = await loadSources({
597
+ sourcesPath: undefined,
598
+ useDefaultPatterns: true,
599
+ outputDir: tempDir,
600
+ docsDir: path.join(testDir, "docs"),
601
+ });
602
+
603
+ expect(result.datasourcesList).toBeDefined();
604
+ expect(Array.isArray(result.datasourcesList)).toBe(true);
605
+ expect(result.datasourcesList.length).toBe(0);
606
+
607
+ // undefined is handled gracefully by skipping sourcesPath processing
608
+ });
609
+ });
610
+
611
+ describe("Function robustness issues - EXPOSES INPUT HANDLING PROBLEMS", () => {
612
+ test("should handle invalid inputs gracefully - EXPOSES POOR ERROR HANDLING", async () => {
613
+ // ROBUSTNESS TEST: Function should validate inputs and provide helpful errors
614
+ const invalidInputs = [
615
+ { value: [null], description: "array with null element", shouldFail: true },
616
+ { value: [undefined], description: "array with undefined element", shouldFail: true },
617
+ { value: ["", null], description: "mixed array with null", shouldFail: true },
618
+ ];
619
+
620
+ for (const { value: invalidInput, shouldFail } of invalidInputs) {
621
+ if (shouldFail) {
622
+ // These should provide helpful error messages, not crash unexpectedly
623
+ const result = await loadSources({
624
+ sourcesPath: invalidInput,
625
+ useDefaultPatterns: true,
626
+ outputDir: tempDir,
627
+ docsDir: path.join(testDir, "docs"),
628
+ });
629
+ expect(result.datasourcesList).toBeDefined();
630
+ expect(Array.isArray(result.datasourcesList)).toBe(true);
631
+ expect(result.datasourcesList.length).toBe(0);
632
+ }
633
+ }
634
+
635
+ // Test inputs that should work but be handled gracefully
636
+ const shouldWorkInputs = [
637
+ null, // Should be treated like undefined
638
+ "", // Should be treated like undefined
639
+ 123, // Should provide helpful error
640
+ {}, // Should provide helpful error
641
+ [""], // Should filter out empty strings
642
+ ];
643
+
644
+ for (const validInput of shouldWorkInputs) {
645
+ // These tests expect graceful handling but may FAIL due to poor input validation
646
+ const result = await loadSources({
647
+ sourcesPath: validInput,
648
+ useDefaultPatterns: true,
649
+ outputDir: tempDir,
650
+ docsDir: path.join(testDir, "docs"),
651
+ });
652
+
653
+ expect(result.datasourcesList).toBeDefined();
654
+ // Some of these tests will FAIL, exposing input validation issues
655
+ }
656
+ });
657
+ });
658
+
659
+ describe("File system edge cases", () => {
660
+ test("should handle symbolic links", async () => {
661
+ // Skip on systems that don't support symlinks
662
+ try {
663
+ const { symlink } = await import("node:fs/promises");
664
+ const symlinkPath = path.join(testDir, "symlink-to-src");
665
+ await symlink(path.join(testDir, "src"), symlinkPath);
666
+
667
+ const result = await loadSources({
668
+ sourcesPath: symlinkPath,
669
+ includePatterns: ["*.js"],
670
+ useDefaultPatterns: false,
671
+ outputDir: tempDir,
672
+ docsDir: path.join(testDir, "docs"),
673
+ });
674
+
675
+ expect(result.datasourcesList).toBeDefined();
676
+ const filePaths = result.datasourcesList.map((f) => f.sourceId);
677
+ expect(filePaths.some((element) => element.includes("index.js"))).toBe(true);
678
+ } catch {
679
+ // Skip test on systems that don't support symlinks
680
+ expect(true).toBe(true);
681
+ }
682
+ });
683
+
684
+ test("should handle very deep directory structures", async () => {
685
+ // Create deep nested structure
686
+ const deepPath = path.join(testDir, "a/b/c/d/e/f/g/h");
687
+ await mkdir(deepPath, { recursive: true });
688
+ await writeFile(path.join(deepPath, "deep.js"), "console.log('deep');");
689
+
690
+ const result = await loadSources({
691
+ sourcesPath: testDir,
692
+ includePatterns: ["**/*.js"],
693
+ useDefaultPatterns: false,
694
+ outputDir: tempDir,
695
+ docsDir: path.join(testDir, "docs"),
696
+ });
697
+
698
+ expect(result.datasourcesList).toBeDefined();
699
+ const filePaths = result.datasourcesList.map((f) => f.sourceId);
700
+ expect(filePaths.some((element) => element.includes("a/b/c/d/e/f/g/h/deep.js"))).toBe(true);
701
+ });
702
+
703
+ test("should handle files with unusual names", async () => {
704
+ // Create files with special characters in names
705
+ await writeFile(path.join(testDir, "file with spaces.js"), "console.log('spaces');");
706
+ await writeFile(path.join(testDir, "file-with-dashes.js"), "console.log('dashes');");
707
+ await writeFile(
708
+ path.join(testDir, "file_with_underscores.js"),
709
+ "console.log('underscores');",
710
+ );
711
+ await writeFile(path.join(testDir, "file.with.dots.js"), "console.log('dots');");
712
+ await writeFile(path.join(testDir, "文件中文名.js"), "console.log('chinese');");
713
+
714
+ const result = await loadSources({
715
+ sourcesPath: testDir,
716
+ includePatterns: ["*.js"],
717
+ excludePatterns: ["**/test/**", "*_test*"],
718
+ useDefaultPatterns: false,
719
+ outputDir: tempDir,
720
+ docsDir: path.join(testDir, "docs"),
721
+ });
722
+
723
+ expect(result.datasourcesList).toBeDefined();
724
+ const filePaths = result.datasourcesList.map((f) => f.sourceId);
725
+
726
+ expect(filePaths.some((element) => element.includes("file with spaces.js"))).toBe(true);
727
+ expect(filePaths.some((element) => element.includes("file-with-dashes.js"))).toBe(true);
728
+ expect(filePaths.some((element) => element.includes("file_with_underscores.js"))).toBe(true);
729
+ expect(filePaths.some((element) => element.includes("file.with.dots.js"))).toBe(true);
730
+ expect(filePaths.some((element) => element.includes("文件中文名.js"))).toBe(true);
731
+ });
732
+
733
+ test("should handle empty directories", async () => {
734
+ const emptyDir = path.join(testDir, "empty");
735
+ await mkdir(emptyDir, { recursive: true });
736
+
737
+ const result = await loadSources({
738
+ sourcesPath: emptyDir,
739
+ useDefaultPatterns: true,
740
+ outputDir: tempDir,
741
+ docsDir: path.join(testDir, "docs"),
742
+ });
743
+
744
+ expect(result.datasourcesList).toBeDefined();
745
+ expect(result.datasourcesList.length).toBe(0);
746
+ });
747
+ });
748
+
749
+ describe("Pattern matching edge cases", () => {
750
+ test("should handle negation patterns correctly", async () => {
751
+ const result = await loadSources({
752
+ sourcesPath: testDir,
753
+ includePatterns: ["**/*.js"],
754
+ excludePatterns: ["!src/components/**"], // Negation should include
755
+ useDefaultPatterns: false,
756
+ outputDir: tempDir,
757
+ docsDir: path.join(testDir, "docs"),
758
+ });
759
+
760
+ expect(result.datasourcesList).toBeDefined();
761
+ const filePaths = result.datasourcesList.map((f) => f.sourceId);
762
+
763
+ // Should still exclude test files by default gitignore/patterns
764
+ expect(filePaths.some((element) => element.includes("src/index.js"))).toBe(true);
765
+ expect(filePaths.some((element) => element.includes("src/components/Button.js"))).toBe(true);
766
+ });
767
+
768
+ test("should handle conflicting include/exclude patterns", async () => {
769
+ const result = await loadSources({
770
+ sourcesPath: testDir,
771
+ includePatterns: ["src/**/*.js"], // Include all JS in src
772
+ excludePatterns: ["src/**/*.js"], // But exclude all JS in src
773
+ useDefaultPatterns: false,
774
+ outputDir: tempDir,
775
+ docsDir: path.join(testDir, "docs"),
776
+ });
777
+
778
+ expect(result.datasourcesList).toBeDefined();
779
+ const filePaths = result.datasourcesList.map((f) => f.sourceId);
780
+
781
+ // Exclude should win over include
782
+ expect(filePaths.some((element) => element.includes("src/index.js"))).toBe(false);
783
+ expect(filePaths.some((element) => element.includes("src/components/Button.js"))).toBe(false);
784
+ });
785
+
786
+ test("should handle case sensitivity in patterns", async () => {
787
+ // Create files with different cases
788
+ await writeFile(path.join(testDir, "CamelCase.js"), "console.log('CamelCase');");
789
+ await writeFile(path.join(testDir, "lowercase.js"), "console.log('lowercase');");
790
+ await writeFile(path.join(testDir, "UPPERCASE.JS"), "console.log('UPPERCASE');");
791
+
792
+ const result = await loadSources({
793
+ sourcesPath: testDir,
794
+ includePatterns: ["*.[jJ][sS]"], // Case insensitive for extension
795
+ useDefaultPatterns: false,
796
+ outputDir: tempDir,
797
+ docsDir: path.join(testDir, "docs"),
798
+ });
799
+
800
+ expect(result.datasourcesList).toBeDefined();
801
+ const filePaths = result.datasourcesList.map((f) => f.sourceId);
802
+
803
+ expect(filePaths.some((element) => element.includes("CamelCase.js"))).toBe(true);
804
+ expect(filePaths.some((element) => element.includes("lowercase.js"))).toBe(true);
805
+ expect(filePaths.some((element) => element.includes("UPPERCASE.JS"))).toBe(true);
806
+ });
807
+
808
+ test("should handle extremely complex glob patterns", async () => {
809
+ const result = await loadSources({
810
+ sourcesPath: testDir,
811
+ includePatterns: [
812
+ "src/**/!(test|spec)*{.js,.ts,.jsx,.tsx}", // Complex negation with alternatives
813
+ "**/@(components|utils|services)/**/*.{js,ts}", // At-patterns with alternatives
814
+ ],
815
+ useDefaultPatterns: false,
816
+ outputDir: tempDir,
817
+ docsDir: path.join(testDir, "docs"),
818
+ });
819
+
820
+ expect(result.datasourcesList).toBeDefined();
821
+ // Just verify it doesn't crash and returns some results
822
+ expect(Array.isArray(result.datasourcesList)).toBe(true);
823
+ });
824
+ });
825
+
826
+ describe("Performance and scale tests", () => {
827
+ test("should handle large number of patterns efficiently", async () => {
828
+ const manyPatterns = Array(100)
829
+ .fill(0)
830
+ .map((_, i) => `**/*${i}*.js`);
831
+
832
+ const startTime = Date.now();
833
+ const result = await loadSources({
834
+ sourcesPath: testDir,
835
+ includePatterns: manyPatterns,
836
+ useDefaultPatterns: false,
837
+ outputDir: tempDir,
838
+ docsDir: path.join(testDir, "docs"),
839
+ });
840
+ const endTime = Date.now();
841
+
842
+ expect(result.datasourcesList).toBeDefined();
843
+ expect(endTime - startTime).toBeLessThan(5000); // Should complete within 5 seconds
844
+ });
845
+
846
+ test("should handle very long paths", async () => {
847
+ const longDirName = "a".repeat(100);
848
+ const longPath = path.join(testDir, longDirName);
849
+ await mkdir(longPath, { recursive: true });
850
+ await writeFile(path.join(longPath, "file.js"), "console.log('long path');");
851
+
852
+ const result = await loadSources({
853
+ sourcesPath: testDir,
854
+ includePatterns: ["**/*.js"],
855
+ useDefaultPatterns: false,
856
+ outputDir: tempDir,
857
+ docsDir: path.join(testDir, "docs"),
858
+ });
859
+
860
+ expect(result.datasourcesList).toBeDefined();
861
+ const filePaths = result.datasourcesList.map((f) => f.sourceId);
862
+ expect(filePaths.some((element) => element.includes(longDirName))).toBe(true);
863
+ });
864
+ });
865
+
866
+ describe("Error handling and resilience", () => {
867
+ test("should handle permission denied gracefully", async () => {
868
+ // This test may not work on all systems, so we wrap in try-catch
869
+ try {
870
+ const restrictedDir = path.join(testDir, "restricted");
871
+ await mkdir(restrictedDir, { recursive: true, mode: 0o000 });
872
+
873
+ const result = await loadSources({
874
+ sourcesPath: restrictedDir,
875
+ useDefaultPatterns: true,
876
+ outputDir: tempDir,
877
+ docsDir: path.join(testDir, "docs"),
878
+ });
879
+
880
+ expect(result.datasourcesList).toBeDefined();
881
+ // Should handle gracefully without crashing
882
+ } catch {
883
+ // Skip on systems where permission tests don't work
884
+ expect(true).toBe(true);
885
+ }
886
+ });
887
+
888
+ test("should handle malformed glob patterns", async () => {
889
+ const result = await loadSources({
890
+ sourcesPath: [`${testDir}/[unclosed`, `${testDir}/**/{unclosed`],
891
+ useDefaultPatterns: false,
892
+ outputDir: tempDir,
893
+ docsDir: path.join(testDir, "docs"),
894
+ });
895
+
896
+ expect(result.datasourcesList).toBeDefined();
897
+ // Should not crash, even with malformed patterns
898
+ expect(Array.isArray(result.datasourcesList)).toBe(true);
899
+ });
900
+
901
+ test("should handle circular symbolic links", async () => {
902
+ try {
903
+ const { symlink, unlink } = await import("node:fs/promises");
904
+ const link1 = path.join(testDir, "link1");
905
+ const link2 = path.join(testDir, "link2");
906
+
907
+ // Create circular links
908
+ await symlink(link2, link1);
909
+ await symlink(link1, link2);
910
+
911
+ const result = await loadSources({
912
+ sourcesPath: testDir,
913
+ includePatterns: ["**/*"],
914
+ useDefaultPatterns: false,
915
+ outputDir: tempDir,
916
+ docsDir: path.join(testDir, "docs"),
917
+ });
918
+
919
+ expect(result.datasourcesList).toBeDefined();
920
+ // Should handle circular links without infinite loop
921
+
922
+ // Clean up circular links immediately to prevent interference with other tests
923
+ try {
924
+ await unlink(link1);
925
+ } catch {
926
+ // Ignore cleanup errors
927
+ }
928
+ try {
929
+ await unlink(link2);
930
+ } catch {
931
+ // Ignore cleanup errors
932
+ }
933
+ } catch {
934
+ // Skip on systems that don't support symlinks
935
+ expect(true).toBe(true);
936
+ }
937
+ });
938
+ });
939
+
940
+ describe("Integration with generateYAML output", () => {
941
+ test("should process typical generateYAML sourcesPath configurations", async () => {
942
+ const typicalConfigs = [
943
+ ["./src", "./lib"],
944
+ ["**/*.{js,ts,jsx,tsx}"],
945
+ ["src/**/*.js", "lib/**/*.ts", "docs/**/*.md"],
946
+ ["./", "!node_modules", "!dist"],
947
+ ];
948
+
949
+ for (const sourcesPathConfig of typicalConfigs) {
950
+ const result = await loadSources({
951
+ sourcesPath: sourcesPathConfig,
952
+ useDefaultPatterns: true,
953
+ outputDir: tempDir,
954
+ docsDir: path.join(testDir, "docs"),
955
+ });
956
+
957
+ expect(result.datasourcesList).toBeDefined();
958
+ expect(Array.isArray(result.datasourcesList)).toBe(true);
959
+ // Should not crash with any typical config
960
+ }
961
+ });
962
+ });
963
+
964
+ // Global cleanup to ensure test directories are fully removed
965
+ afterAll(async () => {
966
+ const testDirBase = path.join(__dirname, "test-content-generator");
967
+ try {
968
+ await rm(testDirBase, { recursive: true, force: true });
969
+ } catch {
970
+ // Try with system command as final fallback
971
+ try {
972
+ const { exec } = await import("node:child_process");
973
+ const { promisify } = await import("node:util");
974
+ const execAsync = promisify(exec);
975
+ await execAsync(`rm -rf "${testDirBase}"`);
976
+ } catch {
977
+ // If we still can't clean up, warn but don't fail
978
+ console.warn(`Warning: Could not fully clean up test directory: ${testDirBase}`);
979
+ }
980
+ }
981
+ });
358
982
  });