Prechádzať zdrojové kódy

Cover Carbs Required alarm type with coordinator + store + alert tests

trioneer 2 týždňov pred
rodič
commit
3353cd228b

+ 4 - 0
Trio.xcodeproj/project.pbxproj

@@ -248,6 +248,7 @@
 		BD11A001000000000000A00C /* NotLoopingMonitorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD11A000000000000000A00C /* NotLoopingMonitorTests.swift */; };
 		BD11A001000000000000A00B /* TrioModalAlertSchedulerTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD11A000000000000000A00B /* TrioModalAlertSchedulerTests.swift */; };
 		BD11A001000000000000A00A /* GlucoseAlertCoordinatorTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD11A000000000000000A00A /* GlucoseAlertCoordinatorTests.swift */; };
+		CA06000000000000000010C2 /* GlucoseAlertsStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA06000000000000000010C1 /* GlucoseAlertsStoreTests.swift */; };
 		BD11A001000000000000A009 /* TrioAlertCategoryTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD11A000000000000000A009 /* TrioAlertCategoryTests.swift */; };
 		BD11A001000000000000A008 /* GlucoseAlertTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD11A000000000000000A008 /* GlucoseAlertTests.swift */; };
 		BD11A001000000000000A007 /* GlucoseAlertConfigurationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD11A000000000000000A007 /* GlucoseAlertConfigurationTests.swift */; };
@@ -1265,6 +1266,7 @@
 		BD11A000000000000000A00C /* NotLoopingMonitorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NotLoopingMonitorTests.swift; sourceTree = "<group>"; };
 		BD11A000000000000000A00B /* TrioModalAlertSchedulerTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrioModalAlertSchedulerTests.swift; sourceTree = "<group>"; };
 		BD11A000000000000000A00A /* GlucoseAlertCoordinatorTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseAlertCoordinatorTests.swift; sourceTree = "<group>"; };
+		CA06000000000000000010C1 /* GlucoseAlertsStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseAlertsStoreTests.swift; sourceTree = "<group>"; };
 		BD11A000000000000000A009 /* TrioAlertCategoryTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrioAlertCategoryTests.swift; sourceTree = "<group>"; };
 		BD11A000000000000000A008 /* GlucoseAlertTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseAlertTests.swift; sourceTree = "<group>"; };
 		BD11A000000000000000A007 /* GlucoseAlertConfigurationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseAlertConfigurationTests.swift; sourceTree = "<group>"; };
@@ -3037,6 +3039,7 @@
 				BD11A000000000000000A00C /* NotLoopingMonitorTests.swift */,
 				BD11A000000000000000A00B /* TrioModalAlertSchedulerTests.swift */,
 				BD11A000000000000000A00A /* GlucoseAlertCoordinatorTests.swift */,
+				CA06000000000000000010C1 /* GlucoseAlertsStoreTests.swift */,
 				BD11A000000000000000A009 /* TrioAlertCategoryTests.swift */,
 				BD11A000000000000000A008 /* GlucoseAlertTests.swift */,
 				BD11A000000000000000A007 /* GlucoseAlertConfigurationTests.swift */,
@@ -5621,6 +5624,7 @@
 				BD11A001000000000000A00C /* NotLoopingMonitorTests.swift in Sources */,
 				BD11A001000000000000A00B /* TrioModalAlertSchedulerTests.swift in Sources */,
 				BD11A001000000000000A00A /* GlucoseAlertCoordinatorTests.swift in Sources */,
+				CA06000000000000000010C2 /* GlucoseAlertsStoreTests.swift in Sources */,
 				BD11A001000000000000A009 /* TrioAlertCategoryTests.swift in Sources */,
 				BD11A001000000000000A008 /* GlucoseAlertTests.swift in Sources */,
 				BD11A001000000000000A007 /* GlucoseAlertConfigurationTests.swift in Sources */,

+ 22 - 1
TrioTests/GlucoseAlertCoordinatorTests.swift

@@ -37,11 +37,12 @@ import Testing
         ))
     }
 
-    @Test("type priority order: urgentLow < low < forecastedLow < high") func priorityOrder() {
+    @Test("type priority order: urgentLow < low < forecastedLow < high < carbsRequired") func priorityOrder() {
         #expect(GlucoseAlertType.urgentLow.priority == 0)
         #expect(GlucoseAlertType.low.priority == 1)
         #expect(GlucoseAlertType.forecastedLow.priority == 2)
         #expect(GlucoseAlertType.high.priority == 3)
+        #expect(GlucoseAlertType.carbsRequired.priority == 4)
         #expect(GlucoseAlertType.urgentLow.priority < GlucoseAlertType.low.priority)
     }
 
@@ -49,4 +50,24 @@ import Testing
         #expect(GlucoseAlertCoordinator.shouldRetract(type: .low, latestMgDL: 75, thresholdMgDL: 70))
         #expect(!GlucoseAlertCoordinator.shouldRetract(type: .low, latestMgDL: 74, thresholdMgDL: 70))
     }
+
+    // MARK: - Carbs Required predicates (mg/dL-based gates are no-ops)
+
+    /// Carbs Required is determination-driven (`evaluateCarbsRequired(_:)`),
+    /// not reading-driven. The shared mg/dL `breached` predicate must never
+    /// return true for it, regardless of value / threshold combination.
+    @Test("carbsRequired never breaches via the mg/dL predicate") func carbsRequiredNeverBreaches() {
+        #expect(!GlucoseAlertCoordinator.breached(type: .carbsRequired, latestMgDL: 0, thresholdMgDL: 0))
+        #expect(!GlucoseAlertCoordinator.breached(type: .carbsRequired, latestMgDL: 200, thresholdMgDL: 10))
+        #expect(!GlucoseAlertCoordinator.breached(type: .carbsRequired, latestMgDL: 10, thresholdMgDL: 200))
+    }
+
+    @Test("carbsRequired never retracts via the mg/dL predicate") func carbsRequiredNeverRetractsViaMgDL() {
+        #expect(!GlucoseAlertCoordinator.shouldRetract(
+            type: .carbsRequired, latestMgDL: 100, thresholdMgDL: 10, recoveryMarginMgDL: 5
+        ))
+        #expect(!GlucoseAlertCoordinator.shouldRetract(
+            type: .carbsRequired, latestMgDL: 0, thresholdMgDL: 50, recoveryMarginMgDL: 5
+        ))
+    }
 }

