Jelajahi Sumber

Merge branch 'dev' into feat/cgm-lifetime-indicator

Deniz Cengiz 2 minggu lalu
induk
melakukan
26aa7249c4

+ 2 - 2
Config.xcconfig

@@ -18,8 +18,8 @@ BUNDLE_IDENTIFIER = org.nightscout.$(DEVELOPMENT_TEAM).trio
 TRIO_APP_GROUP_ID = group.org.nightscout.$(DEVELOPMENT_TEAM).trio.trio-app-group
 TRIO_APP_GROUP_ID = group.org.nightscout.$(DEVELOPMENT_TEAM).trio.trio-app-group
 
 
 // The developers set the version numbers, please leave them alone
 // The developers set the version numbers, please leave them alone
-APP_VERSION = 0.8.2
-APP_DEV_VERSION = 0.8.2.1
+APP_VERSION = 0.8.3
+APP_DEV_VERSION = 0.8.3.2
 APP_BUILD_NUMBER = 1
 APP_BUILD_NUMBER = 1
 COPYRIGHT_NOTICE =
 COPYRIGHT_NOTICE =
 
 

+ 1 - 1
OmnipodKit

@@ -1 +1 @@
-Subproject commit 80069aa0d7562814187e6b0eed499277f5841cf5
+Subproject commit 4d254498cf016806b3e0ec33dd5fbfdf6e2135a6

+ 4 - 0
Trio.xcodeproj/project.pbxproj

@@ -588,6 +588,7 @@
 		CEE9A6592BBB418300EB5194 /* CalibrationsDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEE9A6542BBB418300EB5194 /* CalibrationsDataFlow.swift */; };
 		CEE9A6592BBB418300EB5194 /* CalibrationsDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEE9A6542BBB418300EB5194 /* CalibrationsDataFlow.swift */; };
 		CEE9A65C2BBB41C800EB5194 /* CalibrationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEE9A65B2BBB41C800EB5194 /* CalibrationService.swift */; };
 		CEE9A65C2BBB41C800EB5194 /* CalibrationService.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEE9A65B2BBB41C800EB5194 /* CalibrationService.swift */; };
 		CEE9A65E2BBC9F6500EB5194 /* CalibrationsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEE9A65D2BBC9F6500EB5194 /* CalibrationsTests.swift */; };
 		CEE9A65E2BBC9F6500EB5194 /* CalibrationsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEE9A65D2BBC9F6500EB5194 /* CalibrationsTests.swift */; };
+		CA02000000000000000010C2 /* DeliveryLimitsSyncTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA02000000000000000010C1 /* DeliveryLimitsSyncTests.swift */; };
 		CEF1ED6B2D58FB5800FAF41E /* CGMOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEF1ED6A2D58FB4600FAF41E /* CGMOptions.swift */; };
 		CEF1ED6B2D58FB5800FAF41E /* CGMOptions.swift in Sources */ = {isa = PBXBuildFile; fileRef = CEF1ED6A2D58FB4600FAF41E /* CGMOptions.swift */; };
 		D6D02515BBFBE64FEBE89856 /* HistoryRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 881E04BA5E0A003DE8E0A9C6 /* HistoryRootView.swift */; };
 		D6D02515BBFBE64FEBE89856 /* HistoryRootView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 881E04BA5E0A003DE8E0A9C6 /* HistoryRootView.swift */; };
 		D6DEC113821A7F1056C4AA1E /* NightscoutConfigDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F2A13DF0EDEEEDC4106AA2A /* NightscoutConfigDataFlow.swift */; };
 		D6DEC113821A7F1056C4AA1E /* NightscoutConfigDataFlow.swift in Sources */ = {isa = PBXBuildFile; fileRef = 2F2A13DF0EDEEEDC4106AA2A /* NightscoutConfigDataFlow.swift */; };
