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

Early exit tests

This commit adds test cases to check all of the ways the determine
basal function can exit early before hitting the core dosing
logic. There are corresponding Javascript tests that show that these
are the right conditions to check. It also includes code to wrap up
these early exits into error objects.

We removed the `enableSMB` variable in the Determination that we used
for debugging and updated the determineBasal JS to remove it from
there as well.
Sam King 9 месяцев назад
Родитель
Сommit
c828d52014

+ 0 - 2
Model/JSONImporter.swift

@@ -528,7 +528,6 @@ extension Determination: Codable {
         case carbRatio = "CR"
         case received
         case receivedAlt = "recieved"
-        case enableSMB
     }
 
     init(from decoder: Decoder) throws {
@@ -605,7 +604,6 @@ extension Determination: Codable {
         try container.encodeIfPresent(threshold, forKey: .threshold)
         try container.encodeIfPresent(carbRatio, forKey: .carbRatio)
         try container.encodeIfPresent(received, forKey: .received) // always encode the correct spelling
-        try container.encodeIfPresent(enableSMB, forKey: .enableSMB)
     }
 
     func checkForRequiredFields() throws {

+ 4 - 0
Trio.xcodeproj/project.pbxproj

@@ -287,6 +287,7 @@
 		3B5CD2CD2D4AECD500CE213C /* ProfileIsfTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B5CD2C42D4AECD500CE213C /* ProfileIsfTests.swift */; };
 		3B5CD2CE2D4AECD500CE213C /* ProfileBasalTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B5CD2C12D4AECD500CE213C /* ProfileBasalTests.swift */; };
 		3B5F45B62D6A239500F70982 /* DoubleApproximateMatching.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B5F45B52D6A239000F70982 /* DoubleApproximateMatching.swift */; };
+		3B8221B22E5882E300585156 /* DetermineBasalEarlyExitTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 3B8221B12E5882D900585156 /* DetermineBasalEarlyExitTests.swift */; };
 		3B8B5D332DF5238000365ED3 /* as-profile.json in Resources */ = {isa = PBXBuildFile; fileRef = 3B8B5D302DF5238000365ED3 /* as-profile.json */; };
 		3B8B5D342DF5238000365ED3 /* as-glucose.json in Resources */ = {isa = PBXBuildFile; fileRef = 3B8B5D2F2DF5238000365ED3 /* as-glucose.json */; };
 		3B8B5D352DF5238000365ED3 /* as-pump.json in Resources */ = {isa = PBXBuildFile; fileRef = 3B8B5D312DF5238000365ED3 /* as-pump.json */; };
@@ -1213,6 +1214,7 @@
 		3B5CD2C52D4AECD500CE213C /* ProfileJavascriptTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileJavascriptTests.swift; sourceTree = "<group>"; };
 		3B5CD2C62D4AECD500CE213C /* ProfileTargetsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProfileTargetsTests.swift; sourceTree = "<group>"; };
 		3B5F45B52D6A239000F70982 /* DoubleApproximateMatching.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DoubleApproximateMatching.swift; sourceTree = "<group>"; };
+		3B8221B12E5882D900585156 /* DetermineBasalEarlyExitTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DetermineBasalEarlyExitTests.swift; sourceTree = "<group>"; };
 		3B8B5D2D2DF5238000365ED3 /* as-basal.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "as-basal.json"; sourceTree = "<group>"; };
 		3B8B5D2E2DF5238000365ED3 /* as-carbs.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "as-carbs.json"; sourceTree = "<group>"; };
 		3B8B5D2F2DF5238000365ED3 /* as-glucose.json */ = {isa = PBXFileReference; lastKnownFileType = text.json; path = "as-glucose.json"; sourceTree = "<group>"; };
@@ -2937,6 +2939,7 @@
 				3B8B5D3B2DF523B800365ED3 /* AutosensJsonTests.swift */,
 				3BBC22622DF5B93900169236 /* AutosensTests.swift */,
 				DD30B9FF2E0745C400DA677C /* DetermineBasalDeltaCalculationTests.swift */,
