diff --git a/Packages/StrandAnalytics/Sources/StrandAnalytics/WorkoutDetector.swift b/Packages/StrandAnalytics/Sources/StrandAnalytics/WorkoutDetector.swift index 40e5ccff7e..70b3090cbd 100644 --- a/Packages/StrandAnalytics/Sources/StrandAnalytics/WorkoutDetector.swift +++ b/Packages/StrandAnalytics/Sources/StrandAnalytics/WorkoutDetector.swift @@ -372,8 +372,8 @@ public enum WorkoutDetector { /// #510: backfill ONLY the avgHr/maxHr/energyKcal/strain fields `real` doesn't already have, from a /// detected bout's own computed values — never touching a field that's already present, whether - /// typed by the user, imported, or filled by an earlier pass. `real` unchanged (`==`) means it - /// already had everything, so the caller can tell whether a write is actually needed. Kotlin twin + /// typed by the user, imported, or filled by an earlier pass. `real` unchanged (`==`) means no + /// available value was filled, so the caller can tell whether a write is actually needed. Kotlin twin /// IntelligenceEngine.backfillWorkoutFromDetectedBout. public static func backfillWorkout(_ real: WorkoutRow, avgBpm: Int, peakHR: Int, caloriesKcal: Double?, strain: Double?) -> WorkoutRow { WorkoutRow( @@ -383,7 +383,8 @@ public enum WorkoutDetector { avgHr: real.avgHr ?? avgBpm, maxHr: real.maxHr ?? peakHR, strain: real.strain ?? strain, - distanceM: real.distanceM, zonesJSON: real.zonesJSON, notes: real.notes) + distanceM: real.distanceM, zonesJSON: real.zonesJSON, notes: real.notes, + steps: real.steps) } } diff --git a/Packages/StrandAnalytics/Tests/StrandAnalyticsTests/WorkoutDetectorBackfillTests.swift b/Packages/StrandAnalytics/Tests/StrandAnalyticsTests/WorkoutDetectorBackfillTests.swift index 558f3b3c8b..ce122b41a7 100644 --- a/Packages/StrandAnalytics/Tests/StrandAnalyticsTests/WorkoutDetectorBackfillTests.swift +++ b/Packages/StrandAnalytics/Tests/StrandAnalyticsTests/WorkoutDetectorBackfillTests.swift @@ -8,19 +8,21 @@ import WhoopStore /// though the detector had already computed a valid average for that exact window. final class WorkoutDetectorBackfillTests: XCTestCase { - private func manualRow(avgHr: Int? = nil, maxHr: Int? = nil, energyKcal: Double? = nil, strain: Double? = nil) -> WorkoutRow { + private func manualRow(avgHr: Int? = nil, maxHr: Int? = nil, energyKcal: Double? = nil, + strain: Double? = nil, steps: Int? = 4_200) -> WorkoutRow { WorkoutRow(startTs: 1_000, endTs: 1_600, sport: "Running", source: "manual", durationS: 600, energyKcal: energyKcal, avgHr: avgHr, maxHr: maxHr, - strain: strain, distanceM: nil, zonesJSON: nil, notes: nil) + strain: strain, distanceM: nil, zonesJSON: nil, notes: nil, steps: steps) } - func testFillsAllMissingFields() { + func testFillsAllMissingFieldsAndPreservesSteps() { let real = manualRow() let filled = WorkoutDetector.backfillWorkout(real, avgBpm: 150, peakHR: 170, caloriesKcal: 80.0, strain: 9.5) XCTAssertEqual(filled.avgHr, 150) XCTAssertEqual(filled.maxHr, 170) XCTAssertEqual(filled.energyKcal, 80.0) XCTAssertEqual(filled.strain, 9.5) + XCTAssertEqual(filled.steps, 4_200, "stored per-session steps must survive backfill") // The rest of the row is carried over untouched. XCTAssertEqual(filled.startTs, real.startTs) XCTAssertEqual(filled.sport, real.sport) @@ -37,7 +39,7 @@ final class WorkoutDetectorBackfillTests: XCTestCase { XCTAssertEqual(filled.strain, 9.5, "a missing field must still be filled") } - func testRowWithEverythingAlreadyPresentIsUnchanged() { + func testRowWithStepsAndEverythingAlreadyPresentIsUnchanged() { let real = manualRow(avgHr: 140, maxHr: 160, energyKcal: 50.0, strain: 8.0) let filled = WorkoutDetector.backfillWorkout(real, avgBpm: 150, peakHR: 170, caloriesKcal: 80.0, strain: 9.5) XCTAssertEqual(filled, real, "nothing to fill -> byte-identical row, so the caller can skip the write")