From b0578898d17bab886fccb108006196e1c0e48ad8 Mon Sep 17 00:00:00 2001 From: Johnny Huynh <27847622+johnnyhuy@users.noreply.github.com> Date: Fri, 13 Nov 2026 02:25:47 +0700 Subject: [PATCH] fix: count each body region once per workout Co-authored-by: OpenAI Codex --- Sources/BetterFit/Models/Recovery.swift | 13 +-- Tests/BetterFitTests/IntegrationTests.swift | 2 + Tests/BetterFitTests/RecoveryTests.swift | 5 +- .../WorkoutRegionRecoveryTests.swift | 80 +++++++++++++++++++ docs/api.md | 2 +- 5 files changed, 92 insertions(+), 10 deletions(-) create mode 100644 Tests/BetterFitTests/WorkoutRegionRecoveryTests.swift diff --git a/Sources/BetterFit/Models/Recovery.swift b/Sources/BetterFit/Models/Recovery.swift index 88eb346..03107f0 100644 --- a/Sources/BetterFit/Models/Recovery.swift +++ b/Sources/BetterFit/Models/Recovery.swift @@ -13,15 +13,16 @@ public struct BodyMapRecovery: Codable, Equatable { self.lastUpdated = lastUpdated } - /// Update recovery status after a workout + /// Update each targeted body region once per recorded workout. public mutating func recordWorkout(_ workout: Workout) { - let muscleGroups = workout.exercises.flatMap { $0.exercise.muscleGroups } + let workedRegions = Set(workout.exercises.flatMap { workoutExercise in + workoutExercise.exercise.muscleGroups.map { group in + BodyRegion(rawValue: group.bodyMapRegion) ?? .other + } + }) - for group in muscleGroups { - let region = BodyRegion(rawValue: group.bodyMapRegion) ?? .other + for region in workedRegions { let currentStatus = regions[region] ?? .recovered - - // Mark as worked regions[region] = currentStatus.afterWorkout() } diff --git a/Tests/BetterFitTests/IntegrationTests.swift b/Tests/BetterFitTests/IntegrationTests.swift index ee60361..3b1d694 100644 --- a/Tests/BetterFitTests/IntegrationTests.swift +++ b/Tests/BetterFitTests/IntegrationTests.swift @@ -58,6 +58,8 @@ final class IntegrationTests: XCTestCase { let streak = betterFit.socialManager.getCurrentStreak() XCTAssertEqual(streak, 1) + + XCTAssertEqual(betterFit.bodyMapManager.getRecoveryStatus(for: .legs), .fatigued) } func testTemplateToWorkoutIntegration() { diff --git a/Tests/BetterFitTests/RecoveryTests.swift b/Tests/BetterFitTests/RecoveryTests.swift index 4e3fde0..71005ae 100644 --- a/Tests/BetterFitTests/RecoveryTests.swift +++ b/Tests/BetterFitTests/RecoveryTests.swift @@ -49,8 +49,7 @@ final class RecoveryTests: XCTestCase { manager.recordWorkout(workout) let status = manager.getRecoveryStatus(for: .legs) - // Squat works both quads and glutes, which both map to legs region - // So it gets hit twice: recovered -> fatigued -> sore - XCTAssertEqual(status, .sore) + // A compound exercise updates its body region once per workout. + XCTAssertEqual(status, .fatigued) } } diff --git a/Tests/BetterFitTests/WorkoutRegionRecoveryTests.swift b/Tests/BetterFitTests/WorkoutRegionRecoveryTests.swift new file mode 100644 index 0000000..dc59254 --- /dev/null +++ b/Tests/BetterFitTests/WorkoutRegionRecoveryTests.swift @@ -0,0 +1,80 @@ +import Foundation +import Testing + +import BetterFit + +struct WorkoutRegionRecoveryTests { + + // MARK: - Recording + + @Test(arguments: [ + [.quads, .glutes], + [.quads, .quads], + [.hamstrings, .glutes, .calves] + ] as [[MuscleGroup]]) + func compoundExerciseFatiguesRegionOnce(_ muscleGroups: [MuscleGroup]) { + var recovery = BodyMapRecovery() + + recovery.recordWorkout(workout([muscleGroups])) + + #expect(recovery.regions == [.legs: .fatigued]) + } + + @Test + func multipleExercisesFatigueEachTargetedRegionOnce() { + var recovery = BodyMapRecovery(regions: [.chest: .slightlyFatigued]) + + recovery.recordWorkout(workout([ + [.quads, .glutes], [.hamstrings, .calves], + [.biceps, .triceps], [.forearms], [.back, .lats] + ])) + + #expect(recovery.regions == [ + .legs: .fatigued, .arms: .fatigued, .back: .fatigued, + .chest: .slightlyFatigued + ]) + } + + @Test + func separateWorkoutsStillEscalateFatigue() { + var recovery = BodyMapRecovery() + + recovery.recordWorkout(workout([[.quads, .glutes]])) + #expect(recovery.regions[.legs] == .fatigued) + + recovery.recordWorkout(workout([[.hamstrings, .calves]])) + #expect(recovery.regions[.legs] == .sore) + } + + @Test(arguments: [RecoveryStatus.slightlyFatigued, .fatigued, .sore]) + func existingFatigueStillEscalates(_ status: RecoveryStatus) { + var recovery = BodyMapRecovery(regions: [.arms: status]) + + recovery.recordWorkout(workout([[.biceps, .triceps], [.forearms]])) + + #expect(recovery.regions == [.arms: .sore]) + } + + @Test(arguments: [[], [[]]] as [[[MuscleGroup]]]) + func workoutsWithoutTargetMusclesLeaveRegionsUnchanged(_ groups: [[MuscleGroup]]) { + let regions: [BodyRegion: RecoveryStatus] = [.chest: .fatigued, .legs: .sore] + var recovery = BodyMapRecovery(regions: regions) + + recovery.recordWorkout(workout(groups)) + + #expect(recovery.regions == regions) + } + + // MARK: - Fixtures + + private func workout(_ groups: [[MuscleGroup]]) -> Workout { + Workout( + name: "Training session", + exercises: groups.map { muscles in + WorkoutExercise(exercise: Exercise( + name: "Exercise", equipmentRequired: .bodyweight, muscleGroups: muscles + )) + } + ) + } +} diff --git a/docs/api.md b/docs/api.md index 4ddcbf8..0348193 100644 --- a/docs/api.md +++ b/docs/api.md @@ -211,7 +211,7 @@ public struct BodyMapRecovery: Codable, Equatable - `lastUpdated: Date` #### Methods -- `recordWorkout(_:)` - Update recovery after workout +- `recordWorkout(_:)` - Update each targeted body region once per recorded workout. Multiple muscles or exercises in the same region share one fatigue transition; separate recorded workouts can increase fatigue further. - `updateRecovery()` - Update based on time elapsed ### `RecoveryStatus`