// Portée déduite du terminal focalisé. Le piège central : une session de GROUPE a pour cwd le parent // commun des dépôts, donc la rattacher par chemin donnerait un worktree arbitraire (ou rien). import { describe, expect, it } from 'vitest'; import { resolveScope, SCOPE_ALL, scopeContains, worktreesForSession, worktreesInScope } from '../src/lib/context-scope'; const wt = (repoId: string, path: string): { repoId: string; path: string } => ({ repoId, path }); const WORKTREES = [ wt('api', '/repos/api'), wt('api', '/repos/api/.worktrees/feature'), wt('web', '/repos/web'), wt('docs', '/ailleurs/docs'), ]; const GROUPS = [ { id: 'stack', repoIds: ['api', 'web'] }, { id: 'tout', repoIds: ['api', 'web', 'docs'] }, ]; describe('resolveScope', () => { it('session de groupe : portée GROUPE, jamais un worktree (cwd = parent commun)', () => { const scope = resolveScope({ cwd: '/repos', addedDirs: ['/repos/api', '/repos/web'], groupId: 'stack' }, WORKTREES, GROUPS); expect(scope).toEqual({ kind: 'group', groupId: 'stack' }); }); it('groupId inconnu des données live : tout voir plutôt qu’une portée vide', () => { expect(resolveScope({ cwd: '/repos', groupId: 'disparu' }, WORKTREES, GROUPS)).toEqual(SCOPE_ALL); }); it('session multi-dépôts SANS groupId : groupe qui les contient tous', () => { const scope = resolveScope({ cwd: '/repos/api', addedDirs: ['/repos/web'] }, WORKTREES, GROUPS); expect(scope).toEqual({ kind: 'group', groupId: 'stack' }); }); it('multi-dépôts sans groupe commun : tout voir', () => { const scope = resolveScope({ cwd: '/repos/api', addedDirs: ['/ailleurs/docs'] }, WORKTREES, [{ id: 'stack', repoIds: ['api', 'web'] }]); expect(scope).toEqual(SCOPE_ALL); }); it('terminal lancé dans un SOUS-répertoire : worktree du projet (contenance, pas égalité)', () => { const scope = resolveScope({ cwd: '/repos/web/packages/front' }, WORKTREES, GROUPS); expect(scope).toEqual({ kind: 'worktree', repoId: 'web', wtPath: '/repos/web' }); }); it('worktrees imbriqués : le plus profond gagne', () => { const scope = resolveScope({ cwd: '/repos/api/.worktrees/feature/src' }, WORKTREES, GROUPS); expect(scope).toEqual({ kind: 'worktree', repoId: 'api', wtPath: '/repos/api/.worktrees/feature' }); }); it('cwd hors de tout worktree connu, ou pas de session : null (l’appelant se rabat)', () => { expect(resolveScope({ cwd: '/tmp/scratch' }, WORKTREES, GROUPS)).toBeNull(); expect(resolveScope(null, WORKTREES, GROUPS)).toBeNull(); }); }); describe('scopeContains', () => { it('all : tout ; worktree : la paire exacte ; groupe : tout worktree d’un dépôt du groupe', () => { expect(scopeContains(SCOPE_ALL, { repoId: 'docs', wtPath: '/ailleurs/docs' }, GROUPS)).toBe(true); const only = { kind: 'worktree', repoId: 'api', wtPath: '/repos/api' } as const; expect(scopeContains(only, { repoId: 'api', wtPath: '/repos/api' }, GROUPS)).toBe(true); expect(scopeContains(only, { repoId: 'api', wtPath: '/repos/api/.worktrees/feature' }, GROUPS)).toBe(false); expect(scopeContains(only, { repoId: 'web', wtPath: '/repos/web' }, GROUPS)).toBe(false); const group = { kind: 'group', groupId: 'stack' } as const; expect(scopeContains(group, { repoId: 'web', wtPath: '/repos/web' }, GROUPS)).toBe(true); expect(scopeContains(group, { repoId: 'api', wtPath: '/repos/api/.worktrees/feature' }, GROUPS)).toBe(true); expect(scopeContains(group, { repoId: 'docs', wtPath: '/ailleurs/docs' }, GROUPS)).toBe(false); // groupe supprimé entre-temps : plus rien ne matche, l'appelant doit déjà avoir replié sur 'all' expect(scopeContains({ kind: 'group', groupId: 'disparu' }, { repoId: 'api', wtPath: '/repos/api' }, GROUPS)).toBe(false); }); }); describe('worktreesForSession / worktreesInScope', () => { it('cwd + addedDirs, dédupliqués, le plus profond gagnant', () => { const covered = worktreesForSession({ cwd: '/repos/api/src', addedDirs: ['/repos/api/lib', '/repos/web'] }, WORKTREES); expect(covered.map((w) => w.path)).toEqual(['/repos/api', '/repos/web']); }); it('un répertoire hors worktree connu est simplement ignoré', () => { expect(worktreesForSession({ cwd: '/tmp/x', addedDirs: ['/repos/web'] }, WORKTREES).map((w) => w.repoId)).toEqual(['web']); }); it('worktreesInScope filtre la liste complète', () => { expect(worktreesInScope(SCOPE_ALL, WORKTREES, GROUPS)).toHaveLength(4); expect(worktreesInScope({ kind: 'group', groupId: 'stack' }, WORKTREES, GROUPS).map((w) => w.path)).toEqual([ '/repos/api', '/repos/api/.worktrees/feature', '/repos/web', ]); expect(worktreesInScope({ kind: 'worktree', repoId: 'web', wtPath: '/repos/web' }, WORKTREES, GROUPS).map((w) => w.repoId)).toEqual(['web']); }); });