Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 11 additions & 5 deletions internal/tui/screens/sync_progress.go
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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 {
Expand Down Expand Up @@ -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))
Expand Down
127 changes: 127 additions & 0 deletions internal/tui/screens/sync_progress_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -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")
})
}