MinimedKitAlertEmissionTests.swift 40 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835
  1. import Foundation
  2. import LoopKit
  3. import Testing
  4. @testable import Trio
  5. /// Pins how MinimedKit's emitted LoopKit Alerts are routed through Trio's
  6. /// alert layer, as recorded by the synthesis audit over the managers / pump /
  7. /// MinimedKit sources (`MinimedKit/PumpManager/MinimedPumpManager.swift`).
  8. ///
  9. /// What this suite pins:
  10. /// - The CURRENT (not ideal) registry behavior for every alert MinimedKit
  11. /// issues. MinimedKit issues all four alerts with managerIdentifier
  12. /// "Minimed500" (its pluginIdentifier), but `AlertCatalogRegistry` keys its
  13. /// Minimed entries under "Minimed" (AlertCatalogRegistry.swift:88-93). Since
  14. /// `lookup` matches the full `Alert.Identifier` and there is no
  15. /// "Minimed500"-style fallback (only "Omni:pumpFault" has one), every
  16. /// lookup of an actually-emitted identifier returns nil. Trio then falls
  17. /// back to the alert's own plugin level, which is LoopKit's default
  18. /// `.timeSensitive`.
  19. /// - The documented escalation gap, as a ratchet that fails when the gap is
  20. /// fixed (forcing this file to be updated).
  21. ///
  22. /// One-line gap summary: PumpReservoirEmpty is taxonomy-Critical (N4 ->
  23. /// `.critical`) but its effective level is `.timeSensitive` because the
  24. /// intended registry entry ("Minimed","PumpReservoirEmpty"=.critical) is
  25. /// unreachable under the emitted managerIdentifier "Minimed500" — so an
  26. /// out-of-insulin condition never escalates to a critical interruption.
  27. @Suite("Trio Alert Emission: MinimedKit") struct MinimedKitAlertEmissionTests {
  28. private func id(_ manager: String, _ alertID: String) -> Alert.Identifier {
  29. Alert.Identifier(managerIdentifier: manager, alertIdentifier: alertID)
  30. }
  31. /// The managerIdentifier MinimedKit actually issues with (its
  32. /// pluginIdentifier), NOT the "Minimed" key the registry uses.
  33. private static let emittedManagerIdentifier = "Minimed500"
  34. /// Emitted alerts from the synthesis audit. `currentRegistryLevel` is the
  35. /// level `lookup(id("Minimed500", alertID))` returns TODAY (all nil due to
  36. /// the managerIdentifier mismatch). `taxonomyLevel` is what the row should
  37. /// be per taxonomy; `isGap` is true when the effective level
  38. /// (registry-or-default `.timeSensitive`) is less severe than taxonomy.
  39. struct Row {
  40. let alertIdentifier: String
  41. let currentRegistryLevel: Alert.InterruptionLevel?
  42. let taxonomyLevel: Alert.InterruptionLevel
  43. let isGap: Bool
  44. }
  45. private static let rows: [Row] = [
  46. // F2 -> .timeSensitive. Registry intends ("Minimed","lowRLBattery")=
  47. // .timeSensitive but it is unreachable; effective default
  48. // .timeSensitive == taxonomy, not a gap.
  49. // MinimedKit/PumpManager/MinimedPumpManager.swift:263-268
  50. Row(alertIdentifier: "lowRLBattery", currentRegistryLevel: nil, taxonomyLevel: .timeSensitive, isGap: false),
  51. // F2 -> .timeSensitive. ("Minimed","PumpBatteryLow")=.timeSensitive
  52. // unreachable; effective default == taxonomy, not a gap.
  53. // MinimedKit/PumpManager/MinimedPumpManager.swift:495-510
  54. Row(alertIdentifier: "PumpBatteryLow", currentRegistryLevel: nil, taxonomyLevel: .timeSensitive, isGap: false),
  55. // N4 -> .critical. GAP: ("Minimed","PumpReservoirEmpty")=.critical
  56. // unreachable under "Minimed500"; effective default .timeSensitive is
  57. // LESS severe than taxonomy .critical.
  58. // MinimedKit/PumpManager/MinimedPumpManager.swift:560-600
  59. Row(alertIdentifier: "PumpReservoirEmpty", currentRegistryLevel: nil, taxonomyLevel: .critical, isGap: true),
  60. // F1 -> .timeSensitive. ("Minimed","PumpReservoirLow")=.timeSensitive
  61. // unreachable; effective default == taxonomy, not a gap.
  62. // MinimedKit/PumpManager/MinimedPumpManager.swift:571-610
  63. Row(alertIdentifier: "PumpReservoirLow", currentRegistryLevel: nil, taxonomyLevel: .timeSensitive, isGap: false)
  64. ]
  65. // MARK: - Registry behavior (pinned to CURRENT, must be green)
  66. @Test(
  67. "registry behavior is pinned for every emitted alert",
  68. arguments: rows
  69. ) func registryBehaviorPinned(_ row: Row) {
  70. // Asserts CURRENT behavior: lookup under the EMITTED managerIdentifier
  71. // "Minimed500" returns the recorded level (nil today). This is not the
  72. // ideal — it documents the managerIdentifier mismatch.
  73. #expect(
  74. AlertCatalogRegistry.lookup(
  75. id(Self.emittedManagerIdentifier, row.alertIdentifier)
  76. )?.interruptionLevel == row.currentRegistryLevel
  77. )
  78. }
  79. // MARK: - Known escalation gaps (ratchet)
  80. /// AlertIdentifiers whose effective level is LESS severe than their
  81. /// taxonomy level today. Documented expectation per identifier:
  82. ///
  83. /// - "PumpReservoirEmpty": SHOULD be `.critical` (taxonomy N4). The
  84. /// registry author intended this — ("Minimed","PumpReservoirEmpty") is
  85. /// registered at .critical (AlertCatalogRegistry.swift:90) — but
  86. /// MinimedKit issues with managerIdentifier "Minimed500", so the entry
  87. /// is dead and the alert falls back to .timeSensitive. Out-of-insulin
  88. /// never escalates to a critical interruption.
  89. /// Source: MinimedKit/PumpManager/MinimedPumpManager.swift:560-600
  90. ///
  91. /// This stays green now and FAILS (prompting an update here) once the
  92. /// managerIdentifier mismatch is fixed and the gap closes.
  93. private static let knownEscalationGaps: Set<String> = [
  94. "PumpReservoirEmpty"
  95. ]
  96. @Test("known escalation gaps are exactly as documented") func knownEscalationGapsExact() {
  97. let computed = Set(Self.rows.filter(\.isGap).map(\.alertIdentifier))
  98. #expect(computed == Self.knownEscalationGaps)
  99. }
  100. }
  101. // MARK: - Message classification
  102. /// SPEC — This suite catalogs every user-reportable string MinimedKit can
  103. /// surface (PumpAlarmType titles, PumpErrorCode / PumpOpsError /
  104. /// PumpCommandError / MinimedPumpManagerError descriptions, SetBolusError,
  105. /// the four LoopKit `issueAlert` constructions, PumpStatusHighlight banners,
  106. /// settings-view alerts, the time-change action sheet, and history/glucose
  107. /// page parse errors) and pins how `TrioAlertClassifier.categorize(error:)`
  108. /// routes each one.
  109. ///
  110. /// IMPORTANT mismatch this pins: in production the classifier is fed
  111. /// `String(describing: error)` — i.e. the Swift *case name* of an enum error,
  112. /// not the localized display string a user sees. Its checks then match
  113. /// concatenated, lowercased tokens (`"reservoirempty"`, `"noresponse"`,
  114. /// `"comms"`, …) that only ever appear in case-name form. We instead feed each
  115. /// row's real DISPLAY string through a `StubError` whose `String(describing:)`
  116. /// IS that exact prose. Because natural-language prose has spaces and never
  117. /// contains those concatenated tokens, almost every real MinimedKit message
  118. /// falls through to `.other`. The only display strings that DO hit a bucket
  119. /// are ones whose substrings coincidentally coincide with a classifier token:
  120. /// "Comms with another pump detected" -> `.commsTransient` (contains "comms")
  121. /// and "Bolus may have failed: …" -> `.deliveryUncertain`.
  122. ///
  123. /// This is the audit over the managers / pump / MinimedKit sources
  124. /// (`MinimedKit/...`). Each row pins CURRENT behavior (must be green) and the
  125. /// gap ratchet below fails when the classifier is improved to map the
  126. /// taxonomy-intended messages.
  127. @Suite("Trio Alert Emission: MinimedKit — Classification") struct MinimedKitMessageClassificationTests {
  128. private struct StubError: Error, CustomStringConvertible { let description: String }
  129. struct Row {
  130. let identifier: String
  131. let message: String
  132. let role: String
  133. let taxonomy: String
  134. let expected: TrioAlertCategory
  135. }
  136. static let rows: [Row] = [
  137. // MinimedKit/PumpEvents/PumpAlarmPumpEvent.swift:53
  138. Row(
  139. identifier: "PumpAlarmType.autoOff",
  140. message: "Auto-Off Alarm",
  141. role: "errorMessage",
  142. taxonomy: "N1",
  143. expected: .other("Auto-Off Alarm")
  144. ),
  145. // MinimedKit/PumpEvents/PumpAlarmPumpEvent.swift:55
  146. Row(
  147. identifier: "PumpAlarmType.batteryOutLimitExceeded",
  148. message: "Battery Out Limit",
  149. role: "errorMessage",
  150. taxonomy: "N5",
  151. expected: .other("Battery Out Limit")
  152. ),
  153. // MinimedKit/PumpEvents/PumpAlarmPumpEvent.swift:57
  154. Row(
  155. identifier: "PumpAlarmType.noDelivery",
  156. message: "No Delivery Alarm",
  157. role: "errorMessage",
  158. taxonomy: "N1",
  159. expected: .other("No Delivery Alarm")
  160. ),
  161. // MinimedKit/PumpEvents/PumpAlarmPumpEvent.swift:59
  162. Row(
  163. identifier: "PumpAlarmType.batteryDepleted",
  164. message: "Battery Depleted",
  165. role: "errorMessage",
  166. taxonomy: "N5",
  167. expected: .other("Battery Depleted")
  168. ),
  169. // MinimedKit/PumpEvents/PumpAlarmPumpEvent.swift:61
  170. Row(
  171. identifier: "PumpAlarmType.deviceReset",
  172. message: "Device Reset",
  173. role: "errorMessage",
  174. taxonomy: "N1",
  175. expected: .other("Device Reset")
  176. ),
  177. // MinimedKit/PumpEvents/PumpAlarmPumpEvent.swift:63
  178. Row(
  179. identifier: "PumpAlarmType.deviceResetBatteryIssue17",
  180. message: "BatteryIssue17",
  181. role: "errorMessage",
  182. taxonomy: "N1",
  183. expected: .other("BatteryIssue17")
  184. ),
  185. // MinimedKit/PumpEvents/PumpAlarmPumpEvent.swift:65
  186. Row(
  187. identifier: "PumpAlarmType.deviceResetBatteryIssue21",
  188. message: "BatteryIssue21",
  189. role: "errorMessage",
  190. taxonomy: "N1",
  191. expected: .other("BatteryIssue21")
  192. ),
  193. // MinimedKit/PumpEvents/PumpAlarmPumpEvent.swift:67
  194. Row(
  195. identifier: "PumpAlarmType.reprogramError",
  196. message: "Reprogram Error",
  197. role: "errorMessage",
  198. taxonomy: "N1",
  199. expected: .other("Reprogram Error")
  200. ),
  201. // MinimedKit/PumpEvents/PumpAlarmPumpEvent.swift:69
  202. Row(
  203. identifier: "PumpAlarmType.emptyReservoir",
  204. message: "Empty Reservoir",
  205. role: "errorMessage",
  206. taxonomy: "N4",
  207. expected: .other("Empty Reservoir")
  208. ),
  209. // MinimedKit/PumpEvents/PumpAlarmPumpEvent.swift:71
  210. Row(
  211. identifier: "PumpAlarmType.unknownType",
  212. message: "Unknown Alarm",
  213. role: "errorMessage",
  214. taxonomy: "N1",
  215. expected: .other("Unknown Alarm")
  216. ),
  217. // MinimedKit/PumpManager/DoseStore.swift:133
  218. Row(
  219. identifier: "ClearAlarmPumpEvent",
  220. message: "Clear Alarm",
  221. role: "errorMessage",
  222. taxonomy: "N14",
  223. expected: .other("Clear Alarm")
  224. ),
  225. // MinimedKit/PumpManager/DoseStore.swift:144
  226. Row(
  227. identifier: "JournalEntryPumpLowBatteryPumpEvent",
  228. message: "Low Battery",
  229. role: "errorMessage",
  230. taxonomy: "F2",
  231. expected: .other("Low Battery")
  232. ),
  233. // MinimedKit/PumpManager/DoseStore.swift:147
  234. Row(
  235. identifier: "JournalEntryPumpLowReservoirPumpEvent",
  236. message: "Low Reservoir",
  237. role: "errorMessage",
  238. taxonomy: "F1",
  239. expected: .other("Low Reservoir")
  240. ),
  241. // MinimedKit/Messages/PumpErrorMessageBody.swift:21
  242. Row(
  243. identifier: "PumpErrorCode.commandRefused",
  244. message: "Command refused",
  245. role: "errorMessage",
  246. taxonomy: "N9",
  247. expected: .other("Command refused")
  248. ),
  249. // MinimedKit/Messages/PumpErrorMessageBody.swift:21
  250. Row(
  251. identifier: "PumpErrorCode.commandRefused",
  252. message: "Check that the pump is not suspended or priming, or has a percent temp basal type",
  253. role: "errorMessage",
  254. taxonomy: "N9",
  255. expected: .other("Check that the pump is not suspended or priming, or has a percent temp basal type")
  256. ),
  257. // MinimedKit/Messages/PumpErrorMessageBody.swift:23
  258. Row(
  259. identifier: "PumpErrorCode.maxSettingExceeded",
  260. message: "Max setting exceeded",
  261. role: "errorMessage",
  262. taxonomy: "N9",
  263. expected: .other("Max setting exceeded")
  264. ),
  265. // MinimedKit/Messages/PumpErrorMessageBody.swift:25
  266. Row(
  267. identifier: "PumpErrorCode.bolusInProgress",
  268. message: "Bolus in progress",
  269. role: "errorMessage",
  270. taxonomy: "N9",
  271. expected: .other("Bolus in progress")
  272. ),
  273. // MinimedKit/Messages/PumpErrorMessageBody.swift:27
  274. Row(
  275. identifier: "PumpErrorCode.pageDoesNotExist",
  276. message: "History page does not exist",
  277. role: "errorMessage",
  278. taxonomy: "N9",
  279. expected: .other("History page does not exist")
  280. ),
  281. // MinimedKit/PumpManager/MinimedPumpMessageSender.swift:90
  282. Row(
  283. identifier: "PumpOpsError.unknownPumpErrorCode",
  284. message: "Unknown pump error code: %1$@",
  285. role: "errorMessage",
  286. taxonomy: "N9",
  287. expected: .other("Unknown pump error code: %1$@")
  288. ),
  289. // MinimedKit/PumpManager/PumpOpsError.swift:41
  290. Row(
  291. identifier: "PumpOpsError.bolusInProgress",
  292. message: "A bolus is already in progress",
  293. role: "errorMessage",
  294. taxonomy: "N9",
  295. expected: .other("A bolus is already in progress")
  296. ),
  297. // MinimedKit/PumpManager/PumpOpsError.swift:43
  298. Row(
  299. identifier: "PumpOpsError.couldNotDecode",
  300. message: "Invalid response during %1$@: %2$@",
  301. role: "errorMessage",
  302. taxonomy: "N8",
  303. expected: .other("Invalid response during %1$@: %2$@")
  304. ),
  305. // MinimedKit/PumpManager/PumpOpsError.swift:45
  306. Row(
  307. identifier: "PumpOpsError.crosstalk",
  308. message: "Comms with another pump detected",
  309. role: "errorMessage",
  310. taxonomy: "N8",
  311. expected: .commsTransient
  312. ),
  313. // MinimedKit/PumpManager/PumpOpsError.swift:47
  314. Row(
  315. identifier: "PumpOpsError.noResponse",
  316. message: "Pump did not respond",
  317. role: "errorMessage",
  318. taxonomy: "N8",
  319. expected: .other("Pump did not respond")
  320. ),
  321. // MinimedKit/PumpManager/PumpOpsError.swift:49
  322. Row(
  323. identifier: "PumpOpsError.pumpSuspended",
  324. message: "Pump is suspended",
  325. role: "errorMessage",
  326. taxonomy: "N9",
  327. expected: .other("Pump is suspended")
  328. ),
  329. // MinimedKit/PumpManager/PumpOpsError.swift:53
  330. Row(
  331. identifier: "PumpOpsError.unexpectedResponse",
  332. message: "Unexpected response %1$@",
  333. role: "errorMessage",
  334. taxonomy: "N8",
  335. expected: .other("Unexpected response %1$@")
  336. ),
  337. // MinimedKit/PumpManager/PumpOpsError.swift:55
  338. Row(
  339. identifier: "PumpOpsError.unknownPumpErrorCode",
  340. message: "Unknown pump error code: %1$@",
  341. role: "errorMessage",
  342. taxonomy: "N9",
  343. expected: .other("Unknown pump error code: %1$@")
  344. ),
  345. // MinimedKit/PumpManager/PumpOpsError.swift:57
  346. Row(
  347. identifier: "PumpOpsError.unknownPumpModel",
  348. message: "Unknown pump model: %@",
  349. role: "errorMessage",
  350. taxonomy: "N9",
  351. expected: .other("Unknown pump model: %@")
  352. ),
  353. // MinimedKit/PumpManager/PumpOpsError.swift:59
  354. Row(
  355. identifier: "PumpOpsError.unknownResponse",
  356. message: "Unknown response during %1$@: %2$@",
  357. role: "errorMessage",
  358. taxonomy: "N8",
  359. expected: .other("Unknown response during %1$@: %2$@")
  360. ),
  361. // MinimedKit/PumpManager/PumpOpsSession.swift:939
  362. Row(
  363. identifier: "PumpOpsError.rfCommsFailure",
  364. message: "No pump responses during scan",
  365. role: "errorMessage",
  366. taxonomy: "N8",
  367. expected: .other("No pump responses during scan")
  368. ),
  369. // MinimedKit/PumpManager/PumpOpsSession.swift:554
  370. Row(
  371. identifier: "PumpOpsError.rfCommsFailure",
  372. message: "Confirmed that temp basal failed, and ",
  373. role: "errorMessage",
  374. taxonomy: "N9",
  375. expected: .other("Confirmed that temp basal failed, and ")
  376. ),
  377. // MinimedKit/PumpManager/PumpOpsSession.swift:1042
  378. Row(
  379. identifier: "PumpOpsError.rfCommsFailure",
  380. message: "Short history page: (n) bytes. Expected 1024",
  381. role: "errorMessage",
  382. taxonomy: "N8",
  383. expected: .other("Short history page: (n) bytes. Expected 1024")
  384. ),
  385. // MinimedKit/PumpManager/PumpOpsSession.swift:1149
  386. Row(
  387. identifier: "PumpOpsError.rfCommsFailure",
  388. message: "Short glucose history page: (n) bytes. Expected 1024",
  389. role: "errorMessage",
  390. taxonomy: "N8",
  391. expected: .other("Short glucose history page: (n) bytes. Expected 1024")
  392. ),
  393. // MinimedKit/PumpManager/MinimedPumpManagerError.swift:26
  394. Row(
  395. identifier: "MinimedPumpManagerError.noRileyLink",
  396. message: "No RileyLink Connected",
  397. role: "errorMessage",
  398. taxonomy: "N8",
  399. expected: .other("No RileyLink Connected")
  400. ),
  401. // MinimedKit/PumpManager/MinimedPumpManagerError.swift:26
  402. Row(
  403. identifier: "MinimedPumpManagerError.noRileyLink",
  404. message: "Make sure your RileyLink is nearby and powered on",
  405. role: "errorMessage",
  406. taxonomy: "N8",
  407. expected: .other("Make sure your RileyLink is nearby and powered on")
  408. ),
  409. // MinimedKit/PumpManager/MinimedPumpManagerError.swift:28
  410. Row(
  411. identifier: "MinimedPumpManagerError.bolusInProgress",
  412. message: "Bolus in Progress",
  413. role: "errorMessage",
  414. taxonomy: "N9",
  415. expected: .other("Bolus in Progress")
  416. ),
  417. // MinimedKit/PumpManager/MinimedPumpManagerError.swift:30
  418. Row(
  419. identifier: "MinimedPumpManagerError.pumpSuspended",
  420. message: "Pump is Suspended",
  421. role: "errorMessage",
  422. taxonomy: "N9",
  423. expected: .other("Pump is Suspended")
  424. ),
  425. // MinimedKit/PumpManager/MinimedPumpManagerError.swift:32
  426. Row(
  427. identifier: "MinimedPumpManagerError.insulinTypeNotConfigured",
  428. message: "Insulin Type is not configured",
  429. role: "errorMessage",
  430. taxonomy: "N13",
  431. expected: .other("Insulin Type is not configured")
  432. ),
  433. // MinimedKit/PumpManager/MinimedPumpManagerError.swift:32
  434. Row(
  435. identifier: "MinimedPumpManagerError.insulinTypeNotConfigured",
  436. message: "Go to pump settings and select insulin type",
  437. role: "errorMessage",
  438. taxonomy: "N13",
  439. expected: .other("Go to pump settings and select insulin type")
  440. ),
  441. // MinimedKit/PumpManager/MinimedPumpManagerError.swift:36
  442. Row(
  443. identifier: "MinimedPumpManagerError.tuneFailed",
  444. message: "RileyLink radio tune failed: (underlying error)",
  445. role: "errorMessage",
  446. taxonomy: "N8",
  447. expected: .other("RileyLink radio tune failed: (underlying error)")
  448. ),
  449. // MinimedKit/PumpManager/MinimedPumpManagerError.swift:40
  450. Row(
  451. identifier: "MinimedPumpManagerError.storageFailure",
  452. message: "Unable to store pump data",
  453. role: "errorMessage",
  454. taxonomy: "N9",
  455. expected: .other("Unable to store pump data")
  456. ),
  457. // MinimedKit/PumpManager/PumpOpsSession.swift:30
  458. Row(
  459. identifier: "SetBolusError.uncertain",
  460. message: "Bolus may have failed: %1$@",
  461. role: "errorMessage",
  462. taxonomy: "N3",
  463. expected: .deliveryUncertain
  464. ),
  465. // MinimedKit/PumpManager/PumpOpsSession.swift:30
  466. Row(
  467. identifier: "SetBolusError.uncertain",
  468. message: "Please check your pump bolus history to determine if the bolus was delivered.",
  469. role: "errorMessage",
  470. taxonomy: "N3",
  471. expected: .other("Please check your pump bolus history to determine if the bolus was delivered.")
  472. ),
  473. // MinimedKit/PumpManager/PumpOpsSession.swift:30
  474. Row(
  475. identifier: "SetBolusError.uncertain",
  476. message: "Loop sent a bolus command to the pump, but was unable to confirm…",
  477. role: "errorMessage",
  478. taxonomy: "N3",
  479. expected: .other("Loop sent a bolus command to the pump, but was unable to confirm…")
  480. ),
  481. // MinimedKit/Messages/Models/HistoryPage.swift:21
  482. Row(
  483. identifier: "HistoryPageError.invalidCRC",
  484. message: "History page failed crc check",
  485. role: "errorMessage",
  486. taxonomy: "N8",
  487. expected: .other("History page failed crc check")
  488. ),
  489. // MinimedKit/Messages/Models/HistoryPage.swift:23
  490. Row(
  491. identifier: "HistoryPageError.unknownEventType",
  492. message: "Unknown history record type: %$1@",
  493. role: "errorMessage",
  494. taxonomy: "N8",
  495. expected: .other("Unknown history record type: %$1@")
  496. ),
  497. // MinimedKit/Messages/Models/GlucosePage.swift:20
  498. Row(
  499. identifier: "GlucosePageError.invalidCRC",
  500. message: "Glucose page failed crc check",
  501. role: "errorMessage",
  502. taxonomy: "N11",
  503. expected: .other("Glucose page failed crc check")
  504. ),
  505. // MinimedKit/PumpManager/MinimedPumpManager.swift:263-268
  506. Row(
  507. identifier: "lowRLBattery",
  508. message: "Low RileyLink Battery",
  509. role: "alertTitle",
  510. taxonomy: "F2",
  511. expected: .other("Low RileyLink Battery")
  512. ),
  513. // MinimedKit/PumpManager/MinimedPumpManager.swift:263-268
  514. Row(
  515. identifier: "lowRLBattery",
  516. message: "\"%1$@\" has a low battery",
  517. role: "alertBody",
  518. taxonomy: "F2",
  519. expected: .other("\"%1$@\" has a low battery")
  520. ),
  521. // MinimedKit/PumpManager/MinimedPumpManager.swift:495-510
  522. Row(
  523. identifier: "PumpBatteryLow",
  524. message: "Pump Battery Low",
  525. role: "alertTitle",
  526. taxonomy: "F2",
  527. expected: .other("Pump Battery Low")
  528. ),
  529. // MinimedKit/PumpManager/MinimedPumpManager.swift:495-510
  530. Row(
  531. identifier: "PumpBatteryLow",
  532. message: "Change the pump battery immediately",
  533. role: "alertBody",
  534. taxonomy: "F2",
  535. expected: .other("Change the pump battery immediately")
  536. ),
  537. // MinimedKit/PumpManager/MinimedPumpManager.swift:560-600
  538. Row(
  539. identifier: "PumpReservoirEmpty",
  540. message: "Pump Reservoir Empty",
  541. role: "alertTitle",
  542. taxonomy: "N4",
  543. expected: .other("Pump Reservoir Empty")
  544. ),
  545. // MinimedKit/PumpManager/MinimedPumpManager.swift:560-600
  546. Row(
  547. identifier: "PumpReservoirEmpty",
  548. message: "Change the pump reservoir now",
  549. role: "alertBody",
  550. taxonomy: "N4",
  551. expected: .other("Change the pump reservoir now")
  552. ),
  553. // MinimedKit/PumpManager/MinimedPumpManager.swift:571-610
  554. Row(
  555. identifier: "PumpReservoirLow",
  556. message: "Pump Reservoir Low",
  557. role: "alertTitle",
  558. taxonomy: "F1",
  559. expected: .other("Pump Reservoir Low")
  560. ),
  561. // MinimedKit/PumpManager/MinimedPumpManager.swift:571-610
  562. Row(
  563. identifier: "PumpReservoirLow",
  564. message: "%1$@ U left: %2$@",
  565. role: "alertBody",
  566. taxonomy: "F1",
  567. expected: .other("%1$@ U left: %2$@")
  568. ),
  569. // MinimedKit/PumpManager/MinimedPumpManager.swift:571-610
  570. Row(
  571. identifier: "PumpReservoirLow",
  572. message: "%1$@ U left",
  573. role: "alertBody",
  574. taxonomy: "F1",
  575. expected: .other("%1$@ U left")
  576. ),
  577. // MinimedKit/PumpManager/MinimedPumpManager.swift:468
  578. Row(
  579. identifier: "PumpStatusHighlight.suspended",
  580. message: "Insulin Suspended",
  581. role: "notificationBody",
  582. taxonomy: "N2",
  583. expected: .other("Insulin Suspended")
  584. ),
  585. // MinimedKit/PumpManager/MinimedPumpManager.swift:475
  586. Row(
  587. identifier: "PumpStatusHighlight.signalLoss",
  588. message: "Signal Loss",
  589. role: "notificationBody",
  590. taxonomy: "N8",
  591. expected: .other("Signal Loss")
  592. ),
  593. // MinimedKitUI/Views/MinimedPumpSettingsViewModel.swift:148
  594. Row(
  595. identifier: "MinimedSettingsViewAlert.resumeError",
  596. message: "Error Resuming",
  597. role: "alertTitle",
  598. taxonomy: "N9",
  599. expected: .other("Error Resuming")
  600. ),
  601. // MinimedKitUI/Views/MinimedPumpSettingsViewModel.swift:157
  602. Row(
  603. identifier: "MinimedSettingsViewAlert.suspendError",
  604. message: "Error Suspending",
  605. role: "alertTitle",
  606. taxonomy: "N9",
  607. expected: .other("Error Suspending")
  608. ),
  609. // MinimedKitUI/Views/MinimedPumpSettingsViewModel.swift:250
  610. Row(
  611. identifier: "MinimedSettingsViewAlert.syncTimeError",
  612. message: "Error Syncing Time",
  613. role: "alertTitle",
  614. taxonomy: "N9",
  615. expected: .other("Error Syncing Time")
  616. ),
  617. // MinimedKitUI/Views/MinimedPumpSettingsView.swift:324-333
  618. Row(
  619. identifier: "MinimedPumpSettingsView.timeChangeActionSheet",
  620. message: "Time Change Detected",
  621. role: "alertTitle",
  622. taxonomy: "N14",
  623. expected: .other("Time Change Detected")
  624. ),
  625. // MinimedKitUI/Views/MinimedPumpSettingsView.swift:324-333
  626. Row(
  627. identifier: "MinimedPumpSettingsView.timeChangeActionSheet",
  628. message: "The time on your pump is different from the current time. Do you want to update the time on your pump to the current time?",
  629. role: "alertBody",
  630. taxonomy: "N14",
  631. expected: .other(
  632. "The time on your pump is different from the current time. Do you want to update the time on your pump to the current time?"
  633. )
  634. )
  635. ]
  636. // MARK: - Classification (pinned to CURRENT, must be green)
  637. @Test("each (identifier, message) classifies as pinned", arguments: rows) func eachMessageClassifiesAsPinned(_ row: Row) {
  638. #expect(TrioAlertClassifier.categorize(error: StubError(description: row.message)) == row.expected)
  639. }
  640. // MARK: - Classifier coverage gaps (ratchet)
  641. /// "identifier — message" keys for rows whose DISPLAY string falls through
  642. /// to `.other` today even though its taxonomy bucket is a real category the
  643. /// classifier *could* map. Each entry names the bucket it SHOULD hit and
  644. /// why the substring classifier misses it (the spaced prose never contains
  645. /// the concatenated token the classifier looks for):
  646. ///
  647. /// - "PumpAlarmType.noDelivery — No Delivery Alarm": SHOULD be `.occlusion`
  648. /// (N1). Classifier looks for "occlusion"/"occluded"; the title has
  649. /// neither. MinimedKit/PumpEvents/PumpAlarmPumpEvent.swift:57
  650. /// - "PumpAlarmType.autoOff — Auto-Off Alarm",
  651. /// "PumpAlarmType.deviceReset — Device Reset",
  652. /// "PumpAlarmType.deviceResetBatteryIssue17 — BatteryIssue17",
  653. /// "PumpAlarmType.deviceResetBatteryIssue21 — BatteryIssue21",
  654. /// "PumpAlarmType.reprogramError — Reprogram Error",
  655. /// "PumpAlarmType.unknownType — Unknown Alarm": SHOULD be
  656. /// `.hardwareFault` (N1). Classifier looks for "fault"/"patchfault";
  657. /// none of these titles contain it.
  658. /// MinimedKit/PumpEvents/PumpAlarmPumpEvent.swift:53-71
  659. /// - "PumpAlarmType.emptyReservoir — Empty Reservoir",
  660. /// "PumpReservoirEmpty — Pump Reservoir Empty",
  661. /// "PumpReservoirEmpty — Change the pump reservoir now": SHOULD be
  662. /// `.reservoirEmpty` (N4). Classifier looks for the concatenated tokens
  663. /// "reservoirempty"/"emptyreservoir"; the spaced prose ("empty
  664. /// reservoir", "reservoir empty") contains neither.
  665. /// PumpAlarmPumpEvent.swift:69; MinimedPumpManager.swift:560-600
  666. /// - "JournalEntryPumpLowReservoirPumpEvent — Low Reservoir",
  667. /// "PumpReservoirLow — Pump Reservoir Low",
  668. /// "PumpReservoirLow — %1$@ U left: %2$@",
  669. /// "PumpReservoirLow — %1$@ U left": SHOULD be `.reservoirLow` (F1).
  670. /// Classifier looks for "lowreservoir"; "low reservoir"/"reservoir low"
  671. /// (and the formatted bodies) never contain it.
  672. /// DoseStore.swift:147; MinimedPumpManager.swift:571-610
  673. /// - comms/transient rows that SHOULD be `.commsTransient` (N8) but miss
  674. /// because the classifier wants "communication"/"comms"/"notconnected"/
  675. /// "noresponse"/"timeout"/"rssi" and the prose never spells those tokens
  676. /// contiguously:
  677. /// "PumpOpsError.couldNotDecode — Invalid response during %1$@: %2$@",
  678. /// "PumpOpsError.noResponse — Pump did not respond" (spaced, not
  679. /// "noresponse"),
  680. /// "PumpOpsError.unexpectedResponse — Unexpected response %1$@",
  681. /// "PumpOpsError.unknownResponse — Unknown response during %1$@: %2$@",
  682. /// "PumpOpsError.rfCommsFailure — No pump responses during scan",
  683. /// "PumpOpsError.rfCommsFailure — Short history page: (n) bytes. Expected 1024",
  684. /// "PumpOpsError.rfCommsFailure — Short glucose history page: (n) bytes. Expected 1024",
  685. /// "MinimedPumpManagerError.noRileyLink — No RileyLink Connected"
  686. /// ("not connected" is two words, not "notconnected"),
  687. /// "MinimedPumpManagerError.noRileyLink — Make sure your RileyLink is nearby and powered on",
  688. /// "MinimedPumpManagerError.tuneFailed — RileyLink radio tune failed: (underlying error)",
  689. /// "HistoryPageError.invalidCRC — History page failed crc check",
  690. /// "HistoryPageError.unknownEventType — Unknown history record type: %$1@",
  691. /// "PumpStatusHighlight.signalLoss — Signal Loss".
  692. /// Sources: PumpOpsError.swift:43-59; PumpOpsSession.swift:554-1149;
  693. /// MinimedPumpManagerError.swift:26-36; HistoryPage.swift:21-23;
  694. /// MinimedPumpManager.swift:475
  695. /// - "SetBolusError.uncertain — Please check your pump bolus history to
  696. /// determine if the bolus was delivered.",
  697. /// "SetBolusError.uncertain — Loop sent a bolus command to the pump, but
  698. /// was unable to confirm…": SHOULD be `.deliveryUncertain` (N3). Only the
  699. /// sibling "Bolus may have failed: …" string matches (the classifier
  700. /// special-cases that exact spaced phrase); these accompanying bodies do
  701. /// not contain "bolus may have failed"/"uncertaindelivery"/
  702. /// "unacknowledged". MinimedKit/PumpManager/PumpOpsSession.swift:30
  703. ///
  704. /// Stays green now (pins the misses); FAILS — forcing this file to be
  705. /// updated — once the classifier is improved to map these messages.
  706. static let classifierCoverageGaps: Set<String> = [
  707. "PumpAlarmType.autoOff — Auto-Off Alarm",
  708. "PumpAlarmType.noDelivery — No Delivery Alarm",
  709. "PumpAlarmType.deviceReset — Device Reset",
  710. "PumpAlarmType.deviceResetBatteryIssue17 — BatteryIssue17",
  711. "PumpAlarmType.deviceResetBatteryIssue21 — BatteryIssue21",
  712. "PumpAlarmType.reprogramError — Reprogram Error",
  713. "PumpAlarmType.emptyReservoir — Empty Reservoir",
  714. "PumpAlarmType.unknownType — Unknown Alarm",
  715. "JournalEntryPumpLowReservoirPumpEvent — Low Reservoir",
  716. "PumpOpsError.couldNotDecode — Invalid response during %1$@: %2$@",
  717. "PumpOpsError.noResponse — Pump did not respond",
  718. "PumpOpsError.unexpectedResponse — Unexpected response %1$@",
  719. "PumpOpsError.unknownResponse — Unknown response during %1$@: %2$@",
  720. "PumpOpsError.rfCommsFailure — No pump responses during scan",
  721. "PumpOpsError.rfCommsFailure — Short history page: (n) bytes. Expected 1024",
  722. "PumpOpsError.rfCommsFailure — Short glucose history page: (n) bytes. Expected 1024",
  723. "MinimedPumpManagerError.noRileyLink — No RileyLink Connected",
  724. "MinimedPumpManagerError.noRileyLink — Make sure your RileyLink is nearby and powered on",
  725. "MinimedPumpManagerError.tuneFailed — RileyLink radio tune failed: (underlying error)",
  726. "SetBolusError.uncertain — Please check your pump bolus history to determine if the bolus was delivered.",
  727. "SetBolusError.uncertain — Loop sent a bolus command to the pump, but was unable to confirm…",
  728. "HistoryPageError.invalidCRC — History page failed crc check",
  729. "HistoryPageError.unknownEventType — Unknown history record type: %$1@",
  730. "PumpReservoirEmpty — Pump Reservoir Empty",
  731. "PumpReservoirEmpty — Change the pump reservoir now",
  732. "PumpReservoirLow — Pump Reservoir Low",
  733. "PumpReservoirLow — %1$@ U left: %2$@",
  734. "PumpReservoirLow — %1$@ U left",
  735. "PumpStatusHighlight.signalLoss — Signal Loss"
  736. ]
  737. /// Taxonomy bucket each row was intended to map to (from the audit). A row
  738. /// is a gap when it currently falls to `.other(message)` yet its taxonomy
  739. /// bucket is something other than "other".
  740. static let taxonomyBuckets: [String: String] = [
  741. "PumpAlarmType.autoOff — Auto-Off Alarm": "hardwareFault",
  742. "PumpAlarmType.batteryOutLimitExceeded — Battery Out Limit": "other",
  743. "PumpAlarmType.noDelivery — No Delivery Alarm": "occlusion",
  744. "PumpAlarmType.batteryDepleted — Battery Depleted": "other",
  745. "PumpAlarmType.deviceReset — Device Reset": "hardwareFault",
  746. "PumpAlarmType.deviceResetBatteryIssue17 — BatteryIssue17": "hardwareFault",
  747. "PumpAlarmType.deviceResetBatteryIssue21 — BatteryIssue21": "hardwareFault",
  748. "PumpAlarmType.reprogramError — Reprogram Error": "hardwareFault",
  749. "PumpAlarmType.emptyReservoir — Empty Reservoir": "reservoirEmpty",
  750. "PumpAlarmType.unknownType — Unknown Alarm": "hardwareFault",
  751. "ClearAlarmPumpEvent — Clear Alarm": "other",
  752. "JournalEntryPumpLowBatteryPumpEvent — Low Battery": "other",
  753. "JournalEntryPumpLowReservoirPumpEvent — Low Reservoir": "reservoirLow",
  754. "PumpErrorCode.commandRefused — Command refused": "other",
  755. "PumpErrorCode.commandRefused — Check that the pump is not suspended or priming, or has a percent temp basal type": "other",
  756. "PumpErrorCode.maxSettingExceeded — Max setting exceeded": "other",
  757. "PumpErrorCode.bolusInProgress — Bolus in progress": "other",
  758. "PumpErrorCode.pageDoesNotExist — History page does not exist": "other",
  759. "PumpOpsError.unknownPumpErrorCode — Unknown pump error code: %1$@": "other",
  760. "PumpOpsError.bolusInProgress — A bolus is already in progress": "other",
  761. "PumpOpsError.couldNotDecode — Invalid response during %1$@: %2$@": "commsTransient",
  762. "PumpOpsError.crosstalk — Comms with another pump detected": "commsTransient",
  763. "PumpOpsError.noResponse — Pump did not respond": "commsTransient",
  764. "PumpOpsError.pumpSuspended — Pump is suspended": "other",
  765. "PumpOpsError.unexpectedResponse — Unexpected response %1$@": "commsTransient",
  766. "PumpOpsError.unknownPumpModel — Unknown pump model: %@": "other",
  767. "PumpOpsError.unknownResponse — Unknown response during %1$@: %2$@": "commsTransient",
  768. "PumpOpsError.rfCommsFailure — No pump responses during scan": "commsTransient",
  769. "PumpOpsError.rfCommsFailure — Confirmed that temp basal failed, and ": "other",
  770. "PumpOpsError.rfCommsFailure — Short history page: (n) bytes. Expected 1024": "commsTransient",
  771. "PumpOpsError.rfCommsFailure — Short glucose history page: (n) bytes. Expected 1024": "commsTransient",
  772. "MinimedPumpManagerError.noRileyLink — No RileyLink Connected": "commsTransient",
  773. "MinimedPumpManagerError.noRileyLink — Make sure your RileyLink is nearby and powered on": "commsTransient",
  774. "MinimedPumpManagerError.bolusInProgress — Bolus in Progress": "other",
  775. "MinimedPumpManagerError.pumpSuspended — Pump is Suspended": "other",
  776. "MinimedPumpManagerError.insulinTypeNotConfigured — Insulin Type is not configured": "other",
  777. "MinimedPumpManagerError.insulinTypeNotConfigured — Go to pump settings and select insulin type": "other",
  778. "MinimedPumpManagerError.tuneFailed — RileyLink radio tune failed: (underlying error)": "commsTransient",
  779. "MinimedPumpManagerError.storageFailure — Unable to store pump data": "other",
  780. "SetBolusError.uncertain — Bolus may have failed: %1$@": "deliveryUncertain",
  781. "SetBolusError.uncertain — Please check your pump bolus history to determine if the bolus was delivered.": "deliveryUncertain",
  782. "SetBolusError.uncertain — Loop sent a bolus command to the pump, but was unable to confirm…": "deliveryUncertain",
  783. "HistoryPageError.invalidCRC — History page failed crc check": "commsTransient",
  784. "HistoryPageError.unknownEventType — Unknown history record type: %$1@": "commsTransient",
  785. "GlucosePageError.invalidCRC — Glucose page failed crc check": "other",
  786. "lowRLBattery — Low RileyLink Battery": "other",
  787. "lowRLBattery — \"%1$@\" has a low battery": "other",
  788. "PumpBatteryLow — Pump Battery Low": "other",
  789. "PumpBatteryLow — Change the pump battery immediately": "other",
  790. "PumpReservoirEmpty — Pump Reservoir Empty": "reservoirEmpty",
  791. "PumpReservoirEmpty — Change the pump reservoir now": "reservoirEmpty",
  792. "PumpReservoirLow — Pump Reservoir Low": "reservoirLow",
  793. "PumpReservoirLow — %1$@ U left: %2$@": "reservoirLow",
  794. "PumpReservoirLow — %1$@ U left": "reservoirLow",
  795. "PumpStatusHighlight.suspended — Insulin Suspended": "other",
  796. "PumpStatusHighlight.signalLoss — Signal Loss": "commsTransient",
  797. "MinimedSettingsViewAlert.resumeError — Error Resuming": "other",
  798. "MinimedSettingsViewAlert.suspendError — Error Suspending": "other",
  799. "MinimedSettingsViewAlert.syncTimeError — Error Syncing Time": "other",
  800. "MinimedPumpSettingsView.timeChangeActionSheet — Time Change Detected": "other",
  801. "MinimedPumpSettingsView.timeChangeActionSheet — The time on your pump is different from the current time. Do you want to update the time on your pump to the current time?": "other"
  802. ]
  803. @Test("classifier coverage gaps are exactly as documented") func classifierCoverageGapsExact() {
  804. let computed = Set(
  805. Self.rows
  806. .filter { row in
  807. let bucket = Self.taxonomyBuckets["\(row.identifier) — \(row.message)"] ?? "other"
  808. return row.expected == .other(row.message) && bucket != "other"
  809. }
  810. .map { "\($0.identifier) — \($0.message)" }
  811. )
  812. #expect(computed == Self.classifierCoverageGaps)
  813. }
  814. }