Merge backfill into Sync now, replace Full backfill with a destructive Reset all

Sync now now runs Backfill (resumes from the watermark, so a widened history
horizon is picked up automatically) then IncrementalSync then detail-fill, in
one click. "Full backfill" is gone -- in its place, "Reset all" (styled as a
destructive action, gated by a confirm dialog) wipes every synced activity
and its laps/kind assignments and rewinds the backfill watermark, so the next
sync performs a genuinely fresh pull instead of trying to patch up existing
rows with newer schema fields.
This commit is contained in:
2026-07-19 12:41:38 +02:00
parent af41aa0f7f
commit 7f7e4b10f0
10 changed files with 235 additions and 12 deletions

View File

@@ -0,0 +1,71 @@
package store
import (
"context"
"testing"
)
func TestResetAllSyncedData_DeletesActivitiesCascadesAndRewindsWatermark(t *testing.T) {
db := openTestDB(t)
ctx := context.Background()
activityID, err := db.UpsertActivity(ctx, Activity{GarminActivityID: 1, ActivityType: "running", StartTimeUTC: "2026-07-11 06:00:00", RawJSON: "{}"})
if err != nil {
t.Fatalf("UpsertActivity: %v", err)
}
kindID, err := db.CreateWorkoutKind(ctx, WorkoutKind{Name: "Test Reset Kind", RuleJSON: `{"match":"all","conditions":[]}`, IsActive: true})
if err != nil {
t.Fatalf("CreateWorkoutKind: %v", err)
}
if err := db.ReplaceLaps(ctx, activityID, []Lap{{LapIndex: 1}}); err != nil {
t.Fatalf("ReplaceLaps: %v", err)
}
if _, err := db.InsertKindAssignment(ctx, KindAssignment{
ActivityID: activityID, WorkoutKindID: &kindID, AssignmentSource: AssignmentSourceRuleEngine,
Status: AssignmentStatusAssigned, CandidateKindsJSON: "[]",
}); err != nil {
t.Fatalf("InsertKindAssignment: %v", err)
}
if err := db.UpdateSyncState(ctx, "2020-01-01", true); err != nil {
t.Fatalf("UpdateSyncState: %v", err)
}
if err := db.ResetAllSyncedData(ctx); err != nil {
t.Fatalf("ResetAllSyncedData: %v", err)
}
activities, err := db.ListActivities(ctx, ActivityFilter{})
if err != nil {
t.Fatalf("ListActivities: %v", err)
}
if len(activities) != 0 {
t.Errorf("expected 0 activities after reset, got %d", len(activities))
}
laps, err := db.LapsForActivity(ctx, activityID)
if err != nil {
t.Fatalf("LapsForActivity: %v", err)
}
if len(laps) != 0 {
t.Errorf("expected laps to cascade-delete, got %d", len(laps))
}
if _, ok, err := db.CurrentAssignment(ctx, activityID); err != nil || ok {
t.Errorf("expected kind assignment to cascade-delete, ok=%v err=%v", ok, err)
}
state, err := db.GetSyncState(ctx)
if err != nil {
t.Fatalf("GetSyncState: %v", err)
}
if state.EarliestSyncedDate != nil || state.BackfillComplete {
t.Errorf("expected watermark rewound to fresh state, got %+v", state)
}
// Workout kinds (the user's taxonomy) must survive a reset.
kind, ok, err := db.GetWorkoutKind(ctx, kindID)
if err != nil || !ok {
t.Fatalf("GetWorkoutKind after reset: ok=%v err=%v", ok, err)
}
if kind.Name != "Test Reset Kind" {
t.Errorf("workout kind was unexpectedly affected by reset: %+v", kind)
}
}