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

Test: ForecastedGlucoseEvaluator min-curve selection

Pin the forecasted-low signal: min reduction across IOB/COB/UAM/ZT at the
horizon index, nil when predictions are absent or out of range, and correct
index math for non-default horizons. Guards against a min/max flip silently
masking an impending low.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
James Woglom недель назад: 2
Родитель
Сommit
9a04647927
2 измененных файлов с 131 добавлено и 0 удалено
  1. 4 0
      Trio.xcodeproj/project.pbxproj
  2. 127 0
      TrioTests/ForecastedGlucoseEvaluatorTests.swift

+ 4 - 0
Trio.xcodeproj/project.pbxproj

@@ -237,6 +237,7 @@
 		38FCF3D625E8FDF40078B0D1 /* MD5.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38FCF3D525E8FDF40078B0D1 /* MD5.swift */; };
 		38FCF3F925E902C20078B0D1 /* FileStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38FCF3F825E902C20078B0D1 /* FileStorageTests.swift */; };
 		BD11A001000000000000A002 /* AlertMuterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD11A000000000000000A002 /* AlertMuterTests.swift */; };
+		BD11A001000000000000A006 /* ForecastedGlucoseEvaluatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD11A000000000000000A006 /* ForecastedGlucoseEvaluatorTests.swift */; };
 		BD11A001000000000000A003 /* DeviceAlertsStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD11A000000000000000A003 /* DeviceAlertsStoreTests.swift */; };
 		38FCF3FD25E997A80078B0D1 /* PumpHistoryStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38FCF3FC25E997A80078B0D1 /* PumpHistoryStorage.swift */; };
 		38FE826A25CC82DB001FF17A /* NetworkService.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38FE826925CC82DB001FF17A /* NetworkService.swift */; };
@@ -1239,6 +1240,7 @@
 		38FCF3F125E9028E0078B0D1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
 		38FCF3F825E902C20078B0D1 /* FileStorageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileStorageTests.swift; sourceTree = "<group>"; };
 		BD11A000000000000000A002 /* AlertMuterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertMuterTests.swift; sourceTree = "<group>"; };
+		BD11A000000000000000A006 /* ForecastedGlucoseEvaluatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ForecastedGlucoseEvaluatorTests.swift; sourceTree = "<group>"; };
 		BD11A000000000000000A003 /* DeviceAlertsStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceAlertsStoreTests.swift; sourceTree = "<group>"; };
 		38FCF3FC25E997A80078B0D1 /* PumpHistoryStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PumpHistoryStorage.swift; sourceTree = "<group>"; };
 		38FE826925CC82DB001FF17A /* NetworkService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkService.swift; sourceTree = "<group>"; };
@@ -2997,6 +2999,7 @@
 				3BAAE60B2DE776630049589B /* DynamicISFEnableTests.swift */,
 				38FCF3F825E902C20078B0D1 /* FileStorageTests.swift */,
 				BD11A000000000000000A002 /* AlertMuterTests.swift */,
+				BD11A000000000000000A006 /* ForecastedGlucoseEvaluatorTests.swift */,
 				BD11A000000000000000A003 /* DeviceAlertsStoreTests.swift */,
 				3B997DCE2DC00A3A006B6BB2 /* JSONImporterTests.swift */,
 				CE1F6DD82BADF4620064EB8D /* PluginManagerTests.swift */,
@@ -5565,6 +5568,7 @@
 				3B1C5C482D68E269004E9273 /* IobHistoryTests.swift in Sources */,
 				38FCF3F925E902C20078B0D1 /* FileStorageTests.swift in Sources */,
 				BD11A001000000000000A002 /* AlertMuterTests.swift in Sources */,
+				BD11A001000000000000A006 /* ForecastedGlucoseEvaluatorTests.swift in Sources */,
 				BD11A001000000000000A003 /* DeviceAlertsStoreTests.swift in Sources */,
 				3B8221B22E5882E300585156 /* DetermineBasalEarlyExitTests.swift in Sources */,
 				3BA643EA2ED9FAD8007BC31F /* DetermineBasalAggressiveDosingTests.swift in Sources */,

+ 127 - 0
TrioTests/ForecastedGlucoseEvaluatorTests.swift

