// useWatchedWorktrees : propriétaire unique des abonnements `watch` (worktree_changes). On vérifie // qu'il suit l'attention de l'utilisateur (worktree actif + dépôts dépliés), qu'il libère ce qui n'est // plus regardé, et qu'il ne dépasse jamais le plafond d'abonnements. import { describe, it, expect, beforeEach, vi } from 'vitest'; import { createPinia, setActivePinia } from 'pinia'; import { effectScope, nextTick } from 'vue'; import type { WorktreeSummary } from '@arboretum/shared'; const watched: string[] = []; const released: string[] = []; const listeners = new Map void>(); vi.mock('../src/lib/ws-client', () => ({ wsClient: { watchWorktree: (repoId: string, path: string, listener: (e: { repoId: string; path: string }) => void) => { const key = `${repoId}\0${path}`; watched.push(key); listeners.set(key, listener); return () => released.push(key); }, }, })); const { useWatchedWorktrees, MAX_WATCHED } = await import('../src/composables/useWatchedWorktrees'); const { useIdeStore } = await import('../src/stores/ide'); const { useWorktreesStore } = await import('../src/stores/worktrees'); function fakeStorage() { const m = new Map(); return { getItem: (k: string) => (m.has(k) ? (m.get(k) as string) : null), setItem: (k: string, v: string) => void m.set(k, v), removeItem: (k: string) => void m.delete(k), clear: () => m.clear(), }; } function wt(repoId: string, path: string): WorktreeSummary { return { repoId, path, branch: 'main', head: 'abc1234', detached: false, locked: false, prunable: false, isMain: true, git: { ahead: 0, behind: 0, dirtyCount: 0, upstream: null }, sessions: [], }; } describe('useWatchedWorktrees', () => { beforeEach(() => { watched.length = 0; released.length = 0; listeners.clear(); (globalThis as unknown as { localStorage: unknown }).localStorage = fakeStorage(); setActivePinia(createPinia()); }); it('observe le worktree actif même si aucun dépôt n’est déplié', () => { const ide = useIdeStore(); ide.setActiveWorktree('r1', '/wt/a'); const scope = effectScope(); scope.run(() => useWatchedWorktrees()); expect(watched).toEqual(['r1\0/wt/a']); scope.stop(); expect(released).toEqual(['r1\0/wt/a']); }); it('observe tous les worktrees d’un dépôt déplié et libère au repliage', async () => { const ide = useIdeStore(); const worktrees = useWorktreesStore(); worktrees.worktrees = [wt('r1', '/wt/a'), wt('r1', '/wt/b'), wt('r2', '/wt/c')]; const scope = effectScope(); scope.run(() => useWatchedWorktrees()); expect(watched).toEqual([]); ide.toggleRepo('r1'); await nextTick(); expect(watched).toEqual(['r1\0/wt/a', 'r1\0/wt/b']); ide.toggleRepo('r1'); await nextTick(); expect(released).toEqual(['r1\0/wt/a', 'r1\0/wt/b']); scope.stop(); }); it('ne réabonne pas ce qui est déjà observé', async () => { const ide = useIdeStore(); const worktrees = useWorktreesStore(); worktrees.worktrees = [wt('r1', '/wt/a'), wt('r1', '/wt/b')]; ide.toggleRepo('r1'); const scope = effectScope(); scope.run(() => useWatchedWorktrees()); expect(watched).toHaveLength(2); // le worktree actif fait déjà partie du dépôt déplié : aucun abonnement supplémentaire ide.setActiveWorktree('r1', '/wt/a'); await nextTick(); expect(watched).toHaveLength(2); expect(released).toEqual([]); scope.stop(); }); it('borne le nombre d’abonnements et garde le worktree actif en tête', async () => { const ide = useIdeStore(); const worktrees = useWorktreesStore(); worktrees.worktrees = Array.from({ length: MAX_WATCHED + 5 }, (_, i) => wt('r1', `/wt/${i}`)); ide.setActiveWorktree('r1', `/wt/${MAX_WATCHED + 4}`); // le dernier, hors plafond sans priorité ide.toggleRepo('r1'); const scope = effectScope(); scope.run(() => useWatchedWorktrees()); expect(watched).toHaveLength(MAX_WATCHED); expect(watched[0]).toBe(`r1\0/wt/${MAX_WATCHED + 4}`); scope.stop(); }); it('incrémente le jeton d’invalidation du store à chaque worktree_changes', () => { const ide = useIdeStore(); const worktrees = useWorktreesStore(); ide.setActiveWorktree('r1', '/wt/a'); const scope = effectScope(); scope.run(() => useWatchedWorktrees()); expect(worktrees.changeVersion('r1', '/wt/a')).toBe(0); listeners.get('r1\0/wt/a')?.({ repoId: 'r1', path: '/wt/a' }); expect(worktrees.changeVersion('r1', '/wt/a')).toBe(1); listeners.get('r1\0/wt/a')?.({ repoId: 'r1', path: '/wt/a' }); expect(worktrees.changeVersion('r1', '/wt/a')).toBe(2); scope.stop(); }); });