import { describe, it, expect, vi, beforeEach } from 'vitest' import { ProjectSyncManager, type SyncResult } from './remote-sync.js' import type { ProjectRepository } from '../db/repositories/project-repository.js' vi.mock('simple-git', () => ({ simpleGit: vi.fn(), })) vi.mock('./project-clones.js', () => ({ ensureProjectClone: vi.fn().mockResolvedValue('/tmp/fake-clone'), })) vi.mock('../logger/index.js', () => ({ createModuleLogger: () => ({ info: vi.fn(), warn: vi.fn(), error: vi.fn(), }), })) function makeRepo(overrides: Partial = {}): ProjectRepository { return { findAll: vi.fn().mockResolvedValue([]), findById: vi.fn().mockResolvedValue(null), create: vi.fn(), update: vi.fn().mockResolvedValue({}), delete: vi.fn(), findProjectsByInitiativeId: vi.fn().mockResolvedValue([]), setInitiativeProjects: vi.fn().mockResolvedValue(undefined), ...overrides, } as unknown as ProjectRepository } const project1 = { id: 'proj-1', name: 'alpha', url: 'https://github.com/org/alpha', defaultBranch: 'main', lastFetchedAt: null, createdAt: new Date(), updatedAt: new Date(), } const project2 = { id: 'proj-2', name: 'beta', url: 'https://github.com/org/beta', defaultBranch: 'main', lastFetchedAt: null, createdAt: new Date(), updatedAt: new Date(), } describe('ProjectSyncManager', () => { // eslint-disable-next-line @typescript-eslint/no-explicit-any let simpleGitMock: any beforeEach(async () => { const mod = await import('simple-git') simpleGitMock = vi.mocked(mod.simpleGit) simpleGitMock.mockReset() }) describe('syncAllProjects', () => { it('returns empty array when no projects exist', async () => { const repo = makeRepo({ findAll: vi.fn().mockResolvedValue([]) }) const manager = new ProjectSyncManager(repo, '/workspace') const results = await manager.syncAllProjects() expect(results).toEqual([]) }) it('returns success result for each project when all succeed', async () => { const mockGit = { fetch: vi.fn().mockResolvedValue({}), raw: vi.fn().mockResolvedValue(''), } simpleGitMock.mockReturnValue(mockGit) const repo = makeRepo({ findAll: vi.fn().mockResolvedValue([project1, project2]), findById: vi.fn() .mockResolvedValueOnce(project1) .mockResolvedValueOnce(project2), update: vi.fn().mockResolvedValue({}), }) const manager = new ProjectSyncManager(repo, '/workspace') const results = await manager.syncAllProjects() expect(results).toHaveLength(2) expect(results[0]).toMatchObject({ projectId: 'proj-1', projectName: 'alpha', success: true, fetched: true, }) expect(results[1]).toMatchObject({ projectId: 'proj-2', projectName: 'beta', success: true, fetched: true, }) }) it('returns partial failure when the second project fetch throws', async () => { const mockGitSuccess = { fetch: vi.fn().mockResolvedValue({}), raw: vi.fn().mockResolvedValue(''), } const mockGitFail = { fetch: vi.fn().mockRejectedValue(new Error('network error')), raw: vi.fn().mockResolvedValue(''), } simpleGitMock .mockReturnValueOnce(mockGitSuccess) .mockReturnValueOnce(mockGitFail) const repo = makeRepo({ findAll: vi.fn().mockResolvedValue([project1, project2]), findById: vi.fn() .mockResolvedValueOnce(project1) .mockResolvedValueOnce(project2), update: vi.fn().mockResolvedValue({}), }) const manager = new ProjectSyncManager(repo, '/workspace') const results = await manager.syncAllProjects() expect(results).toHaveLength(2) expect(results[0]).toMatchObject({ projectId: 'proj-1', success: true }) expect(results[1]).toMatchObject({ projectId: 'proj-2', success: false, error: expect.any(String), }) }) }) describe('SyncResult shape', () => { it('result always contains projectId and success fields', async () => { const mockGit = { fetch: vi.fn().mockResolvedValue({}), raw: vi.fn().mockResolvedValue(''), } simpleGitMock.mockReturnValue(mockGit) const repo = makeRepo({ findAll: vi.fn().mockResolvedValue([project1]), findById: vi.fn().mockResolvedValue(project1), update: vi.fn().mockResolvedValue({}), }) const manager = new ProjectSyncManager(repo, '/workspace') const results = await manager.syncAllProjects() expect(results[0]).toMatchObject({ projectId: expect.any(String), success: expect.any(Boolean), }) }) }) describe('failure counting logic', () => { it('counts failures from SyncResult array', () => { const results: Pick[] = [ { success: true }, { success: false }, { success: true }, { success: false }, ] const failed = results.filter(r => !r.success) expect(failed.length).toBe(2) }) }) })