@@ -700,18 +700,20 @@ class LoopDataManagerTests: XCTestCase {
700700 LoopQuantity ( unit : . milligramsPerDeciliter , doubleValue : low ) ... LoopQuantity ( unit : . milligramsPerDeciliter , doubleValue : high )
701701 }
702702 let window = DateInterval ( start : d ( - . hours ( 3 ) ) , end : d ( - . minutes ( 30 ) ) )
703- settingsProvider . targetRangeHistory = [
704- AbsoluteScheduleValue ( startDate : window . start , endDate : d ( - . hours ( 2 ) ) , value : target ( 100 , 110 ) ) ,
705- AbsoluteScheduleValue ( startDate : d ( - . hours ( 2 ) ) , endDate : window . end , value : target ( 120 , 130 ) ) ,
706- ]
703+ let store = try await reportingSettingsStore ( [
704+ StoredSettings ( date : window . start , glucoseTargetRangeSchedule : GlucoseRangeSchedule (
705+ unit : . milligramsPerDeciliter , dailyItems : [ RepeatingScheduleValue ( startTime : 0 , value : DoubleRange ( minValue : 100 , maxValue : 110 ) ) ] , timeZone : . utcTimeZone ) ) ,
706+ StoredSettings ( date : d ( - . hours ( 2 ) ) , glucoseTargetRangeSchedule : GlucoseRangeSchedule (
707+ unit : . milligramsPerDeciliter , dailyItems : [ RepeatingScheduleValue ( startTime : 0 , value : DoubleRange ( minValue : 120 , maxValue : 130 ) ) ] , timeZone : . utcTimeZone ) ) ,
708+ ] )
707709 var preset = TemporaryScheduleOverride ( context : . custom ,
708710 settings : TemporaryPresetSettings ( targetRange : target ( 150 , 160 ) , insulinNeedsScaleFactor : 1 ) ,
709711 startDate : d ( - . minutes ( 150 ) ) , duration : . finite ( . hours ( 2 ) ) ,
710712 enactTrigger : . local , syncIdentifier : UUID ( ) )
711713 preset . actualEnd = . early ( d ( - . minutes ( 90 ) ) )
712714 temporaryPresetsManager . presetHistory . recordOverride ( preset , at : d ( - . minutes ( 150 ) ) )
713715
714- let bands = try await loopDataManager . glucoseTargetBands ( in : window )
716+ let bands = try await loopDataManager . glucoseTargetBands ( in : window , settingsStore : store )
715717 XCTAssertEqual ( bands , [
716718 TargetBand ( start : window . start , end : d ( - . minutes ( 150 ) ) , range : 100 ... 110 ) ,
717719 TargetBand ( start : d ( - . minutes ( 150 ) ) , end : d ( - . minutes ( 90 ) ) , range : 150 ... 160 ) ,
@@ -720,11 +722,64 @@ class LoopDataManagerTests: XCTestCase {
720722 }
721723
722724 func testTargetBandsAreUnknownBeforeTherapyIsConfigured ( ) async throws {
723- settingsProvider . settings . glucoseTargetRangeSchedule = nil
724- let bands = try await loopDataManager . glucoseTargetBands ( in : DateInterval ( start : d ( - . hours ( 3 ) ) , end : now ) )
725+ // Today's profile must not fill in a period whose saved settings had no profile.
726+ let store = try await reportingSettingsStore ( [ StoredSettings ( date : d ( - . hours ( 4 ) ) ) ] )
727+ let bands = try await loopDataManager . glucoseTargetBands ( in : DateInterval ( start : d ( - . hours ( 3 ) ) , end : now ) , settingsStore : store )
725728 XCTAssertTrue ( bands . isEmpty )
726729 }
727730
731+ func testReportingSchedulesLeaveUnrecordedHistoryUnknown ( ) async throws {
732+ let window = DateInterval ( start : d ( - . hours ( 4 ) ) , end : now )
733+ let first = window . start . addingTimeInterval ( . hours ( 1 ) )
734+ let missing = first . addingTimeInterval ( . hours ( 1 ) )
735+ let resumed = missing . addingTimeInterval ( . hours ( 1 ) )
736+ func profile ( at date : Date , rate : Double , target : Double ) -> StoredSettings {
737+ StoredSettings ( date : date , glucoseTargetRangeSchedule : GlucoseRangeSchedule (
738+ unit : . milligramsPerDeciliter , dailyItems : [ RepeatingScheduleValue ( startTime : 0 , value : DoubleRange ( minValue : target , maxValue : target + 10 ) ) ] , timeZone : . utcTimeZone ) ,
739+ basalRateSchedule : BasalRateSchedule ( dailyItems : [ RepeatingScheduleValue ( startTime : 0 , value : rate ) ] , timeZone : . utcTimeZone ) )
740+ }
741+ let store = try await reportingSettingsStore ( [
742+ profile ( at : first , rate : 1 , target : 100 ) ,
743+ StoredSettings ( date : missing ) ,
744+ profile ( at : resumed , rate : 2 , target : 120 ) ,
745+ ] )
746+
747+ let basal = try await LoopHistoryProvider . basalHistory ( in : window , from : store )
748+ let schedule = basal . map { LoopHistoryMath . ScheduleValue ( start : $0 . startDate , end : $0 . endDate , rate : $0 . value ) }
749+ let delivered = LoopHistoryMath . basalSegments ( window : window , schedule : schedule , deliveries : [ ] ,
750+ programmedRates : schedule , recordsStart : window . start )
751+ XCTAssertEqual ( delivered . reduce ( 0 ) { $0 + $1 . units } , 3 , accuracy : 1e-9 ,
752+ " Only the two recorded hours can supply inferred basal, not the earlier time or missing profile " )
753+ XCTAssertNil ( LoopHistoryMath . scheduledUnits ( basal . map { ScheduledBasalPoint ( start : $0 . startDate , end : $0 . endDate , rate : $0 . value ) } , over : window ) )
754+ let targets = try await loopDataManager . glucoseTargetBands ( in : window , settingsStore : store )
755+ XCTAssertEqual ( targets , [ TargetBand ( start : first , end : missing , range : 100 ... 110 ) ,
756+ TargetBand ( start : resumed , end : window . end , range : 120 ... 130 ) ] )
757+
758+ // A query starting during a known profile still includes the setting saved before its start.
759+ let inside = DateInterval ( start : first . addingTimeInterval ( . minutes ( 30 ) ) , end : missing )
760+ let partial = try await LoopHistoryProvider . basalHistory ( in : inside , from : store )
761+ XCTAssertEqual ( partial . first ? . startDate , inside . start )
762+ XCTAssertEqual ( partial . reduce ( 0 ) { $0 + $1 . value * $1 . endDate . timeIntervalSince ( $1 . startDate ) / 3600 } , 0.5 , accuracy : 1e-9 )
763+ let before = DateInterval ( start : window . start , end : first )
764+ let earlierBasal = try await LoopHistoryProvider . basalHistory ( in : before , from : store )
765+ let earlierTargets = try await loopDataManager . glucoseTargetBands ( in : before , settingsStore : store )
766+ XCTAssertTrue ( earlierBasal . isEmpty )
767+ XCTAssertTrue ( earlierTargets . isEmpty )
768+ }
769+
770+ private func reportingSettingsStore ( _ settings : [ StoredSettings ] ) async throws -> SettingsStore {
771+ let store = SettingsStore ( store : PersistenceController . mock ( ) , expireAfter : . days ( 3650 ) )
772+ for record in settings {
773+ try await withCheckedThrowingContinuation { ( continuation : CheckedContinuation < Void , Error > ) in
774+ store . storeSettings ( record ) { error in
775+ if let error { continuation . resume ( throwing : error ) }
776+ else { continuation . resume ( ) }
777+ }
778+ }
779+ }
780+ return store
781+ }
782+
728783 func testFetchDataWithHighInsulinNeedsPresetMitigation ( ) async throws {
729784 var input = try await loopDataManager . fetchData ( for : now )
730785 XCTAssertEqual ( input . targetMgdl . count , 1 )
0 commit comments