Преглед изворни кода

Test: GlucoseAlertConfiguration day/night window resolution

Pin isNight(at:) across the wrap-around (22→06) and same-day branches,
boundary equality (nightStart inclusive, dayStart exclusive), and the
degenerate equal-start case. This resolver feeds both glucose-alarm gating
and device-config variant selection, so a wraparound/off-by-one bug would
apply the wrong day/night config.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
James Woglom пре 2 недеља
родитељ
комит
7d803b8258
2 измењених фајлова са 141 додато и 0 уклоњено
  1. 4 0
      Trio.xcodeproj/project.pbxproj
  2. 137 0
      TrioTests/GlucoseAlertConfigurationTests.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 */; };
+		BD11A001000000000000A007 /* GlucoseAlertConfigurationTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD11A000000000000000A007 /* GlucoseAlertConfigurationTests.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 */; };
@@ -1240,6 +1241,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>"; };
+		BD11A000000000000000A007 /* GlucoseAlertConfigurationTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseAlertConfigurationTests.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>"; };
@@ -2999,6 +3001,7 @@
 				3BAAE60B2DE776630049589B /* DynamicISFEnableTests.swift */,
 				38FCF3F825E902C20078B0D1 /* FileStorageTests.swift */,
 				BD11A000000000000000A002 /* AlertMuterTests.swift */,
+				BD11A000000000000000A007 /* GlucoseAlertConfigurationTests.swift */,
 				BD11A000000000000000A006 /* ForecastedGlucoseEvaluatorTests.swift */,
 				BD11A000000000000000A003 /* DeviceAlertsStoreTests.swift */,
 				3B997DCE2DC00A3A006B6BB2 /* JSONImporterTests.swift */,
@@ -5568,6 +5571,7 @@
 				3B1C5C482D68E269004E9273 /* IobHistoryTests.swift in Sources */,
 				38FCF3F925E902C20078B0D1 /* FileStorageTests.swift in Sources */,
 				BD11A001000000000000A002 /* AlertMuterTests.swift in Sources */,
+				BD11A001000000000000A007 /* GlucoseAlertConfigurationTests.swift in Sources */,
 				BD11A001000000000000A006 /* ForecastedGlucoseEvaluatorTests.swift in Sources */,
 				BD11A001000000000000A003 /* DeviceAlertsStoreTests.swift in Sources */,
 				3B8221B22E5882E300585156 /* DetermineBasalEarlyExitTests.swift in Sources */,

+ 137 - 0
TrioTests/GlucoseAlertConfigurationTests.swift

