feat(store): add workout_not_found_at, exclude it from missing-workout queries
A confirmed-404 workout must stop being retried forever, but workout_raw_json should never be fabricated -- it stays null exactly as it does for "not yet fetched." workout_not_found_at is the separate marker ActivitiesMissingWorkout/CountActivitiesMissingWorkout now check for, mirroring the existing pattern of the other _fetched_at columns. UpsertActivity's ON CONFLICT clause already never touches these columns, so the marker persists across every later re-sync.
This commit is contained in:
@@ -459,6 +459,80 @@ func TestActivitiesMissingWorkout_OnlyIncludesActivitiesWithDetailsAlreadyFetche
|
||||
}
|
||||
}
|
||||
|
||||
func TestActivitiesMissingWorkout_ExcludesConfirmedNotFound(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)
|
||||
// Details already fetched, workout confirmed 404 on Garmin -- must not
|
||||
// be retried, so must not appear in ActivitiesMissingWorkout/Count.
|
||||
notFound := Activity{
|
||||
GarminActivityID: 1, WorkoutID: &workoutID,
|
||||
StartTimeUTC: "2026-07-01 06:00:00", RawJSON: "{}",
|
||||
}
|
||||
idA, err := db.UpsertActivity(ctx, userID, notFound)
|
||||
if err != nil {
|
||||
t.Fatalf("UpsertActivity (a): %v", err)
|
||||
}
|
||||
if err := db.SetActivityDetails(ctx, userID, idA, "{}"); err != nil {
|
||||
t.Fatalf("SetActivityDetails (a): %v", err)
|
||||
}
|
||||
if err := db.SetActivitySplitsFetched(ctx, userID, idA); err != nil {
|
||||
t.Fatalf("SetActivitySplitsFetched (a): %v", err)
|
||||
}
|
||||
if err := db.SetActivityWorkoutNotFound(ctx, userID, idA); err != nil {
|
||||
t.Fatalf("SetActivityWorkoutNotFound (a): %v", err)
|
||||
}
|
||||
|
||||
// A genuinely still-pending activity (details fetched, workout not yet
|
||||
// attempted) must still be included, for contrast.
|
||||
stillPending := Activity{
|
||||
GarminActivityID: 2, WorkoutID: &workoutID,
|
||||
StartTimeUTC: "2026-07-02 06:00:00", RawJSON: "{}",
|
||||
}
|
||||
idB, err := db.UpsertActivity(ctx, userID, stillPending)
|
||||
if err != nil {
|
||||
t.Fatalf("UpsertActivity (b): %v", err)
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
n, err := db.CountActivitiesMissingWorkout(ctx, userID)
|
||||
if err != nil {
|
||||
t.Fatalf("CountActivitiesMissingWorkout: %v", err)
|
||||
}
|
||||
if n != 1 {
|
||||
t.Fatalf("CountActivitiesMissingWorkout = %d, want 1 (only activity b)", n)
|
||||
}
|
||||
|
||||
pending, err := db.ActivitiesMissingWorkout(ctx, userID, 10)
|
||||
if err != nil {
|
||||
t.Fatalf("ActivitiesMissingWorkout: %v", err)
|
||||
}
|
||||
if len(pending) != 1 || pending[0].ID != idB {
|
||||
t.Fatalf("ActivitiesMissingWorkout = %+v, want only activity b (id %d)", pending, idB)
|
||||
}
|
||||
|
||||
confirmed, _, err := db.GetActivity(ctx, userID, idA)
|
||||
if err != nil {
|
||||
t.Fatalf("GetActivity (a): %v", err)
|
||||
}
|
||||
if confirmed.WorkoutNotFoundAt == nil {
|
||||
t.Error("activity a's WorkoutNotFoundAt is nil, want it set")
|
||||
}
|
||||
if confirmed.WorkoutRawJSON != nil {
|
||||
t.Error("activity a's WorkoutRawJSON should stay nil -- not-found is recorded separately, not faked")
|
||||
}
|
||||
}
|
||||
|
||||
func contains(s string, substrs ...string) bool {
|
||||
lower := strings.ToLower(s)
|
||||
for _, substr := range substrs {
|
||||
|
||||
Reference in New Issue
Block a user