feat(store): add ActivitiesMissingWorkout/CountActivitiesMissingWorkout
Mirrors ActivitiesMissingDetails/CountActivitiesMissingDetails, but queries workout_id IS NOT NULL AND workout_raw_json IS NULL -- independent of whether details/splits were ever fetched, since workout_id is known at initial upsert time. This also fixes a latent gap: an activity whose details were fetched successfully but whose workout fetch failed in the same run previously had no way to ever be retried, since ActivitiesMissingDetails stops returning it the moment details_fetched_at/splits_fetched_at are set.
This commit is contained in:
@@ -356,6 +356,86 @@ func TestCurrentAssignment_ScopedToOwningUser(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestActivitiesMissingWorkout_IndependentOfDetailsFetchStatus(t *testing.T) {
|
||||
db := openTestDB(t)
|
||||
ctx := context.Background()
|
||||
userID, err := db.ProvisionUser(ctx, "test-sub", "Test")
|
||||
if err != nil {
|
||||
t.Fatalf("ProvisionUser: %v", err)
|
||||
}
|
||||
|
||||
workoutID := int64(999)
|
||||
withWorkoutNoDetails := Activity{
|
||||
GarminActivityID: 1, WorkoutID: &workoutID,
|
||||
StartTimeUTC: "2026-07-01 06:00:00", RawJSON: "{}",
|
||||
}
|
||||
idA, err := db.UpsertActivity(ctx, userID, withWorkoutNoDetails)
|
||||
if err != nil {
|
||||
t.Fatalf("UpsertActivity (a): %v", err)
|
||||
}
|
||||
|
||||
withWorkoutAndDetails := Activity{
|
||||
GarminActivityID: 2, WorkoutID: &workoutID,
|
||||
StartTimeUTC: "2026-07-02 06:00:00", RawJSON: "{}",
|
||||
}
|
||||
idB, err := db.UpsertActivity(ctx, userID, withWorkoutAndDetails)
|
||||
if err != nil {
|
||||
t.Fatalf("UpsertActivity (b): %v", err)
|
||||
}
|
||||
// Simulate an activity whose details/splits were already fetched in a
|
||||
// prior run, but whose workout fetch failed back then -- this is exactly
|
||||
// the case ActivitiesMissingWorkout must still surface, since it's
|
||||
// queried independently of details_fetched_at/splits_fetched_at.
|
||||
if err := db.SetActivityDetails(ctx, userID, idB, "{}"); err != nil {
|
||||
t.Fatalf("SetActivityDetails (b): %v", err)
|
||||
}
|
||||
if err := db.SetActivitySplitsFetched(ctx, userID, idB); err != nil {
|
||||
t.Fatalf("SetActivitySplitsFetched (b): %v", err)
|
||||
}
|
||||
|
||||
noWorkout := Activity{
|
||||
GarminActivityID: 3, StartTimeUTC: "2026-07-03 06:00:00", RawJSON: "{}",
|
||||
}
|
||||
if _, err := db.UpsertActivity(ctx, userID, noWorkout); err != nil {
|
||||
t.Fatalf("UpsertActivity (c): %v", err)
|
||||
}
|
||||
|
||||
alreadyHasWorkout := Activity{
|
||||
GarminActivityID: 4, WorkoutID: &workoutID,
|
||||
StartTimeUTC: "2026-07-04 06:00:00", RawJSON: "{}",
|
||||
}
|
||||
idD, err := db.UpsertActivity(ctx, userID, alreadyHasWorkout)
|
||||
if err != nil {
|
||||
t.Fatalf("UpsertActivity (d): %v", err)
|
||||
}
|
||||
if err := db.SetActivityWorkout(ctx, userID, idD, `{"segments":[]}`); err != nil {
|
||||
t.Fatalf("SetActivityWorkout (d): %v", err)
|
||||
}
|
||||
|
||||
n, err := db.CountActivitiesMissingWorkout(ctx, userID)
|
||||
if err != nil {
|
||||
t.Fatalf("CountActivitiesMissingWorkout: %v", err)
|
||||
}
|
||||
if n != 2 {
|
||||
t.Fatalf("CountActivitiesMissingWorkout = %d, want 2 (activities a and b)", n)
|
||||
}
|
||||
|
||||
pending, err := db.ActivitiesMissingWorkout(ctx, userID, 10)
|
||||
if err != nil {
|
||||
t.Fatalf("ActivitiesMissingWorkout: %v", err)
|
||||
}
|
||||
if len(pending) != 2 {
|
||||
t.Fatalf("ActivitiesMissingWorkout returned %d activities, want 2", len(pending))
|
||||
}
|
||||
ids := map[int64]bool{}
|
||||
for _, a := range pending {
|
||||
ids[a.ID] = true
|
||||
}
|
||||
if !ids[idA] || !ids[idB] {
|
||||
t.Errorf("ActivitiesMissingWorkout = %+v, want to include activities a and b", pending)
|
||||
}
|
||||
}
|
||||
|
||||
func contains(s string, substrs ...string) bool {
|
||||
lower := strings.ToLower(s)
|
||||
for _, substr := range substrs {
|
||||
|
||||
Reference in New Issue
Block a user