Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 9 additions & 2 deletions Loop/Managers/LoopDataManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -389,8 +389,15 @@ final class LoopDataManager: ObservableObject {
recommendationEffectInterval: recommendationEffectInterval
)

// Sensitivity and override timelines must also cover the carb history range so that
// any carb entry within that window (including a synthetic `potentialCarbEntry` later
// appended for a manual bolus recommendation) maps to an ISF and any active override.
// Otherwise `closestPrior(to: entry.startDate)` returns nil downstream and the algorithm
// traps in `CarbStatusBuilder` construction.
let scheduleQueryStart = min(neededSensitivityTimeline.start, carbsStart)

let sensitivity = try await settingsProvider.getInsulinSensitivityHistory(
startDate: neededSensitivityTimeline.start,
startDate: scheduleQueryStart,
endDate: neededSensitivityTimeline.end
)

Expand All @@ -404,7 +411,7 @@ final class LoopDataManager: ObservableObject {
throw LoopError.configurationError(.maximumBasalRatePerHour)
}

var overrides = temporaryPresetsManager.presetHistory.getOverrideHistory(startDate: neededSensitivityTimeline.start, endDate: forecastEndTime)
var overrides = temporaryPresetsManager.presetHistory.getOverrideHistory(startDate: scheduleQueryStart, endDate: forecastEndTime)

// For recommendation, we should consider preMeal override to be ending at time of dose
if presumePresetEndingNow,
Expand Down
33 changes: 33 additions & 0 deletions LoopTests/Managers/LoopDataManagerTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -459,6 +459,39 @@ class LoopDataManagerTests: XCTestCase {

}

func testFetchDataSensitivityCoversCarbHistoryStart() async throws {
// A bolus recommendation may append a `potentialCarbEntry` with a startDate up to
// ~12h in the past. The sensitivity and override timelines must extend at least that
// far back; otherwise `closestPrior` on those timelines returns nil for the appended
// entry and `CarbStatusBuilder` construction traps. See LOOP-5693.
let input = try await loopDataManager.fetchData(for: now)
let carbsStart = now.addingTimeInterval(CarbMath.dateAdjustmentPast + .minutes(-1))

XCTAssertFalse(input.sensitivity.isEmpty)
XCTAssertLessThanOrEqual(input.sensitivity.first!.startDate, carbsStart)
}

func testRecommendManualBolusWithPastPotentialCarbEntryDoesNotCrash() async throws {
// Regression for LOOP-5693: a potential carb entry with a startDate older than the
// dose/glucose-derived sensitivity timeline used to trap inside LoopAlgorithm because
// the sensitivity schedule didn't cover the carb entry's startDate.
glucoseStore.storedGlucose = [
StoredGlucoseSample(startDate: d(.minutes(-13)), quantity: .glucose(value: 130)),
StoredGlucoseSample(startDate: d(.minutes(-8)), quantity: .glucose(value: 140)),
StoredGlucoseSample(startDate: d(.minutes(-3)), quantity: .glucose(value: 150)),
]

let pastEntry = NewCarbEntry(
quantity: .carbs(value: 30),
startDate: d(.hours(-11)),
foodType: nil,
absorptionTime: .hours(3)
)

let recommendation = try await loopDataManager.recommendManualBolus(potentialCarbEntry: pastEntry)
XCTAssertNotNil(recommendation)
}

func testFetchDataWithHighInsulinNeedsPresetMitigation() async throws {
var input = try await loopDataManager.fetchData(for: now)
XCTAssertEqual(input.target.count, 1)
Expand Down