@@ -0,0 +1,127 @@
+import Foundation
+import Testing
+
+@testable import Trio
+
+@Suite("Trio Alerts: ForecastedGlucoseEvaluator") struct ForecastedGlucoseEvaluatorTests {
+    /// Builds a `Determination` defaulting every non-predictions field, varying only `predictions`.
+    private func makeDetermination(predictions: Predictions?) -> Determination {
+        Determination(
+            id: nil,
+            reason: "",
+            units: nil,
+            insulinReq: nil,
+            eventualBG: nil,
+            sensitivityRatio: nil,
+            rate: nil,
+            duration: nil,
+            iob: nil,
+            cob: nil,
+            predictions: predictions,
+            deliverAt: nil,
+            carbsReq: nil,
+            temp: nil,
+            bg: nil,
+            reservoir: nil,
+            isf: nil,
+            timestamp: nil,
+            tdd: nil,
+            current_target: nil,
+            minDelta: nil,
+            expectedDelta: nil,
+            minGuardBG: nil,
+            minPredBG: nil,
+            threshold: nil,
+            carbRatio: nil,
+            received: nil
+        )
+    }
+
+    @Test("Reduces to min across all four curves at default horizon (+20min, index 4)") func minAcrossAllFourCurves() {
+        let predictions = Predictions(
+            iob: [100, 101, 102, 103, 104],
+            zt: [130, 131, 132, 133, 99],
+            cob: [110, 111, 112, 113, 90],
+            uam: [120, 121, 122, 123, 95]
+        )
+        let determination = makeDetermination(predictions: predictions)
+
+        let result = ForecastedGlucoseEvaluator.evaluate(determination: determination)
+        #expect(result != nil)
+        #expect(result?.predictedGlucose == Decimal(90))
+        #expect(result?.horizonMinutes == 20)
+        #expect(result?.curvesUsed == Set([.iob, .cob, .uam, .zt]))
+        #expect(result?.perCurve[.iob] == Decimal(104))
+        #expect(result?.perCurve[.cob] == Decimal(90))
+        #expect(result?.perCurve[.uam] == Decimal(95))
+        #expect(result?.perCurve[.zt] == Decimal(99))
+    }
+
+    @Test("nil predictions returns nil") func nilPredictionsReturnsNil() {
+        let determination = makeDetermination(predictions: nil)
+        #expect(ForecastedGlucoseEvaluator.evaluate(determination: determination) == nil)
+    }
+
+    @Test("All curves shorter than the horizon index returns nil") func indexOutOfBoundsAllShortReturnsNil() {
+        let predictions = Predictions(
+            iob: [100, 101, 102],
+            zt: [130, 131, 132],
+            cob: [110, 111, 112],
+            uam: [120, 121, 122]
+        )
+        let determination = makeDetermination(predictions: predictions)
+        #expect(ForecastedGlucoseEvaluator.evaluate(determination: determination) == nil)
+    }
+
+    @Test("Only one populated curve is used") func onlyOneCurvePopulated() {
+        let predictions = Predictions(
+            iob: [80, 81, 82, 83, 84],
+            zt: nil,
+            cob: nil,
+            uam: nil
+        )
+        let determination = makeDetermination(predictions: predictions)
+
+        let result = ForecastedGlucoseEvaluator.evaluate(determination: determination)
+        #expect(result != nil)
+        #expect(result?.predictedGlucose == Decimal(84))
+        #expect(result?.curvesUsed == Set([.iob]))
+    }
+
+    @Test("Non-default horizon selects the matching index") func nonDefaultHorizonSelectsCorrectIndex() {
+        let predictions = Predictions(
+            iob: [100, 90, 80, 70],
+            zt: nil,
+            cob: [105, 95, 85, 75],
+            uam: nil
+        )
+        let determination = makeDetermination(predictions: predictions)
+
+        let result = ForecastedGlucoseEvaluator.evaluate(determination: determination, horizonMinutes: 10)
+        #expect(result != nil)
+        #expect(result?.predictedGlucose == Decimal(80))
+        #expect(result?.curvesUsed == Set([.iob, .cob]))
+        #expect(result?.horizonMinutes == 10)
+    }
+
+    @Test("Curves with differing lengths skip those missing the index") func differingLengthsSomeMissing() {
+        let predictions = Predictions(
+            iob: [100, 101, 102, 103, 104],
+            zt: [130, 131, 132],
+            cob: [110, 111],
+            uam: [120, 121, 122, 123, 70]
+        )
+        let determination = makeDetermination(predictions: predictions)
+
+        let result = ForecastedGlucoseEvaluator.evaluate(determination: determination)
+        #expect(result != nil)
+        #expect(result?.predictedGlucose == Decimal(70))
+        #expect(result?.curvesUsed == Set([.iob, .uam]))
+    }
+
+    @Test("All-empty arrays return nil") func emptyArraysReturnNil() {
+        let predictions = Predictions(iob: [], zt: [], cob: [], uam: [])
+        let determination = makeDetermination(predictions: predictions)
+        #expect(ForecastedGlucoseEvaluator.evaluate(determination: determination) == nil)
+    }
+}