@@ -0,0 +1,137 @@
+import Foundation
+import Testing
+
+@testable import Trio
+
+@Suite("Trio Alerts: GlucoseAlertConfiguration.isNight") struct GlucoseAlertConfigurationTests {
+    /// Fixed UTC calendar so date math is deterministic regardless of host TZ.
+    private static let utcCalendar: Calendar = {
+        var cal = Calendar(identifier: .gregorian)
+        cal.timeZone = TimeZone(identifier: "UTC")!
+        return cal
+    }()
+
+    /// Build a Date on a fixed day (2026-06-22) via the UTC calendar.
+    private static func makeDate(hour: Int, minute: Int, second: Int = 0) -> Date {
+        var components = DateComponents()
+        components.year = 2026
+        components.month = 6
+        components.day = 22
+        components.hour = hour
+        components.minute = minute
+        components.second = second
+        return utcCalendar.date(from: components)!
+    }
+
+    // MARK: - Default window (day 06:00 / night 22:00, wrap)
+
+    @Test("Default: 02:00 is night") func defaultEarlyMorningIsNight() {
+        let config = GlucoseAlertConfiguration()
+        #expect(config.isNight(at: Self.makeDate(hour: 2, minute: 0), calendar: Self.utcCalendar))
+    }
+
+    @Test("Default: 10:00 is day") func defaultMorningIsDay() {
+        let config = GlucoseAlertConfiguration()
+        #expect(!config.isNight(at: Self.makeDate(hour: 10, minute: 0), calendar: Self.utcCalendar))
+    }
+
+    @Test("Default: 14:00 is day") func defaultAfternoonIsDay() {
+        let config = GlucoseAlertConfiguration()
+        #expect(!config.isNight(at: Self.makeDate(hour: 14, minute: 0), calendar: Self.utcCalendar))
+    }
+
+    @Test("Default: 23:00 is night") func defaultLateEveningIsNight() {
+        let config = GlucoseAlertConfiguration()
+        #expect(config.isNight(at: Self.makeDate(hour: 23, minute: 0), calendar: Self.utcCalendar))
+    }
+
+    // MARK: - Boundary (default)
+
+    @Test("Default boundary: 22:00:00 is night (nightStart inclusive)") func defaultNightStartInclusive() {
+        let config = GlucoseAlertConfiguration()
+        #expect(config.isNight(at: Self.makeDate(hour: 22, minute: 0, second: 0), calendar: Self.utcCalendar))
+    }
+
+    @Test("Default boundary: 06:00:00 is day (dayStart exclusive)") func defaultDayStartExclusive() {
+        let config = GlucoseAlertConfiguration()
+        #expect(!config.isNight(at: Self.makeDate(hour: 6, minute: 0, second: 0), calendar: Self.utcCalendar))
+    }
+
+    @Test("Default boundary: 05:59:59 is night") func defaultJustBeforeDayStartIsNight() {
+        let config = GlucoseAlertConfiguration()
+        #expect(config.isNight(at: Self.makeDate(hour: 5, minute: 59, second: 59), calendar: Self.utcCalendar))
+    }
+
+    @Test("Default boundary: 21:59:59 is day") func defaultJustBeforeNightStartIsDay() {
+        let config = GlucoseAlertConfiguration()
+        #expect(!config.isNight(at: Self.makeDate(hour: 21, minute: 59, second: 59), calendar: Self.utcCalendar))
+    }
+
+    // MARK: - Non-wrap config (dayStart 07:00, nightStart 01:00)
+
+    @Test("Non-wrap: 03:00 is night") func nonWrapInsideNightIsNight() {
+        let config = GlucoseAlertConfiguration(
+            dayStart: TimeOfDay(hour: 7, minute: 0),
+            nightStart: TimeOfDay(hour: 1, minute: 0)
+        )
+        #expect(config.isNight(at: Self.makeDate(hour: 3, minute: 0), calendar: Self.utcCalendar))
+    }
+
+    @Test("Non-wrap: 12:00 is day") func nonWrapMiddayIsDay() {
+        let config = GlucoseAlertConfiguration(
+            dayStart: TimeOfDay(hour: 7, minute: 0),
+            nightStart: TimeOfDay(hour: 1, minute: 0)
+        )
+        #expect(!config.isNight(at: Self.makeDate(hour: 12, minute: 0), calendar: Self.utcCalendar))
+    }
+
+    @Test("Non-wrap: 00:30 is day (before nightStart)") func nonWrapBeforeNightStartIsDay() {
+        let config = GlucoseAlertConfiguration(
+            dayStart: TimeOfDay(hour: 7, minute: 0),
+            nightStart: TimeOfDay(hour: 1, minute: 0)
+        )
+        #expect(!config.isNight(at: Self.makeDate(hour: 0, minute: 30), calendar: Self.utcCalendar))
+    }
+
+    @Test("Non-wrap boundary: 07:00:00 is day (dayStart exclusive)") func nonWrapDayStartExclusive() {
+        let config = GlucoseAlertConfiguration(
+            dayStart: TimeOfDay(hour: 7, minute: 0),
+            nightStart: TimeOfDay(hour: 1, minute: 0)
+        )
+        #expect(!config.isNight(at: Self.makeDate(hour: 7, minute: 0, second: 0), calendar: Self.utcCalendar))
+    }
+
+    @Test("Non-wrap boundary: 01:00:00 is night (nightStart inclusive)") func nonWrapNightStartInclusive() {
+        let config = GlucoseAlertConfiguration(
+            dayStart: TimeOfDay(hour: 7, minute: 0),
+            nightStart: TimeOfDay(hour: 1, minute: 0)
+        )
+        #expect(config.isNight(at: Self.makeDate(hour: 1, minute: 0, second: 0), calendar: Self.utcCalendar))
+    }
+
+    // MARK: - Degenerate equal (dayStart == nightStart == 06:00) — proves wrap >= branch
+
+    @Test("Equal: 05:00 is night") func equalBeforeIsNight() {
+        let config = GlucoseAlertConfiguration(
+            dayStart: TimeOfDay(hour: 6, minute: 0),
+            nightStart: TimeOfDay(hour: 6, minute: 0)
+        )
+        #expect(config.isNight(at: Self.makeDate(hour: 5, minute: 0), calendar: Self.utcCalendar))
+    }
+
+    @Test("Equal: 06:00:00 is night (wrap >= branch)") func equalAtBoundaryIsNight() {
+        let config = GlucoseAlertConfiguration(
+            dayStart: TimeOfDay(hour: 6, minute: 0),
+            nightStart: TimeOfDay(hour: 6, minute: 0)
+        )
+        #expect(config.isNight(at: Self.makeDate(hour: 6, minute: 0, second: 0), calendar: Self.utcCalendar))
+    }
+
+    @Test("Equal: 10:00 is night") func equalAfterIsNight() {
+        let config = GlucoseAlertConfiguration(
+            dayStart: TimeOfDay(hour: 6, minute: 0),
+            nightStart: TimeOfDay(hour: 6, minute: 0)
+        )
+        #expect(config.isNight(at: Self.makeDate(hour: 10, minute: 0), calendar: Self.utcCalendar))
+    }
+}