import { describe, it, expect, vi, beforeEach } from 'vitest'; import { SyncService } from '../../src/main/services/SyncService.js'; vi.mock('../../src/main/services/GitClient.js'); import { GitClient } from '../../src/main/services/GitClient.js'; describe('SyncService.sync — 양방향', () => { let svc: SyncService; let exportSvc: { export: ReturnType }; let importSvc: { applySyncFromDir: ReturnType }; let gitInstance: { isRepo: ReturnType; hasRemote: ReturnType; addAll: ReturnType; hasUncommittedChanges: ReturnType; commit: ReturnType; fetch: ReturnType; refExists: ReturnType; rebaseOnto: ReturnType; rebaseAbort: ReturnType; listConflicts: ReturnType; push: ReturnType; run: ReturnType; }; beforeEach(() => { exportSvc = { export: vi.fn(async () => {}) }; importSvc = { applySyncFromDir: vi.fn(async () => ({ changedCount: 0 })) }; gitInstance = { isRepo: vi.fn(async () => true), hasRemote: vi.fn(async () => true), addAll: vi.fn(async () => {}), hasUncommittedChanges: vi.fn(async () => true), commit: vi.fn(async () => ({ changed: true, sha: 'abc' })), fetch: vi.fn(async () => ({ stdout: '', stderr: '', exitCode: 0 })), refExists: vi.fn(async () => true), rebaseOnto: vi.fn(async () => ({ stdout: '', stderr: '', exitCode: 0 })), rebaseAbort: vi.fn(async () => ({ stdout: '', stderr: '', exitCode: 0 })), listConflicts: vi.fn(async () => []), push: vi.fn(async () => {}), run: vi.fn(async () => ({ stdout: '', stderr: '', exitCode: 0 })) }; (GitClient as unknown as ReturnType).mockImplementation(function () { return gitInstance; }); svc = new SyncService( '/tmp/profile', exportSvc as unknown as never, importSvc as unknown as never ); }); it('happy path — 6단계 모두 호출, ok:true', async () => { const r = await svc.sync(); expect(exportSvc.export).toHaveBeenCalled(); expect(gitInstance.addAll).toHaveBeenCalled(); expect(gitInstance.commit).toHaveBeenCalled(); expect(gitInstance.fetch).toHaveBeenCalled(); expect(gitInstance.rebaseOnto).toHaveBeenCalledWith('origin/main'); expect(importSvc.applySyncFromDir).toHaveBeenCalled(); expect(gitInstance.push).toHaveBeenCalled(); expect(r.ok).toBe(true); expect(r.pushed).toBe(true); }); it('local 변경 없음 → commit skip + 다음 단계 진행', async () => { gitInstance.hasUncommittedChanges.mockResolvedValueOnce(false); const r = await svc.sync(); expect(gitInstance.commit).not.toHaveBeenCalled(); expect(gitInstance.fetch).toHaveBeenCalled(); expect(r.ok).toBe(true); }); it('rebase 실패 → abort + reason=conflict + conflicts 포함 (path + localText/remoteText)', async () => { gitInstance.rebaseOnto.mockResolvedValueOnce({ stdout: '', stderr: 'CONFLICT', exitCode: 1 }); gitInstance.listConflicts.mockResolvedValueOnce(['notes/aaa.md', 'notes/bbb.md']); // Cut E final review fix — runSync calls git.run(['show', ':2:path']) and ':3:path' // for each conflict. Mock returns ours/theirs text per call. gitInstance.run .mockResolvedValueOnce({ stdout: 'aaa local', stderr: '', exitCode: 0 }) // :2:notes/aaa.md .mockResolvedValueOnce({ stdout: 'aaa remote', stderr: '', exitCode: 0 }) // :3:notes/aaa.md .mockResolvedValueOnce({ stdout: 'bbb local', stderr: '', exitCode: 0 }) // :2:notes/bbb.md .mockResolvedValueOnce({ stdout: 'bbb remote', stderr: '', exitCode: 0 }); // :3:notes/bbb.md const r = await svc.sync(); expect(gitInstance.rebaseAbort).toHaveBeenCalled(); expect(r.ok).toBe(false); expect(r.reason).toBe('conflict'); expect(r.conflicts).toEqual([ { path: 'notes/aaa.md', localText: 'aaa local', remoteText: 'aaa remote' }, { path: 'notes/bbb.md', localText: 'bbb local', remoteText: 'bbb remote' } ]); expect(gitInstance.push).not.toHaveBeenCalled(); }); it('fetch 실패 → reason 반환', async () => { gitInstance.fetch.mockResolvedValueOnce({ stdout: '', stderr: 'no network', exitCode: 1 }); const r = await svc.sync(); expect(r.ok).toBe(false); expect(r.reason).toContain('fetch failed'); expect(gitInstance.rebaseOnto).not.toHaveBeenCalled(); }); it('not configured → ok:false + reason=not_configured', async () => { gitInstance.isRepo.mockResolvedValueOnce(false); const r = await svc.sync(); expect(r.ok).toBe(false); expect(r.reason).toBe('not_configured'); }); });