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
23 changes: 22 additions & 1 deletion Strand/Screens/TodayView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -4062,7 +4062,28 @@ struct TodayView: View {
calibrationSampleDays: Int) -> Bool {
// Optional-chained deliberately: an unset (or unparseable) key is NOT a 4.0. The key only ever
// holds a `WhoopModel` rawValue, so nil here means "no strap has been identified", not "4.0".
(WhoopModel(rawValue: selectedModelRaw)?.deviceFamily == .whoop4 && hasDayData)
let family = WhoopModel(rawValue: selectedModelRaw)?.deviceFamily
// #1523 follow-up: a POSITIVELY identified 5/MG never sees this, whatever calibration state the
// profile carries from an earlier strap. #1579 stopped a partial sample-day count activating the
// affordance but left the coefficient paths able to, and those are profile-global — so a user who
// calibrated a 4.0 and then moved to a 5.0 still got the 4.0 prompt on any day the 5.0 logged no
// steps and no estimate existed. That is the same complaint #1523 opened, on a narrower trigger.
//
// The justification for the coefficient paths was preserving estimate behaviour across that
// migration, but this gate does not control the estimate: `estSteps` comes from `stepsEstByDay`
// and is computed independently. All this gate decides is whether a BLANK tile offers to
// calibrate — and a strap that reports steps natively has nothing to calibrate.
//
// Android has been immune by construction all along: `stepsCalibrationPrompt` returns early on
// `model != WhoopModel.WHOOP4.name` before reading any calibration state — and this is written the
// same way round, "positively identified and NOT a 4.0", rather than "is a 5". Those are the same
// set today because `WhoopModel` has exactly two cases, but they stop being the same the moment a
// third is added, and the version that would then be wrong is the one naming a specific family.
if let family, family != .whoop4 { return false }
// The coefficient paths stay for everything else, and are NOT redundant with the family check: a
// legacy 4.0 owner whose `selectedWhoopModel` was never written still has a coefficient, and
// dropping these would silently take the gear away from them.
return (family == .whoop4 && hasDayData)
|| calibrationCoefficient > 0
|| manualCoefficient > 0
}
Expand Down
38 changes: 26 additions & 12 deletions StrandTests/TodayStepsPipelineTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,18 +31,32 @@ final class TodayStepsPipelineTests: XCTestCase {
XCTAssertFalse(active(model: .whoop4, hasDayData: false, sampleDays: 3))
}

func testFittedCoefficientPreservesMigratedFourPointZeroProfile() {
XCTAssertTrue(active(model: .whoop5mg,
hasDayData: true,
calibrationCoefficient: 0.42,
sampleDays: 5))
}

func testManualCoefficientPreservesMigratedFourPointZeroProfile() {
XCTAssertTrue(active(model: .whoop5mg,
hasDayData: true,
manualCoefficient: 0.35,
sampleDays: 1))
/// #1523 follow-up: these two asserted TRUE when the suite landed, on the grounds that a profile
/// migrating from a calibrated 4.0 to a 5.0 should keep its estimate behaviour. That reasoning does
/// not apply to THIS gate — `estSteps` is computed independently in `stepsEstByDay`, and all this
/// decides is whether a blank tile offers to calibrate. A 5/MG reports steps natively, so it has
/// nothing to calibrate, and showing it the 4.0 prompt is the complaint #1523 opened.
func testFittedCoefficientDoesNotPromptOnAFivePointZero() {
XCTAssertFalse(active(model: .whoop5mg,
hasDayData: true,
calibrationCoefficient: 0.42,
sampleDays: 5))
}

func testManualCoefficientDoesNotPromptOnAFivePointZero() {
XCTAssertFalse(active(model: .whoop5mg,
hasDayData: true,
manualCoefficient: 0.35,
sampleDays: 1))
}

/// …but the coefficient paths must NOT simply be deleted, which is the tempting reading of the two
/// above. A legacy 4.0 owner whose `selectedWhoopModel` key was never written has no family to match
/// on, and only the coefficient says they are mid-estimate. Dropping these would silently take the
/// calibration gear away from exactly the users #1491 restored it for.
func testACoefficientStillActivatesWhenNoModelWasEverRecorded() {
XCTAssertTrue(active(model: nil, hasDayData: true, calibrationCoefficient: 0.42))
XCTAssertTrue(active(model: nil, hasDayData: true, manualCoefficient: 0.35))
}

func testUnsetModelAndPartialSampleDaysStayInactive() {
Expand Down
Loading