import { describe, expect, it, afterEach } from 'vitest'; import { mkdtempSync, mkdirSync, rmSync, symlinkSync } from 'node:fs'; import { tmpdir } from 'node:os'; import { join } from 'node:path'; import { scanForRepos } from '../src/core/repo-scanner.js'; const dirs: string[] = []; afterEach(() => { for (const d of dirs.splice(0)) rmSync(d, { recursive: true, force: true }); }); function tmpRoot(): string { const d = mkdtempSync(join(tmpdir(), 'arb-scan-')); dirs.push(d); return d; } // un « repo » pour le scanner = un dossier contenant `.git` (le scanner ne lance jamais git). function makeRepo(...segs: string[]): void { mkdirSync(join(...segs, '.git'), { recursive: true }); } const limits = { maxDepth: 6, maxRepos: 2000 }; describe('scanForRepos', () => { it('trouve un repo simple', async () => { const root = tmpRoot(); makeRepo(root, 'proj'); const { paths, truncated } = await scanForRepos([root], limits); expect(paths).toEqual([join(root, 'proj')]); expect(truncated).toBe(false); }); it('ne descend pas dans un repo trouvé (sous-repo ignoré)', async () => { const root = tmpRoot(); makeRepo(root, 'a'); makeRepo(root, 'a', 'sub'); // imbriqué : doit être ignoré const { paths } = await scanForRepos([root], limits); expect(paths).toEqual([join(root, 'a')]); }); it('ignore node_modules et les dotdirs', async () => { const root = tmpRoot(); makeRepo(root, 'node_modules', 'pkg'); makeRepo(root, '.hidden', 'x'); makeRepo(root, 'real'); const { paths } = await scanForRepos([root], limits); expect(paths).toEqual([join(root, 'real')]); }); it('respecte maxDepth', async () => { const root = tmpRoot(); makeRepo(root, 'a', 'b', 'c', 'deep'); // repo à profondeur 4 const shallow = await scanForRepos([root], { maxDepth: 2, maxRepos: 2000 }); expect(shallow.paths).toEqual([]); const deep = await scanForRepos([root], { maxDepth: 4, maxRepos: 2000 }); expect(deep.paths).toEqual([join(root, 'a', 'b', 'c', 'deep')]); }); it('ne suit pas les symlinks (cycle terminé, pas de doublon)', async () => { const root = tmpRoot(); makeRepo(root, 'proj'); try { symlinkSync(root, join(root, 'loop')); // cycle vers la racine } catch { /* symlink non autorisé sous certains CI : le test reste valide sans le lien */ } const { paths } = await scanForRepos([root], limits); expect(paths).toEqual([join(root, 'proj')]); }); it('tronque à maxRepos', async () => { const root = tmpRoot(); makeRepo(root, 'r1'); makeRepo(root, 'r2'); makeRepo(root, 'r3'); const { paths, truncated } = await scanForRepos([root], { maxDepth: 6, maxRepos: 2 }); expect(truncated).toBe(true); expect(paths.length).toBeLessThanOrEqual(2); }); it('ignore une racine inexistante sans lever', async () => { const { paths } = await scanForRepos(['/nope/does/not/exist'], limits); expect(paths).toEqual([]); }); it('scanne plusieurs racines', async () => { const r1 = tmpRoot(); const r2 = tmpRoot(); makeRepo(r1, 'one'); makeRepo(r2, 'two'); const { paths } = await scanForRepos([r1, r2], limits); expect([...paths].sort()).toEqual([join(r1, 'one'), join(r2, 'two')].sort()); }); });