AlertCatalogRegistryEntriesTests.swift 7.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184
  1. import Foundation
  2. import LoopKit
  3. import Testing
  4. @testable import Trio
  5. /// Pins `AlertCatalogRegistry.entries` and the static, exact-match
  6. /// `lookup(_:)` behavior. Pump plugins emit at LoopKit's default
  7. /// (`.timeSensitive`); this table is the sole escalation source. An unknown
  8. /// `(manager, alertId)` returns nil (pass-through). The Omni hex-fault parser
  9. /// is covered separately in `AlertCatalogRegistryOmniFaultTests`.
  10. @Suite("TrioAlert: CatalogRegistry — static exact-match entries") struct AlertCatalogRegistryEntriesTests {
  11. private func id(_ manager: String, _ alertID: String) -> Alert.Identifier {
  12. Alert.Identifier(managerIdentifier: manager, alertIdentifier: alertID)
  13. }
  14. // MARK: - A: critical entries
  15. @Test(
  16. "Critical entries escalate to .critical",
  17. arguments: [
  18. ("Omni", "unexpectedAlert"),
  19. ("Minimed", "PumpReservoirEmpty"),
  20. ("Dana", "batteryZeroPercent"),
  21. ("Dana", "pumpError"),
  22. ("Dana", "occlusion"),
  23. ("Dana", "shutdown"),
  24. ("Dana", "emptyReservoir"),
  25. ("Dana", "checkShaft"),
  26. // Both spellings are registered: MedtrumKit ships the misspelled
  27. // "patch-occlussion" (double-s) today; "patch-occlusion" is
  28. // forward-compat for when the upstream typo is fixed. Both escalate.
  29. ("Medtrum", "com.nightscout.medtrumkit.patch-occlussion"), // double-s (current MedtrumKit)
  30. ("Medtrum", "com.nightscout.medtrumkit.patch-occlusion"), // single-s (future MedtrumKit)
  31. ("Medtrum", "com.nightscout.medtrumkit.patch-fault"),
  32. ("Medtrum", "com.nightscout.medtrumkit.patch-empty"),
  33. ("LibreLoopCGMManager", "sensorExpiry.warning2h"),
  34. ("LibreLoopCGMManager", "sensorExpiry.sessionEnded"),
  35. ("LibreLoopCGMManager", "sensorAttention"),
  36. ("trio.aps", "loop.notActive")
  37. ]
  38. ) func criticalEntries(manager: String, alertID: String) {
  39. #expect(AlertCatalogRegistry.lookup(id(manager, alertID))?.interruptionLevel == .critical)
  40. }
  41. // MARK: - B: representative .timeSensitive entries
  42. @Test(
  43. "Representative entries land at .timeSensitive",
  44. arguments: [
  45. ("Omni", "lowReservoir"),
  46. ("Minimed", "PumpBatteryLow"),
  47. ("Dana", "lowBattery"),
  48. ("Dana", "unknown"),
  49. ("Medtrum", "com.nightscout.medtrumkit.reservoir-low"),
  50. ("Medtrum", "com.nightscout.medtrumkit.patch-daily-limit"),
  51. ("LibreLoopCGMManager", "sensorExpiry.warning24h"),
  52. ("LibreLoopCGMManager", "reconnectNeedsReScan"),
  53. ("trio.aps", "glucoseDataStale")
  54. ]
  55. ) func timeSensitiveEntries(manager: String, alertID: String) {
  56. #expect(AlertCatalogRegistry.lookup(id(manager, alertID))?.interruptionLevel == .timeSensitive)
  57. }
  58. // MARK: - C: representative .active entries
  59. @Test(
  60. "Representative entries stay at .active",
  61. arguments: [
  62. ("Omni", "userPodExpiration"),
  63. ("Dana", "basalCompare"),
  64. ("Medtrum", "com.nightscout.medtrumkit.patch-expired"),
  65. ("trio.aps", "algorithmError")
  66. ]
  67. ) func activeEntries(manager: String, alertID: String) {
  68. #expect(AlertCatalogRegistry.lookup(id(manager, alertID))?.interruptionLevel == .active)
  69. }
  70. // MARK: - D: unknown identifiers pass through (nil)
  71. @Test(
  72. "Unknown identifiers return nil (pass-through)",
  73. arguments: [
  74. ("Dana", "totallyMadeUp"),
  75. ("NoSuchManager", "lowReservoir"),
  76. ("", "")
  77. ]
  78. ) func unknownReturnsNil(manager: String, alertID: String) {
  79. #expect(AlertCatalogRegistry.lookup(id(manager, alertID)) == nil)
  80. }
  81. // MARK: - E: wrong manager, right alert id → nil
  82. @Test(
  83. "Right alert id under the wrong manager returns nil",
  84. arguments: [
  85. ("Minimed", "unexpectedAlert"),
  86. ("Dana", "PumpReservoirEmpty")
  87. ]
  88. ) func wrongManagerRightID(manager: String, alertID: String) {
  89. #expect(AlertCatalogRegistry.lookup(id(manager, alertID)) == nil)
  90. }
  91. // MARK: - F: right manager, wrong alert id → nil
  92. // Dana's occlusion entry is the single-s "occlusion"; the double-s
  93. // "occlussion" is not a Dana key and must return nil. (The double-s
  94. // spelling IS a valid key under the Medtrum manager — see test A — so this
  95. // also confirms entries are scoped per-manager.)
  96. @Test(
  97. "Wrong alert id under the right manager returns nil",
  98. arguments: [
  99. ("Dana", "occlussion"), // double-s is not a Dana key — must NOT match
  100. ("Medtrum", "patch-fault"), // missing com.nightscout.medtrumkit. prefix
  101. ("Omni", "lowReservoir ") // trailing space
  102. ]
  103. ) func rightManagerWrongID(manager: String, alertID: String) {
  104. #expect(AlertCatalogRegistry.lookup(id(manager, alertID)) == nil)
  105. }
  106. // MARK: - G: manager-scoped lowRLBattery
  107. @Test("lowRLBattery resolves per-manager for Omni and Minimed") func lowRLBatteryIsManagerScoped() {
  108. let omni = AlertCatalogRegistry.lookup(id("Omni", "lowRLBattery"))
  109. let minimed = AlertCatalogRegistry.lookup(id("Minimed", "lowRLBattery"))
  110. #expect(omni != nil)
  111. #expect(minimed != nil)
  112. #expect(omni?.identifier.managerIdentifier == "Omni")
  113. #expect(minimed?.identifier.managerIdentifier == "Minimed")
  114. }
  115. // MARK: - H: invariant — no duplicate manager+alert keys
  116. @Test("entries have unique manager+alert identifiers") func entriesHaveUniqueKeys() {
  117. let keys = Set(AlertCatalogRegistry.entries.map(\.identifier))
  118. #expect(keys.count == AlertCatalogRegistry.entries.count)
  119. }
  120. // MARK: - I: LibreLoop CGM entries map to CGM-side concepts
  121. @Test(
  122. "LibreLoop sensor-expiry entries collapse to .cgmExpiringSoon / .cgmExpired",
  123. arguments: [
  124. ("sensorExpiry.warning24h", Alert.CatalogConcept.cgmExpiringSoon),
  125. ("sensorExpiry.warning2h", Alert.CatalogConcept.cgmExpiringSoon),
  126. ("sensorExpiry.sessionEnded", Alert.CatalogConcept.cgmExpired),
  127. ("sensorAttention", Alert.CatalogConcept.cgmReplacementNeeded),
  128. ("reconnectNeedsReScan", Alert.CatalogConcept.cgmReconnectNeeded)
  129. ]
  130. ) func libreLoopConcepts(alertID: String, expected: Alert.CatalogConcept) {
  131. #expect(AlertCatalogRegistry.lookup(id("LibreLoopCGMManager", alertID))?.concept == expected)
  132. }
  133. @Test("LibreLoop expiry + attention entries are categorized under Sensor") func libreLoopSensorCategory() {
  134. let sensorIDs = ["sensorExpiry.warning24h", "sensorExpiry.warning2h", "sensorExpiry.sessionEnded", "sensorAttention"]
  135. for alertID in sensorIDs {
  136. #expect(AlertCatalogRegistry.lookup(id("LibreLoopCGMManager", alertID))?.category == "Sensor")
  137. }
  138. }
  139. @Test("LibreLoop reconnect is categorized under Connectivity") func libreLoopConnectivityCategory() {
  140. #expect(AlertCatalogRegistry.lookup(id("LibreLoopCGMManager", "reconnectNeedsReScan"))?.category == "Connectivity")
  141. }
  142. // MARK: - J: Trio-internal Algorithm entries
  143. @Test(
  144. "Trio-internal entries map to Algorithm-side concepts",
  145. arguments: [
  146. ("loop.notActive", Alert.CatalogConcept.notLooping),
  147. ("algorithmError", Alert.CatalogConcept.algorithmError),
  148. ("glucoseDataStale", Alert.CatalogConcept.glucoseDataStale)
  149. ]
  150. ) func trioAlgorithmConcepts(alertID: String, expected: Alert.CatalogConcept) {
  151. #expect(AlertCatalogRegistry.lookup(id("trio.aps", alertID))?.concept == expected)
  152. }
  153. @Test("Trio-internal entries are categorized under Algorithm") func trioAlgorithmCategory() {
  154. for alertID in ["loop.notActive", "algorithmError", "glucoseDataStale"] {
  155. #expect(AlertCatalogRegistry.lookup(id("trio.aps", alertID))?.category == "Algorithm")
  156. }
  157. }
  158. }