DanaKitAlertEmissionTests.swift 58 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171
  1. import Foundation
  2. import LoopKit
  3. import Testing
  4. @testable import Trio
  5. /// Pins how DanaKit's emitted LoopKit Alerts are routed through Trio's alert
  6. /// layer, as recorded by the synthesis audit over the managers / pump /
  7. /// DanaKit sources (`DanaKit/Packets/DanaNotifyAlarm.swift`,
  8. /// `DanaKit/PumpManager/PumpManagerAlert.swift`,
  9. /// `DanaKit/PumpManager/DanaKitPumpManager.swift`).
  10. ///
  11. /// What this suite pins:
  12. /// - The CURRENT (not ideal) registry behavior for every alert DanaKit
  13. /// issues. Unlike MinimedKit, DanaKit issues all 15 `PumpManagerAlert`
  14. /// cases via `delegate.issueAlert` with managerIdentifier "Dana", which
  15. /// MATCHES the registry key (AlertCatalogRegistry.swift:99-115). Every
  16. /// emission therefore resolves to a registry `CatalogEntry` and is
  17. /// overridden to that entry's interruptionLevel; the plugin itself sets no
  18. /// interruptionLevel, so without a registry hit they would fall back to
  19. /// LoopKit's default `.timeSensitive`.
  20. /// - The documented escalation gaps, as a ratchet that fails when a gap is
  21. /// fixed (forcing this file to be updated).
  22. ///
  23. /// No classifier rows: every `DanaKitPumpManagerError` reaching
  24. /// `APSManager.processError` is wrapped in LoopKit's `PumpManagerError` before
  25. /// leaving the completion handler, so the exact `String(describing:)` input is
  26. /// the wrapping, not the Dana case name. That input cannot be stated from the
  27. /// synthesis audit alone, so classifier rows are omitted rather than guessed.
  28. ///
  29. /// One-line gap summary: three taxonomy-Critical emissions are
  30. /// under-escalated by the registry — `basalMax` and `dailyMax` (N2 Delivery
  31. /// Suspended/Stopped -> `.critical`) are registered `.active`, and `unknown`
  32. /// (N1 Hardware Fault catch-all -> `.critical`) is registered `.timeSensitive`
  33. /// — so a daily/basal hard stop or an unrecognized pump alarm reaches the user
  34. /// below critical.
  35. @Suite("Trio Alert Emission: DanaKit") struct DanaKitAlertEmissionTests {
  36. private func id(_ manager: String, _ alertID: String) -> Alert.Identifier {
  37. Alert.Identifier(managerIdentifier: manager, alertIdentifier: alertID)
  38. }
  39. /// The managerIdentifier DanaKit actually issues with, which matches the
  40. /// registry's "Dana" key (so lookups resolve, unlike MinimedKit).
  41. private static let emittedManagerIdentifier = "Dana"
  42. /// Emitted alerts from the synthesis audit. `currentRegistryLevel` is the
  43. /// level `lookup(id("Dana", alertID))` returns TODAY. `taxonomyLevel` is
  44. /// what the row should be per taxonomy; `isGap` is true when the effective
  45. /// level (the registry level here, since all "Dana" lookups resolve) is
  46. /// less severe than taxonomy.
  47. struct Row {
  48. let alertIdentifier: String
  49. let currentRegistryLevel: Alert.InterruptionLevel?
  50. let taxonomyLevel: Alert.InterruptionLevel
  51. let isGap: Bool
  52. }
  53. private static let rows: [Row] = [
  54. // N5-pump -> .critical. Registry .critical matches taxonomy.
  55. // DanaKit/Packets/DanaNotifyAlarm.swift:11
  56. Row(alertIdentifier: "batteryZeroPercent", currentRegistryLevel: .critical, taxonomyLevel: .critical, isGap: false),
  57. // N1 Hardware Fault -> .critical. Registry .critical matches.
  58. // DanaKit/Packets/DanaNotifyAlarm.swift:12
  59. Row(alertIdentifier: "pumpError", currentRegistryLevel: .critical, taxonomyLevel: .critical, isGap: false),
  60. // N1 -> .critical. Registry .critical matches.
  61. // DanaKit/Packets/DanaNotifyAlarm.swift:13
  62. Row(alertIdentifier: "occlusion", currentRegistryLevel: .critical, taxonomyLevel: .critical, isGap: false),
  63. // F2 Battery Low (High) -> .timeSensitive. Registry .timeSensitive matches.
  64. // DanaKit/Packets/DanaNotifyAlarm.swift:14
  65. Row(alertIdentifier: "lowBattery", currentRegistryLevel: .timeSensitive, taxonomyLevel: .timeSensitive, isGap: false),
  66. // N2 Delivery Suspended -> .critical. Registry .critical matches.
  67. // DanaKit/Packets/DanaNotifyAlarm.swift:15
  68. Row(alertIdentifier: "shutdown", currentRegistryLevel: .critical, taxonomyLevel: .critical, isGap: false),
  69. // N14 Informational/Status -> .active. Registry .active matches.
  70. // DanaKit/Packets/DanaNotifyAlarm.swift:16
  71. Row(alertIdentifier: "basalCompare", currentRegistryLevel: .active, taxonomyLevel: .active, isGap: false),
  72. // N14 -> .active. Registry .active matches. Codes 0x07/0xFF both map here.
  73. // DanaKit/Packets/DanaNotifyAlarm.swift:17
  74. Row(alertIdentifier: "bloodSugarMeasure", currentRegistryLevel: .active, taxonomyLevel: .active, isGap: false),
  75. // F1 Insulin Supply Low (High) -> .timeSensitive. Registry .timeSensitive
  76. // matches. Codes 0x08/0xFE both map here.
  77. // DanaKit/Packets/DanaNotifyAlarm.swift:19
  78. Row(
  79. alertIdentifier: "remainingInsulinLevel",
  80. currentRegistryLevel: .timeSensitive,
  81. taxonomyLevel: .timeSensitive,
  82. isGap: false
  83. ),
  84. // N4 Reservoir Empty -> .critical. Registry .critical matches.
  85. // DanaKit/Packets/DanaNotifyAlarm.swift:21
  86. Row(alertIdentifier: "emptyReservoir", currentRegistryLevel: .critical, taxonomyLevel: .critical, isGap: false),
  87. // N1 Hardware Fault -> .critical. Registry .critical matches.
  88. // DanaKit/Packets/DanaNotifyAlarm.swift:22
  89. Row(alertIdentifier: "checkShaft", currentRegistryLevel: .critical, taxonomyLevel: .critical, isGap: false),
  90. // N2 Delivery Suspended/Stopped -> .critical. GAP: registry registers
  91. // .active, which is LESS severe than taxonomy .critical.
  92. // DanaKit/Packets/DanaNotifyAlarm.swift:23
  93. Row(alertIdentifier: "basalMax", currentRegistryLevel: .active, taxonomyLevel: .critical, isGap: true),
  94. // N2 -> .critical. GAP: registry registers .active, LESS severe than
  95. // taxonomy .critical.
  96. // DanaKit/Packets/DanaNotifyAlarm.swift:24
  97. Row(alertIdentifier: "dailyMax", currentRegistryLevel: .active, taxonomyLevel: .critical, isGap: true),
  98. // N14 Informational/Status -> .active. Registry .active matches.
  99. // DanaKit/Packets/DanaNotifyAlarm.swift:25
  100. Row(alertIdentifier: "bloodSugarCheckMiss", currentRegistryLevel: .active, taxonomyLevel: .active, isGap: false),
  101. // N1 Hardware Fault catch-all for unmapped alarm codes -> .critical.
  102. // GAP: registry registers .timeSensitive, LESS severe than taxonomy
  103. // .critical, so an unrecognized pump alarm reaches the user below
  104. // critical.
  105. // DanaKit/Packets/DanaNotifyAlarm.swift:29
  106. Row(alertIdentifier: "unknown", currentRegistryLevel: .timeSensitive, taxonomyLevel: .critical, isGap: true),
  107. // N10 Authentication/Security -> .timeSensitive. Registry .timeSensitive
  108. // matches. DEAD CASE: defined with full copy but never constructed/fired
  109. // via issueAlert (the equivalent text is an ad hoc SwiftUI string at
  110. // DanaKitScanViewModel.swift:77, which Trio never receives). Listed for
  111. // completeness; no gap if it were ever fired.
  112. // DanaKit/PumpManager/PumpManagerAlert.swift:18
  113. Row(alertIdentifier: "ble5InvalidKeys", currentRegistryLevel: .timeSensitive, taxonomyLevel: .timeSensitive, isGap: false)
  114. ]
  115. // MARK: - Registry behavior (pinned to CURRENT, must be green)
  116. @Test(
  117. "registry behavior is pinned for every emitted alert",
  118. arguments: rows
  119. ) func registryBehaviorPinned(_ row: Row) {
  120. // Asserts CURRENT behavior: lookup under the EMITTED managerIdentifier
  121. // "Dana" returns the recorded level. All Dana lookups resolve (the
  122. // managerIdentifier matches the registry key); this documents the
  123. // level the registry overrides each emission to today, not the ideal.
  124. #expect(
  125. AlertCatalogRegistry.lookup(
  126. id(Self.emittedManagerIdentifier, row.alertIdentifier)
  127. )?.interruptionLevel == row.currentRegistryLevel
  128. )
  129. }
  130. // MARK: - Known escalation gaps (ratchet)
  131. /// AlertIdentifiers whose effective level is LESS severe than their
  132. /// taxonomy level today. Documented expectation per identifier:
  133. ///
  134. /// - "basalMax": SHOULD be `.critical` (taxonomy N2 Delivery
  135. /// Suspended/Stopped). Registry registers it `.active`
  136. /// (AlertCatalogRegistry.swift:110). A basal hard stop reaches the user
  137. /// below critical.
  138. /// Source: DanaKit/Packets/DanaNotifyAlarm.swift:23
  139. ///
  140. /// - "dailyMax": SHOULD be `.critical` (taxonomy N2). Registry registers
  141. /// it `.active` (AlertCatalogRegistry.swift:111). A daily-insulin hard
  142. /// stop reaches the user below critical.
  143. /// Source: DanaKit/Packets/DanaNotifyAlarm.swift:24
  144. ///
  145. /// - "unknown": SHOULD be `.critical` (taxonomy N1 Hardware Fault). This
  146. /// is the catch-all for unmapped pump alarm codes; registry registers it
  147. /// `.timeSensitive` (AlertCatalogRegistry.swift:114), so an unrecognized
  148. /// pump alarm reaches the user below critical.
  149. /// Source: DanaKit/Packets/DanaNotifyAlarm.swift:29
  150. ///
  151. /// (basalMax/dailyMax carry warning-style copy — "contact your distributer
  152. /// to increase the limit" — so the N2 classification itself may warrant
  153. /// review; as mapped per classified.md they are gaps.)
  154. ///
  155. /// This stays green now and FAILS (prompting an update here) once a gap is
  156. /// closed in the registry.
  157. private static let knownEscalationGaps: Set<String> = [
  158. "basalMax",
  159. "dailyMax",
  160. "unknown"
  161. ]
  162. @Test("known escalation gaps are exactly as documented") func knownEscalationGapsExact() {
  163. let computed = Set(Self.rows.filter(\.isGap).map(\.alertIdentifier))
  164. #expect(computed == Self.knownEscalationGaps)
  165. }
  166. }
  167. // MARK: - Message Classification
  168. /// SPEC — How DanaKit's reportable user-facing messages are handled by
  169. /// `TrioAlertClassifier.categorize(error:)`.
  170. ///
  171. /// IMPORTANT — what production actually feeds the classifier vs. what this
  172. /// suite feeds: in prod, `categorize(error:)` falls through to
  173. /// `categorize(pumpError:)`, which keys off `String(describing: error)` — i.e.
  174. /// the Swift *case name* of the error value (e.g. "noConnection",
  175. /// "failedTempBasalAdjustment"), NOT the human-readable display copy. This
  176. /// suite instead catalogs every reportable DanaKit *message* (alert titles /
  177. /// bodies, error descriptions, notification copy, onboarding/validation
  178. /// strings) with its emission identifier, and pins how the ordered substring
  179. /// classifier would route that exact natural-language text.
  180. ///
  181. /// Why almost everything lands in `.other`: `categorize(pumpError:)`
  182. /// lowercases the string and runs ordered `contains` checks for concatenated
  183. /// tokens ("uncertaindelivery", "occlusion", "reservoirempty", "fault",
  184. /// "timeout", "communication", "bolusfailed", ...). Natural prose rarely
  185. /// contains those exact (space-free) tokens, so it falls through to
  186. /// `.other(originalString)`. The only natural matches here are:
  187. /// - the occlusion alert TITLE ("Occlusion" contains "occlusion") ->
  188. /// `.occlusion`; its BODY ("Check the reservoir and infus...") does NOT,
  189. /// so the body is a gap.
  190. /// - connectivity strings that literally contain the word "timeout"
  191. /// ("Connection timeout is hit...", "Timeout has been hit...",
  192. /// "A bolus timeout is active...") -> `.commsTransient`.
  193. /// Notably the reservoir-empty copy ("Reservoir is empty. Replace it now!" /
  194. /// "Empty reservoir") does NOT contain "reservoirempty"/"emptyreservoir" (the
  195. /// space breaks the token), so it falls to `.other` despite an N4 taxonomy.
  196. ///
  197. /// Each `Row` pairs an emission identifier with its exact display string and
  198. /// pins the category `categorize(error:)` returns for that string TODAY. The
  199. /// pinned `expected` reflects the classifier's ACTUAL output on the verbatim
  200. /// message (green by construction); see the per-row sourceRef comments, which
  201. /// cite the managers / pump / DanaKit sources
  202. /// (`DanaKit/Packets/DanaNotifyAlarm.swift`,
  203. /// `DanaKit/PumpManager/*.swift`, `DanaKit/Packets/DanaBolusStart.swift`,
  204. /// `DanaKitUI/*`, `Common/NotificationHelper.swift`).
  205. @Suite("Trio Alert Emission: DanaKit — Classification") struct DanaKitMessageClassificationTests {
  206. /// An error whose `String(describing:)` is exactly the catalogued display
  207. /// string, so the classifier matches over the real natural-language text.
  208. private struct StubError: Error, CustomStringConvertible { let description: String }
  209. /// One reportable DanaKit emission: its emission `identifier`, the exact
  210. /// `message` text, its `role` (alertTitle/alertBody/errorMessage/
  211. /// notificationTitle/notificationBody/validation), the `taxonomy` category,
  212. /// the `taxonomyBucket` it belongs to, and the `expected` category
  213. /// `categorize(error:)` returns for that message TODAY. `.other(message)`
  214. /// carries the EXACT original string.
  215. struct Row {
  216. let identifier: String
  217. let message: String
  218. let role: String
  219. let taxonomy: String
  220. let taxonomyBucket: String
  221. let expected: TrioAlertCategory
  222. }
  223. static let rows: [Row] = [
  224. // DanaKit/Packets/DanaNotifyAlarm.swift:11
  225. Row(
  226. identifier: "batteryZeroPercent",
  227. message: "Pump battery 0%",
  228. role: "alertTitle",
  229. taxonomy: "N5 Battery Dead / Power Lost",
  230. taxonomyBucket: "other",
  231. expected: .other("Pump battery 0%")
  232. ),
  233. // DanaKit/Packets/DanaNotifyAlarm.swift:11
  234. Row(
  235. identifier: "batteryZeroPercent",
  236. message: "Battery is empty. Replace it now!",
  237. role: "alertBody",
  238. taxonomy: "N5 Battery Dead / Power Lost",
  239. taxonomyBucket: "other",
  240. expected: .other("Battery is empty. Replace it now!")
  241. ),
  242. // DanaKit/Packets/DanaNotifyAlarm.swift:12
  243. Row(
  244. identifier: "pumpError",
  245. message: "Pump error",
  246. role: "alertTitle",
  247. taxonomy: "N1 Hardware Fault / Device Alarm",
  248. taxonomyBucket: "hardwareFault",
  249. expected: .other("Pump error")
  250. ),
  251. // DanaKit/Packets/DanaNotifyAlarm.swift:12
  252. Row(
  253. identifier: "pumpError",
  254. message: "Check the pump and try again",
  255. role: "alertBody",
  256. taxonomy: "N1 Hardware Fault / Device Alarm",
  257. taxonomyBucket: "hardwareFault",
  258. expected: .other("Check the pump and try again")
  259. ),
  260. // DanaKit/Packets/DanaNotifyAlarm.swift:13
  261. Row(
  262. identifier: "occlusion",
  263. message: "Occlusion",
  264. role: "alertTitle",
  265. taxonomy: "N1 Hardware Fault / Device Alarm",
  266. taxonomyBucket: "occlusion",
  267. expected: .occlusion
  268. ),
  269. // DanaKit/Packets/DanaNotifyAlarm.swift:13
  270. Row(
  271. identifier: "occlusion",
  272. message: "Check the reservoir and infus and try again",
  273. role: "alertBody",
  274. taxonomy: "N1 Hardware Fault / Device Alarm",
  275. taxonomyBucket: "occlusion",
  276. expected: .other("Check the reservoir and infus and try again")
  277. ),
  278. // DanaKit/Packets/DanaNotifyAlarm.swift:14
  279. Row(
  280. identifier: "lowBattery",
  281. message: "Low pump battery",
  282. role: "alertTitle",
  283. taxonomy: "F2 Battery Low (Warning)",
  284. taxonomyBucket: "other",
  285. expected: .other("Low pump battery")
  286. ),
  287. // DanaKit/Packets/DanaNotifyAlarm.swift:14
  288. Row(
  289. identifier: "lowBattery",
  290. message: "Pump battery needs to be replaced soon",
  291. role: "alertBody",
  292. taxonomy: "F2 Battery Low (Warning)",
  293. taxonomyBucket: "other",
  294. expected: .other("Pump battery needs to be replaced soon")
  295. ),
  296. // DanaKit/Packets/DanaNotifyAlarm.swift:15
  297. Row(
  298. identifier: "shutdown",
  299. message: "Pump shutdown",
  300. role: "alertTitle",
  301. taxonomy: "N2 Delivery Suspended / Stopped",
  302. taxonomyBucket: "other",
  303. expected: .other("Pump shutdown")
  304. ),
  305. // DanaKit/Packets/DanaNotifyAlarm.swift:15
  306. Row(
  307. identifier: "shutdown",
  308. message: "There has not been any interactions with the pump for too long. Either disable this function in the pump or interact with the pump",
  309. role: "alertBody",
  310. taxonomy: "N2 Delivery Suspended / Stopped",
  311. taxonomyBucket: "other",
  312. expected: .other(
  313. "There has not been any interactions with the pump for too long. Either disable this function in the pump or interact with the pump"
  314. )
  315. ),
  316. // DanaKit/Packets/DanaNotifyAlarm.swift:16
  317. Row(
  318. identifier: "basalCompare",
  319. message: "Basal Compare",
  320. role: "alertTitle",
  321. taxonomy: "N14 Informational / Status",
  322. taxonomyBucket: "other",
  323. expected: .other("Basal Compare")
  324. ),
  325. // DanaKit/Packets/DanaNotifyAlarm.swift:17 (alarm 0x07); :18 (alarm 0xFF)
  326. Row(
  327. identifier: "bloodSugarMeasure",
  328. message: "Blood glucose Measure",
  329. role: "alertTitle",
  330. taxonomy: "N14 Informational / Status",
  331. taxonomyBucket: "other",
  332. expected: .other("Blood glucose Measure")
  333. ),
  334. // DanaKit/Packets/DanaNotifyAlarm.swift:19 (alarm 0x08); :20 (alarm 0xFE)
  335. Row(
  336. identifier: "remainingInsulinLevel",
  337. message: "Remaining insulin level",
  338. role: "alertTitle",
  339. taxonomy: "F1 Insulin Supply Low (Warning)",
  340. taxonomyBucket: "reservoirLow",
  341. expected: .other("Remaining insulin level")
  342. ),
  343. // DanaKit/Packets/DanaNotifyAlarm.swift:21
  344. Row(
  345. identifier: "emptyReservoir",
  346. message: "Empty reservoir",
  347. role: "alertTitle",
  348. taxonomy: "N4 Reservoir Empty / Out of Insulin",
  349. taxonomyBucket: "reservoirEmpty",
  350. expected: .other("Empty reservoir")
  351. ),
  352. // DanaKit/Packets/DanaNotifyAlarm.swift:21
  353. Row(
  354. identifier: "emptyReservoir",
  355. message: "Reservoir is empty. Replace it now!",
  356. role: "alertBody",
  357. taxonomy: "N4 Reservoir Empty / Out of Insulin",
  358. taxonomyBucket: "reservoirEmpty",
  359. expected: .other("Reservoir is empty. Replace it now!")
  360. ),
  361. // DanaKit/Packets/DanaNotifyAlarm.swift:22
  362. Row(
  363. identifier: "checkShaft",
  364. message: "Check chaft",
  365. role: "alertTitle",
  366. taxonomy: "N1 Hardware Fault / Device Alarm",
  367. taxonomyBucket: "hardwareFault",
  368. expected: .other("Check chaft")
  369. ),
  370. // DanaKit/Packets/DanaNotifyAlarm.swift:22
  371. Row(
  372. identifier: "checkShaft",
  373. message: "The pump has detected an issue with its chaft. Please remove the reservoir, check everything and try again",
  374. role: "alertBody",
  375. taxonomy: "N1 Hardware Fault / Device Alarm",
  376. taxonomyBucket: "hardwareFault",
  377. expected: .other(
  378. "The pump has detected an issue with its chaft. Please remove the reservoir, check everything and try again"
  379. )
  380. ),
  381. // DanaKit/Packets/DanaNotifyAlarm.swift:23
  382. Row(
  383. identifier: "basalMax",
  384. message: "Basal limit reached",
  385. role: "alertTitle",
  386. taxonomy: "N2 Delivery Suspended / Stopped",
  387. taxonomyBucket: "other",
  388. expected: .other("Basal limit reached")
  389. ),
  390. // DanaKit/Packets/DanaNotifyAlarm.swift:23
  391. Row(
  392. identifier: "basalMax",
  393. message: "Your daily basal limit has been reached. Please contact your Dana distributer to increase the limit",
  394. role: "alertBody",
  395. taxonomy: "N2 Delivery Suspended / Stopped",
  396. taxonomyBucket: "other",
  397. expected: .other(
  398. "Your daily basal limit has been reached. Please contact your Dana distributer to increase the limit"
  399. )
  400. ),
  401. // DanaKit/Packets/DanaNotifyAlarm.swift:24
  402. Row(
  403. identifier: "dailyMax",
  404. message: "Daily limit reached",
  405. role: "alertTitle",
  406. taxonomy: "N2 Delivery Suspended / Stopped",
  407. taxonomyBucket: "other",
  408. expected: .other("Daily limit reached")
  409. ),
  410. // DanaKit/Packets/DanaNotifyAlarm.swift:24
  411. Row(
  412. identifier: "dailyMax",
  413. message: "Your daily insulin limit has been reached. Please contact your Dana distributer to increase the limit",
  414. role: "alertBody",
  415. taxonomy: "N2 Delivery Suspended / Stopped",
  416. taxonomyBucket: "other",
  417. expected: .other(
  418. "Your daily insulin limit has been reached. Please contact your Dana distributer to increase the limit"
  419. )
  420. ),
  421. // DanaKit/Packets/DanaNotifyAlarm.swift:25
  422. Row(
  423. identifier: "bloodSugarCheckMiss",
  424. message: "Missed Blood glucose check",
  425. role: "alertTitle",
  426. taxonomy: "N14 Informational / Status",
  427. taxonomyBucket: "other",
  428. expected: .other("Missed Blood glucose check")
  429. ),
  430. // DanaKit/Packets/DanaNotifyAlarm.swift:25
  431. Row(
  432. identifier: "bloodSugarCheckMiss",
  433. message: "A blood glucose check reminder has been setup in your pump and is triggered. Please remove it or give your glucose level to the pump",
  434. role: "alertBody",
  435. taxonomy: "N14 Informational / Status",
  436. taxonomyBucket: "other",
  437. expected: .other(
  438. "A blood glucose check reminder has been setup in your pump and is triggered. Please remove it or give your glucose level to the pump"
  439. )
  440. ),
  441. // DanaKit/Packets/DanaNotifyAlarm.swift:29
  442. Row(
  443. identifier: "unknown",
  444. message: "Unknown error",
  445. role: "alertTitle",
  446. taxonomy: "N1 Hardware Fault / Device Alarm",
  447. taxonomyBucket: "hardwareFault",
  448. expected: .other("Unknown error")
  449. ),
  450. // DanaKit/Packets/DanaNotifyAlarm.swift:29
  451. Row(
  452. identifier: "unknown",
  453. message: "An unknown error has occurred during processing the alert from the pump. Please report this",
  454. role: "alertBody",
  455. taxonomy: "N1 Hardware Fault / Device Alarm",
  456. taxonomyBucket: "hardwareFault",
  457. expected: .other("An unknown error has occurred during processing the alert from the pump. Please report this")
  458. ),
  459. // DanaKit/PumpManager/PumpManagerAlert.swift:18 (dead case, defined copy)
  460. Row(
  461. identifier: "ble5InvalidKeys",
  462. message: "ERROR: Failed to pair device",
  463. role: "alertTitle",
  464. taxonomy: "N10 Authentication / Security",
  465. taxonomyBucket: "other",
  466. expected: .other("ERROR: Failed to pair device")
  467. ),
  468. // DanaKit/PumpManager/PumpManagerAlert.swift:18 (dead case); also ad-hoc DanaKitScanViewModel.swift:77
  469. Row(
  470. identifier: "ble5InvalidKeys",
  471. message: "Failed to pair to <device>. Please go to your bluetooth settings, forget this device, and try again",
  472. role: "alertBody",
  473. taxonomy: "N10 Authentication / Security",
  474. taxonomyBucket: "other",
  475. expected: .other(
  476. "Failed to pair to <device>. Please go to your bluetooth settings, forget this device, and try again"
  477. )
  478. ),
  479. // DanaKit/PumpManager/DanaKitPumpManager.swift:641, :894
  480. Row(
  481. identifier: "DanaKitPumpManagerError.pumpIsBusy",
  482. message: "Action has been canceled, because the pump is busy",
  483. role: "errorMessage",
  484. taxonomy: "N9 Command / Operation Failure",
  485. taxonomyBucket: "other",
  486. expected: .other("Action has been canceled, because the pump is busy")
  487. ),
  488. // DanaKit/PumpManager/DanaKitPumpManager.swift:665, :905; DanaBolusStart.swift:56
  489. Row(
  490. identifier: "DanaKitPumpManagerError.pumpSuspended",
  491. message: "The insulin delivery has been suspend. Action failed",
  492. role: "errorMessage",
  493. taxonomy: "N9 Command / Operation Failure",
  494. taxonomyBucket: "other",
  495. expected: .other("The insulin delivery has been suspend. Action failed")
  496. ),
  497. // DanaKit/Packets/DanaBolusStart.swift:58
  498. Row(
  499. identifier: "DanaKitPumpManagerError.bolusTimeoutActive",
  500. message: "A bolus timeout is active. The loop cycle cannot be completed till the timeout is inactive",
  501. role: "errorMessage",
  502. taxonomy: "N9 Command / Operation Failure",
  503. taxonomyBucket: "bolusFailed",
  504. expected: .commsTransient
  505. ),
  506. // DanaKit/Packets/DanaBolusStart.swift:60
  507. Row(
  508. identifier: "DanaKitPumpManagerError.bolusMaxViolation",
  509. message: "The max bolus limit is reached. Please try a lower amount or increase the limit",
  510. role: "errorMessage",
  511. taxonomy: "N9 Command / Operation Failure",
  512. taxonomyBucket: "bolusFailed",
  513. expected: .other("The max bolus limit is reached. Please try a lower amount or increase the limit")
  514. ),
  515. // DanaKit/Packets/DanaBolusStart.swift:62
  516. Row(
  517. identifier: "DanaKitPumpManagerError.unknown(bolusCommandError)",
  518. message: "Unknown error occured: bolusCommandError",
  519. role: "errorMessage",
  520. taxonomy: "N9 Command / Operation Failure",
  521. taxonomyBucket: "bolusFailed",
  522. expected: .other("Unknown error occured: bolusCommandError")
  523. ),
  524. // DanaKit/Packets/DanaBolusStart.swift:64
  525. Row(
  526. identifier: "DanaKitPumpManagerError.unknown(Invalid bolus speed error)",
  527. message: "Unknown error occured: Invalid bolus speed error",
  528. role: "errorMessage",
  529. taxonomy: "N9 Command / Operation Failure",
  530. taxonomyBucket: "bolusFailed",
  531. expected: .other("Unknown error occured: Invalid bolus speed error")
  532. ),
  533. // DanaKit/Packets/DanaBolusStart.swift:66
  534. Row(
  535. identifier: "DanaKitPumpManagerError.bolusInsulinLimitViolation",
  536. message: "The max daily insulin limit is reached. Please try a lower amount or increase the limit",
  537. role: "errorMessage",
  538. taxonomy: "N9 Command / Operation Failure",
  539. taxonomyBucket: "bolusFailed",
  540. expected: .other("The max daily insulin limit is reached. Please try a lower amount or increase the limit")
  541. ),
  542. // DanaKit/Packets/DanaBolusStart.swift:68
  543. Row(
  544. identifier: "DanaKitPumpManagerError.unknown(Unknown error: <code>)",
  545. message: "Unknown error occured: Unknown error: <code>",
  546. role: "errorMessage",
  547. taxonomy: "N9 Command / Operation Failure",
  548. taxonomyBucket: "bolusFailed",
  549. expected: .other("Unknown error occured: Unknown error: <code>")
  550. ),
  551. // DanaKit/PumpManager/DanaKitPumpManager.swift:742, :848, :1101, :1247, :1318, :1416, :1521, :1590
  552. Row(
  553. identifier: "DanaKitPumpManagerError.unknown",
  554. message: "Unknown error occured: <error>",
  555. role: "errorMessage",
  556. taxonomy: "N8 Connectivity / Communication",
  557. taxonomyBucket: "commsTransient",
  558. expected: .other("Unknown error occured: <error>")
  559. ),
  560. // DanaKit/PumpManager/DanaKitPumpManager.swift:750, :795, :1105, :1251, :1322, :1421, :1526, :1594
  561. Row(
  562. identifier: "DanaKitPumpManagerError.noConnection",
  563. message: "Failed to make a connection: <detail>",
  564. role: "errorMessage",
  565. taxonomy: "N8 Connectivity / Communication",
  566. taxonomyBucket: "commsTransient",
  567. expected: .other("Failed to make a connection: <detail>")
  568. ),
  569. // DanaKit/PumpManager/DanaKitPumpManager.swift:811 // NOTE: audit currentCategory was "other"; the verbatim placeholder text contains "communication", so categorize() returns .commsTransient. Pinned to actual behavior.
  570. Row(
  571. identifier: "PumpManagerError.communication",
  572. message: "(no DanaKit-specific message — Loop's generic communication error)",
  573. role: "errorMessage",
  574. taxonomy: "N9 Command / Operation Failure",
  575. taxonomyBucket: "other",
  576. expected: .commsTransient
  577. ),
  578. // DanaKit/PumpManager/DanaKitPumpManager.swift:940
  579. Row(
  580. identifier: "DanaKitPumpManagerError.failedTempBasalAdjustment",
  581. message: "Failed to adjust temp basal. Temp basal below 15 min is unsupported... (floor duration)",
  582. role: "errorMessage",
  583. taxonomy: "N9 Command / Operation Failure",
  584. taxonomyBucket: "other",
  585. expected: .other("Failed to adjust temp basal. Temp basal below 15 min is unsupported... (floor duration)")
  586. ),
  587. // DanaKit/PumpManager/DanaKitPumpManager.swift:959
  588. Row(
  589. identifier: "DanaKitPumpManagerError.failedTempBasalAdjustment",
  590. message: "Failed to adjust temp basal. Basal schedule is not available...",
  591. role: "errorMessage",
  592. taxonomy: "N9 Command / Operation Failure",
  593. taxonomyBucket: "other",
  594. expected: .other("Failed to adjust temp basal. Basal schedule is not available...")
  595. ),
  596. // DanaKit/PumpManager/DanaKitPumpManager.swift:990
  597. Row(
  598. identifier: "DanaKitPumpManagerError.failedTempBasalAdjustment",
  599. message: "Failed to adjust temp basal. Could not cancel old temp basal",
  600. role: "errorMessage",
  601. taxonomy: "N9 Command / Operation Failure",
  602. taxonomyBucket: "other",
  603. expected: .other("Failed to adjust temp basal. Could not cancel old temp basal")
  604. ),
  605. // DanaKit/PumpManager/DanaKitPumpManager.swift:1024
  606. Row(
  607. identifier: "DanaKitPumpManagerError.failedTempBasalAdjustment",
  608. message: "Failed to adjust temp basal. Pump rejected command (15 min)",
  609. role: "errorMessage",
  610. taxonomy: "N9 Command / Operation Failure",
  611. taxonomyBucket: "other",
  612. expected: .other("Failed to adjust temp basal. Pump rejected command (15 min)")
  613. ),
  614. // DanaKit/PumpManager/DanaKitPumpManager.swift:1051
  615. Row(
  616. identifier: "DanaKitPumpManagerError.failedTempBasalAdjustment",
  617. message: "Failed to adjust temp basal. Pump rejected command (30 min)",
  618. role: "errorMessage",
  619. taxonomy: "N9 Command / Operation Failure",
  620. taxonomyBucket: "other",
  621. expected: .other("Failed to adjust temp basal. Pump rejected command (30 min)")
  622. ),
  623. // DanaKit/PumpManager/DanaKitPumpManager.swift:1083
  624. Row(
  625. identifier: "DanaKitPumpManagerError.failedTempBasalAdjustment",
  626. message: "Failed to adjust temp basal. Pump rejected command (full hour)",
  627. role: "errorMessage",
  628. taxonomy: "N9 Command / Operation Failure",
  629. taxonomyBucket: "other",
  630. expected: .other("Failed to adjust temp basal. Pump rejected command (full hour)")
  631. ),
  632. // DanaKit/PumpManager/DanaKitPumpManager.swift:1204, :1281
  633. Row(
  634. identifier: "DanaKitPumpManagerError.failedSuspensionAdjustment",
  635. message: "Failed to adjust suspension",
  636. role: "errorMessage",
  637. taxonomy: "N9 Command / Operation Failure",
  638. taxonomyBucket: "other",
  639. expected: .other("Failed to adjust suspension")
  640. ),
  641. // DanaKit/PumpManager/DanaKitPumpManager.swift:1353, :1367
  642. Row(
  643. identifier: "DanaKitPumpManagerError.failedBasalAdjustment",
  644. message: "Failed to adjust basal",
  645. role: "errorMessage",
  646. taxonomy: "N9 Command / Operation Failure",
  647. taxonomyBucket: "other",
  648. expected: .other("Failed to adjust basal")
  649. ),
  650. // DanaKit/PumpManager/DanaKitPumpManager.swift:1373
  651. Row(
  652. identifier: "DanaKitPumpManagerError.failedBasalGeneration",
  653. message: "Failed to generate Dana basal program",
  654. role: "errorMessage",
  655. taxonomy: "N9 Command / Operation Failure",
  656. taxonomyBucket: "other",
  657. expected: .other("Failed to generate Dana basal program")
  658. ),
  659. // DanaKit/PumpManager/DanaKitPumpManager.swift:1485
  660. Row(
  661. identifier: "DanaKitPumpManagerError.unknown(Pump refused to send basal rates back)",
  662. message: "Unknown error occured: Pump refused to send basal rates back",
  663. role: "errorMessage",
  664. taxonomy: "N9 Command / Operation Failure",
  665. taxonomyBucket: "other",
  666. expected: .other("Unknown error occured: Pump refused to send basal rates back")
  667. ),
  668. // DanaKit/PumpManager/DanaKitPumpManager.swift:1498
  669. Row(
  670. identifier: "DanaKitPumpManagerError.unknown(Pump refused to send bolus step back)",
  671. message: "Unknown error occured: Pump refused to send bolus step back",
  672. role: "errorMessage",
  673. taxonomy: "N9 Command / Operation Failure",
  674. taxonomyBucket: "other",
  675. expected: .other("Unknown error occured: Pump refused to send bolus step back")
  676. ),
  677. // DanaKit/PumpManager/DanaKitPumpManager.swift:1570
  678. Row(
  679. identifier: "DanaKitPumpManagerError.failedTimeAdjustment",
  680. message: "Failed to adjust pump time",
  681. role: "errorMessage",
  682. taxonomy: "N9 Command / Operation Failure",
  683. taxonomyBucket: "other",
  684. expected: .other("Failed to adjust pump time")
  685. ),
  686. // DanaKit/PumpManager/DanaKitPumpManagerError.swift:10 (dead case, defined copy)
  687. Row(
  688. identifier: "DanaKitPumpManagerError.unsupportedTempBasal",
  689. message: "Setting temp basal is not supported at this time. Duration: %lld sec",
  690. role: "errorMessage",
  691. taxonomy: "N9 Command / Operation Failure",
  692. taxonomyBucket: "other",
  693. expected: .other("Setting temp basal is not supported at this time. Duration: %lld sec")
  694. ),
  695. // DanaKit/PumpManager/PeripheralManager.swift:63
  696. Row(
  697. identifier: "DanaKitPumpManagerError.unknown(A command is already running)",
  698. message: "A command is already running",
  699. role: "errorMessage",
  700. taxonomy: "N9 Command / Operation Failure",
  701. taxonomyBucket: "other",
  702. expected: .other("A command is already running")
  703. ),
  704. // DanaKit/Packets/DanaBasalSetProfileRate.swift:11
  705. Row(
  706. identifier: "DanaKitPumpManagerError.unknown(Invalid basal rate. Expected length = 24)",
  707. message: "Invalid basal rate. Expected length = 24",
  708. role: "errorMessage",
  709. taxonomy: "N9 Command / Operation Failure",
  710. taxonomyBucket: "other",
  711. expected: .other("Invalid basal rate. Expected length = 24")
  712. ),
  713. // DanaKit/PumpManager/DanaKitPumpManager.swift:1931 (delegate.pumpManager(_:didError:))
  714. Row(
  715. identifier: "PumpManagerError.uncertainDelivery",
  716. message: "(Loop's standard uncertain-delivery error)",
  717. role: "errorMessage",
  718. taxonomy: "N3 Uncertain Delivery / Unacknowledged Command",
  719. taxonomyBucket: "deliveryUncertain",
  720. expected: .other("(Loop's standard uncertain-delivery error)")
  721. ),
  722. // Common/NotificationHelper.swift:14-22
  723. Row(
  724. identifier: "com.bastiaanv.continuous-ble.disconnect-reminder",
  725. message: "Pump is still disconnected",
  726. role: "notificationTitle",
  727. taxonomy: "N8 Connectivity / Communication",
  728. taxonomyBucket: "commsTransient",
  729. expected: .other("Pump is still disconnected")
  730. ),
  731. // Common/NotificationHelper.swift:14-22
  732. Row(
  733. identifier: "com.bastiaanv.continuous-ble.disconnect-reminder",
  734. message: "Your pump is still disconnected after the set period!",
  735. role: "notificationBody",
  736. taxonomy: "N8 Connectivity / Communication",
  737. taxonomyBucket: "commsTransient",
  738. expected: .other("Your pump is still disconnected after the set period!")
  739. ),
  740. // Common/NotificationHelper.swift:34-42
  741. Row(
  742. identifier: "com.bastiaanv.continuous-ble.disconnect-warning",
  743. message: "Pump is disconnected",
  744. role: "notificationTitle",
  745. taxonomy: "N8 Connectivity / Communication",
  746. taxonomyBucket: "commsTransient",
  747. expected: .other("Pump is disconnected")
  748. ),
  749. // Common/NotificationHelper.swift:34-42
  750. Row(
  751. identifier: "com.bastiaanv.continuous-ble.disconnect-warning",
  752. message: "Your pump is disconnected longer than 5 minutes!",
  753. role: "notificationBody",
  754. taxonomy: "N8 Connectivity / Communication",
  755. taxonomyBucket: "commsTransient",
  756. expected: .other("Your pump is disconnected longer than 5 minutes!")
  757. ),
  758. // DanaKitUI/Views/Onboarding/DanaKitScanView.swift:54-64
  759. Row(
  760. identifier: "DanaKitScanView connection-error alert",
  761. message: "Error while connecting to device",
  762. role: "alertTitle",
  763. taxonomy: "N12 Setup / Pairing / Activation",
  764. taxonomyBucket: "other",
  765. expected: .other("Error while connecting to device")
  766. ),
  767. // DanaKitUI/Views/Onboarding/DanaKitScanView.swift:65-82
  768. Row(
  769. identifier: "DanaKitScanView PIN-prompt alert",
  770. message: "Dana-RS v3 found!",
  771. role: "alertTitle",
  772. taxonomy: "N12 Setup / Pairing / Activation",
  773. taxonomyBucket: "other",
  774. expected: .other("Dana-RS v3 found!")
  775. ),
  776. // DanaKitUI/ViewModels/DanaKitScanViewModel.swift:91
  777. Row(
  778. identifier: "connectionErrorMessage (ConnectionResult.timeout)",
  779. message: "Connection timeout is hit...",
  780. role: "errorMessage",
  781. taxonomy: "N12 Setup / Pairing / Activation",
  782. taxonomyBucket: "other",
  783. expected: .commsTransient
  784. ),
  785. // DanaKitUI/ViewModels/DanaKitScanViewModel.swift:95
  786. Row(
  787. identifier: "connectionErrorMessage (ConnectionResult.alreadyConnectedAndBusy)",
  788. message: "Device is already connected...",
  789. role: "errorMessage",
  790. taxonomy: "N12 Setup / Pairing / Activation",
  791. taxonomyBucket: "other",
  792. expected: .other("Device is already connected...")
  793. ),
  794. // DanaKitUI/Views/Settings/DanaKitRefillReservoirCannula.swift:52
  795. Row(
  796. identifier: "failedReservoirAmount label",
  797. message: "Failed to set reservoir amount. Re-sync pump data and try again please",
  798. role: "validation",
  799. taxonomy: "N12 Setup / Pairing / Activation",
  800. taxonomyBucket: "other",
  801. expected: .other("Failed to set reservoir amount. Re-sync pump data and try again please")
  802. ),
  803. // DanaKitUI/Views/Settings/DanaKitRefillReservoirCannula.swift:96
  804. Row(
  805. identifier: "failedTubeAmount label",
  806. message: "Failed to prime the tube. Please try again later",
  807. role: "validation",
  808. taxonomy: "N12 Setup / Pairing / Activation",
  809. taxonomyBucket: "other",
  810. expected: .other("Failed to prime the tube. Please try again later")
  811. ),
  812. // DanaKitUI/Views/Settings/DanaKitRefillReservoirCannula.swift:149
  813. Row(
  814. identifier: "failedPrimeAmount label",
  815. message: "Failed to prime the cannula. Please try again later",
  816. role: "validation",
  817. taxonomy: "N12 Setup / Pairing / Activation",
  818. taxonomyBucket: "other",
  819. expected: .other("Failed to prime the cannula. Please try again later")
  820. ),
  821. // DanaKitUI/ViewModels/DanaKitScanViewModel.swift:140
  822. Row(
  823. identifier: "pinCodePromptError (invalid lengths)",
  824. message: "Received invalid pincode lengths. Try again",
  825. role: "validation",
  826. taxonomy: "N13 Configuration / Validation",
  827. taxonomyBucket: "other",
  828. expected: .other("Received invalid pincode lengths. Try again")
  829. ),
  830. // DanaKitUI/ViewModels/DanaKitScanViewModel.swift:150
  831. Row(
  832. identifier: "pinCodePromptError (invalid hex)",
  833. message: "Received invalid hex strings. Try again",
  834. role: "validation",
  835. taxonomy: "N13 Configuration / Validation",
  836. taxonomyBucket: "other",
  837. expected: .other("Received invalid hex strings. Try again")
  838. ),
  839. // DanaKitUI/ViewModels/DanaKitScanViewModel.swift:172
  840. Row(
  841. identifier: "pinCodePromptError (checksum)",
  842. message: "Checksum failed. Try again",
  843. role: "validation",
  844. taxonomy: "N13 Configuration / Validation",
  845. taxonomyBucket: "other",
  846. expected: .other("Checksum failed. Try again")
  847. ),
  848. // DanaKit/PumpManager/PeripheralManager.swift:337
  849. Row(
  850. identifier: "NSError (Passkey request failed) -> ConnectionResult.failure",
  851. message: "Passkey request failed",
  852. role: "errorMessage",
  853. taxonomy: "N10 Authentication / Security",
  854. taxonomyBucket: "other",
  855. expected: .other("Passkey request failed")
  856. ),
  857. // DanaKit/PumpManager/PeripheralManager.swift:425
  858. Row(
  859. identifier: "NSError (PUMP_CHECK error, wrong serial number) -> ConnectionResult.failure",
  860. message: "PUMP_CHECK error, wrong serial number",
  861. role: "errorMessage",
  862. taxonomy: "N10 Authentication / Security",
  863. taxonomyBucket: "other",
  864. expected: .other("PUMP_CHECK error, wrong serial number")
  865. ),
  866. // DanaKit/PumpManager/PeripheralManager.swift:453
  867. Row(
  868. identifier: "NSError (Invalid password) -> ConnectionResult.failure",
  869. message: "Invalid password",
  870. role: "errorMessage",
  871. taxonomy: "N10 Authentication / Security",
  872. taxonomyBucket: "other",
  873. expected: .other("Invalid password")
  874. ),
  875. // DanaKit/PumpManager/PeripheralManager.swift:109
  876. Row(
  877. identifier: "NSError (Timeout has been hit) -> DanaKitPumpManagerError.unknown",
  878. message: "Timeout has been hit...",
  879. role: "errorMessage",
  880. taxonomy: "N8 Connectivity / Communication",
  881. taxonomyBucket: "commsTransient",
  882. expected: .commsTransient
  883. ),
  884. // DanaKit/PumpManager/PeripheralManager.swift:140
  885. Row(
  886. identifier: "NSError (Failed to discover dana data service) -> ConnectionResult.failure",
  887. message: "Failed to discover dana data service...",
  888. role: "errorMessage",
  889. taxonomy: "N8 Connectivity / Communication",
  890. taxonomyBucket: "commsTransient",
  891. expected: .other("Failed to discover dana data service...")
  892. ),
  893. // DanaKit/PumpManager/PeripheralManager.swift:161
  894. Row(
  895. identifier: "NSError (Failed to discover dana write or read characteristic) -> ConnectionResult.failure",
  896. message: "Failed to discover dana write or read characteristic",
  897. role: "errorMessage",
  898. taxonomy: "N8 Connectivity / Communication",
  899. taxonomyBucket: "commsTransient",
  900. expected: .other("Failed to discover dana write or read characteristic")
  901. ),
  902. // DanaKit/PumpManager/PeripheralManager.swift:133
  903. Row(
  904. identifier: "CoreBluetooth Error (didDiscoverServices) -> ConnectionResult.failure",
  905. message: "(system error text)",
  906. role: "errorMessage",
  907. taxonomy: "N8 Connectivity / Communication",
  908. taxonomyBucket: "commsTransient",
  909. expected: .other("(system error text)")
  910. ),
  911. // DanaKit/PumpManager/PeripheralManager.swift:151
  912. Row(
  913. identifier: "CoreBluetooth Error (didDiscoverCharacteristics) -> ConnectionResult.failure",
  914. message: "(system error text)",
  915. role: "errorMessage",
  916. taxonomy: "N8 Connectivity / Communication",
  917. taxonomyBucket: "commsTransient",
  918. expected: .other("(system error text)")
  919. ),
  920. // DanaKit/PumpManager/PeripheralManager.swift:172
  921. Row(
  922. identifier: "CoreBluetooth Error (notify enable failed) -> ConnectionResult.failure",
  923. message: "(system error text)",
  924. role: "errorMessage",
  925. taxonomy: "N8 Connectivity / Communication",
  926. taxonomyBucket: "commsTransient",
  927. expected: .other("(system error text)")
  928. ),
  929. // DanaKit/PumpManager/PeripheralManager.swift:183
  930. Row(
  931. identifier: "CoreBluetooth Error (didUpdateValueFor) -> ConnectionResult.failure",
  932. message: "(system error text)",
  933. role: "errorMessage",
  934. taxonomy: "N8 Connectivity / Communication",
  935. taxonomyBucket: "commsTransient",
  936. expected: .other("(system error text)")
  937. ),
  938. // DanaKit/PumpManager/PeripheralManager.swift:381, :392
  939. Row(
  940. identifier: "NSError (Invalid hwModel) -> ConnectionResult.failure",
  941. message: "Invalid hwModel",
  942. role: "errorMessage",
  943. taxonomy: "N8 Connectivity / Communication",
  944. taxonomyBucket: "commsTransient",
  945. expected: .other("Invalid hwModel")
  946. ),
  947. // DanaKit/PumpManager/PeripheralManager.swift:419
  948. Row(
  949. identifier: "NSError (PUMP_CHECK error) -> ConnectionResult.failure",
  950. message: "PUMP_CHECK error",
  951. role: "errorMessage",
  952. taxonomy: "N8 Connectivity / Communication",
  953. taxonomyBucket: "commsTransient",
  954. expected: .other("PUMP_CHECK error")
  955. ),
  956. // DanaKit/PumpManager/PeripheralManager.swift:422
  957. Row(
  958. identifier: "NSError (PUMP_CHECK_BUSY error) -> ConnectionResult.failure",
  959. message: "PUMP_CHECK_BUSY error",
  960. role: "errorMessage",
  961. taxonomy: "N8 Connectivity / Communication",
  962. taxonomyBucket: "commsTransient",
  963. expected: .other("PUMP_CHECK_BUSY error")
  964. ),
  965. // DanaKit/PumpManager/BluetoothManager.swift:53
  966. Row(
  967. identifier: "NSError (Invalid bluetooth state) -> startScan throw",
  968. message: "Invalid bluetooth state - state: <n>",
  969. role: "errorMessage",
  970. taxonomy: "N8 Connectivity / Communication",
  971. taxonomyBucket: "commsTransient",
  972. expected: .other("Invalid bluetooth state - state: <n>")
  973. ),
  974. // DanaKit/PumpManager/BluetoothManager.swift:77
  975. Row(
  976. identifier: "NSError (Invalid identifier) -> connect throw",
  977. message: "Invalid identifier - <id>",
  978. role: "errorMessage",
  979. taxonomy: "N8 Connectivity / Communication",
  980. taxonomyBucket: "commsTransient",
  981. expected: .other("Invalid identifier - <id>")
  982. ),
  983. // DanaKit/PumpManager/BluetoothManager.swift:124; ContinousBluetoothManager.swift:73; InteractiveBluetoothManager.swift:182
  984. Row(
  985. identifier: "NSError (No connected device) -> finishV3Pairing throw",
  986. message: "No connected device",
  987. role: "errorMessage",
  988. taxonomy: "N8 Connectivity / Communication",
  989. taxonomyBucket: "commsTransient",
  990. expected: .other("No connected device")
  991. ),
  992. // DanaKit/PumpManager/BluetoothManager.swift:237
  993. Row(
  994. identifier: "NSError (No pumpManager) -> ConnectionResult.failure",
  995. message: "No pumpManager",
  996. role: "errorMessage",
  997. taxonomy: "N8 Connectivity / Communication",
  998. taxonomyBucket: "commsTransient",
  999. expected: .other("No pumpManager")
  1000. ),
  1001. // DanaKit/PumpManager/ContinousBluetoothManager.swift:147
  1002. Row(
  1003. identifier: "NSError (Couldn't reconnect) -> noConnection",
  1004. message: "Couldn't reconnect",
  1005. role: "errorMessage",
  1006. taxonomy: "N8 Connectivity / Communication",
  1007. taxonomyBucket: "commsTransient",
  1008. expected: .other("Couldn't reconnect")
  1009. ),
  1010. // DanaKit/PumpManager/ContinousBluetoothManager.swift:165
  1011. Row(
  1012. identifier: "NSError (Device is forced disconnected) -> noConnection",
  1013. message: "Device is forced disconnected...",
  1014. role: "errorMessage",
  1015. taxonomy: "N8 Connectivity / Communication",
  1016. taxonomyBucket: "commsTransient",
  1017. expected: .other("Device is forced disconnected...")
  1018. ),
  1019. // DanaKit/PumpManager/InteractiveBluetoothManager.swift:158
  1020. Row(
  1021. identifier: "NSError (Pump is not onboarded) -> noConnection",
  1022. message: "Pump is not onboarded",
  1023. role: "errorMessage",
  1024. taxonomy: "N8 Connectivity / Communication",
  1025. taxonomyBucket: "commsTransient",
  1026. expected: .other("Pump is not onboarded")
  1027. )
  1028. ]
  1029. // MARK: - Classification pinned to CURRENT (must be green)
  1030. @Test(
  1031. "each (identifier, message) classifies as pinned",
  1032. arguments: rows
  1033. ) func classificationPinned(_ row: Row) {
  1034. // Feeds the EXACT display string through StubError so the substring
  1035. // classifier matches over the real text, and pins the category it
  1036. // returns today. Almost all DanaKit prose -> .other(message).
  1037. #expect(
  1038. TrioAlertClassifier.categorize(error: StubError(description: row.message)) == row.expected
  1039. )
  1040. }
  1041. // MARK: - Classifier coverage gaps (ratchet)
  1042. /// "identifier — message" keys for emissions that the substring classifier
  1043. /// drops to `.other` even though the taxonomy implies a real, mappable
  1044. /// bucket. The classifier SHOULD route each below, but the natural-language
  1045. /// text lacks the concatenated token it scans for. Per gap, the bucket it
  1046. /// should hit and why the tokens miss:
  1047. ///
  1048. /// - N1 Hardware Fault -> `.hardwareFault` (token "fault"): "Pump error",
  1049. /// "Check the pump and try again", "Check chaft", "The pump has detected
  1050. /// an issue with its chaft...", "Unknown error", "An unknown error has
  1051. /// occurred...". None contains "fault".
  1052. /// Source: DanaKit/Packets/DanaNotifyAlarm.swift:12, :22, :29
  1053. /// - N1 occlusion BODY -> `.occlusion` (token "occlusion"): "Check the
  1054. /// reservoir and infus and try again" — the title matches, the body does
  1055. /// not contain "occlusion".
  1056. /// Source: DanaKit/Packets/DanaNotifyAlarm.swift:13
  1057. /// - F1 Insulin Supply Low -> `.reservoirLow` (token "lowreservoir"):
  1058. /// "Remaining insulin level" does not contain "lowreservoir".
  1059. /// Source: DanaKit/Packets/DanaNotifyAlarm.swift:19
  1060. /// - N4 Reservoir Empty -> `.reservoirEmpty` (tokens "reservoirempty"/
  1061. /// "emptyreservoir"): "Empty reservoir" and "Reservoir is empty. Replace
  1062. /// it now!" both break the token across a space.
  1063. /// Source: DanaKit/Packets/DanaNotifyAlarm.swift:21
  1064. /// - N9 bolus-failure copy -> `.bolusFailed` (token "bolusfailed"): the
  1065. /// max-bolus/insulin-limit and bolus "Unknown error occured: ..." strings
  1066. /// never contain "bolusfailed".
  1067. /// Source: DanaKit/Packets/DanaBolusStart.swift:60, :62, :64, :66, :68
  1068. /// - N8 connectivity copy -> `.commsTransient` (tokens "communication"/
  1069. /// "comms"/"timeout"/"notconnected"/...): the generic
  1070. /// "Unknown error occured: <error>", "Failed to make a connection:
  1071. /// <detail>", disconnect notifications, CoreBluetooth/NSError discovery
  1072. /// and PUMP_CHECK / bluetooth-state strings lack any of the literal
  1073. /// tokens.
  1074. /// Source: DanaKit/PumpManager/DanaKitPumpManager.swift:742...;
  1075. /// Common/NotificationHelper.swift:14-22, :34-42;
  1076. /// DanaKit/PumpManager/PeripheralManager.swift:133, :140, :151, :161,
  1077. /// :172, :183, :381, :419, :422; BluetoothManager.swift:53, :77, :124,
  1078. /// :237; ContinousBluetoothManager.swift:147, :165;
  1079. /// InteractiveBluetoothManager.swift:158
  1080. /// - N3 Uncertain Delivery -> `.deliveryUncertain` (tokens
  1081. /// "uncertaindelivery"/"unacknowledged"/"bolus may have failed"): the
  1082. /// catalogued placeholder for Loop's standard uncertain-delivery error
  1083. /// contains none of these literal tokens.
  1084. /// Source: DanaKit/PumpManager/DanaKitPumpManager.swift:1931
  1085. ///
  1086. /// NOT captured by this ratchet (already classifies as a non-`.other`
  1087. /// bucket, but the WRONG one): "A bolus timeout is active..." (bolusFailed
  1088. /// taxonomy) contains "timeout" so it routes to `.commsTransient`, not
  1089. /// `.bolusFailed`. It is a real gap in the audit but the recompute below
  1090. /// only catches `.other` drops (expected == .other while bucket != other),
  1091. /// so it is documented here rather than listed.
  1092. /// Source: DanaKit/Packets/DanaBolusStart.swift:58
  1093. ///
  1094. /// Stays green now; FAILS (forcing this file to be updated) when the
  1095. /// classifier improves and one of these strings starts mapping.
  1096. static let classifierCoverageGaps: Set<String> = [
  1097. "pumpError — Pump error",
  1098. "pumpError — Check the pump and try again",
  1099. "occlusion — Check the reservoir and infus and try again",
  1100. "remainingInsulinLevel — Remaining insulin level",
  1101. "emptyReservoir — Empty reservoir",
  1102. "emptyReservoir — Reservoir is empty. Replace it now!",
  1103. "checkShaft — Check chaft",
  1104. "checkShaft — The pump has detected an issue with its chaft. Please remove the reservoir, check everything and try again",
  1105. "unknown — Unknown error",
  1106. "unknown — An unknown error has occurred during processing the alert from the pump. Please report this",
  1107. "DanaKitPumpManagerError.bolusMaxViolation — The max bolus limit is reached. Please try a lower amount or increase the limit",
  1108. "DanaKitPumpManagerError.unknown(bolusCommandError) — Unknown error occured: bolusCommandError",
  1109. "DanaKitPumpManagerError.unknown(Invalid bolus speed error) — Unknown error occured: Invalid bolus speed error",
  1110. "DanaKitPumpManagerError.bolusInsulinLimitViolation — The max daily insulin limit is reached. Please try a lower amount or increase the limit",
  1111. "DanaKitPumpManagerError.unknown(Unknown error: <code>) — Unknown error occured: Unknown error: <code>",
  1112. "DanaKitPumpManagerError.unknown — Unknown error occured: <error>",
  1113. "DanaKitPumpManagerError.noConnection — Failed to make a connection: <detail>",
  1114. "PumpManagerError.uncertainDelivery — (Loop's standard uncertain-delivery error)",
  1115. "com.bastiaanv.continuous-ble.disconnect-reminder — Pump is still disconnected",
  1116. "com.bastiaanv.continuous-ble.disconnect-reminder — Your pump is still disconnected after the set period!",
  1117. "com.bastiaanv.continuous-ble.disconnect-warning — Pump is disconnected",
  1118. "com.bastiaanv.continuous-ble.disconnect-warning — Your pump is disconnected longer than 5 minutes!",
  1119. "NSError (Failed to discover dana data service) -> ConnectionResult.failure — Failed to discover dana data service...",
  1120. "NSError (Failed to discover dana write or read characteristic) -> ConnectionResult.failure — Failed to discover dana write or read characteristic",
  1121. "CoreBluetooth Error (didDiscoverServices) -> ConnectionResult.failure — (system error text)",
  1122. "CoreBluetooth Error (didDiscoverCharacteristics) -> ConnectionResult.failure — (system error text)",
  1123. "CoreBluetooth Error (notify enable failed) -> ConnectionResult.failure — (system error text)",
  1124. "CoreBluetooth Error (didUpdateValueFor) -> ConnectionResult.failure — (system error text)",
  1125. "NSError (Invalid hwModel) -> ConnectionResult.failure — Invalid hwModel",
  1126. "NSError (PUMP_CHECK error) -> ConnectionResult.failure — PUMP_CHECK error",
  1127. "NSError (PUMP_CHECK_BUSY error) -> ConnectionResult.failure — PUMP_CHECK_BUSY error",
  1128. "NSError (Invalid bluetooth state) -> startScan throw — Invalid bluetooth state - state: <n>",
  1129. "NSError (Invalid identifier) -> connect throw — Invalid identifier - <id>",
  1130. "NSError (No connected device) -> finishV3Pairing throw — No connected device",
  1131. "NSError (No pumpManager) -> ConnectionResult.failure — No pumpManager",
  1132. "NSError (Couldn't reconnect) -> noConnection — Couldn't reconnect",
  1133. "NSError (Device is forced disconnected) -> noConnection — Device is forced disconnected...",
  1134. "NSError (Pump is not onboarded) -> noConnection — Pump is not onboarded"
  1135. ]
  1136. @Test("classifier coverage gaps are exactly as documented") func classifierCoverageGapsExact() {
  1137. // A gap is an emission the classifier drops to .other while its
  1138. // taxonomy bucket is non-other. Recompute from rows independently of
  1139. // the pinned set: expected == .other(message) AND taxonomyBucket is not
  1140. // "other". If a string starts matching a real token its expected leaves
  1141. // .other and it drops out of `computed`, breaking this equality and
  1142. // forcing an update here.
  1143. let computed: Set<String> = Set(
  1144. Self.rows.compactMap { row -> String? in
  1145. guard case .other = row.expected, row.taxonomyBucket != "other" else { return nil }
  1146. return "\(row.identifier) — \(row.message)"
  1147. }
  1148. )
  1149. #expect(computed == Self.classifierCoverageGaps)
  1150. }
  1151. }