@aigne/doc-smith 0.6.0 → 0.7.1
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.
- package/.aigne/doc-smith/config.yaml +70 -0
- package/.aigne/doc-smith/output/structure-plan.json +152 -0
- package/.aigne/doc-smith/preferences.yml +31 -0
- package/.aigne/doc-smith/upload-cache.yaml +288 -0
- package/.github/workflows/ci.yml +46 -0
- package/.github/workflows/reviewer.yml +2 -1
- package/CHANGELOG.md +17 -0
- package/README.md +33 -15
- package/agents/chat.yaml +30 -0
- package/agents/check-structure-plan.mjs +1 -1
- package/agents/docs-fs.yaml +25 -0
- package/agents/exit.mjs +6 -0
- package/agents/feedback-refiner.yaml +5 -1
- package/agents/find-items-by-paths.mjs +10 -4
- package/agents/fs.mjs +60 -0
- package/agents/input-generator.mjs +150 -91
- package/agents/load-config.mjs +0 -5
- package/agents/load-sources.mjs +61 -8
- package/agents/publish-docs.mjs +27 -12
- package/agents/retranslate.yaml +1 -1
- package/agents/team-publish-docs.yaml +2 -2
- package/aigne.yaml +1 -0
- package/docs/_sidebar.md +17 -0
- package/docs/advanced-how-it-works.md +104 -0
- package/docs/advanced-how-it-works.zh.md +104 -0
- package/docs/advanced-quality-assurance.md +64 -0
- package/docs/advanced-quality-assurance.zh.md +64 -0
- package/docs/advanced.md +28 -0
- package/docs/advanced.zh.md +28 -0
- package/docs/changelog.md +272 -0
- package/docs/changelog.zh.md +272 -0
- package/docs/cli-reference.md +185 -0
- package/docs/cli-reference.zh.md +185 -0
- package/docs/configuration-interactive-setup.md +82 -0
- package/docs/configuration-interactive-setup.zh.md +82 -0
- package/docs/configuration-language-support.md +64 -0
- package/docs/configuration-language-support.zh.md +64 -0
- package/docs/configuration-llm-setup.md +90 -0
- package/docs/configuration-llm-setup.zh.md +90 -0
- package/docs/configuration-preferences.md +122 -0
- package/docs/configuration-preferences.zh.md +123 -0
- package/docs/configuration.md +173 -0
- package/docs/configuration.zh.md +173 -0
- package/docs/features-generate-documentation.md +82 -0
- package/docs/features-generate-documentation.zh.md +82 -0
- package/docs/features-publish-your-docs.md +98 -0
- package/docs/features-publish-your-docs.zh.md +98 -0
- package/docs/features-translate-documentation.md +83 -0
- package/docs/features-translate-documentation.zh.md +83 -0
- package/docs/features-update-and-refine.md +86 -0
- package/docs/features-update-and-refine.zh.md +86 -0
- package/docs/features.md +56 -0
- package/docs/features.zh.md +56 -0
- package/docs/getting-started.md +74 -0
- package/docs/getting-started.zh.md +74 -0
- package/docs/overview.md +48 -0
- package/docs/overview.zh.md +48 -0
- package/media.md +19 -0
- package/package.json +13 -10
- package/prompts/content-detail-generator.md +7 -3
- package/prompts/document/custom-components.md +80 -0
- package/prompts/document/d2-chart/diy-examples.md +44 -0
- package/prompts/document/d2-chart/official-examples.md +708 -0
- package/prompts/document/d2-chart/rules.md +48 -0
- package/prompts/document/detail-generator.md +12 -15
- package/prompts/document/structure-planning.md +1 -3
- package/prompts/feedback-refiner.md +81 -60
- package/prompts/structure-planning.md +20 -3
- package/tests/check-detail-result.test.mjs +3 -4
- package/tests/conflict-resolution.test.mjs +237 -0
- package/tests/input-generator.test.mjs +940 -0
- package/tests/load-sources.test.mjs +627 -3
- package/tests/preferences-utils.test.mjs +94 -0
- package/tests/save-value-to-config.test.mjs +182 -5
- package/tests/utils.test.mjs +49 -0
- package/utils/conflict-detector.mjs +72 -1
- package/utils/constants.mjs +125 -124
- package/utils/kroki-utils.mjs +162 -0
- package/utils/markdown-checker.mjs +98 -70
- package/utils/utils.mjs +96 -28
|
@@ -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
|
-
|
|
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
|
});
|