// Règles d'affichage de la vue Changements et de l'index Git. Fonctions pures, testées sans DOM. import { describe, expect, it } from 'vitest'; import type { RepoSummary, WorktreeGitStatus, WorktreeSummary } from '@arboretum/shared'; import { dirtyFileCount, groupWorktreesByRepo, hasPendingWork, pendingWorktreeCount, sortForIndex, } from '../src/lib/changes-model'; const git = (over: Partial = {}): WorktreeGitStatus => ({ ahead: 0, behind: 0, dirtyCount: 0, upstream: 'origin/main', ...over, }); const wt = (path: string, over: Partial = {}): WorktreeSummary => ({ repoId: 'r1', path, branch: path.split('/').pop() ?? null, head: 'abc1234', detached: false, locked: false, prunable: false, isMain: false, git: git(), sessions: [], ...over, }); const repo = (id: string, label = id): RepoSummary => ({ id, path: `/repos/${id}`, label, hidden: false, valid: true }) as unknown as RepoSummary; describe('hasPendingWork', () => { it('un worktree propre et à jour n a rien à traiter', () => { expect(hasPendingWork(git())).toBe(false); }); it('détecte chaque forme de travail en attente', () => { expect(hasPendingWork(git({ dirtyCount: 2 }))).toBe(true); expect(hasPendingWork(git({ stagedCount: 1 }))).toBe(true); expect(hasPendingWork(git({ unstagedCount: 1 }))).toBe(true); expect(hasPendingWork(git({ conflictCount: 1 }))).toBe(true); expect(hasPendingWork(git({ ahead: 3 }))).toBe(true); // rien à committer mais à pousser }); it('être en retard ne demande rien de notre côté', () => { expect(hasPendingWork(git({ behind: 5 }))).toBe(false); }); it('les compteurs fins étant optionnels dans le protocole, dirtyCount reste le filet', () => { // Cas d'un daemon plus ancien : ni stagedCount ni unstagedCount, seulement dirtyCount. expect(hasPendingWork({ ahead: 0, behind: 0, dirtyCount: 4, upstream: null })).toBe(true); }); }); describe('dirtyFileCount', () => { it('prend le maximum entre dirtyCount et la somme des compteurs fins', () => { expect(dirtyFileCount(git({ dirtyCount: 3 }))).toBe(3); expect(dirtyFileCount(git({ dirtyCount: 3, stagedCount: 2, unstagedCount: 2 }))).toBe(4); }); }); describe('groupWorktreesByRepo', () => { const repos = [repo('r1', 'alpha'), repo('r2', 'beta')]; const byRepo: Record = { r1: [ wt('/wt/main', { isMain: true, git: git({ dirtyCount: 2, unstagedCount: 2 }) }), wt('/wt/feature'), ], r2: [wt('/wt/clean', { repoId: 'r2' })], }; const forRepo = (id: string): WorktreeSummary[] => byRepo[id] ?? []; it('ne garde que ce qui a du travail, et masque un dépôt sans worktree retenu', () => { const groups = groupWorktreesByRepo(repos, forRepo, { showClean: false, active: null }); expect(groups).toHaveLength(1); expect(groups[0]?.repo.id).toBe('r1'); expect(groups[0]?.worktrees.map((w) => w.path)).toEqual(['/wt/main']); expect(groups[0]?.dirtyFiles).toBe(2); }); it('showClean inclut tout', () => { const groups = groupWorktreesByRepo(repos, forRepo, { showClean: true, active: null }); expect(groups.map((g) => g.repo.id)).toEqual(['r1', 'r2']); expect(groups[0]?.worktrees).toHaveLength(2); }); it('le worktree actif reste affiché même propre (la vue ne se vide pas après un commit)', () => { const groups = groupWorktreesByRepo(repos, forRepo, { showClean: false, active: { repoId: 'r2', wtPath: '/wt/clean' }, }); expect(groups.map((g) => g.repo.id)).toEqual(['r1', 'r2']); expect(groups[1]?.worktrees.map((w) => w.path)).toEqual(['/wt/clean']); }); }); describe('sortForIndex', () => { it('ce qui demande une action d abord, puis le principal, puis le chemin', () => { const list = [ wt('/b/clean'), wt('/a/main', { isMain: true }), wt('/c/dirty', { git: git({ dirtyCount: 1 }) }), ]; expect(sortForIndex(list).map((w) => w.path)).toEqual(['/c/dirty', '/a/main', '/b/clean']); }); it('ne mute pas la liste reçue', () => { const list = [wt('/b'), wt('/a')]; sortForIndex(list); expect(list.map((w) => w.path)).toEqual(['/b', '/a']); }); }); describe('pendingWorktreeCount', () => { it('compte les worktrees à traiter, tous dépôts visibles confondus', () => { const forRepo = (id: string): WorktreeSummary[] => id === 'r1' ? [wt('/a', { git: git({ dirtyCount: 1 }) }), wt('/b')] : [wt('/c', { repoId: 'r2', git: git({ ahead: 2 }) })]; expect(pendingWorktreeCount([repo('r1'), repo('r2')], forRepo)).toBe(2); }); });