Add Race workout kind with Garmin eventType auto-detection, sync-time running filter, and pill-based review queue filter
Race is the 8th fixed workout kind, seeded with a real (not placeholder) rule since Garmin Connect's eventType.typeKey reports "race" for manually-tagged race activities. Non-running activity types (padel, cycling, strength training, ...) are now dropped at sync time instead of being stored. The Review Queue's type filter is now clickable exclusive pill buttons instead of a dropdown.
This commit is contained in:
@@ -62,6 +62,40 @@ func TestBackfill_StoresActivitiesAndRecordsSyncRun(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestBackfill_SkipsNonRunningActivities(t *testing.T) {
|
||||
db := openTestDB(t)
|
||||
ctx := context.Background()
|
||||
|
||||
m := &mock.Client{Activities: []garmin.Activity{
|
||||
{ActivityID: 1, ActivityType: garmin.ActivityType{TypeKey: "running"},
|
||||
StartTimeGMT: "2026-07-01 06:00:00", Distance: 5000, Duration: 1500},
|
||||
{ActivityID: 2, ActivityType: garmin.ActivityType{TypeKey: "trail_running"},
|
||||
StartTimeGMT: "2026-07-02 06:00:00", Distance: 8000, Duration: 2400},
|
||||
{ActivityID: 3, ActivityType: garmin.ActivityType{TypeKey: "paddelball"},
|
||||
StartTimeGMT: "2026-07-03 06:00:00", Distance: 0, Duration: 1800},
|
||||
{ActivityID: 4, ActivityType: garmin.ActivityType{TypeKey: "indoor_cycling"},
|
||||
StartTimeGMT: "2026-07-04 06:00:00", Distance: 0, Duration: 1800},
|
||||
}}
|
||||
svc := NewService(m, db, Config{}, fixedNow(time.Date(2026, 7, 11, 0, 0, 0, 0, time.UTC)))
|
||||
|
||||
if err := svc.Backfill(ctx); err != nil {
|
||||
t.Fatalf("Backfill: %v", err)
|
||||
}
|
||||
|
||||
activities, err := db.ListActivities(ctx, store.ActivityFilter{})
|
||||
if err != nil {
|
||||
t.Fatalf("ListActivities: %v", err)
|
||||
}
|
||||
if len(activities) != 2 {
|
||||
t.Fatalf("expected 2 stored (running-only) activities, got %d: %+v", len(activities), activities)
|
||||
}
|
||||
for _, a := range activities {
|
||||
if a.GarminActivityID != 1 && a.GarminActivityID != 2 {
|
||||
t.Errorf("unexpected non-running activity stored: %+v", a)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFillPendingDetailsAndClassify_EndToEnd(t *testing.T) {
|
||||
db := openTestDB(t)
|
||||
ctx := context.Background()
|
||||
@@ -162,6 +196,21 @@ func TestBuildMetricContext_DerivesExpectedMetrics(t *testing.T) {
|
||||
if got := ctx["lap_interval_pattern"]; got != 0 {
|
||||
t.Errorf("lap_interval_pattern = %v, want 0", got)
|
||||
}
|
||||
if got := ctx["is_race"]; got != 0 {
|
||||
t.Errorf("is_race = %v, want 0 (EventTypeKey not set)", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildMetricContext_DerivesIsRace(t *testing.T) {
|
||||
ctx := buildMetricContext(store.Activity{EventTypeKey: "race"}, nil, 0)
|
||||
if got := ctx["is_race"]; got != 1 {
|
||||
t.Errorf("is_race = %v, want 1 when EventTypeKey is \"race\"", got)
|
||||
}
|
||||
|
||||
ctx = buildMetricContext(store.Activity{EventTypeKey: "training"}, nil, 0)
|
||||
if got := ctx["is_race"]; got != 0 {
|
||||
t.Errorf("is_race = %v, want 0 when EventTypeKey is not \"race\"", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBackfill_SecondRunIsANoOpOnceHorizonFullyCovered(t *testing.T) {
|
||||
|
||||
Reference in New Issue
Block a user