+ 29 - 0
TrioTests/GlucoseAlertTests.swift

@@ -54,6 +54,35 @@ import Testing
         #expect(a.shouldEvaluate == true)
     }
 
+    @Test("carbsRequired does not evaluate when disabled") func carbsRequiredDisabledDoesNotEvaluate() {
+        var a = GlucoseAlert(type: .carbsRequired)
+        a.isEnabled = false
+        #expect(a.shouldEvaluate == false)
+    }
+
+    @Test("carbsRequired evaluates when enabled") func carbsRequiredEnabledEvaluates() {
+        var a = GlucoseAlert(type: .carbsRequired)
+        a.isEnabled = true
+        #expect(a.shouldEvaluate == true)
+    }
+
+    // MARK: - Group A.5: type metadata
+
+    @Test("carbsRequired defaults: threshold 10 g, bloop.caf, no override") func carbsRequiredDefaults() {
+        let a = GlucoseAlert(type: .carbsRequired)
+        #expect(a.thresholdMgDL == 10)
+        #expect(a.soundFilename == "bloop.caf")
+        #expect(a.overridesSilenceAndDND == false)
+    }
+
+    @Test("isReadingDriven: true for low family + high, false for forecast + carbs") func isReadingDriven() {
+        #expect(GlucoseAlertType.urgentLow.isReadingDriven)
+        #expect(GlucoseAlertType.low.isReadingDriven)
+        #expect(GlucoseAlertType.high.isReadingDriven)
+        #expect(!GlucoseAlertType.forecastedLow.isReadingDriven)
+        #expect(!GlucoseAlertType.carbsRequired.isReadingDriven)
+    }
+
     // MARK: - Group B: decoder defaults
 
     @Test("Omitted isEnabled defaults to true") func decodeOmittedIsEnabledDefaultsTrue() throws {

+ 64 - 0
TrioTests/GlucoseAlertsStoreTests.swift

@@ -0,0 +1,64 @@
+import Foundation
+import Testing
+
+@testable import Trio
+
+@Suite("Trio Alerts: GlucoseAlertsStore seed + backfill", .serialized) struct GlucoseAlertsStoreTests {
+    private static func makeStore(seed: [GlucoseAlert]? = nil) -> GlucoseAlertsStore {
+        let suiteName = "GlucoseAlertsStoreTests.\(UUID().uuidString)"
+        let defaults = UserDefaults(suiteName: suiteName)!
+        defaults.removePersistentDomain(forName: suiteName)
+        let alertsKey = "alerts.\(suiteName)"
+        let configKey = "config.\(suiteName)"
+        if let seed {
+            let data = try? JSONEncoder().encode(seed)
+            defaults.set(data, forKey: alertsKey)
+        }
+        return GlucoseAlertsStore(defaults: defaults, alertsKey: alertsKey, configKey: configKey)
+    }
+
+    @Test("Fresh store seeds one alert per type") func freshSeedAllTypes() {
+        let store = Self.makeStore()
+        let types = Set(store.alerts.map(\.type))
+        for type in GlucoseAlertType.allCases {
+            #expect(types.contains(type), "Missing seed for \(type)")
+        }
+        #expect(store.alerts.count == GlucoseAlertType.allCases.count)
+    }
+
+    @Test("Legacy load (no carbsRequired) backfills the new type") func legacyLoadBackfillsCarbsRequired() {
+        let legacy: [GlucoseAlert] = [
+            GlucoseAlert(type: .urgentLow),
+            GlucoseAlert(type: .low),
+            GlucoseAlert(type: .forecastedLow),
+            GlucoseAlert(type: .high)
+        ]
+        let store = Self.makeStore(seed: legacy)
+        let types = Set(store.alerts.map(\.type))
+        #expect(types.contains(.carbsRequired))
+        #expect(store.alerts.count == GlucoseAlertType.allCases.count)
+    }
+
+    @Test("Backfill preserves user customizations on existing entries") func backfillPreservesCustomizations() {
+        var custom = GlucoseAlert(type: .low)
+        custom.thresholdMgDL = 65
+        custom.soundFilename = "custom_sound.caf"
+        custom.isEnabled = false
+        let store = Self.makeStore(seed: [custom])
+        let restored = store.alerts.first { $0.type == .low }!
+        #expect(restored.thresholdMgDL == 65)
+        #expect(restored.soundFilename == "custom_sound.caf")
+        #expect(restored.isEnabled == false)
+        // And new types still got appended.
+        #expect(store.alerts.contains { $0.type == .carbsRequired })
+    }
+
+    @Test("Full load doesn't double up on existing types") func fullLoadNoDuplicates() {
+        let full = GlucoseAlertType.allCases.map { GlucoseAlert(type: $0) }
+        let store = Self.makeStore(seed: full)
+        let countsByType = Dictionary(grouping: store.alerts, by: \.type).mapValues(\.count)
+        for type in GlucoseAlertType.allCases {
+            #expect(countsByType[type] == 1, "Duplicated \(type) on load")
+        }
+    }
+}