Просмотр исходного кода

Merge pull request #583 from nightscout/oref-swift-eventual-glucose-below-target

Dosing logic for low eventual glucose
Sam King 8 месяцев назад
Родитель
Сommit
450bb184b3

+ 4 - 0
Trio.xcodeproj/project.pbxproj

@@ -217,6 +217,7 @@
 		3B1C5C452D68E269004E9273 /* IobTotalTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B1C5C382D68E269004E9273 /* IobTotalTests.swift */; };
 		3B1C5C472D68E269004E9273 /* IobCalculateTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B1C5C352D68E269004E9273 /* IobCalculateTests.swift */; };
 		3B1C5C482D68E269004E9273 /* IobHistoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B1C5C362D68E269004E9273 /* IobHistoryTests.swift */; };
+		3B1DB90F2E63C14C00BD814B /* DetermineBasalLowEventualGlucoseTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B1DB90E2E63C14200BD814B /* DetermineBasalLowEventualGlucoseTests.swift */; };
 		3B214EEA2E29632900046304 /* determine-basal-prepare.js in Resources */ = {isa = PBXBuildFile; fileRef = 3B214EE92E29631F00046304 /* determine-basal-prepare.js */; };
 		3B2A3BC12E2B19C600658FB9 /* DynamicISF.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B2A3BC02E2B19C600658FB9 /* DynamicISF.swift */; };
 		3B2A3BC32E2B19F700658FB9 /* DynamicISFTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B2A3BC22E2B19F700658FB9 /* DynamicISFTests.swift */; };
@@ -1167,6 +1168,7 @@
 		3B1C5C382D68E269004E9273 /* IobTotalTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IobTotalTests.swift; sourceTree = "<group>"; };
 		3B1C5C3D2D68E269004E9273 /* Extensions.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Extensions.swift; sourceTree = "<group>"; };
 		3B1C5C3E2D68E269004E9273 /* IobJsonTypes.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IobJsonTypes.swift; sourceTree = "<group>"; };
+		3B1DB90E2E63C14200BD814B /* DetermineBasalLowEventualGlucoseTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetermineBasalLowEventualGlucoseTests.swift; sourceTree = "<group>"; };
 		3B214EE92E29631F00046304 /* determine-basal-prepare.js */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.javascript; path = "determine-basal-prepare.js"; sourceTree = "<group>"; };
 		3B2A3BC02E2B19C600658FB9 /* DynamicISF.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynamicISF.swift; sourceTree = "<group>"; };
 		3B2A3BC22E2B19F700658FB9 /* DynamicISFTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DynamicISFTests.swift; sourceTree = "<group>"; };
@@ -2952,6 +2954,7 @@
 				3B8221B12E5882D900585156 /* DetermineBasalEarlyExitTests.swift */,
 				3BAC929A2E55FF4F00B853DA /* DetermineBasalEnableSmbTests.swift */,
 				3BE9F0BA2E28C993001B14EB /* DetermineBasalJsonTests.swift */,
+				3B1DB90E2E63C14200BD814B /* DetermineBasalLowEventualGlucoseTests.swift */,
 				3B2A3BC22E2B19F700658FB9 /* DynamicISFTests.swift */,
 				3B1C5C352D68E269004E9273 /* IobCalculateTests.swift */,
 				3B2CE68A2E24ADF3005EF782 /* IobGenerateTests.swift */,
