Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7752,6 +7752,11 @@ class ActiveSessionEngine(
} else {
variableWarmupTarget ?: seedParams.reps
}
val usesUnlimitedRepTarget = requiresMachine &&
!isBodyweightAtStart &&
!isTimedCableAtStart &&
variableWarmupTarget == null &&
(isJustLiftMode || seedParams.isJustLift || seedParams.isAMRAP)
val outgoingLease = executionGuard.currentLease
val executionSeed = ExecutionSeed(
sessionId = KmpUtils.randomUUID(),
Expand All @@ -7762,6 +7767,7 @@ class ActiveSessionEngine(
isJustLift = isJustLiftMode || seedParams.isJustLift,
isAmrap = seedParams.isAMRAP,
isTimedCable = isTimedCableAtStart,
usesUnlimitedRepTarget = usesUnlimitedRepTarget,
)
beforeExecutionBeginForTest?.invoke()
val leaseResult = when {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@ package com.devil.phoenixproject.presentation.manager

import com.devil.phoenixproject.data.repository.RepNotification

private const val UNLIMITED_REPS_SET_TOTAL = 252

internal sealed interface RepFreshnessState {
data object AwaitingEvidence : RepFreshnessState
data class LegacyBaseline(val topCounter: Int, val completeCounter: Int) : RepFreshnessState
Expand Down Expand Up @@ -56,13 +58,16 @@ internal class RepNotificationFreshnessGate {
val identity = lease.identity()
if (notification.isLegacyFormat) return evaluateLegacy(identity, notification)

// Issue #698/#700: Just Lift and AMRAP with target=0 use unlimited
// target semantics (0xFF/252), so the device-reported repsSetTotal
// will never match the finite UI lease target. Exempt both from
// target equality check. AMRAP with a finite target (>0) must still
// match — only unlimited AMRAP gets the exemption.
// Issue #698/#700/#712: Just Lift and AMRAP use unlimited target
// semantics on the wire (0xFF, reported back as 252). Routine AMRAP
// leases retain their configured finite UI fallback target (for
// example 10), so lease.workingRepTarget == 0 is not a reliable
// unlimited discriminator. The notification sentinel is.
val isUnlimitedAmrapPacket = lease.isAmrap &&
lease.usesUnlimitedRepTarget &&
notification.repsSetTotal == UNLIMITED_REPS_SET_TOTAL
Comment thread
9thLevelSoftware marked this conversation as resolved.
val targetMatches = lease.isJustLift ||
(lease.isAmrap && lease.workingRepTarget == 0) ||
isUnlimitedAmrapPacket ||
notification.repsSetTotal == 0 ||
notification.repsSetTotal == lease.workingRepTarget
if (!targetMatches) return RepFreshnessDecision.Drop(RepDropReason.TARGET_MISMATCH)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ internal data class ExecutionSeed(
val isJustLift: Boolean = false,
val isAmrap: Boolean = false,
val isTimedCable: Boolean = false,
val usesUnlimitedRepTarget: Boolean = false,
)

internal data class ExecutionLease(
Expand All @@ -67,6 +68,7 @@ internal data class ExecutionLease(
val isJustLift: Boolean,
val isAmrap: Boolean,
val isTimedCable: Boolean,
val usesUnlimitedRepTarget: Boolean = false,
val activationCutoverTimestampMs: Long? = null,
)

Expand Down Expand Up @@ -405,6 +407,7 @@ internal class WorkoutExecutionGuard(
isJustLift = seed.isJustLift,
isAmrap = seed.isAmrap,
isTimedCable = seed.isTimedCable,
usesUnlimitedRepTarget = seed.usesUnlimitedRepTarget,
)
currentLeaseRef.value?.let { outgoingLease ->
if (sameIdentity(completionClaim?.lease, outgoingLease)) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ class RoutineSetWeightResolverTest {
)

@Test
fun `resolves programmed, scaled, and manually adjusted routine set weights`() {
fun `resolves programmed scaled and manually adjusted routine set weights`() {
data class Case(
val name: String,
val routineExercise: RoutineExercise,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -202,7 +202,12 @@ class RepNotificationFreshnessGateTest {
@Test
fun `amrap lease accepts repsSetTotal 252 despite finite UI target`() {
val gate = RepNotificationFreshnessGate()
val lease = activeLease(target = 0, cutover = 1_000L).copy(isAmrap = true)
// Routine AMRAP retains its configured UI fallback target on the lease
// even though the machine command uses the unlimited 0xFF sentinel.
val lease = activeLease(target = 10, cutover = 1_000L).copy(
isAmrap = true,
usesUnlimitedRepTarget = true,
)

// First packet establishes baseline and arms
assertEquals(RepFreshnessDecision.BaselineOnly, gate.evaluate(lease, modernPacket(timestamp = 1_001L)))
Expand All @@ -218,7 +223,10 @@ class RepNotificationFreshnessGateTest {
@Test
fun `amrap lease does not treat repsSetCount as terminal`() {
val gate = RepNotificationFreshnessGate()
val lease = activeLease(target = 0, cutover = 1_000L).copy(isAmrap = true)
val lease = activeLease(target = 0, cutover = 1_000L).copy(
isAmrap = true,
usesUnlimitedRepTarget = true,
)

// AMRAP should never treat repsSetCount as terminal, same as Just Lift
assertEquals(RepFreshnessDecision.BaselineOnly, gate.evaluate(lease, modernPacket(timestamp = 1_001L)))
Expand All @@ -228,6 +236,20 @@ class RepNotificationFreshnessGateTest {
)
}

@Test
fun `amrap variable warmup execution rejects delayed unlimited packet`() {
val gate = RepNotificationFreshnessGate()
val lease = activeLease(target = 3, cutover = 1_000L).copy(
isAmrap = true,
usesUnlimitedRepTarget = false,
)

assertEquals(
RepFreshnessDecision.Drop(RepDropReason.TARGET_MISMATCH),
gate.evaluate(lease, modernPacket(repsSetCount = 1, repsSetTotal = 252, timestamp = 1_001L)),
)
}

@Test
fun `finite amrap lease still rejects mismatched repsSetTotal`() {
val gate = RepNotificationFreshnessGate()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -101,7 +101,7 @@ class RestoredRuntimeTimerRaceTest {
val staleReset = async(Dispatchers.Default) {
harness.dwsm.resetForNewWorkout()
}
withContext(Dispatchers.IO) {
withContext(Dispatchers.Default) {
withTimeout(2_000L) { staleDetachEntered.await() }
}

Expand All @@ -118,7 +118,7 @@ class RestoredRuntimeTimerRaceTest {
assertTrue(newerJob.isActive)

releaseStaleDetach.complete(Unit)
withContext(Dispatchers.IO) {
withContext(Dispatchers.Default) {
withTimeout(2_000L) { staleReset.await() }
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -256,7 +256,7 @@ class WorkoutExecutionGuardRestoredRuntimeTest {
var ownerWasCurrentBeforeCallbackReturned = false
var publications = 0

val publication = async(Dispatchers.IO) {
val publication = async(Dispatchers.Default) {
guard.commitRestoredTimerPublication(
owner = owner,
candidateStillCurrent = { true },
Expand All @@ -270,7 +270,7 @@ class WorkoutExecutionGuardRestoredRuntimeTest {
}

withTimeout(2_000) { callbackEntered.await() }
val supersession = async(Dispatchers.IO) {
val supersession = async(Dispatchers.Default) {
supersessionStarted.complete(Unit)
guard.supersedeRecoveryPublication()
supersessionFinished.complete(Unit)
Expand Down
Loading