import * as fs from 'fs'; import * as os from 'os'; import * as path from 'path'; import { type ExternalContextFile,externalContextScanner } from '@/utils/externalContextScanner'; describe('externalContextScanner', () => { let tempDir: string; beforeEach(() => { // Create a temp directory for testing tempDir = fs.mkdtempSync(path.join(os.tmpdir(), 'claudian-test-')); // Create test file structure fs.mkdirSync(path.join(tempDir, 'subdir')); fs.writeFileSync(path.join(tempDir, 'file1.txt'), 'content1'); fs.writeFileSync(path.join(tempDir, 'file2.md'), 'content2'); fs.writeFileSync(path.join(tempDir, 'subdir', 'file3.ts'), 'content3'); // Create hidden file (should be skipped) fs.writeFileSync(path.join(tempDir, '.hidden'), 'hidden'); // Clear cache before each test externalContextScanner.invalidateCache(); }); afterEach(() => { // Clean up temp directory fs.rmSync(tempDir, { recursive: true, force: true }); }); describe('scanPaths', () => { it('should scan directory and return files', () => { const files = externalContextScanner.scanPaths([tempDir]); expect(files.length).toBe(3); expect(files.map((f: ExternalContextFile) => f.name).sort()).toEqual(['file1.txt', 'file2.md', 'file3.ts']); }); it('should include file metadata', () => { const files = externalContextScanner.scanPaths([tempDir]); const file1 = files.find((f: ExternalContextFile) => f.name === 'file1.txt'); expect(file1).toBeDefined(); expect(file1!.path).toBe(path.join(tempDir, 'file1.txt')); expect(file1!.relativePath).toBe('file1.txt'); expect(file1!.contextRoot).toBe(tempDir); expect(file1!.mtime).toBeGreaterThan(0); }); it('should include files in subdirectories', () => { const files = externalContextScanner.scanPaths([tempDir]); const file3 = files.find((f: ExternalContextFile) => f.name === 'file3.ts'); expect(file3).toBeDefined(); expect(file3!.relativePath).toBe(path.join('subdir', 'file3.ts')); }); it('should skip hidden files', () => { const files = externalContextScanner.scanPaths([tempDir]); const hidden = files.find((f: ExternalContextFile) => f.name === '.hidden'); expect(hidden).toBeUndefined(); }); it('should skip hidden directories', () => { // Create a hidden directory with a file const hiddenDir = path.join(tempDir, '.hidden-dir'); fs.mkdirSync(hiddenDir); fs.writeFileSync(path.join(hiddenDir, 'secret.txt'), 'secret'); const files = externalContextScanner.scanPaths([tempDir]); const secret = files.find((f: ExternalContextFile) => f.name === 'secret.txt'); expect(secret).toBeUndefined(); }); it('should skip node_modules', () => { // Create node_modules with a file const nodeModules = path.join(tempDir, 'node_modules'); fs.mkdirSync(nodeModules); fs.writeFileSync(path.join(nodeModules, 'package.json'), '{}'); const files = externalContextScanner.scanPaths([tempDir]); const pkg = files.find((f: ExternalContextFile) => f.name === 'package.json'); expect(pkg).toBeUndefined(); }); it('should handle non-existent paths', () => { const files = externalContextScanner.scanPaths(['/non/existent/path']); expect(files).toEqual([]); }); it('should handle multiple external context paths', () => { // Create a second temp directory const tempDir2 = fs.mkdtempSync(path.join(os.tmpdir(), 'claudian-test2-')); fs.writeFileSync(path.join(tempDir2, 'file4.js'), 'content4'); try { const files = externalContextScanner.scanPaths([tempDir, tempDir2]); expect(files.length).toBe(4); expect(files.map((f: ExternalContextFile) => f.name).sort()).toEqual([ 'file1.txt', 'file2.md', 'file3.ts', 'file4.js', ]); } finally { fs.rmSync(tempDir2, { recursive: true, force: true }); } }); }); describe('caching', () => { it('should cache results', () => { // First scan const files1 = externalContextScanner.scanPaths([tempDir]); // Add a new file fs.writeFileSync(path.join(tempDir, 'new-file.txt'), 'new content'); // Second scan should use cache const files2 = externalContextScanner.scanPaths([tempDir]); expect(files1.length).toBe(files2.length); expect(files2.find((f: ExternalContextFile) => f.name === 'new-file.txt')).toBeUndefined(); }); it('should respect cache invalidation', () => { // First scan externalContextScanner.scanPaths([tempDir]); // Add a new file fs.writeFileSync(path.join(tempDir, 'new-file.txt'), 'new content'); // Invalidate cache externalContextScanner.invalidateCache(); // Second scan should see new file const files = externalContextScanner.scanPaths([tempDir]); expect(files.find((f: ExternalContextFile) => f.name === 'new-file.txt')).toBeDefined(); }); it('should invalidate specific path', () => { // Create a second temp directory const tempDir2 = fs.mkdtempSync(path.join(os.tmpdir(), 'claudian-test2-')); fs.writeFileSync(path.join(tempDir2, 'file4.js'), 'content4'); try { // First scan both paths externalContextScanner.scanPaths([tempDir, tempDir2]); // Add files to both fs.writeFileSync(path.join(tempDir, 'new1.txt'), 'new1'); fs.writeFileSync(path.join(tempDir2, 'new2.txt'), 'new2'); // Invalidate only first path externalContextScanner.invalidatePath(tempDir); // Second scan const files = externalContextScanner.scanPaths([tempDir, tempDir2]); // Should see new file in first path (invalidated) expect(files.find((f: ExternalContextFile) => f.name === 'new1.txt')).toBeDefined(); // Should NOT see new file in second path (still cached) expect(files.find((f: ExternalContextFile) => f.name === 'new2.txt')).toBeUndefined(); } finally { fs.rmSync(tempDir2, { recursive: true, force: true }); } }); }); describe('home path expansion', () => { it('should expand home paths', () => { // This test uses a real path that should exist const files = externalContextScanner.scanPaths(['~']); // Should not throw and should return some files (or empty if home is empty) expect(Array.isArray(files)).toBe(true); }); }); });