@@ -5221,6 +5224,7 @@
 			files = (
 				3B5CD2C92D4AECD500CE213C /* ProfileCarbsTests.swift in Sources */,
 				3B8B5D3E2DF5240C00365ED3 /* TimeZoneForTests.swift in Sources */,
+				3B1DB90F2E63C14C00BD814B /* DetermineBasalLowEventualGlucoseTests.swift in Sources */,
 				3B5CD2CA2D4AECD500CE213C /* ProfileJavascriptTests.swift in Sources */,
 				3B5CD2CB2D4AECD500CE213C /* ProfileTargetsTests.swift in Sources */,
 				3BAC92CD2E57859D00B853DA /* SetTempBasalTests.swift in Sources */,

+ 26 - 4
Trio/Sources/APS/OpenAPSSwift/DetermineBasal/DetermineBasalGenerator.swift

@@ -338,7 +338,7 @@ enum DeterminationGenerator {
 
         // MARK: - Core dosing logic
 
-        let (setTempBasalForLowGlucoseSuspend, lowGlucoseSuspendDetermination) = try DosingEngine.lowGlucoseSuspend(
+        let (shouldSetTempBasalForLowGlucoseSuspend, lowGlucoseSuspendDetermination) = try DosingEngine.lowGlucoseSuspend(
             currentGlucose: currentGlucose,
             minGuardGlucose: forecastResult.minGuardGlucose,
             iob: currentIob,
@@ -353,11 +353,11 @@ enum DeterminationGenerator {
             determination: determination
         )
         determination = lowGlucoseSuspendDetermination
-        if setTempBasalForLowGlucoseSuspend {
+        if shouldSetTempBasalForLowGlucoseSuspend {
             return determination
         }
 
-        let (setTempBasalForSkipNeutralTemp, skipNeutralTempDetermination) = try DosingEngine.skipNeutralTempBasal(
+        let (shouldSetTempBasalForSkipNeutralTemp, skipNeutralTempDetermination) = try DosingEngine.skipNeutralTempBasal(
             smbIsEnabled: smbIsEnabled,
             profile: profile,
             clock: currentTime,
@@ -365,7 +365,29 @@ enum DeterminationGenerator {
             determination: determination
         )
         determination = skipNeutralTempDetermination
-        if setTempBasalForSkipNeutralTemp {
+        if shouldSetTempBasalForSkipNeutralTemp {
+            return determination
+        }
+
+        let (shouldSetTempBasalForLowEventualGlucose, lowEventualGlucoseDetermination) = try DosingEngine
+            .handleLowEventualGlucose(
+                eventualGlucose: forecastResult.eventualGlucose,
+                minGlucose: adjustedGlucoseTargets.minGlucose,
+                targetGlucose: adjustedGlucoseTargets.targetGlucose,
+                minDelta: minDelta,
+                expectedDelta: expectedDelta,
+                carbsRequired: dosingInputs.carbsRequired?.carbs ?? 0,
+                naiveEventualGlucose: naiveEventualGlucose,
+                glucoseStatus: glucoseStatus,
+                currentTemp: currentTemp,
+                basal: basal,
+                profile: profile,
+                determination: determination,
+                adjustedSensitivity: adjustedSensitivity,
+                overrideFactor: trioCustomOrefVariables.overrideFactor()
+            )
+        determination = lowEventualGlucoseDetermination
+        if shouldSetTempBasalForLowEventualGlucose {
             return determination
         }
 

+ 148 - 0
Trio/Sources/APS/OpenAPSSwift/DetermineBasal/DosingEngine.swift

@@ -380,4 +380,152 @@ enum DosingEngine {
             return (shouldSetTempBasal: false, determination: determination)
         }
     }
+
+    /// Handles the case where eventual glucose is predicted to be low.
+    ///
+    /// - Returns: A tuple containing:
+    ///   - `shouldSetTempBasal`: A `Bool` that is `true` if `determineBasal` should exit and apply the recommendation immediately.
+    ///   - `determination`: The (potentially modified) determination object.
+    static func handleLowEventualGlucose(
+        eventualGlucose: Decimal,
+        minGlucose: Decimal,
+        targetGlucose: Decimal,
+        minDelta: Decimal,
+        expectedDelta: Decimal,
+        carbsRequired: Decimal,
+        naiveEventualGlucose: Decimal,
+        glucoseStatus: GlucoseStatus,
+        currentTemp: TempBasal,
+        basal: Decimal,
+        profile: Profile,
+        determination: Determination,
+        adjustedSensitivity: Decimal,
+        overrideFactor: Decimal
+    ) throws -> (shouldSetTempBasal: Bool, determination: Determination) {
+        guard eventualGlucose < minGlucose else {
+            return (shouldSetTempBasal: false, determination: determination)
+        }
+
+        var newDetermination = determination
+        newDetermination
+            .reason +=
+            "Eventual BG \(convertGlucose(profile: profile, glucose: eventualGlucose)) < \(convertGlucose(profile: profile, glucose: minGlucose))"
+
+        // if 5m or 30m avg BG is rising faster than expected delta
+        if minDelta > expectedDelta, minDelta > 0, carbsRequired == 0 {
+            if naiveEventualGlucose < 40 {
+                newDetermination.reason += ", naive_eventualBG < 40. "
+                let finalDetermination = try TempBasalFunctions.setTempBasal(
+                    rate: 0,
+                    duration: 30,
+                    profile: profile,
+                    determination: newDetermination,
+                    currentTemp: currentTemp
+                )
+                return (shouldSetTempBasal: true, determination: finalDetermination)
+            }
+
+            if glucoseStatus.delta > minDelta {
+                newDetermination
+                    .reason +=
+                    ", but Delta \(convertGlucose(profile: profile, glucose: glucoseStatus.delta)) > expectedDelta \(convertGlucose(profile: profile, glucose: expectedDelta))"
+            } else {
+                newDetermination
+                    .reason +=
+                    ", but Min. Delta \(minDelta.jsRounded(scale: 2)) > Exp. Delta \(convertGlucose(profile: profile, glucose: expectedDelta))"
+            }
+
+            let roundedBasal = TempBasalFunctions.roundBasal(profile: profile, basalRate: basal)
+            let roundedCurrentRate = TempBasalFunctions.roundBasal(profile: profile, basalRate: currentTemp.rate)
+
+            if currentTemp.duration > 15, roundedBasal == roundedCurrentRate {
+                newDetermination.reason += ", temp \(currentTemp.rate) ~ req \(basal)U/hr. "
+                return (shouldSetTempBasal: true, determination: newDetermination)
+            } else {
+                newDetermination.reason += "; setting current basal of \(basal) as temp. "
+                let finalDetermination = try TempBasalFunctions.setTempBasal(
+                    rate: basal,
+                    duration: 30,
+                    profile: profile,
+                    determination: newDetermination,
+                    currentTemp: currentTemp
+                )
+                return (shouldSetTempBasal: true, determination: finalDetermination)
+            }
+        }
+
+        // calculate 30m low-temp required to get projected BG up to target
+        var insulinRequired = 2 * min(0, (eventualGlucose - targetGlucose) / adjustedSensitivity)
+        insulinRequired = insulinRequired.jsRounded(scale: 2)
+
+        let naiveInsulinRequired = min(0, (naiveEventualGlucose - targetGlucose) / adjustedSensitivity).jsRounded(scale: 2)
+
+        if minDelta < 0, minDelta > expectedDelta {
+            let newInsulinRequired = (insulinRequired * (minDelta / expectedDelta)).jsRounded(scale: 2)
+            insulinRequired = newInsulinRequired
+        }
+
+        var rate = basal + (2 * insulinRequired)
+        rate = TempBasalFunctions.roundBasal(profile: profile, basalRate: rate)
+
+        let insulinScheduled = Decimal(currentTemp.duration) * (currentTemp.rate - basal) / 60
+        let minInsulinRequired = min(insulinRequired, naiveInsulinRequired)
+
+        if insulinScheduled < minInsulinRequired - basal * 0.3 {
+            newDetermination
+                .reason += ", \(currentTemp.duration)m@\(currentTemp.rate.jsRounded(scale: 2)) is a lot less than needed. "
+            let finalDetermination = try TempBasalFunctions.setTempBasal(
+                rate: rate,
+                duration: 30,
+                profile: profile,
+                determination: newDetermination,
+                currentTemp: currentTemp
+            )
+            return (shouldSetTempBasal: true, determination: finalDetermination)
+        }
+
+        if currentTemp.duration > 5, rate >= currentTemp.rate * 0.8 {
+            newDetermination.reason += ", temp \(currentTemp.rate) ~< req \(rate)U/hr. "
+            return (shouldSetTempBasal: true, determination: newDetermination)
+        } else {
+            if rate <= 0 {
+                guard let currentBasal = profile.currentBasal else {
+                    throw TempBasalFunctionError.invalidBasalRateOnProfile
+                }
+                let glucoseUndershoot = targetGlucose - naiveEventualGlucose
+                let worstCaseInsulinRequired = glucoseUndershoot / adjustedSensitivity
+                var durationRequired = (60 * worstCaseInsulinRequired / (currentBasal * overrideFactor)).jsRounded()
+
+                if durationRequired < 0 {
+                    durationRequired = 0
+                } else {
+                    durationRequired = (durationRequired / 30).jsRounded() * 30
+                    durationRequired = min(120, max(0, durationRequired))
+                }
+
+                if durationRequired > 0 {
+                    newDetermination.reason += ", setting \(durationRequired)m zero temp. "
+                    let finalDetermination = try TempBasalFunctions.setTempBasal(
+                        rate: rate,
+                        duration: durationRequired,
+                        profile: profile,
+                        determination: newDetermination,
+                        currentTemp: currentTemp
+                    )
+                    return (shouldSetTempBasal: true, determination: finalDetermination)
+                }
+            } else {
+                newDetermination.reason += ", setting \(rate)U/hr. "
+            }
+
+            let finalDetermination = try TempBasalFunctions.setTempBasal(
+                rate: rate,
+                duration: 30,
+                profile: profile,
+                determination: newDetermination,
+                currentTemp: currentTemp
+            )
+            return (shouldSetTempBasal: true, determination: finalDetermination)
+        }
+    }
 }

+ 338 - 0
TrioTests/OpenAPSSwiftTests/DetermineBasalLowEventualGlucoseTests.swift

@@ -0,0 +1,338 @@
+import Foundation
+import Testing
+@testable import Trio
+
+@Suite("DetermineBasal low eventual glucose") struct DetermineBasalLowEventualGlucoseTests {
+    // Helper to create a mock IOB array with linear decay for testing purposes
+    private func mockIobArray(iob: Decimal, activity: Decimal, currentTime: Date) -> [IobResult] {
+        (0 ..< 48).map { i in
+            IobResult(
+                iob: iob - (activity * Decimal(i)),
+                activity: activity,
+                basaliob: 0,
+                bolusiob: 0,
+                netbasalinsulin: 0,
+                bolusinsulin: 0,
+                time: currentTime,
+                iobWithZeroTemp: IobResult.IobWithZeroTemp(
+                    iob: 0, activity: 0, basaliob: 0, bolusiob: 0, netbasalinsulin: 0, bolusinsulin: 0, time: currentTime
+                ),
+                lastBolusTime: nil,
+                lastTemp: IobResult.LastTemp(
+                    rate: 0,
+                    timestamp: currentTime,
+                    started_at: currentTime,
+                    date: UInt64(currentTime.timeIntervalSince1970 * 1000),
+                    duration: 0
+                )
+            )
+        }
+    }
+
+    private func createDefaultInputs(currentTime: Date = Date()) -> (
+        profile: Profile,
+        preferences: Preferences,
+        currentTemp: TempBasal,
+        iobData: [IobResult],
+        mealData: ComputedCarbs,
+        autosensData: Autosens,
+        reservoirData: Decimal,
+        glucoseStatus: GlucoseStatus,
+        trioCustomOrefVariables: TrioCustomOrefVariables,
+        currentTime: Date
+    ) {
+        var profile = Profile()
+        profile.maxIob = 2.5
+        profile.dia = 3
+        profile.currentBasal = 1.0
+        profile.maxDailyBasal = 1.3
+        profile.maxBasal = 3.5
+        profile.maxBg = 120
+        profile.minBg = 100
+        profile.sens = 50
+        profile.carbRatio = 10
+        profile.thresholdSetting = 80
+        profile.temptargetSet = false
+        profile.bolusIncrement = 0.1
+        profile.useCustomPeakTime = false
+        profile.curve = .rapidActing
+        profile.enableUAM = false // Important for these tests
+
+        var preferences = Preferences()
+        preferences.useNewFormula = false
+        preferences.sigmoid = false
+        preferences.adjustmentFactor = 0.8
+        preferences.adjustmentFactorSigmoid = 0.5
+        preferences.curve = .rapidActing
+        preferences.useCustomPeakTime = false
+
+        let currentTemp = TempBasal(duration: 0, rate: 0, temp: .absolute, timestamp: currentTime)
+        let iobData = mockIobArray(iob: 0, activity: 0, currentTime: currentTime)
+        let mealData = ComputedCarbs(
+            carbs: 0,
+            mealCOB: 0,
+            currentDeviation: 0,
+            maxDeviation: 0,
+            minDeviation: 0,
+            slopeFromMaxDeviation: 0,
+            slopeFromMinDeviation: 0,
+            allDeviations: [0, 0, 0, 0, 0],
+            lastCarbTime: 0
+        )
+        let autosensData = Autosens(ratio: 1.0, newisf: nil)
+        let glucoseStatus = GlucoseStatus(
+            delta: 0,
+            glucose: 115,
+            noise: 1,
+            shortAvgDelta: 0,
+            longAvgDelta: 0.1,
+            date: currentTime,
+            lastCalIndex: nil,
+            device: "test"
+        )
+
+        let trioCustomOrefVariables = TrioCustomOrefVariables(
+            average_total_data: 0,
+            weightedAverage: 0,
+            currentTDD: 0,
+            past2hoursAverage: 0,
+            date: currentTime,
+            overridePercentage: 100,
+            useOverride: false,
+            duration: 0,
+            unlimited: false,
+            overrideTarget: 0,
+            smbIsOff: false,
+            advancedSettings: false,
+            isfAndCr: false,
+            isf: false,
+            cr: false,
+            smbIsScheduledOff: false,
+            start: 0,
+            end: 0,
+            smbMinutes: 30,
+            uamMinutes: 30,
+            shouldProtectDueToHIGH: false
+        )
+
+        return (
+            profile: profile,
+            preferences: preferences,
+            currentTemp: currentTemp,
+            iobData: iobData,
+            mealData: mealData,
+            autosensData: autosensData,
+            reservoirData: 100,
+            glucoseStatus: glucoseStatus,
+            trioCustomOrefVariables: trioCustomOrefVariables,
+            currentTime: currentTime
+        )
+    }
+
+    @Test("should set a low temp when eventual BG is low and rising") func lowTempRising() throws {
+        var (
+            profile, preferences, currentTemp, _, mealData, autosensData, reservoirData, _, trioCustomOrefVariables, currentTime
+        ) = createDefaultInputs()
+
+        profile.minBg = 100
+        let glucoseStatus = GlucoseStatus(
+            delta: 1, glucose: 90, noise: 1, shortAvgDelta: 1, longAvgDelta: 0.1, date: currentTime, lastCalIndex: nil,
+            device: "test"
+        )
+        let iobData = mockIobArray(iob: 0, activity: 0, currentTime: currentTime)
+
+        let result = try DeterminationGenerator.determineBasal(
+            profile: profile, preferences: preferences, currentTemp: currentTemp, iobData: iobData, mealData: mealData,
+            autosensData: autosensData, reservoirData: reservoirData, glucoseStatus: glucoseStatus,
+            trioCustomOrefVariables: trioCustomOrefVariables, currentTime: currentTime
+        )
+
+        #expect(result?.rate == 0.7)
+        #expect(result?.duration == 30)
+        #expect(result?.reason.contains("setting 0.7U/hr") == true)
+    }
+}
+
+/// these tests should be an exact copy of the JS tests here:
+/// - https://github.com/kingst/trio-oref/blob/dev-fixes-for-swift-comparison/tests/determine-basal-low-eventual-glucose.test.js
+/// We had to extract the key functionality from JS and put it in a function to facilitate testing
+@Suite("DosingEngine.handleLowEventualGlucose") struct HandleLowEventualGlucoseTests {
+    private func defaultProfile() -> Profile {
+        var profile = Profile()
+        profile.minBg = 100
+        profile.targetBg = 100
+        profile.currentBasal = 1.0
+        profile.maxDailyBasal = 1.3
+        profile.maxBasal = 3.5
+        profile.sens = 50
+        return profile
+    }
+
+    private func callHandleLowEventualGlucose(
+        eventualGlucose: Decimal = 90,
+        minGlucose: Decimal? = nil,
+        targetGlucose: Decimal? = nil,
+        minDelta: Decimal = 0,
+        expectedDelta: Decimal = 0,
+        carbsRequired: Decimal = 0,
+        naiveEventualGlucose: Decimal = 90,
+        glucoseStatus: GlucoseStatus = GlucoseStatus(
+            delta: 0,
+            glucose: 100,
+            noise: 1,
+            shortAvgDelta: 0,
+            longAvgDelta: 0,
+            date: Date(),
+            lastCalIndex: nil,
+            device: "test"
+        ),
+        currentTemp: TempBasal = TempBasal(duration: 0, rate: 0, temp: .absolute, timestamp: Date()),
+        basal: Decimal? = nil,
+        profile: Profile? = nil,
+        determination: Determination? = nil,
+        adjustedSensitivity: Decimal? = nil,
+        overrideFactor: Decimal = 1
+    ) throws -> (shouldSetTempBasal: Bool, determination: Determination) {
+        let testProfile = profile ?? defaultProfile()
+        let testDetermination = determination ?? Determination(
+            id: nil,
+            reason: "",
+            units: nil,
+            insulinReq: nil,
+            eventualBG: nil,
+            sensitivityRatio: nil,
+            rate: nil,
+            duration: nil,
+            iob: nil,
+            cob: nil,
+            predictions: nil,
+            deliverAt: nil,
+            carbsReq: nil,
+            temp: nil,
+            bg: nil,
+            reservoir: nil,
+            isf: nil,
+            timestamp: nil,
+            tdd: nil,
+            current_target: nil,
+            manualBolusErrorString: nil,
+            minDelta: nil,
+            expectedDelta: nil,
+            minGuardBG: nil,
+            minPredBG: nil,
+            threshold: nil,
+            carbRatio: nil,
+            received: nil
+        )
+
+        return try DosingEngine.handleLowEventualGlucose(
+            eventualGlucose: eventualGlucose,
+            minGlucose: minGlucose ?? testProfile.minBg!,
+            targetGlucose: targetGlucose ?? testProfile.targetBg!,
+            minDelta: minDelta,
+            expectedDelta: expectedDelta,
+            carbsRequired: carbsRequired,
+            naiveEventualGlucose: naiveEventualGlucose,
+            glucoseStatus: glucoseStatus,
+            currentTemp: currentTemp,
+            basal: basal ?? testProfile.currentBasal!,
+            profile: testProfile,
+            determination: testDetermination,
+            adjustedSensitivity: adjustedSensitivity ?? testProfile.sens!,
+            overrideFactor: overrideFactor
+        )
+    }
+
+    @Test("Guard: eventual glucose is not low") func testEventualGlucoseNotLow() throws {
+        let (shouldSet, determination) = try callHandleLowEventualGlucose(eventualGlucose: 100, minGlucose: 100)
+        #expect(shouldSet == false)
+        #expect(determination.reason == "")
+    }
+
+    @Test("Naive eventual glucose below 40") func testNaiveEventualGlucoseBelow40() throws {
+        let (shouldSet, determination) = try callHandleLowEventualGlucose(
+            minDelta: 1,
+            expectedDelta: 0,
+            carbsRequired: 0,
+            naiveEventualGlucose: 39
+        )
+        #expect(shouldSet == true)
+        #expect(determination.rate == 0)
+        #expect(determination.duration == 30)
+        #expect(determination.reason.contains("naive_eventualBG < 40"))
+    }
+
+    @Test("Min delta > expected, but no carbs required") func testMinDeltaGreaterThanExpectedDeltaAndNoCarbs() throws {
+        let (shouldSet, _) = try callHandleLowEventualGlucose(minDelta: 1, expectedDelta: 0, carbsRequired: 0)
+        #expect(shouldSet == true)
+    }
+
+    @Test("Min delta < 0") func testMinDeltaLessThanZero() throws {
+        let (shouldSet, determination) = try callHandleLowEventualGlucose(minDelta: -1, expectedDelta: -2, carbsRequired: 0)
+        #expect(shouldSet == true)
+        #expect(determination.rate == 0.6)
+    }
+
+    @Test("Current temp rate matches basal") func testCurrentTempRateMatchesBasal() throws {
+        let profile = defaultProfile()
+        let currentTemp = TempBasal(duration: 20, rate: profile.currentBasal!, temp: .absolute, timestamp: Date())
+        let (shouldSet, determination) = try callHandleLowEventualGlucose(
+            minDelta: 1,
+            expectedDelta: 0,
+            carbsRequired: 0,
+            currentTemp: currentTemp,
+            profile: profile
+        )
+        #expect(shouldSet == true)
+        #expect(determination.rate == nil) // No change
+        #expect(determination.reason.contains("temp \(currentTemp.rate) ~ req \(profile.currentBasal!)U/hr."))
+    }
+
+    @Test("Set basal as temp") func testSetBasalAsTemp() throws {
+        let profile = defaultProfile()
+        let (shouldSet, determination) = try callHandleLowEventualGlucose(
+            minDelta: 1,
+            expectedDelta: 0,
+            carbsRequired: 0,
+            profile: profile
+        )
+        #expect(shouldSet == true)
+        #expect(determination.rate == profile.currentBasal)
+        #expect(determination.duration == 30)
+        #expect(determination.reason.contains("setting current basal of \(profile.currentBasal!) as temp."))
+    }
+
+    @Test("Insulin scheduled less than required") func testInsulinScheduledLessThanRequired() throws {
+        let (shouldSet, determination) = try callHandleLowEventualGlucose(
+            eventualGlucose: 80,
+            naiveEventualGlucose: 70,
+            currentTemp: TempBasal(duration: 120, rate: 0, temp: .absolute, timestamp: Date())
+        )
+        #expect(shouldSet == true)
+        #expect(determination.rate == nil)
+        #expect(determination.duration == nil)
+        #expect(determination.reason.contains("is a lot less than needed"))
+    }
+
+    @Test("Rate similar to current temp") func testRateSimilarToCurrentTemp() throws {
+        let currentTemp = TempBasal(duration: 10, rate: 0.1, temp: .absolute, timestamp: Date())
+        let (shouldSet, determination) = try callHandleLowEventualGlucose(
+            eventualGlucose: 99,
+            targetGlucose: 110,
+            currentTemp: currentTemp,
+            adjustedSensitivity: 50
+        )
+
+        #expect(shouldSet == true)
+        #expect(determination.rate == nil) // No change
+        #expect(determination.reason.contains("temp \(currentTemp.rate) ~< req"))
+    }
+
+    @Test("Set zero temp") func testSetZeroTemp() throws {
+        let (shouldSet, determination) = try callHandleLowEventualGlucose(eventualGlucose: 70, naiveEventualGlucose: 60)
+        #expect(shouldSet == true)
+        #expect(determination.rate == 0)
+        #expect(determination.duration! > 0)
+        #expect(determination.reason.contains("setting \(determination.duration!)m zero temp."))
+    }
+}