+				3B8221B12E5882D900585156 /* DetermineBasalEarlyExitTests.swift */,
 				3BAC929A2E55FF4F00B853DA /* DetermineBasalEnableSmbTests.swift */,
 				3BE9F0BA2E28C993001B14EB /* DetermineBasalJsonTests.swift */,
 				3B2A3BC22E2B19F700658FB9 /* DynamicISFTests.swift */,
@@ -5249,6 +5252,7 @@
 				3BC4053B2D931620006A03E9 /* IobJsonTests.swift in Sources */,
 				3BEF6AB72D9750780076089D /* MealJsonTests.swift in Sources */,
 				38FCF3F925E902C20078B0D1 /* FileStorageTests.swift in Sources */,
+				3B8221B22E5882E300585156 /* DetermineBasalEarlyExitTests.swift in Sources */,
 				BD8FC0602D6619DB00B95AED /* CarbsStorageTests.swift in Sources */,
 				DD30BA002E0745C400DA677C /* DetermineBasalDeltaCalculationTests.swift in Sources */,
 				BD8FC05E2D6618CE00B95AED /* BolusCalculatorTests.swift in Sources */,

+ 0 - 9
Trio.xcodeproj/xcshareddata/xcschemes/Trio Tests.xcscheme

@@ -53,15 +53,6 @@
       savedToolIdentifier = ""
       useCustomWorkingDirectory = "NO"
       debugDocumentVersioning = "YES">
-      <MacroExpansion>
-         <BuildableReference
-            BuildableIdentifier = "primary"
-            BlueprintIdentifier = "388E595725AD948C0019842D"
-            BuildableName = "Trio.app"
-            BlueprintName = "Trio"
-            ReferencedContainer = "container:Trio.xcodeproj">
-         </BuildableReference>
-      </MacroExpansion>
    </ProfileAction>
    <AnalyzeAction
       buildConfiguration = "Debug">

+ 1 - 2
Trio/Sources/APS/OpenAPS/OpenAPS.swift

@@ -877,8 +877,7 @@ final class OpenAPS {
             preferences: preferences,
             basalProfile: basalProfile,
             trioCustomOrefVariables: trioCustomOrefVariables,
-            clock: clock,
-            includeDebugOutputs: false
+            clock: clock
         )
         let swiftDuration = Date().timeIntervalSince(startSwiftAt)
 

+ 8 - 0
Trio/Sources/APS/OpenAPSSwift/DetermineBasal/DeterminationError.swift

