diff --git a/shared/src/commonMain/kotlin/com/devil/phoenixproject/presentation/manager/ActiveSessionEngine.kt b/shared/src/commonMain/kotlin/com/devil/phoenixproject/presentation/manager/ActiveSessionEngine.kt index 643fb095..af0a56e0 100644 --- a/shared/src/commonMain/kotlin/com/devil/phoenixproject/presentation/manager/ActiveSessionEngine.kt +++ b/shared/src/commonMain/kotlin/com/devil/phoenixproject/presentation/manager/ActiveSessionEngine.kt @@ -1100,6 +1100,12 @@ class ActiveSessionEngine( // motion; let normal AMRAP / stall auto-stop resume by zeroing the // deadline (the source-of-truth field). deferAutoStopDeadlineMs = 0L + // Issue #652: the same completed-rep boundary is authoritative for + // the shared stall countdown (both DELOAD_OCCURRED and low-velocity + // arms write to coordinator.stallStartTime). Without this, a stale + // countdown started at a turnaround, brief pause, or firmware de-load + // can survive a subsequent valid rep and later auto-complete the set. + resetStallTimer() // Capture rep boundary timestamp BEFORE scoring so scoreCurrentRep() // and processBiomechanicsForRep() both see the correct metric window. diff --git a/shared/src/commonTest/kotlin/com/devil/phoenixproject/presentation/manager/DWSMWorkoutLifecycleTest.kt b/shared/src/commonTest/kotlin/com/devil/phoenixproject/presentation/manager/DWSMWorkoutLifecycleTest.kt index 77ce980a..6316d1ad 100644 --- a/shared/src/commonTest/kotlin/com/devil/phoenixproject/presentation/manager/DWSMWorkoutLifecycleTest.kt +++ b/shared/src/commonTest/kotlin/com/devil/phoenixproject/presentation/manager/DWSMWorkoutLifecycleTest.kt @@ -1204,6 +1204,58 @@ class DWSMWorkoutLifecycleTest { harness.cleanup() } + @Test + fun `Issue 652 - completed Echo rep cancels an armed stall timer`() = runTest { + val harness = DWSMTestHarness(this) + harness.fakeBleRepo.simulateConnect("Vee_Test") + harness.dwsm.updateWorkoutParameters( + WorkoutParameters( + programMode = ProgramMode.Echo, + echoLevel = EchoLevel.HARDER, + reps = 10, + warmupReps = 0, + stallDetectionEnabled = true, + isAMRAP = false, + isJustLift = false, + ), + ) + harness.dwsm.startWorkout(skipCountdown = true) + advanceUntilIdle() + completeWarmupReps(harness, warmupTarget = 3, workingTarget = 10) + completeFirstWorkingRep(harness, warmupTarget = 3, workingTarget = 10) + advanceUntilIdle() + + harness.fakeBleRepo.emitDeloadOccurred() + advanceUntilIdle() + assertNotNull(harness.dwsm.coordinator.stallStartTime) + + harness.fakeBleRepo.emitRepNotification( + RepNotification( + topCounter = 5, + completeCounter = 5, + repsRomCount = 3, + repsRomTotal = 3, + repsSetCount = 2, + repsSetTotal = 10, + rangeTop = 800f, + rangeBottom = 0f, + rawData = ByteArray(24), + timestamp = 5L, + ), + ) + advanceUntilIdle() + + assertEquals(2, harness.dwsm.coordinator.repCount.value.workingReps) + assertEquals( + null, + harness.dwsm.coordinator.stallStartTime, + "A completed working rep proves motion resumed and must cancel the stale stall countdown", + ) + assertFalse(harness.dwsm.coordinator.isCurrentlyStalled) + assertIs(harness.dwsm.coordinator.workoutState.value) + harness.cleanup() + } + @Test fun `Issue 267 Just Lift warmup to working rep transitions without failed stall state`() = runTest { val harness = DWSMTestHarness(this)