@@ -1530,6 +1531,7 @@
 		CEE9A6542BBB418300EB5194 /* CalibrationsDataFlow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CalibrationsDataFlow.swift; sourceTree = "<group>"; };
 		CEE9A6542BBB418300EB5194 /* CalibrationsDataFlow.swift */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.swift; path = CalibrationsDataFlow.swift; sourceTree = "<group>"; };
 		CEE9A65B2BBB41C800EB5194 /* CalibrationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalibrationService.swift; sourceTree = "<group>"; };
 		CEE9A65B2BBB41C800EB5194 /* CalibrationService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalibrationService.swift; sourceTree = "<group>"; };
 		CEE9A65D2BBC9F6500EB5194 /* CalibrationsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalibrationsTests.swift; sourceTree = "<group>"; };
 		CEE9A65D2BBC9F6500EB5194 /* CalibrationsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalibrationsTests.swift; sourceTree = "<group>"; };
+		CA02000000000000000010C1 /* DeliveryLimitsSyncTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeliveryLimitsSyncTests.swift; sourceTree = "<group>"; };
 		CEF1ED6A2D58FB4600FAF41E /* CGMOptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CGMOptions.swift; sourceTree = "<group>"; };
 		CEF1ED6A2D58FB4600FAF41E /* CGMOptions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CGMOptions.swift; sourceTree = "<group>"; };
 		CFCFE0781F9074C2917890E8 /* ManualTempBasalStateModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ManualTempBasalStateModel.swift; sourceTree = "<group>"; };
 		CFCFE0781F9074C2917890E8 /* ManualTempBasalStateModel.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = ManualTempBasalStateModel.swift; sourceTree = "<group>"; };
 		D0BDC6993C1087310EDFC428 /* CarbRatioEditorRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CarbRatioEditorRootView.swift; sourceTree = "<group>"; };
 		D0BDC6993C1087310EDFC428 /* CarbRatioEditorRootView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = CarbRatioEditorRootView.swift; sourceTree = "<group>"; };
@@ -2887,6 +2889,7 @@
 				BD8FC0552D66187700B95AED /* CoreDataTests */,
 				BD8FC0552D66187700B95AED /* CoreDataTests */,
 				38FCF3F125E9028E0078B0D1 /* Info.plist */,
 				38FCF3F125E9028E0078B0D1 /* Info.plist */,
 				CEE9A65D2BBC9F6500EB5194 /* CalibrationsTests.swift */,
 				CEE9A65D2BBC9F6500EB5194 /* CalibrationsTests.swift */,
+				CA02000000000000000010C1 /* DeliveryLimitsSyncTests.swift */,
 				3BAAE60B2DE776630049589B /* DynamicISFEnableTests.swift */,
 				3BAAE60B2DE776630049589B /* DynamicISFEnableTests.swift */,
 				38FCF3F825E902C20078B0D1 /* FileStorageTests.swift */,
 				38FCF3F825E902C20078B0D1 /* FileStorageTests.swift */,
 				3B997DCE2DC00A3A006B6BB2 /* JSONImporterTests.swift */,
 				3B997DCE2DC00A3A006B6BB2 /* JSONImporterTests.swift */,
@@ -5343,6 +5346,7 @@
 				BD8FC0542D66186000B95AED /* TestError.swift in Sources */,
 				BD8FC0542D66186000B95AED /* TestError.swift in Sources */,
 				DDDD0FFB2F4E22C000F9C645 /* GlucoseSmoothingTests.swift in Sources */,
 				DDDD0FFB2F4E22C000F9C645 /* GlucoseSmoothingTests.swift in Sources */,
 				CEE9A65E2BBC9F6500EB5194 /* CalibrationsTests.swift in Sources */,
 				CEE9A65E2BBC9F6500EB5194 /* CalibrationsTests.swift in Sources */,
+				CA02000000000000000010C2 /* DeliveryLimitsSyncTests.swift in Sources */,
 				BD8FC0622D6619E600B95AED /* OverrideStorageTests.swift in Sources */,
 				BD8FC0622D6619E600B95AED /* OverrideStorageTests.swift in Sources */,
 				BD8FC0592D66189700B95AED /* TestAssembly.swift in Sources */,
 				BD8FC0592D66189700B95AED /* TestAssembly.swift in Sources */,
 				DDC6CA6D2DD90A2A0060EE25 /* LocalizationTests.swift in Sources */,
 				DDC6CA6D2DD90A2A0060EE25 /* LocalizationTests.swift in Sources */,

