diff --git a/internal/tui/screens/sync_progress.go b/internal/tui/screens/sync_progress.go index 6bf2382..db7dafc 100644 --- a/internal/tui/screens/sync_progress.go +++ b/internal/tui/screens/sync_progress.go @@ -318,7 +318,7 @@ func (s *SyncProgressScreen) Update(msg tea.Msg) (tea.Model, tea.Cmd) { // Update progress bar and ETA (skip for complete status - handled above) if s.totalRepos > 0 { - done := s.cloned + s.updated + s.upToDate + s.skipped + s.failed + done := s.cloned + s.updated + s.upToDate + s.skipped + s.failed + s.archived cmds = append(cmds, s.progress.SetPercent(float64(done)/float64(s.totalRepos))) // Recalculate ETA only when done count actually changes (repo completed) @@ -327,9 +327,15 @@ func (s *SyncProgressScreen) Update(msg tea.Msg) (tea.Model, tea.Cmd) { s.lastETADoneCount = done // Only calculate ETA after warmup period - minSamples := min(10, s.totalRepos/10) - if minSamples < 3 { - minSamples = 3 + // For initial sync (mostly clones), we can show ETA sooner since each operation takes longer + // For incremental sync (mostly updates), wait for more samples for accuracy + minSamples := 1 // Start with 1 for initial sync + if s.cloned == 0 && done > 0 { + // Incremental sync (no clones yet) - use more conservative warmup + minSamples = min(10, s.totalRepos/10) + if minSamples < 3 { + minSamples = 3 + } } if done >= minSamples { @@ -389,7 +395,7 @@ func (s *SyncProgressScreen) View() string { // Progress total := s.totalRepos - done := s.cloned + s.updated + s.upToDate + s.skipped + s.failed + done := s.cloned + s.updated + s.upToDate + s.skipped + s.failed + s.archived if total > 0 { pct := float64(done) / float64(total) content.WriteString(s.progress.ViewAs(pct)) diff --git a/internal/tui/screens/sync_progress_test.go b/internal/tui/screens/sync_progress_test.go index a3d28da..31fb0ab 100644 --- a/internal/tui/screens/sync_progress_test.go +++ b/internal/tui/screens/sync_progress_test.go @@ -559,3 +559,130 @@ func TestTeaModelInterface(t *testing.T) { // This is a compile-time check that SyncProgressScreen implements tea.Model var _ tea.Model = (*SyncProgressScreen)(nil) } + +func TestETACalculationDuringInitialSync(t *testing.T) { + // Test that ETA is calculated and displayed during initial sync (clones) + // after just 1 repository completes + + t.Run("ETA calculated after 1 clone completes", func(t *testing.T) { + screen := &SyncProgressScreen{ + totalRepos: 10, + cloned: 1, // First clone completed + updated: 0, + upToDate: 0, + reposCompleted: 1, + lastETADoneCount: 0, + lastDisplayedETA: 0, + startTime: time.Now().Add(-10 * time.Second), // Started 10 seconds ago + } + + done := screen.cloned + screen.updated + screen.upToDate + screen.skipped + screen.failed + screen.archived + + // Simulate the ETA calculation logic for initial sync + if done > screen.lastETADoneCount { + screen.lastETADoneCount = done + + // Initial sync logic (cloned > 0 means we're doing clones) + minSamples := 1 // For initial sync with clones + if screen.cloned == 0 && done > 0 { + // Incremental sync - more conservative + minSamples = 3 + } + + if done >= minSamples { + elapsed := time.Since(screen.startTime) + avgPerRepo := elapsed / time.Duration(done) + remaining := screen.totalRepos - done + newETA := avgPerRepo * time.Duration(remaining) + screen.lastDisplayedETA = newETA + } + } + + // Verify ETA was calculated + assert.Greater(t, screen.lastDisplayedETA, time.Duration(0), "ETA should be calculated after 1 clone") + assert.Equal(t, 1, screen.lastETADoneCount, "lastETADoneCount should be updated") + + // Verify ETA is reasonable (should be roughly 90 seconds for 9 remaining repos at 10s/repo) + expectedETA := 90 * time.Second + assert.InDelta(t, expectedETA.Seconds(), screen.lastDisplayedETA.Seconds(), 5.0, + "ETA should be approximately 90 seconds") + }) + + t.Run("ETA not calculated until 3 updates complete for incremental sync", func(t *testing.T) { + screen := &SyncProgressScreen{ + totalRepos: 10, + cloned: 0, // No clones, incremental sync + updated: 2, // Only 2 updates so far + upToDate: 0, + reposCompleted: 2, + lastETADoneCount: 0, + lastDisplayedETA: 0, + startTime: time.Now().Add(-2 * time.Second), + } + + done := screen.cloned + screen.updated + screen.upToDate + screen.skipped + screen.failed + screen.archived + + // Simulate the ETA calculation logic + if done > screen.lastETADoneCount { + screen.lastETADoneCount = done + + // Incremental sync logic (no clones) + minSamples := 1 + if screen.cloned == 0 && done > 0 { + // Incremental sync - more conservative + minSamples = 3 + } + + if done >= minSamples { + elapsed := time.Since(screen.startTime) + avgPerRepo := elapsed / time.Duration(done) + remaining := screen.totalRepos - done + newETA := avgPerRepo * time.Duration(remaining) + screen.lastDisplayedETA = newETA + } + } + + // Verify ETA was NOT calculated (need 3 samples for incremental sync) + assert.Equal(t, time.Duration(0), screen.lastDisplayedETA, + "ETA should not be calculated with only 2 updates in incremental sync") + assert.Equal(t, 2, screen.lastETADoneCount, "lastETADoneCount should still be updated") + }) + + t.Run("ETA calculated after 3 updates in incremental sync", func(t *testing.T) { + screen := &SyncProgressScreen{ + totalRepos: 10, + cloned: 0, // No clones, incremental sync + updated: 3, // 3 updates completed + upToDate: 0, + reposCompleted: 3, + lastETADoneCount: 0, + lastDisplayedETA: 0, + startTime: time.Now().Add(-6 * time.Second), + } + + done := screen.cloned + screen.updated + screen.upToDate + screen.skipped + screen.failed + screen.archived + + // Simulate the ETA calculation logic + if done > screen.lastETADoneCount { + screen.lastETADoneCount = done + + // Incremental sync logic + minSamples := 1 + if screen.cloned == 0 && done > 0 { + minSamples = 3 + } + + if done >= minSamples { + elapsed := time.Since(screen.startTime) + avgPerRepo := elapsed / time.Duration(done) + remaining := screen.totalRepos - done + newETA := avgPerRepo * time.Duration(remaining) + screen.lastDisplayedETA = newETA + } + } + + // Verify ETA was calculated + assert.Greater(t, screen.lastDisplayedETA, time.Duration(0), + "ETA should be calculated after 3 updates in incremental sync") + }) +}