@@ -3,6 +3,8 @@ import Foundation
 enum DeterminationError: LocalizedError, Equatable {
     case missingGlucoseStatus
     case missingProfile
+    case missingCurrentBasal
+    case missingMinBg
     case staleGlucoseData(ageMinutes: Double)
     case glucoseOutOfRange(glucose: Decimal)
     case cgmNoiseTooHigh(noise: Int)
@@ -18,6 +20,12 @@ enum DeterminationError: LocalizedError, Equatable {
             return String(localized: "No glucose status; cannot determine basal.")
         case .missingProfile:
             return String(localized: "No profile; cannot determine basal.")
+        case .missingCurrentBasal:
+            // string copied from JS
+            return String(localized: "Error: could not get current basal rate")
+        case .missingMinBg:
+            // string copied from JS including trailing space
+            return String(localized: "Error: could not determine target_bg. ")
         case let .staleGlucoseData(ageMinutes):
             return String(localized: "Glucose data is too old (\(ageMinutes) min ago).")
         case let .glucoseOutOfRange(glucose):

+ 124 - 96
Trio/Sources/APS/OpenAPSSwift/DetermineBasal/DetermineBasalGenerator.swift

@@ -11,6 +11,7 @@ enum DeterminationGenerator {
     // override data can just be fetched from the DB
     // handling via overrideManager ?
 
+    /// Top-level determination generator, callers should use this function
     static func generate(
         profile: Profile,
         preferences: Preferences,
@@ -18,14 +19,43 @@ enum DeterminationGenerator {
         iobData: [IobResult],
         mealData: ComputedCarbs,
         autosensData: Autosens,
-        reservoirData _: Decimal,
+        reservoirData: Decimal,
         glucose: [BloodGlucose],
         trioCustomOrefVariables: TrioCustomOrefVariables,
-        currentTime: Date,
-        includeDebugOutputs: Bool
+        currentTime: Date
     ) throws -> Determination? {
-        var autosensData = autosensData
         let glucoseStatus = try Self.getGlucoseStatus(glucoseReadings: glucose)
+        guard let glucoseStatus = glucoseStatus else { throw DeterminationError.missingInputs }
+        return try determineBasal(
+            profile: profile,
+            preferences: preferences,
+            currentTemp: currentTemp,
+            iobData: iobData,
+            mealData: mealData,
+            autosensData: autosensData,
+            reservoirData: reservoirData,
+            glucoseStatus: glucoseStatus,
+            trioCustomOrefVariables: trioCustomOrefVariables,
+            currentTime: currentTime
+        )
+    }
+
+    /// Internal function to implement the core determine basal logic. We have a separate function
+    /// from `generate` so that we can pass GlucoseStatus values directly into the function
+    /// for testing.
+    static func determineBasal(
+        profile: Profile,
+        preferences: Preferences,
+        currentTemp: TempBasal,
+        iobData: [IobResult],
+        mealData: ComputedCarbs,
+        autosensData: Autosens,
+        reservoirData _: Decimal,
+        glucoseStatus: GlucoseStatus,
+        trioCustomOrefVariables: TrioCustomOrefVariables,
+        currentTime: Date
+    ) throws -> Determination? {
+        var autosensData = autosensData
 
         try checkDeterminationInputs(
             glucoseStatus: glucoseStatus,
@@ -35,11 +65,9 @@ enum DeterminationGenerator {
             currentTime: currentTime,
         )
 
-        guard let glucoseStatus = glucoseStatus else { throw DeterminationError.missingInputs }
-
         let currentGlucose: Decimal = glucoseStatus.glucose
 
-        if let errorDetermination = handleTempBasalCases(
+        if let errorDetermination = try handleTempBasalCases(
             glucoseStatus: glucoseStatus,
             profile: profile,
             currentTemp: currentTemp,
@@ -48,6 +76,50 @@ enum DeterminationGenerator {
             return errorDetermination
         }
 
+        // Safety check: current temp vs. last temp in iob
+        guard let lastTempTarget = iobData.first?.lastTemp else {
+            throw DeterminationError.missingIob
+        }
+        if !checkCurrentTempBasalRateSafety(
+            currentTemp: currentTemp,
+            lastTempTarget: lastTempTarget,
+            currentTime: currentTime
+        ) {
+            let reason =
+                "Safety check: currentTemp does not match lastTemp in IOB or lastTemp ended long ago; canceling temp basal."
+            return Determination(
+                id: UUID(),
+                reason: reason,
+                units: nil,
+                insulinReq: nil,
+                eventualBG: nil,
+                sensitivityRatio: nil,
+                rate: 0,
+                duration: 0,
+                iob: iobData.first?.iob,
+                cob: nil,
+                predictions: nil,
+                deliverAt: currentTime,
+                carbsReq: nil,
+                temp: .absolute,
+                bg: glucoseStatus.glucose,
+                reservoir: nil,
+                isf: profile.sens,
+                timestamp: currentTime,
+                tdd: nil,
+                current_target: profile.targetBg,
+                insulinForManualBolus: nil,
+                manualBolusErrorString: nil,
+                minDelta: nil,
+                expectedDelta: nil,
+                minGuardBG: nil,
+                minPredBG: nil,
+                threshold: nil,
+                carbRatio: profile.carbRatio,
+                received: false
+            )
+        }
+
         let dynamicIsfResult = DynamicISF.calculate(
             profile: profile,
             preferences: preferences,
@@ -227,92 +299,42 @@ enum DeterminationGenerator {
 
         // TODO: STOPPING at LINE 1264
 
-        var determination: Determination
-        // Safety check: current temp vs. last temp in iob
-        guard let lastTempTarget = iobData.first?.lastTemp else {
-            throw DeterminationError.missingIob
-        }
-        if !checkCurrentTempBasalRateSafety(
-            currentTemp: currentTemp,
-            lastTempTarget: lastTempTarget,
-            currentTime: currentTime
-        ) {
-            let reason =
-                "Safety check: currentTemp does not match lastTemp in IOB or lastTemp ended long ago; canceling temp basal."
-            determination = Determination(
-                id: UUID(),
-                reason: reason,
-                units: nil,
-                insulinReq: nil,
-                eventualBG: nil,
-                sensitivityRatio: nil,
-                rate: 0,
-                duration: 0,
-                iob: iobData.first?.iob,
-                cob: nil,
-                predictions: nil,
-                deliverAt: currentTime,
-                carbsReq: nil,
-                temp: .absolute,
-                bg: glucoseStatus.glucose,
-                reservoir: nil,
-                isf: profile.sens,
-                timestamp: currentTime,
-                tdd: nil,
-                current_target: profile.targetBg,
-                insulinForManualBolus: nil,
-                manualBolusErrorString: nil,
-                minDelta: nil,
-                expectedDelta: nil,
-                minGuardBG: nil,
-                minPredBG: nil,
-                threshold: nil,
-                carbRatio: profile.carbRatio,
-                received: false
-            )
-        } else {
-            // FIXME: properly populate all fields!
-            determination = Determination(
-                id: UUID(),
-                reason: dosingInputs.reason,
-                units: nil,
-                insulinReq: nil,
-                eventualBG: Int(forecastResult.eventualGlucose),
-                sensitivityRatio: sensitivityRatio, // this would only the AS-adjusted one for now
-                rate: nil,
-                duration: nil,
-                iob: iobData.first?.iob,
-                cob: mealData.mealCOB,
-                predictions: Predictions(
-                    iob: forecastResult.iob.map { Int($0.jsRounded()) },
-                    zt: forecastResult.zt.map { Int($0.jsRounded()) },
-                    cob: forecastResult.cob?.map { Int($0.jsRounded()) },
-                    uam: forecastResult.uam?.map { Int($0.jsRounded()) }
-                ),
-                deliverAt: currentTime,
-                carbsReq: dosingInputs.carbsRequired?.carbs,
-                temp: nil,
-                bg: currentGlucose,
-                reservoir: nil,
-                isf: nil,
-                timestamp: currentTime,
-                tdd: nil,
-                current_target: nil,
-                insulinForManualBolus: nil,
-                manualBolusErrorString: nil,
-                minDelta: nil,
-                expectedDelta: expectedDelta,
-                minGuardBG: forecastResult.minGuardGlucose,
-                minPredBG: forecastResult.minForecastedGlucose,
-                threshold: threshold.jsRounded(),
-                carbRatio: forecastResult.adjustedCarbRatio.jsRounded(scale: 1),
-                received: false,
-            )
-        }
-
-        if includeDebugOutputs {
-            determination.enableSMB = smbDecision.isEnabled
-        }
+        var determination = Determination(
+            id: UUID(),
+            reason: dosingInputs.reason,
+            units: nil,
+            insulinReq: nil,
+            eventualBG: Int(forecastResult.eventualGlucose),
+            sensitivityRatio: sensitivityRatio, // this would only the AS-adjusted one for now
+            rate: nil,
+            duration: nil,
+            iob: iobData.first?.iob,
+            cob: mealData.mealCOB,
+            predictions: Predictions(
+                iob: forecastResult.iob.map { Int($0.jsRounded()) },
+                zt: forecastResult.zt.map { Int($0.jsRounded()) },
+                cob: forecastResult.cob?.map { Int($0.jsRounded()) },
+                uam: forecastResult.uam?.map { Int($0.jsRounded()) }
+            ),
+            deliverAt: currentTime,
+            carbsReq: dosingInputs.carbsRequired?.carbs,
+            temp: nil,
+            bg: currentGlucose,
+            reservoir: nil,
+            isf: nil,
+            timestamp: currentTime,
+            tdd: nil,
+            current_target: nil,
+            insulinForManualBolus: nil,
+            manualBolusErrorString: nil,
+            minDelta: nil,
+            expectedDelta: expectedDelta,
+            minGuardBG: forecastResult.minGuardGlucose,
+            minPredBG: forecastResult.minForecastedGlucose,
+            threshold: threshold.jsRounded(),
+            carbRatio: forecastResult.adjustedCarbRatio.jsRounded(scale: 1),
+            received: false,
+        )
 
         // TODO: how to handle output?
         // TODO: how to handle logging?
@@ -333,11 +355,15 @@ enum DeterminationGenerator {
         guard let profile = profile else {
             throw DeterminationError.missingProfile
         }
+        guard profile.minBg != nil else {
+            throw DeterminationError.missingMinBg
+        }
         let glucoseAge = currentTime.timeIntervalSince(glucoseStatus.date)
         if glucoseAge > 15 * 60 {
             throw DeterminationError.staleGlucoseData(ageMinutes: glucoseAge / 60)
         }
-        if glucoseStatus.glucose < 39 || glucoseStatus.glucose > 600 {
+        // we have to allow 38 values so that we can cancel high temps
+        if glucoseStatus.glucose < 38 || glucoseStatus.glucose > 600 {
             throw DeterminationError.glucoseOutOfRange(glucose: glucoseStatus.glucose)
         }
         guard let _ = iobData else {
@@ -350,7 +376,7 @@ enum DeterminationGenerator {
         profile: Profile,
         currentTemp: TempBasal?,
         currentTime: Date
-    ) -> Determination? {
+    ) throws -> Determination? {
         let glucose = glucoseStatus.glucose
         let noise = glucoseStatus.noise
         let bgTime = glucoseStatus.date
@@ -361,7 +387,9 @@ enum DeterminationGenerator {
         let device = glucoseStatus.device
 
         // Always use profile-supplied basal
-        let basal = profile.currentBasal ?? profile.basalFor(time: currentTime)
+        guard let basal = profile.currentBasal else {
+            throw DeterminationError.missingCurrentBasal
+        }
 
         // Compose tick for log
         let tick: String = (delta > -0.5) ? "+\(delta.rounded(toPlaces: 0))" : "\(delta.rounded(toPlaces: 0))"

+ 11 - 4
Trio/Sources/APS/OpenAPSSwift/OpenAPSSwift.swift

@@ -54,8 +54,7 @@ struct OpenAPSSwift {
         preferences: JSON,
         basalProfile: JSON,
         trioCustomOrefVariables: JSON,
-        clock: Date,
-        includeDebugOutputs: Bool
+        clock: Date
     ) -> (OrefFunctionResult, DetermineBasalInputs?) {
         var determineBasalInputs: DetermineBasalInputs?
 
@@ -105,12 +104,20 @@ struct OpenAPSSwift {
                 reservoirData: reservoir ?? 100,
                 glucose: glucose,
                 trioCustomOrefVariables: trioCustomOrefVariables,
-                currentTime: clock,
-                includeDebugOutputs: includeDebugOutputs
+                currentTime: clock
             )
 
             return (try .success(JSONBridge.to(rawDetermination)), determineBasalInputs)
 
+        } catch let determinationError as DeterminationError {
+            // if we get a determination error we want to return it as a JSON
+            // object that is { "error": "some error" }
+            do {
+                let response = try JSONBridge.to(DeterminationErrorResponse(error: determinationError.localizedDescription))
+                return (.success(response), determineBasalInputs)
+            } catch {
+                return (.failure(determinationError), determineBasalInputs)
+            }
         } catch {
             return (.failure(error), determineBasalInputs)
         }

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

@@ -93591,6 +93591,12 @@
         }
       }
     },
+    "Error: could not determine target_bg. " : {
+
+    },
+    "Error: could not get current basal rate" : {
+
+    },
     "Error! Bolus cancellation failed with error: %@" : {
       "comment" : "Error message for canceling a bolus",
       "localizations" : {

+ 4 - 4
Trio/Sources/Models/Determination.swift

@@ -1,5 +1,9 @@
 import Foundation
 
+struct DeterminationErrorResponse: JSON, Equatable {
+    let error: String
+}
+
 struct Determination: JSON, Equatable {
     let id: UUID?
     var reason: String
@@ -34,9 +38,6 @@ struct Determination: JSON, Equatable {
     var threshold: Decimal?
     let carbRatio: Decimal?
     let received: Bool?
-
-    // only used for debugging
-    var enableSMB: Bool? = nil
 }
 
 struct Predictions: JSON, Equatable {
@@ -77,7 +78,6 @@ extension Determination {
         case threshold
         case carbRatio = "CR"
         case received
-        case enableSMB
     }
 }
 

+ 470 - 0
TrioTests/OpenAPSSwiftTests/DetermineBasalEarlyExitTests.swift

@@ -0,0 +1,470 @@
+import Foundation
+import Testing
+@testable import Trio
+
+@Suite("DetermineBasal early exits before core dosing logic") struct DetermineBasalEarlyExitTests {
+    private func createDefaultInputs() -> (
+        profile: Profile,
+        preferences: Preferences,
+        currentTemp: TempBasal,
+        iobData: [IobResult],
+        mealData: ComputedCarbs,
+        autosensData: Autosens,
+        reservoirData: Decimal,
+        glucoseStatus: GlucoseStatus,
+        trioCustomOrefVariables: TrioCustomOrefVariables,
+        currentTime: Date
+    ) {
+        let currentTime = Date()
+        var profile = Profile()
+        profile.maxIob = 2.5
+        profile.dia = 3
+        profile.currentBasal = 0.9
+        profile.maxDailyBasal = 1.3
+        profile.maxBasal = 3.5
+        profile.maxBg = 120
+        profile.minBg = 110
+        profile.sens = 40
+        profile.carbRatio = 10
+        profile.thresholdSetting = 80
+        profile.temptargetSet = false
+        profile.bolusIncrement = 0.1
+        profile.useCustomPeakTime = false
+        profile.curve = .rapidActing
+
+        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 = [IobResult(
+            iob: 0,
+            activity: 0,
+            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: 30
+            )
+        )]
+
+        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 1 from JS
+    @Test("should fail if current_basal is missing") func missingCurrentBasal() throws {
+        var (
+            profile,
+            preferences,
+            currentTemp,
+            iobData,
+            mealData,
+            autosensData,
+            reservoirData,
+            glucoseStatus,
+            trioCustomOrefVariables,
+            currentTime
+        ) = createDefaultInputs()
+        profile.currentBasal = nil
+        profile.basalprofile = [] // ensure basalFor also returns nil
+
+        #expect(throws: DeterminationError.missingCurrentBasal) {
+            _ = try DeterminationGenerator.determineBasal(
+                profile: profile,
+                preferences: preferences,
+                currentTemp: currentTemp,
+                iobData: iobData,
+                mealData: mealData,
+                autosensData: autosensData,
+                reservoirData: reservoirData,
+                glucoseStatus: glucoseStatus,
+                trioCustomOrefVariables: trioCustomOrefVariables,
+                currentTime: currentTime
+            )
+        }
+    }
+
+    // Test 2 from JS
+    @Test("should cancel high temp if BG is 38") func cancelHighTempBG38() throws {
+        let (
+            profile,
+            preferences,
+            _,
+            iobData,
+            mealData,
+            autosensData,
+            reservoirData,
+            _,
+            trioCustomOrefVariables,
+            currentTime
+        ) = createDefaultInputs()
+        let glucoseStatus = GlucoseStatus(
+            delta: 0,
+            glucose: 38,
+            noise: 1,
+            shortAvgDelta: 0,
+            longAvgDelta: 0.1,
+            date: currentTime,
+            lastCalIndex: nil,
+            device: "test"
+        )
+
+        let currentTemp = TempBasal(duration: 30, rate: 1.5, temp: .absolute, timestamp: 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)
+        #expect(result?.duration == 0)
+        #expect(result?.reason.contains("Canceling high temp basal") == true)
+    }
+
+    // Test 3 from JS
+    @Test("should shorten long zero temp if BG data is too old") func shortenLongZeroTempTooOldBG() throws {
+        let (
+            profile,
+            preferences,
+            _,
+            iobData,
+            mealData,
+            autosensData,
+            reservoirData,
+            _,
+            trioCustomOrefVariables,
+            currentTime
+        ) = createDefaultInputs()
+        let glucoseTime = currentTime.addingTimeInterval(-15 * 60)
+        let glucoseStatus = GlucoseStatus(
+            delta: 0,
+            glucose: 115,
+            noise: 1,
+            shortAvgDelta: 0,
+            longAvgDelta: 0.1,
+            date: glucoseTime,
+            lastCalIndex: nil,
+            device: "test"
+        )
+
+        let currentTemp = TempBasal(duration: 60, rate: 0, temp: .absolute, timestamp: 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)
+        #expect(result?.duration == 30)
+        #expect(result?.reason.contains("Shortening") == true)
+    }
+
+    // Test 4 from JS
+    @Test("should do nothing if BG is too old and temp is not high") func doNothingOldBGNotHighTemp() throws {
+        let (
+            profile,
+            preferences,
+            _,
+            iobData,
+            mealData,
+            autosensData,
+            reservoirData,
+            _,
+            trioCustomOrefVariables,
+            currentTime
+        ) = createDefaultInputs()
+        let glucoseTime = currentTime.addingTimeInterval(-15 * 60)
+        let glucoseStatus = GlucoseStatus(
+            delta: 0,
+            glucose: 115,
+            noise: 1,
+            shortAvgDelta: 0,
+            longAvgDelta: 0.1,
+            date: glucoseTime,
+            lastCalIndex: nil,
+            device: "test"
+        )
+
+        let currentTemp = TempBasal(duration: 30, rate: 0.5, temp: .absolute, timestamp: 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.5)
+        #expect(result?.duration == 30)
+        #expect(result?.reason.contains("doing nothing") == true)
+    }
+
+    // Test 5 from JS
+    @Test("should error if target_bg cannot be determined") func errorIfTargetBGMissing() throws {
+        var (
+            profile,
+            preferences,
+            currentTemp,
+            iobData,
+            mealData,
+            autosensData,
+            reservoirData,
+            glucoseStatus,
+            trioCustomOrefVariables,
+            currentTime
+        ) = createDefaultInputs()
+        profile.minBg = nil
+
+        #expect(throws: DeterminationError.missingMinBg) { // Using a placeholder error
+            _ = try DeterminationGenerator.determineBasal(
+                profile: profile,
+                preferences: preferences,
+                currentTemp: currentTemp,
+                iobData: iobData,
+                mealData: mealData,
+                autosensData: autosensData,
+                reservoirData: reservoirData,
+                glucoseStatus: glucoseStatus,
+                trioCustomOrefVariables: trioCustomOrefVariables,
+                currentTime: currentTime
+            )
+        }
+    }
+
+    // Test 6 from JS
+    @Test("should cancel temp if currenttemp and lastTemp from pumphistory do not match") func cancelTempMismatch() throws {
+        let (
+            profile,
+            preferences,
+            _,
+            iobData,
+            mealData,
+            autosensData,
+            reservoirData,
+            glucoseStatus,
+            trioCustomOrefVariables,
+            currentTime
+        ) = createDefaultInputs()
+        let currentTemp = TempBasal(duration: 30, rate: 1.5, temp: .absolute, timestamp: currentTime)
+
+        let lastTempTime = currentTime.addingTimeInterval(-15 * 60)
+        let lastTemp = IobResult.LastTemp(
+            rate: 1.0,
+            timestamp: lastTempTime,
+            started_at: lastTempTime,
+            date: UInt64(lastTempTime.timeIntervalSince1970 * 1000),
+            duration: 30
+        )
+
+        var mutableIobData = iobData
+        mutableIobData[0].lastTemp = lastTemp
+
+        let result = try DeterminationGenerator.determineBasal(
+            profile: profile,
+            preferences: preferences,
+            currentTemp: currentTemp,
+            iobData: mutableIobData,
+            mealData: mealData,
+            autosensData: autosensData,
+            reservoirData: reservoirData,
+            glucoseStatus: glucoseStatus,
+            trioCustomOrefVariables: trioCustomOrefVariables,
+            currentTime: currentTime
+        )
+
+        #expect(result?.rate == 0)
+        #expect(result?.duration == 0)
+        // Note: In swift we use a different reason then JS
+        #expect(
+            result?
+                .reason ==
+                "Safety check: currentTemp does not match lastTemp in IOB or lastTemp ended long ago; canceling temp basal."
+        )
+    }
+
+    // Test 7 from JS
+    @Test("should cancel temp if lastTemp from pumphistory ended long ago") func cancelTempOldLastTemp() throws {
+        let (
+            profile,
+            preferences,
+            _,
+            iobData,
+            mealData,
+            autosensData,
+            reservoirData,
+            glucoseStatus,
+            trioCustomOrefVariables,
+            currentTime
+        ) = createDefaultInputs()
+        let currentTemp = TempBasal(duration: 30, rate: 1.5, temp: .absolute, timestamp: currentTime)
+
+        let lastTempTime = currentTime.addingTimeInterval(-40 * 60)
+        let lastTemp = IobResult.LastTemp(
+            rate: 1.5,
+            timestamp: lastTempTime,
+            started_at: lastTempTime,
+            date: UInt64(lastTempTime.timeIntervalSince1970 * 1000),
+            duration: 30
+        )
+
+        var mutableIobData = iobData
+        mutableIobData[0].lastTemp = lastTemp
+
+        let result = try DeterminationGenerator.determineBasal(
+            profile: profile,
+            preferences: preferences,
+            currentTemp: currentTemp,
+            iobData: mutableIobData,
+            mealData: mealData,
+            autosensData: autosensData,
+            reservoirData: reservoirData,
+            glucoseStatus: glucoseStatus,
+            trioCustomOrefVariables: trioCustomOrefVariables,
+            currentTime: currentTime
+        )
+
+        #expect(result?.rate == 0)
+        #expect(result?.duration == 0)
+        // Note: In swift we use a different reason then JS
+        #expect(
+            result?
+                .reason ==
+                "Safety check: currentTemp does not match lastTemp in IOB or lastTemp ended long ago; canceling temp basal."
+        )
+    }
+
+    // Test 8 from JS
+    @Test("should throw error if eventualBG cannot be calculated") func eventualBGNaN() throws {
+        var (
+            profile,
+            preferences,
+            currentTemp,
+            iobData,
+            mealData,
+            autosensData,
+            reservoirData,
+            glucoseStatus,
+            trioCustomOrefVariables,
+            currentTime
+        ) = createDefaultInputs()
+        profile.sens = .nan
+
+        #expect(throws: DeterminationError.eventualGlucoseCalculationError(sensitivity: .nan, deviation: .nan)) {
+            _ = try DeterminationGenerator.determineBasal(
+                profile: profile,
+                preferences: preferences,
+                currentTemp: currentTemp,
+                iobData: iobData,
+                mealData: mealData,
+                autosensData: autosensData,
+                reservoirData: reservoirData,
+                glucoseStatus: glucoseStatus,
+                trioCustomOrefVariables: trioCustomOrefVariables,
+                currentTime: currentTime
+            )
+        }
+    }
+}

+ 2 - 4
TrioTests/OpenAPSSwiftTests/DetermineBasalJsonTests.swift

@@ -55,8 +55,7 @@ import Testing
             preferences: determineBasalInput.preferences,
             basalProfile: determineBasalInput.basalProfile,
             trioCustomOrefVariables: determineBasalInput.trioCustomOrefVariables,
-            clock: determineBasalInput.clock,
-            includeDebugOutputs: true
+            clock: determineBasalInput.clock
         )
 
         let determineBasalResultJavascript = try await openAps.determineBasalJavascript(
@@ -131,8 +130,7 @@ import Testing
             preferences: determineBasalInput.preferences,
             basalProfile: determineBasalInput.basalProfile,
             trioCustomOrefVariables: determineBasalInput.trioCustomOrefVariables,
-            clock: determineBasalInput.clock,
-            includeDebugOutputs: true
+            clock: determineBasalInput.clock
         )
 
         print("Swift result")

Разница между файлами не показана из-за своего большого размера
+ 1 - 1
TrioTests/OpenAPSSwiftTests/javascript/bundle/determine-basal.js