DetermineBasalEnableSmbTests.swift 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437
  1. import Foundation
  2. import Testing
  3. @testable import Trio
  4. @Suite("DosingEngine: shouldEnableSmb Tests") struct DetermineBasalEnableSmbTests {
  5. /// Helper to create a default set of inputs.
  6. /// Each test can then modify the specific properties relevant to its case.
  7. private func createDefaultInputs() -> (
  8. profile: Profile,
  9. meal: ComputedCarbs,
  10. currentGlucose: Decimal,
  11. adjustedTargetGlucose: Decimal,
  12. minGuardGlucose: Decimal,
  13. threshold: Decimal,
  14. glucoseStatus: GlucoseStatus,
  15. trioCustomOrefVariables: TrioCustomOrefVariables,
  16. clock: Date
  17. ) {
  18. var profile = Profile()
  19. // Ensure default is false so we can test enabling conditions.
  20. profile.enableSMBAlways = false
  21. profile.temptargetSet = false
  22. let meal = ComputedCarbs(
  23. carbs: 0,
  24. mealCOB: 0,
  25. currentDeviation: 0,
  26. maxDeviation: 0,
  27. minDeviation: 0,
  28. slopeFromMaxDeviation: 0,
  29. slopeFromMinDeviation: 0,
  30. allDeviations: [],
  31. lastCarbTime: Date().timeIntervalSince1970
  32. )
  33. let glucoseStatus = GlucoseStatus(
  34. delta: 0,
  35. glucose: 120,
  36. noise: 0,
  37. shortAvgDelta: 0,
  38. longAvgDelta: 0,
  39. date: Date(),
  40. lastCalIndex: nil,
  41. device: "test"
  42. )
  43. let trioCustomOrefVariables = TrioCustomOrefVariables(
  44. average_total_data: 0,
  45. weightedAverage: 0,
  46. currentTDD: 0,
  47. past2hoursAverage: 0,
  48. date: Date(),
  49. overridePercentage: 100,
  50. useOverride: false,
  51. duration: 0,
  52. unlimited: false,
  53. overrideTarget: 0,
  54. smbIsOff: false,
  55. advancedSettings: false,
  56. isfAndCr: false,
  57. isf: false,
  58. cr: false,
  59. smbIsScheduledOff: false,
  60. start: 0,
  61. end: 0,
  62. smbMinutes: 0,
  63. uamMinutes: 0,
  64. shouldProtectDueToHIGH: false
  65. )
  66. return (
  67. profile: profile,
  68. meal: meal,
  69. currentGlucose: 120,
  70. adjustedTargetGlucose: 100,
  71. minGuardGlucose: 110,
  72. threshold: 70,
  73. glucoseStatus: glucoseStatus,
  74. trioCustomOrefVariables: trioCustomOrefVariables,
  75. clock: Date()
  76. )
  77. }
  78. // MARK: - Disabling Conditions
  79. @Test("Should return false by default with no enabling preferences") func defaultIsFalse() throws {
  80. let inputs = createDefaultInputs()
  81. let decision = try DosingEngine.makeSMBDosingDecision(
  82. profile: inputs.profile, meal: inputs.meal, currentGlucose: inputs.currentGlucose,
  83. adjustedTargetGlucose: inputs.adjustedTargetGlucose,
  84. minGuardGlucose: inputs.minGuardGlucose,
  85. threshold: inputs.threshold, glucoseStatus: inputs.glucoseStatus,
  86. trioCustomOrefVariables: inputs.trioCustomOrefVariables, clock: inputs.clock
  87. )
  88. #expect(decision.isEnabled == false)
  89. }
  90. @Test("Should disable SMB when smbIsOff is true") func disableWhenSmbIsOff() throws {
  91. var inputs = createDefaultInputs()
  92. inputs.trioCustomOrefVariables.smbIsOff = true
  93. inputs.profile.enableSMBAlways = true // Ensure smbIsOff takes precedence
  94. let decision = try DosingEngine.makeSMBDosingDecision(
  95. profile: inputs.profile, meal: inputs.meal, currentGlucose: inputs.currentGlucose,
  96. adjustedTargetGlucose: inputs.adjustedTargetGlucose,
  97. minGuardGlucose: inputs.minGuardGlucose,
  98. threshold: inputs.threshold, glucoseStatus: inputs.glucoseStatus,
  99. trioCustomOrefVariables: inputs.trioCustomOrefVariables, clock: inputs.clock
  100. )
  101. #expect(decision.isEnabled == false)
  102. }
  103. @Test("Should disable SMB when shouldProtectDueToHIGH is true") func disableWhenProtectDueToHigh() throws {
  104. var inputs = createDefaultInputs()
  105. inputs.trioCustomOrefVariables.shouldProtectDueToHIGH = true
  106. inputs.profile.enableSMBAlways = true // Ensure protection takes precedence
  107. let decision = try DosingEngine.makeSMBDosingDecision(
  108. profile: inputs.profile, meal: inputs.meal, currentGlucose: inputs.currentGlucose,
  109. adjustedTargetGlucose: inputs.adjustedTargetGlucose,
  110. minGuardGlucose: inputs.minGuardGlucose,
  111. threshold: inputs.threshold, glucoseStatus: inputs.glucoseStatus,
  112. trioCustomOrefVariables: inputs.trioCustomOrefVariables, clock: inputs.clock
  113. )
  114. #expect(decision.isEnabled == false)
  115. }
  116. @Test("Should disable SMB with high temp target when not allowed") func disableWithHighTempTarget() throws {
  117. var inputs = createDefaultInputs()
  118. inputs.profile.allowSMBWithHighTemptarget = false
  119. inputs.profile.temptargetSet = true
  120. inputs.adjustedTargetGlucose = 120
  121. let decision = try DosingEngine.makeSMBDosingDecision(
  122. profile: inputs.profile, meal: inputs.meal, currentGlucose: inputs.currentGlucose,
  123. adjustedTargetGlucose: inputs.adjustedTargetGlucose,
  124. minGuardGlucose: inputs.minGuardGlucose,
  125. threshold: inputs.threshold, glucoseStatus: inputs.glucoseStatus,
  126. trioCustomOrefVariables: inputs.trioCustomOrefVariables, clock: inputs.clock
  127. )
  128. #expect(decision.isEnabled == false)
  129. }
  130. @Test("Should disable SMB when minGuardGlucose is below threshold") func disableWhenMinGuardBelowThreshold() throws {
  131. var inputs = createDefaultInputs()
  132. inputs.profile.enableSMBAlways = true // Enable SMB initially to test the safety override
  133. inputs.minGuardGlucose = 65
  134. inputs.threshold = 70
  135. let decision = try DosingEngine.makeSMBDosingDecision(
  136. profile: inputs.profile, meal: inputs.meal, currentGlucose: inputs.currentGlucose,
  137. adjustedTargetGlucose: inputs.adjustedTargetGlucose,
  138. minGuardGlucose: inputs.minGuardGlucose,
  139. threshold: inputs.threshold, glucoseStatus: inputs.glucoseStatus,
  140. trioCustomOrefVariables: inputs.trioCustomOrefVariables, clock: inputs.clock
  141. )
  142. #expect(decision.isEnabled == false)
  143. #expect(decision.minGuardGlucose == 65)
  144. }
  145. @Test("Should disable SMB when maxDelta is too high") func disableWhenMaxDeltaTooHigh() throws {
  146. var inputs = createDefaultInputs()
  147. inputs.profile.enableSMBAlways = true // Enable SMB initially
  148. inputs.profile.maxDeltaBgThreshold = 0.2
  149. inputs.currentGlucose = 100
  150. // Set maxDelta to be > 20% of currentGlucose
  151. inputs.glucoseStatus = GlucoseStatus(
  152. delta: 21,
  153. glucose: 100,
  154. noise: 0,
  155. shortAvgDelta: 5,
  156. longAvgDelta: 5,
  157. date: Date(),
  158. lastCalIndex: nil,
  159. device: "test"
  160. )
  161. let decision = try DosingEngine.makeSMBDosingDecision(
  162. profile: inputs.profile, meal: inputs.meal, currentGlucose: inputs.currentGlucose,
  163. adjustedTargetGlucose: inputs.adjustedTargetGlucose,
  164. minGuardGlucose: inputs.minGuardGlucose,
  165. threshold: inputs.threshold, glucoseStatus: inputs.glucoseStatus,
  166. trioCustomOrefVariables: inputs.trioCustomOrefVariables, clock: inputs.clock
  167. )
  168. #expect(decision.isEnabled == false)
  169. #expect(decision.reason != nil)
  170. }
  171. // MARK: - Enabling Conditions
  172. @Test("Should enable SMB when enableSMBAlways is true") func enableWhenAlwaysOn() throws {
  173. var inputs = createDefaultInputs()
  174. inputs.profile.enableSMBAlways = true
  175. let decision = try DosingEngine.makeSMBDosingDecision(
  176. profile: inputs.profile, meal: inputs.meal, currentGlucose: inputs.currentGlucose,
  177. adjustedTargetGlucose: inputs.adjustedTargetGlucose,
  178. minGuardGlucose: inputs.minGuardGlucose,
  179. threshold: inputs.threshold, glucoseStatus: inputs.glucoseStatus,
  180. trioCustomOrefVariables: inputs.trioCustomOrefVariables, clock: inputs.clock
  181. )
  182. #expect(decision.isEnabled == true)
  183. }
  184. @Test("Should enable SMB with COB") func enableWithCob() throws {
  185. var inputs = createDefaultInputs()
  186. inputs.profile.enableSMBWithCOB = true
  187. inputs.meal = ComputedCarbs(
  188. carbs: 20,
  189. mealCOB: 10,
  190. currentDeviation: 0,
  191. maxDeviation: 0,
  192. minDeviation: 0,
  193. slopeFromMaxDeviation: 0,
  194. slopeFromMinDeviation: 0,
  195. allDeviations: [],
  196. lastCarbTime: Date().timeIntervalSince1970
  197. )
  198. let decision = try DosingEngine.makeSMBDosingDecision(
  199. profile: inputs.profile, meal: inputs.meal, currentGlucose: inputs.currentGlucose,
  200. adjustedTargetGlucose: inputs.adjustedTargetGlucose,
  201. minGuardGlucose: inputs.minGuardGlucose,
  202. threshold: inputs.threshold, glucoseStatus: inputs.glucoseStatus,
  203. trioCustomOrefVariables: inputs.trioCustomOrefVariables, clock: inputs.clock
  204. )
  205. #expect(decision.isEnabled == true)
  206. }
  207. @Test("Should enable SMB after carbs") func enableAfterCarbs() throws {
  208. var inputs = createDefaultInputs()
  209. inputs.profile.enableSMBAfterCarbs = true
  210. inputs.meal = ComputedCarbs(
  211. carbs: 20,
  212. mealCOB: 0,
  213. currentDeviation: 0,
  214. maxDeviation: 0,
  215. minDeviation: 0,
  216. slopeFromMaxDeviation: 0,
  217. slopeFromMinDeviation: 0,
  218. allDeviations: [],
  219. lastCarbTime: Date().timeIntervalSince1970
  220. )
  221. let decision = try DosingEngine.makeSMBDosingDecision(
  222. profile: inputs.profile, meal: inputs.meal, currentGlucose: inputs.currentGlucose,
  223. adjustedTargetGlucose: inputs.adjustedTargetGlucose,
  224. minGuardGlucose: inputs.minGuardGlucose,
  225. threshold: inputs.threshold, glucoseStatus: inputs.glucoseStatus,
  226. trioCustomOrefVariables: inputs.trioCustomOrefVariables, clock: inputs.clock
  227. )
  228. #expect(decision.isEnabled == true)
  229. }
  230. @Test("Should enable SMB with low temp target") func enableWithLowTempTarget() throws {
  231. var inputs = createDefaultInputs()
  232. inputs.profile.enableSMBWithTemptarget = true
  233. inputs.profile.temptargetSet = true
  234. inputs.adjustedTargetGlucose = 90
  235. let decision = try DosingEngine.makeSMBDosingDecision(
  236. profile: inputs.profile, meal: inputs.meal, currentGlucose: inputs.currentGlucose,
  237. adjustedTargetGlucose: inputs.adjustedTargetGlucose,
  238. minGuardGlucose: inputs.minGuardGlucose,
  239. threshold: inputs.threshold, glucoseStatus: inputs.glucoseStatus,
  240. trioCustomOrefVariables: inputs.trioCustomOrefVariables, clock: inputs.clock
  241. )
  242. #expect(decision.isEnabled == true)
  243. }
  244. @Test("Should enable SMB for high BG") func enableWithHighBg() throws {
  245. var inputs = createDefaultInputs()
  246. inputs.profile.enableSMBHighBg = true
  247. inputs.profile.enableSMBHighBgTarget = 140
  248. inputs.currentGlucose = 145
  249. let decision = try DosingEngine.makeSMBDosingDecision(
  250. profile: inputs.profile, meal: inputs.meal, currentGlucose: inputs.currentGlucose,
  251. adjustedTargetGlucose: inputs.adjustedTargetGlucose,
  252. minGuardGlucose: inputs.minGuardGlucose,
  253. threshold: inputs.threshold, glucoseStatus: inputs.glucoseStatus,
  254. trioCustomOrefVariables: inputs.trioCustomOrefVariables, clock: inputs.clock
  255. )
  256. #expect(decision.isEnabled == true)
  257. }
  258. // MARK: - Scheduled Off Tests
  259. @Test("Scheduled Off (Normal): should disable SMB inside the window") func scheduledOffNormal_Inside() throws {
  260. var inputs = createDefaultInputs()
  261. inputs.profile.enableSMBAlways = true // Ensure schedule is the only reason for failure
  262. inputs.trioCustomOrefVariables.smbIsScheduledOff = true
  263. inputs.trioCustomOrefVariables.start = 9 // 9 AM
  264. inputs.trioCustomOrefVariables.end = 17 // 5 PM
  265. inputs.clock = Calendar.current.date(bySettingHour: 14, minute: 0, second: 0, of: Date())!
  266. let decision = try DosingEngine.makeSMBDosingDecision(
  267. profile: inputs.profile, meal: inputs.meal, currentGlucose: inputs.currentGlucose,
  268. adjustedTargetGlucose: inputs.adjustedTargetGlucose,
  269. minGuardGlucose: inputs.minGuardGlucose,
  270. threshold: inputs.threshold, glucoseStatus: inputs.glucoseStatus,
  271. trioCustomOrefVariables: inputs.trioCustomOrefVariables, clock: inputs.clock
  272. )
  273. #expect(decision.isEnabled == false)
  274. }
  275. @Test("Scheduled Off (Normal): should NOT disable SMB outside the window") func scheduledOffNormal_Outside() throws {
  276. var inputs = createDefaultInputs()
  277. inputs.profile.enableSMBAlways = true
  278. inputs.trioCustomOrefVariables.smbIsScheduledOff = true
  279. inputs.trioCustomOrefVariables.start = 9 // 9 AM
  280. inputs.trioCustomOrefVariables.end = 17 // 5 PM
  281. inputs.clock = Calendar.current.date(bySettingHour: 18, minute: 0, second: 0, of: Date())!
  282. let decision = try DosingEngine.makeSMBDosingDecision(
  283. profile: inputs.profile, meal: inputs.meal, currentGlucose: inputs.currentGlucose,
  284. adjustedTargetGlucose: inputs.adjustedTargetGlucose,
  285. minGuardGlucose: inputs.minGuardGlucose,
  286. threshold: inputs.threshold, glucoseStatus: inputs.glucoseStatus,
  287. trioCustomOrefVariables: inputs.trioCustomOrefVariables, clock: inputs.clock
  288. )
  289. #expect(decision.isEnabled == true)
  290. }
  291. @Test(
  292. "Scheduled Off (Wrapping): should disable SMB inside the window (after midnight)"
  293. ) func scheduledOffWrapping_InsideAfterMidnight() throws {
  294. var inputs = createDefaultInputs()
  295. inputs.profile.enableSMBAlways = true
  296. inputs.trioCustomOrefVariables.smbIsScheduledOff = true
  297. inputs.trioCustomOrefVariables.start = 22 // 10 PM
  298. inputs.trioCustomOrefVariables.end = 6 // 6 AM
  299. inputs.clock = Calendar.current.date(bySettingHour: 2, minute: 0, second: 0, of: Date())!
  300. let decision = try DosingEngine.makeSMBDosingDecision(
  301. profile: inputs.profile, meal: inputs.meal, currentGlucose: inputs.currentGlucose,
  302. adjustedTargetGlucose: inputs.adjustedTargetGlucose,
  303. minGuardGlucose: inputs.minGuardGlucose,
  304. threshold: inputs.threshold, glucoseStatus: inputs.glucoseStatus,
  305. trioCustomOrefVariables: inputs.trioCustomOrefVariables, clock: inputs.clock
  306. )
  307. #expect(decision.isEnabled == false)
  308. }
  309. @Test(
  310. "Scheduled Off (Wrapping): should disable SMB inside the window (before midnight)"
  311. ) func scheduledOffWrapping_InsideBeforeMidnight() throws {
  312. var inputs = createDefaultInputs()
  313. inputs.profile.enableSMBAlways = true
  314. inputs.trioCustomOrefVariables.smbIsScheduledOff = true
  315. inputs.trioCustomOrefVariables.start = 22 // 10 PM
  316. inputs.trioCustomOrefVariables.end = 6 // 6 AM
  317. inputs.clock = Calendar.current.date(bySettingHour: 23, minute: 0, second: 0, of: Date())!
  318. let decision = try DosingEngine.makeSMBDosingDecision(
  319. profile: inputs.profile, meal: inputs.meal, currentGlucose: inputs.currentGlucose,
  320. adjustedTargetGlucose: inputs.adjustedTargetGlucose,
  321. minGuardGlucose: inputs.minGuardGlucose,
  322. threshold: inputs.threshold, glucoseStatus: inputs.glucoseStatus,
  323. trioCustomOrefVariables: inputs.trioCustomOrefVariables, clock: inputs.clock
  324. )
  325. #expect(decision.isEnabled == false)
  326. }
  327. @Test("Scheduled Off (Wrapping): should NOT disable SMB outside the window") func scheduledOffWrapping_Outside() throws {
  328. var inputs = createDefaultInputs()
  329. inputs.profile.enableSMBAlways = true
  330. inputs.trioCustomOrefVariables.smbIsScheduledOff = true
  331. inputs.trioCustomOrefVariables.start = 22 // 10 PM
  332. inputs.trioCustomOrefVariables.end = 6 // 6 AM
  333. inputs.clock = Calendar.current.date(bySettingHour: 12, minute: 0, second: 0, of: Date())!
  334. let decision = try DosingEngine.makeSMBDosingDecision(
  335. profile: inputs.profile, meal: inputs.meal, currentGlucose: inputs.currentGlucose,
  336. adjustedTargetGlucose: inputs.adjustedTargetGlucose,
  337. minGuardGlucose: inputs.minGuardGlucose,
  338. threshold: inputs.threshold, glucoseStatus: inputs.glucoseStatus,
  339. trioCustomOrefVariables: inputs.trioCustomOrefVariables, clock: inputs.clock
  340. )
  341. #expect(decision.isEnabled == true)
  342. }
  343. @Test("Scheduled Off (All Day): should disable SMB") func scheduledOffAllDay() throws {
  344. var inputs = createDefaultInputs()
  345. inputs.profile.enableSMBAlways = true
  346. inputs.trioCustomOrefVariables.smbIsScheduledOff = true
  347. inputs.trioCustomOrefVariables.start = 0
  348. inputs.trioCustomOrefVariables.end = 0
  349. inputs.clock = Calendar.current.date(bySettingHour: 15, minute: 0, second: 0, of: Date())!
  350. let decision = try DosingEngine.makeSMBDosingDecision(
  351. profile: inputs.profile, meal: inputs.meal, currentGlucose: inputs.currentGlucose,
  352. adjustedTargetGlucose: inputs.adjustedTargetGlucose,
  353. minGuardGlucose: inputs.minGuardGlucose,
  354. threshold: inputs.threshold, glucoseStatus: inputs.glucoseStatus,
  355. trioCustomOrefVariables: inputs.trioCustomOrefVariables, clock: inputs.clock
  356. )
  357. #expect(decision.isEnabled == false)
  358. }
  359. @Test("Scheduled Off (Single Hour): should disable SMB inside the window") func scheduledOffSingleHour_Inside() throws {
  360. var inputs = createDefaultInputs()
  361. inputs.profile.enableSMBAlways = true
  362. inputs.trioCustomOrefVariables.smbIsScheduledOff = true
  363. inputs.trioCustomOrefVariables.start = 11 // 11 AM
  364. inputs.trioCustomOrefVariables.end = 11 // 11 AM
  365. inputs.clock = Calendar.current.date(bySettingHour: 11, minute: 30, second: 0, of: Date())!
  366. let decision = try DosingEngine.makeSMBDosingDecision(
  367. profile: inputs.profile, meal: inputs.meal, currentGlucose: inputs.currentGlucose,
  368. adjustedTargetGlucose: inputs.adjustedTargetGlucose,
  369. minGuardGlucose: inputs.minGuardGlucose,
  370. threshold: inputs.threshold, glucoseStatus: inputs.glucoseStatus,
  371. trioCustomOrefVariables: inputs.trioCustomOrefVariables, clock: inputs.clock
  372. )
  373. #expect(decision.isEnabled == false)
  374. }
  375. @Test("Scheduled Off (Single Hour): should NOT disable SMB outside the window") func scheduledOffSingleHour_Outside() throws {
  376. var inputs = createDefaultInputs()
  377. inputs.profile.enableSMBAlways = true
  378. inputs.trioCustomOrefVariables.smbIsScheduledOff = true
  379. inputs.trioCustomOrefVariables.start = 11 // 11 AM
  380. inputs.trioCustomOrefVariables.end = 11 // 11 AM
  381. inputs.clock = Calendar.current.date(bySettingHour: 12, minute: 0, second: 0, of: Date())!
  382. let decision = try DosingEngine.makeSMBDosingDecision(
  383. profile: inputs.profile, meal: inputs.meal, currentGlucose: inputs.currentGlucose,
  384. adjustedTargetGlucose: inputs.adjustedTargetGlucose,
  385. minGuardGlucose: inputs.minGuardGlucose,
  386. threshold: inputs.threshold, glucoseStatus: inputs.glucoseStatus,
  387. trioCustomOrefVariables: inputs.trioCustomOrefVariables, clock: inputs.clock
  388. )
  389. #expect(decision.isEnabled == true)
  390. }
  391. }