test(sync): cover FullSync's phase transitions end-to-end

TestFillPendingDetails_ReportsPhaseAwareProgressThroughActivitiesAndWorkoutsPhases
only ever called FillPendingDetails directly, never exercising
FullSync's own setProgress(PhaseDiscovering, ...) / deferred
setProgress(PhaseIdle, ...) wiring around backfillCore/
incrementalSyncCore -- what handleSyncRun actually runs in production
and what SyncModal's "Discovering activities..." spinner depends on.

Add a mock.Client.Delay field (slept, ctx-cancellable, at the start of
GetActivities) so a test can give the discovering phase real
wall-clock duration, then add
TestFullSync_ReportsPhaseTransitionsThroughDiscoveringToIdle, which
runs FullSync in a goroutine and polls Progress() to confirm it
observes PhaseDiscovering mid-flight and PhaseIdle after completion.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-07-27 09:01:30 +02:00
parent 3a7f305221
commit f7e7b68078
2 changed files with 90 additions and 1 deletions

View File

@@ -630,6 +630,81 @@ func TestFullSync_RecordsOneCombinedSyncRun(t *testing.T) {
}
}
// waitForPhase polls svc.Progress() until it reports phase or timeout
// elapses, returning whether it was observed. Polling (rather than a single
// fixed-delay sample) keeps this robust against slower/loaded CI machines.
func waitForPhase(svc *Service, phase string, timeout time.Duration) bool {
deadline := time.Now().Add(timeout)
for {
if svc.Progress().Phase == phase {
return true
}
if time.Now().After(deadline) {
return svc.Progress().Phase == phase
}
time.Sleep(2 * time.Millisecond)
}
}
// TestFullSync_ReportsPhaseTransitionsThroughDiscoveringToIdle exercises
// FullSync itself (the actual production call path via handleSyncRun),
// unlike TestFillPendingDetails_ReportsPhaseAwareProgressThroughActivitiesAndWorkoutsPhases
// below, which only calls FillPendingDetails directly and so never touches
// FullSync's own s.setProgress(PhaseDiscovering, ...) / deferred
// s.setProgress(PhaseIdle, ...) wiring around backfillCore/
// incrementalSyncCore -- what the frontend's SyncModal "Discovering
// activities..." spinner actually depends on.
func TestFullSync_ReportsPhaseTransitionsThroughDiscoveringToIdle(t *testing.T) {
db := openTestDB(t)
ctx := context.Background()
userID := provisionTestUser(t, db)
const delay = 200 * time.Millisecond
workoutID := int64(101)
m := &mock.Client{
Delay: delay, // gives backfillCore/incrementalSyncCore's GetActivities calls real wall-clock duration
Activities: []garmin.Activity{
{ActivityID: 1, ActivityType: garmin.ActivityType{TypeKey: "running"}, WorkoutID: &workoutID,
StartTimeGMT: "2026-07-05 06:00:00", Distance: 5000, Duration: 1500},
},
Splits: map[int64]garmin.ActivitySplits{1: {ActivityID: 1}},
Details: map[int64]garmin.ActivityDetails{1: {ActivityID: 1}},
Workouts: map[int64]garmin.Workout{workoutID: {}},
}
svc := NewService(m, db, userID, Config{BackfillWindowDays: 10},
fixedNow(time.Date(2026, 7, 11, 0, 0, 0, 0, time.UTC)))
setBackfillHorizon(t, db, userID, 10)
if p := svc.Progress(); p.Phase != PhaseIdle {
t.Fatalf("Progress before FullSync = %+v, want PhaseIdle", p)
}
done := make(chan error, 1)
go func() { done <- svc.FullSync(ctx, 10) }()
// FullSync sets PhaseDiscovering synchronously before backfillCore even
// runs; the mock's Delay on each of backfillCore's and
// incrementalSyncCore's one GetActivities call (~2*delay total) gives
// this comfortable real wall-clock time to observe it before the run
// reaches FillPendingDetails/Idle.
if !waitForPhase(svc, PhaseDiscovering, delay) {
t.Fatalf("Progress().Phase never observed as %q while backfillCore/incrementalSyncCore were running; got %+v", PhaseDiscovering, svc.Progress())
}
select {
case err := <-done:
if err != nil {
t.Fatalf("FullSync: %v", err)
}
case <-time.After(5 * time.Second):
t.Fatal("FullSync did not complete in time")
}
if final := svc.Progress(); final.Phase != PhaseIdle || final.Done != 0 || final.Total != 0 {
t.Errorf("Progress after FullSync returns = %+v, want zero-value PhaseIdle", final)
}
}
func TestFillPendingDetails_ReportsPhaseAwareProgressThroughActivitiesAndWorkoutsPhases(t *testing.T) {
db := openTestDB(t)
ctx := context.Background()