+ 20 - 0
Trio/Sources/APS/DeviceDataManager.swift

@@ -3,6 +3,7 @@ import Combine
 import CoreData
 import CoreData
 import DanaKit
 import DanaKit
 import Foundation
 import Foundation
+import HealthKit
 import LoopKit
 import LoopKit
 import LoopKitUI
 import LoopKitUI
 import MedtrumKit
 import MedtrumKit
@@ -106,6 +107,25 @@ final class BaseDeviceDataManager: DeviceDataManager, Injectable {
                     .bolusIncrement = bolusIncrement > 0 ? bolusIncrement : 0.1
                     .bolusIncrement = bolusIncrement > 0 ? bolusIncrement : 0.1
                 storage.save(modifiedPreferences, as: OpenAPS.Settings.preferences)
                 storage.save(modifiedPreferences, as: OpenAPS.Settings.preferences)
 
 
+                // Ensure the pump manager's delivery limits always reflect the user's
+                // current settings. Without this, the active pump manager instance
+                // may use a stale or default value from deserialized state
+                // — silently rejecting temp basals that oref correctly
+                // determines are within the user's configured maxBasal.
+                let pumpSettings = settingsManager.pumpSettings
+                let deliveryLimits = DeliveryLimits(
+                    maximumBasalRate: HKQuantity(unit: .internationalUnitsPerHour, doubleValue: Double(pumpSettings.maxBasal)),
+                    maximumBolus: HKQuantity(unit: .internationalUnit(), doubleValue: Double(pumpSettings.maxBolus))
+                )
+
+                processQueue.async {
+                    pumpManager.syncDeliveryLimits(limits: deliveryLimits) { result in
+                        if case let .failure(error) = result {
+                            debug(.deviceManager, "syncDeliveryLimits on pump manager init failed: \(error)")
+                        }
+                    }
+                }
+
                 if let medtrumPump = pumpManager as? MedtrumPumpManager {
                 if let medtrumPump = pumpManager as? MedtrumPumpManager {
                     // Medtrum's state.patchExpiresAt is actually lifespan + grace
                     // Medtrum's state.patchExpiresAt is actually lifespan + grace
                     // keeping this in line with omnipod, we will use just the lifetime
                     // keeping this in line with omnipod, we will use just the lifetime

+ 2 - 2
Trio/Sources/APS/OpenAPSSwift/DetermineBasal/DetermineBasalGenerator.swift

@@ -377,7 +377,7 @@ enum DeterminationGenerator {
             temp: nil,
             temp: nil,
             bg: currentGlucose,
             bg: currentGlucose,
             reservoir: nil,
             reservoir: nil,
-            isf: nil,
+            isf: adjustedSensitivity.jsRounded(),
             timestamp: currentTime,
             timestamp: currentTime,
             tdd: nil,
             tdd: nil,
             current_target: adjustedGlucoseTargets.targetGlucose,
             current_target: adjustedGlucoseTargets.targetGlucose,
@@ -486,7 +486,7 @@ enum DeterminationGenerator {
         if forecastResult.eventualGlucose >= adjustedGlucoseTargets.maxGlucose {
         if forecastResult.eventualGlucose >= adjustedGlucoseTargets.maxGlucose {
             determination
             determination
                 .reason +=
                 .reason +=
-                "Eventual BG \(DosingEngine.convertGlucose(profile: profile, glucose: forecastResult.eventualGlucose)) >= \(DosingEngine.convertGlucose(profile: profile, glucose: adjustedGlucoseTargets.maxGlucose)), "
+                "Eventual BG \(forecastResult.eventualGlucose.jsRounded()) >= \(adjustedGlucoseTargets.maxGlucose.jsRounded()), "
         }
         }
 
 
         let (shouldSetTempBasalForIobGreaterThanMax, iobGreaterThanMaxDetermination) = try DosingEngine.iobGreaterThanMax(
         let (shouldSetTempBasalForIobGreaterThanMax, iobGreaterThanMaxDetermination) = try DosingEngine.iobGreaterThanMax(

+ 11 - 20
Trio/Sources/APS/OpenAPSSwift/DetermineBasal/DosingEngine.swift

@@ -88,15 +88,6 @@ enum DosingEngine {
         return false
         return false
     }
     }
 
 
-    /// helper function for reason string glucose output
-    static func convertGlucose(profile: Profile, glucose: Decimal) -> Decimal {
-        let units = profile.outUnits ?? .mgdL
-        switch units {
-        case .mgdL: return glucose.jsRounded()
-        case .mmolL: return glucose.asMmolL
-        }
-    }
-
     /// Top level smb enabling logic
     /// Top level smb enabling logic
     ///
     ///
     /// This function includes both the profile / customOrefVariable checks from JS `enable_smb` as
     /// This function includes both the profile / customOrefVariable checks from JS `enable_smb` as
@@ -135,7 +126,7 @@ enum DosingEngine {
         let maxDeltaGlucoseThreshold = min(profile.maxDeltaBgThreshold, 0.4)
         let maxDeltaGlucoseThreshold = min(profile.maxDeltaBgThreshold, 0.4)
         if glucoseStatus.maxDelta > maxDeltaGlucoseThreshold * currentGlucose {
         if glucoseStatus.maxDelta > maxDeltaGlucoseThreshold * currentGlucose {
             reason =
             reason =
-                "maxDelta \(convertGlucose(profile: profile, glucose: glucoseStatus.maxDelta)) > \(100 * maxDeltaGlucoseThreshold)% of BG \(convertGlucose(profile: profile, glucose: currentGlucose)) - SMB disabled!, "
+                "maxDelta \(glucoseStatus.maxDelta.jsRounded()) > \(100 * maxDeltaGlucoseThreshold)% of BG \(currentGlucose.jsRounded()) - SMB disabled!, "
             smbIsEnabled = false
             smbIsEnabled = false
         }
         }
 
 
@@ -287,16 +278,16 @@ enum DosingEngine {
         if currentGlucose < threshold, iob < suspendThreshold, minDelta > 0, minDelta > expectedDelta {
         if currentGlucose < threshold, iob < suspendThreshold, minDelta > 0, minDelta > expectedDelta {
             let iobString = String(describing: iob)
             let iobString = String(describing: iob)
             let suspendString = String(describing: suspendThreshold.jsRounded(scale: 2))
             let suspendString = String(describing: suspendThreshold.jsRounded(scale: 2))
-            let minDeltaString = String(describing: convertGlucose(profile: profile, glucose: minDelta))
-            let expectedDeltaString = String(describing: convertGlucose(profile: profile, glucose: expectedDelta))
+            let minDeltaString = String(describing: minDelta.jsRounded())
+            let expectedDeltaString = String(describing: expectedDelta.jsRounded())
 
 
             newDetermination
             newDetermination
                 .reason +=
                 .reason +=
                 "IOB \(iobString) < \(suspendString) and minDelta \(minDeltaString) > expectedDelta \(expectedDeltaString); "
                 "IOB \(iobString) < \(suspendString) and minDelta \(minDeltaString) > expectedDelta \(expectedDeltaString); "
             return (shouldSetTempBasal: false, determination: newDetermination)
             return (shouldSetTempBasal: false, determination: newDetermination)
         } else if currentGlucose < threshold || minGuardGlucose < threshold {
         } else if currentGlucose < threshold || minGuardGlucose < threshold {
-            let minGuardGlucoseString = String(describing: convertGlucose(profile: profile, glucose: minGuardGlucose))
-            let thresholdString = String(describing: convertGlucose(profile: profile, glucose: threshold))
+            let minGuardGlucoseString = String(describing: minGuardGlucose.jsRounded())
+            let thresholdString = String(describing: threshold.jsRounded())
             newDetermination.reason += "minGuardBG \(minGuardGlucoseString)<\(thresholdString)"
             newDetermination.reason += "minGuardBG \(minGuardGlucoseString)<\(thresholdString)"
 
 
             let glucoseUndershoot = targetGlucose - minGuardGlucose
             let glucoseUndershoot = targetGlucose - minGuardGlucose
@@ -396,7 +387,7 @@ enum DosingEngine {
         var newDetermination = determination
         var newDetermination = determination
         newDetermination
         newDetermination
             .reason +=
             .reason +=
-            "Eventual BG \(convertGlucose(profile: profile, glucose: eventualGlucose)) < \(convertGlucose(profile: profile, glucose: minGlucose))"
+            "Eventual BG \(eventualGlucose.jsRounded()) < \(minGlucose.jsRounded())"
 
 
         // if 5m or 30m avg glucose is rising faster than expected delta
         // if 5m or 30m avg glucose is rising faster than expected delta
         // BUG: in JS it's doing a "truthiness" check for carbs required
         // BUG: in JS it's doing a "truthiness" check for carbs required
@@ -419,12 +410,12 @@ enum DosingEngine {
             if glucoseStatus.delta > minDelta {
             if glucoseStatus.delta > minDelta {
                 newDetermination
                 newDetermination
                     .reason +=
                     .reason +=
-                    ", but Delta \(convertGlucose(profile: profile, glucose: glucoseStatus.delta)) > expectedDelta \(convertGlucose(profile: profile, glucose: expectedDelta))"
+                    ", but Delta \(glucoseStatus.delta.jsRounded()) > expectedDelta \(expectedDelta.jsRounded())"
             } else {
             } else {
                 let minDeltaFormatted = String(format: "%.2f", Double(truncating: minDelta.jsRounded(scale: 2) as NSNumber))
                 let minDeltaFormatted = String(format: "%.2f", Double(truncating: minDelta.jsRounded(scale: 2) as NSNumber))
                 newDetermination
                 newDetermination
                     .reason +=
                     .reason +=
-                    ", but Min. Delta \(minDeltaFormatted) > Exp. Delta \(convertGlucose(profile: profile, glucose: expectedDelta))"
+                    ", but Min. Delta \(minDeltaFormatted) > Exp. Delta \(expectedDelta.jsRounded())"
             }
             }
 
 
             let roundedBasal = TempBasalFunctions.roundBasal(profile: profile, basalRate: basal)
             let roundedBasal = TempBasalFunctions.roundBasal(profile: profile, basalRate: basal)
@@ -549,12 +540,12 @@ enum DosingEngine {
             if glucoseStatus.delta < minDelta {
             if glucoseStatus.delta < minDelta {
                 newDetermination
                 newDetermination
                     .reason +=
                     .reason +=
-                    "Eventual BG \(convertGlucose(profile: profile, glucose: eventualGlucose)) > \(convertGlucose(profile: profile, glucose: minGlucose)) but Delta \(convertGlucose(profile: profile, glucose: glucoseStatus.delta)) < Exp. Delta \(convertGlucose(profile: profile, glucose: expectedDelta))"
+                    "Eventual BG \(eventualGlucose.jsRounded()) > \(minGlucose.jsRounded()) but Delta \(glucoseStatus.delta.jsRounded()) < Exp. Delta \(expectedDelta.jsRounded())"
             } else {
             } else {
                 let minDeltaFormatted = String(format: "%.2f", Double(truncating: minDelta.jsRounded(scale: 2) as NSNumber))
                 let minDeltaFormatted = String(format: "%.2f", Double(truncating: minDelta.jsRounded(scale: 2) as NSNumber))
                 newDetermination
                 newDetermination
                     .reason +=
                     .reason +=
-                    "Eventual BG \(convertGlucose(profile: profile, glucose: eventualGlucose)) > \(convertGlucose(profile: profile, glucose: minGlucose)) but Min. Delta \(minDeltaFormatted) < Exp. Delta \(convertGlucose(profile: profile, glucose: expectedDelta))"
+                    "Eventual BG \(eventualGlucose.jsRounded()) > \(minGlucose.jsRounded()) but Min. Delta \(minDeltaFormatted) < Exp. Delta \(expectedDelta.jsRounded())"
             }
             }
 
 
             let roundedBasal = TempBasalFunctions.roundBasal(profile: profile, basalRate: basal)
             let roundedBasal = TempBasalFunctions.roundBasal(profile: profile, basalRate: basal)
@@ -604,7 +595,7 @@ enum DosingEngine {
         if !smbIsEnabled {
         if !smbIsEnabled {
             newDetermination
             newDetermination
                 .reason +=
                 .reason +=
-                "\(convertGlucose(profile: profile, glucose: eventualGlucose))-\(convertGlucose(profile: profile, glucose: minForecastGlucose)) in range: no temp required"
+                "\(eventualGlucose.jsRounded())-\(minForecastGlucose.jsRounded()) in range: no temp required"
 
 
             let roundedBasal = TempBasalFunctions.roundBasal(profile: profile, basalRate: basal)
             let roundedBasal = TempBasalFunctions.roundBasal(profile: profile, basalRate: basal)
             let roundedCurrentRate = TempBasalFunctions.roundBasal(profile: profile, basalRate: currentTemp.rate)
             let roundedCurrentRate = TempBasalFunctions.roundBasal(profile: profile, basalRate: currentTemp.rate)

+ 1 - 0
Trio/Sources/Localizations/Main/Localizable.xcstrings

@@ -183477,6 +183477,7 @@
       }
       }
     },
     },
     "Note to Medtronic Pump Users: You must also manually set the max basal rate on the pump to this value or higher." : {
     "Note to Medtronic Pump Users: You must also manually set the max basal rate on the pump to this value or higher." : {
+      "extractionState" : "stale",
       "localizations" : {
       "localizations" : {
         "bg" : {
         "bg" : {
           "stringUnit" : {
           "stringUnit" : {

+ 0 - 3
Trio/Sources/Modules/GeneralSettings/View/UnitsLimitsSettingsRootView.swift

@@ -129,9 +129,6 @@ extension UnitsLimitsSettings {
                         Text(
                         Text(
                             "This is the maximum basal rate allowed to be set or scheduled. This applies to both automatic and manual basal rates."
                             "This is the maximum basal rate allowed to be set or scheduled. This applies to both automatic and manual basal rates."
                         )
                         )
-                        Text(
-                            "Note to Medtronic Pump Users: You must also manually set the max basal rate on the pump to this value or higher."
-                        )
                     }
                     }
                 )
                 )
 
 

+ 0 - 3
Trio/Sources/Modules/Onboarding/View/OnboardingView+Util.swift

@@ -435,9 +435,6 @@ enum DeliveryLimitSubstep: Int, CaseIterable, Identifiable {
                 Text(
                 Text(
                     "This is the maximum basal rate allowed to be set or scheduled. This applies to both automatic and manual basal rates."
                     "This is the maximum basal rate allowed to be set or scheduled. This applies to both automatic and manual basal rates."
                 )
                 )
-                Text(
-                    "Note to Medtronic Pump Users: You must also manually set the max basal rate on the pump to this value or higher."
-                )
             }
             }
         case .maxCOB:
         case .maxCOB:
             return VStack(alignment: .leading, spacing: 8) {
             return VStack(alignment: .leading, spacing: 8) {

+ 67 - 0
TrioTests/DeliveryLimitsSyncTests.swift

@@ -0,0 +1,67 @@
+import Foundation
+import HealthKit
+import LoopKit
+import Testing
+
+@testable import Trio
+
+/// Pins the `PumpSettings` → `DeliveryLimits` mapping that
+/// `BaseDeviceDataManager.pumpManager.didSet` performs inline before calling
+/// `pumpManager.syncDeliveryLimits`. Keep this builder identical to the inline
+/// expression in `DeviceDataManager.swift` so a regression that re-introduces
+/// the default-collapse bug breaks here too.
+@Suite("Delivery Limits Sync Tests") struct DeliveryLimitsSyncTests {
+    private let basalUnit = HKUnit.internationalUnitsPerHour
+    private let bolusUnit = HKUnit.internationalUnit()
+
+    private func makeSettings(maxBasal: Decimal, maxBolus: Decimal) -> PumpSettings {
+        PumpSettings(insulinActionCurve: 6, maxBolus: maxBolus, maxBasal: maxBasal)
+    }
+
+    private func deliveryLimits(from settings: PumpSettings) -> DeliveryLimits {
+        DeliveryLimits(
+            maximumBasalRate: HKQuantity(unit: .internationalUnitsPerHour, doubleValue: Double(settings.maxBasal)),
+            maximumBolus: HKQuantity(unit: .internationalUnit(), doubleValue: Double(settings.maxBolus))
+        )
+    }
+
+    @Test("maxBasal maps to maximumBasalRate in U/hr") func testMaxBasalMapping() {
+        let settings = makeSettings(maxBasal: 5.0, maxBolus: 10.0)
+
+        let limits = deliveryLimits(from: settings)
+
+        #expect(limits.maximumBasalRate?.doubleValue(for: basalUnit) == 5.0)
+    }
+
+    @Test("maxBolus maps to maximumBolus in U") func testMaxBolusMapping() {
+        let settings = makeSettings(maxBasal: 5.0, maxBolus: 10.0)
+
+        let limits = deliveryLimits(from: settings)
+
+        #expect(limits.maximumBolus?.doubleValue(for: bolusUnit) == 10.0)
+    }
+
+    /// The derived limit must be the user's configured value, not the `PumpInitialSettings` default.
+    @Test("User-configured limit is preserved, not collapsed to the 2 U/hr default") func testDoesNotFallBackToDefault() {
+        let configuredMaxBasal: Decimal = 3.0
+        let defaultMaxBasal = PumpConfig.PumpInitialSettings.default.maxBasalRateUnitsPerHour
+
+        let settings = makeSettings(maxBasal: configuredMaxBasal, maxBolus: 10.0)
+        let limits = deliveryLimits(from: settings)
+
+        #expect(limits.maximumBasalRate?.doubleValue(for: basalUnit) == Double(configuredMaxBasal))
+        #expect(limits.maximumBasalRate?.doubleValue(for: basalUnit) != defaultMaxBasal)
+    }
+
+    /// The derived limit must be the user's configured value, not the `PumpInitialSettings` default.
+    @Test("User-configured bolus limit is preserved, not collapsed to the default") func testBolusDoesNotFallBackToDefault() {
+        let configuredMaxBolus: Decimal = 25.0
+        let defaultMaxBolus = PumpConfig.PumpInitialSettings.default.maxBolusUnits
+
+        let settings = makeSettings(maxBasal: 3.0, maxBolus: configuredMaxBolus)
+        let limits = deliveryLimits(from: settings)
+
+        #expect(limits.maximumBolus?.doubleValue(for: bolusUnit) == Double(configuredMaxBolus))
+        #expect(limits.maximumBolus?.doubleValue(for: bolusUnit) != defaultMaxBolus)
+    }
+}

+ 45 - 0
blacklisted-versions.json

@@ -5,6 +5,51 @@
         },
         },
         {
         {
             "version": "0.0.2"
             "version": "0.0.2"
+        },
+        {
+            "version": "0.2.0"
+        },
+        {
+            "version": "0.2.1"
+        },
+        {
+            "version": "0.2.2"
+        },
+        {
+            "version": "0.2.3"
+        },
+        {
+            "version": "0.2.4"
+        },
+        {
+            "version": "0.2.5"
+        },
+        {
+            "version": "0.2.6"
+        },
+        {
+            "version": "0.2.7"
+        },
+        {
+            "version": "0.2.8"
+        },
+        {
+            "version": "0.2.9"
+        },
+        {
+            "version": "0.5.0"
+        },
+        {
+            "version": "0.5.1"
+        },
+        {
+            "version": "0.6.0"
+        },
+        {
+            "version": "0.8.1"
+        },
+        {
+            "version": "0.8.2"
         }
         }
     ]
     ]
 }
 }