TandemKitAlertEmissionTests.swift 61 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186118711881189119011911192119311941195119611971198119912001201120212031204120512061207120812091210121112121213121412151216121712181219122012211222122312241225122612271228122912301231123212331234123512361237123812391240124112421243124412451246124712481249125012511252125312541255125612571258125912601261126212631264126512661267126812691270127112721273127412751276127712781279128012811282128312841285128612871288128912901291
  1. import Foundation
  2. import LoopKit
  3. import Testing
  4. @testable import Trio
  5. /// Pins Trio's alert-routing behavior for the **TandemKit** pump plugin.
  6. ///
  7. /// Rows are derived from the synthesis audit of the Tandem stack
  8. /// (`TandemCore` / `TandemKit` / `TandemKitUI` / `TandemKitPlugin`; pump:
  9. /// Tandem t:slim X2 / Mobi). The audit's central finding is STRUCTURAL:
  10. /// TandemKit issues **zero** LoopKit Alerts. There is no
  11. /// `issueAlert` / `retractAlert` / `Alert(identifier:)` / `DeviceAlert` /
  12. /// `UNUserNotificationCenter` usage anywhere in the shipping targets
  13. /// (audit.md:7-8, Notes:192), so TandemKit never constructs an
  14. /// `Alert.Identifier` and there is no `(managerIdentifier, alertIdentifier)`
  15. /// pair for Trio to route. The PRIMARY emitted-alert table is therefore empty
  16. /// and `AlertCatalogRegistry` has — and could match — no Tandem entries.
  17. ///
  18. /// Every Tandem pump alarm the device surfaces (occlusion N1->Critical;
  19. /// empty/removed cartridge & "No Insulin" N4->Critical; battery-shutdown
  20. /// N5-pump->Critical; temperature/altitude/stuck-button/invalid-date/
  21. /// pump-reset & the non-dismissable Malfunction item N1->Critical;
  22. /// resume-pump / auto-off delivery-stopped N2->Critical; CGM-alert catalog
  23. /// N7->High) is rendered ONLY inside TandemKit's own in-app
  24. /// "Pump notifications" settings list
  25. /// (`NotificationBundle` -> `TandemKitNotificationsView`), which Trio never
  26. /// receives. The `AlertResponseType` description catalog is intercepted in
  27. /// `fetchNotifications` (audit:129,193) and is dead for display, and the lone
  28. /// `didError` call site (`TandemPumpManager.swift:218`) has no callers.
  29. ///
  30. /// GAP SUMMARY: the effective interruption level of every taxonomy-Critical
  31. /// Tandem alarm is "never delivered" — strictly less severe than `.critical`.
  32. /// This is broader than a registry-key mismatch: no `Alert.Identifier` is
  33. /// ever issued, so no escalation is possible until upstream TandemKit issues
  34. /// LoopKit Alerts (and adopts `AlertCatalogVendor`, or Trio adds Tandem
  35. /// registry entries). The remediation gap is documented in this suite's prose
  36. /// rather than as table rows, because there are no emitted-alert rows to ratchet.
  37. ///
  38. /// The only Tandem signals that DO reach Trio are `PumpManagerError`-wrapped
  39. /// `PumpCommError` / `TandemPumpManagerValidationError` values returned through
  40. /// `PumpManager` dosing/limit/time completion handlers (e.g. `enactTempBasal`
  41. /// -> `APSManager:703` `APSError.pumpError(error)` -> `processError` ->
  42. /// `TrioAlertClassifier.categorize`). These are error-STRING emissions, not
  43. /// Alerts. No classifier rows are pinned here: the TandemKit submodule is not
  44. /// present in Trio-dev and the audit recorded the rendered `errorDescription`
  45. /// text rather than the `String(describing:)` form, so the exact classifier
  46. /// input (a LoopKit `PumpManagerError` enum — not `CustomStringConvertible` —
  47. /// wrapping a `PumpCommError`/validation case) cannot be stated with
  48. /// certainty. Per the SCOPE rule those are omitted rather than guessed.
  49. @Suite("Trio Alert Emission: TandemKit") struct TandemKitAlertEmissionTests {
  50. /// TandemKit issues no LoopKit Alerts, so it carries no real
  51. /// `managerIdentifier`. We keep the audit's verbatim placeholder string so
  52. /// the (empty) registry-lookup pin documents exactly what was searched.
  53. private static let managerIdentifier =
  54. "(none — TandemKit issues no LoopKit Alerts; it uses no Alert.Identifier at all)"
  55. private func id(_ manager: String, _ alertID: String) -> Alert.Identifier {
  56. Alert.Identifier(managerIdentifier: manager, alertIdentifier: alertID)
  57. }
  58. // MARK: - Emitted alerts (PRIMARY table)
  59. /// `(alertIdentifier, expectedRegistryLevel)`.
  60. ///
  61. /// EMPTY: TandemKit issues no LoopKit Alerts (audit.md:7-8, Notes:192), so
  62. /// there are no `(managerIdentifier, alertIdentifier)` pairs to pin. If this
  63. /// table ever gains a row, the audit's structural finding has changed and
  64. /// this suite's doc comment must be revisited.
  65. static let alertRows: [(alertID: String, expectedLevel: Alert.InterruptionLevel?)] = []
  66. @Test(
  67. "registry behavior is pinned for every emitted alert",
  68. arguments: alertRows
  69. ) func registryBehaviorIsPinned(row: (alertID: String, expectedLevel: Alert.InterruptionLevel?)) {
  70. let identifier = id(Self.managerIdentifier, row.alertID)
  71. #expect(AlertCatalogRegistry.lookup(identifier)?.interruptionLevel == row.expectedLevel)
  72. }
  73. /// Backstop for the empty PRIMARY table: even the audit's verbatim
  74. /// placeholder manager string resolves to no catalog entry. This pins the
  75. /// structural fact that TandemKit has no escalatable surface in Trio today.
  76. @Test("no Tandem alert identifier resolves in the catalog registry") func noTandemEntryInRegistry() {
  77. #expect(Self.alertRows.isEmpty)
  78. #expect(AlertCatalogRegistry.lookup(id(Self.managerIdentifier, "anything")) == nil)
  79. }
  80. // MARK: - Documented escalation gaps (ratchet)
  81. /// `alertIdentifier`s of emitted alerts whose EFFECTIVE current level is
  82. /// less severe than their taxonomy level.
  83. ///
  84. /// EMPTY by construction: a gap row requires an emitted alert with an
  85. /// `Alert.Identifier`, and TandemKit emits none. The Tandem alarms that the
  86. /// taxonomy rates Critical (occlusion N1, empty/removed cartridge & "No
  87. /// Insulin" N4, battery-shutdown N5-pump, Malfunction/temperature/altitude/
  88. /// stuck-button/invalid-date/pump-reset N1, resume-pump / auto-off N2) and
  89. /// High (CGM-alert catalog N7) never reach Trio at all — they live only in
  90. /// TandemKit's in-app "Pump notifications" list (`NotificationBundle` ->
  91. /// `TandemKitNotificationsView`; audit.md:7-8, 129, 192-193). That is a
  92. /// STRUCTURAL gap (no `Alert.Identifier` is ever issued), strictly broader
  93. /// than a registry-key mismatch, and is documented in the suite prose
  94. /// rather than as a ratchetable row. The fix is upstream: TandemKit must
  95. /// issue LoopKit Alerts and adopt `AlertCatalogVendor` (or Trio must add
  96. /// Tandem registry entries) before any escalation is possible.
  97. static let knownEscalationGaps: Set<String> = []
  98. @Test("known escalation gaps are exactly as documented") func knownEscalationGapsAreExactlyAsDocumented() {
  99. // Recompute the gap set from the PRIMARY table: a row is a gap when its
  100. // effective level (registry level if present, else unknown) is missing.
  101. // With no rows this is empty and matches the documented (empty) set.
  102. // Should TandemKit ever start issuing Alerts and a row be added with an
  103. // unescalated level, this recomputed set will diverge from
  104. // `knownEscalationGaps`, failing the test and prompting an update.
  105. var computed = Set<String>()
  106. for row in Self.alertRows where row.expectedLevel != .critical {
  107. // Placeholder recompute: real gap math would compare against the
  108. // row's taxonomy level. No rows exist, so the loop never executes.
  109. computed.insert(row.alertID)
  110. }
  111. #expect(computed == Self.knownEscalationGaps)
  112. }
  113. }
  114. // MARK: - Message classification (SECONDARY: error-string routing)
  115. /// SPEC — how Trio's `TrioAlertClassifier` would bucket every user-facing
  116. /// **TandemKit** message string, pinned against the live substring matcher.
  117. ///
  118. /// IMPORTANT — what production actually feeds the classifier vs. what this
  119. /// suite feeds it: in production `APSManager` hands
  120. /// `TrioAlertClassifier.categorize(error:)` a LoopKit `PumpManagerError`
  121. /// wrapping a `PumpCommError` / `TandemPumpManagerValidationError`, and the
  122. /// classifier runs `String(describing:)` over THAT — i.e. it sees the Swift
  123. /// **case name** (e.g. `pumpError(...)` / `pumpNotConnected`), not the rendered
  124. /// display string. TandemKit also uses NONE of the LoopKit Alert / AlertIssuer
  125. /// machinery and never delivers a live `pumpManager(_:didError:)`, so there are
  126. /// no real LoopKit `Alert.alertIdentifier` values to attach (that structural
  127. /// finding is pinned by the registry-routing suite above). Every user-facing
  128. /// row here surfaces instead through one of: (1) `PumpCommError` /
  129. /// `TandemPumpManagerValidationError` wrapped in `PumpManagerError` returned to
  130. /// Loop completion handlers, (2) the in-app "Pump notifications" settings list
  131. /// (`NotificationBundle` items whose displayed string is the formatted enum
  132. /// case-name title), (3) `pumpStatusHighlight` strings, (4) SwiftUI
  133. /// `.alert`/ActionSheet/ErrorSheet text, and (5) CGM settings status rows. The
  134. /// `alertIdentifier` column is therefore the error enum case name / source
  135. /// symbol for the row, NOT a LoopKit alert id.
  136. ///
  137. /// This catalog deliberately feeds the classifier the **display string** of
  138. /// each emission so we pin how the substring matcher handles real
  139. /// natural-language text — the worst case for a token matcher. The classifier's
  140. /// `categorize(pumpError:)` tokens are concatenated and space-free
  141. /// ("notconnected", "noresponse", "reservoirempty", "sensorfailed", ...), while
  142. /// TandemKit's prose has spaces, so almost every emission falls through to
  143. /// `.other`. Only strings literally containing "occlusion"/"occluded" hit a real
  144. /// bucket. Connectivity strings ("Pump not connected", "No response from pump",
  145. /// "Pump Disconnected") do NOT match "notconnected"/"noresponse" because of the
  146. /// spaces and wrongly land in `.other` — the principal gaps. See
  147. /// `Trio/Sources/Services/Alerts/TrioAlertCategory.swift`.
  148. ///
  149. /// References: managers — TandemKit / TandemKitUI / TandemCore; pump —
  150. /// Tandem t:slim X2 / Mobi.
  151. @Suite("Trio Alert Emission: TandemKit — Classification") struct TandemKitMessageClassificationTests {
  152. /// A throwaway `Error` whose `String(describing:)` is exactly the emission
  153. /// display string, so `categorize(error:)` matches over natural-language
  154. /// text (mind the spaces: "reservoir is empty" does NOT contain
  155. /// "reservoirempty"; most prose -> `.other`).
  156. private struct StubError: Error, CustomStringConvertible { let description: String }
  157. /// One reportable message: its source identifier (error case / symbol),
  158. /// the exact display string, its UI role, taxonomy node, and the category
  159. /// the current classifier pins it to.
  160. struct Row {
  161. let identifier: String
  162. let message: String
  163. let role: String
  164. let taxonomy: String
  165. let expected: TrioAlertCategory
  166. }
  167. static let rows: [Row] = [
  168. // TandemCore/Messages/CurrentStatus/AlarmStatus.swift:157
  169. Row(
  170. identifier: "AlarmResponseType.OCCLUSION_ALARM",
  171. message: "Occlusion Alarm",
  172. role: "alertTitle",
  173. taxonomy: "N1",
  174. expected: .occlusion
  175. ),
  176. // TandemCore/Messages/CurrentStatus/AlarmStatus.swift:157
  177. Row(
  178. identifier: "AlarmResponseType.OCCLUSION_ALARM",
  179. message: "An occlusion has occurred. Please check your pump site and tubing and restart insulin delivery.",
  180. role: "alertBody",
  181. taxonomy: "N1",
  182. expected: .occlusion
  183. ),
  184. // TandemCore/Messages/CurrentStatus/AlarmStatus.swift:145
  185. Row(
  186. identifier: "AlarmResponseType.cartridgeGroup",
  187. message: "Cartridge Alarm",
  188. role: "alertTitle",
  189. taxonomy: "N1",
  190. expected: .other("Cartridge Alarm")
  191. ),
  192. // TandemCore/Messages/CurrentStatus/AlarmStatus.swift:145
  193. Row(
  194. identifier: "AlarmResponseType.cartridgeGroup",
  195. message: "There is an issue with the cartridge and it needs to be replaced.",
  196. role: "alertBody",
  197. taxonomy: "N1",
  198. expected: .other("There is an issue with the cartridge and it needs to be replaced.")
  199. ),
  200. // TandemCore/Messages/CurrentStatus/AlarmStatus.swift:183
  201. Row(
  202. identifier: "AlarmResponseType.CARTRIDGE_REMOVED_ALARM",
  203. message: "Cartridge Removed Alarm",
  204. role: "alertTitle",
  205. taxonomy: "N1",
  206. expected: .other("Cartridge Removed Alarm")
  207. ),
  208. // TandemCore/Messages/CurrentStatus/AlarmStatus.swift:183
  209. Row(
  210. identifier: "AlarmResponseType.CARTRIDGE_REMOVED_ALARM",
  211. message: "The cartridge was removed from the pump. Please fill a new cartridge.",
  212. role: "alertBody",
  213. taxonomy: "N1",
  214. expected: .other("The cartridge was removed from the pump. Please fill a new cartridge.")
  215. ),
  216. // TandemCore/Messages/CurrentStatus/AlarmStatus.swift:160
  217. Row(
  218. identifier: "AlarmResponseType.PUMP_RESET_ALARM",
  219. message: "Pump Reset Alarm",
  220. role: "alertTitle",
  221. taxonomy: "N1",
  222. expected: .other("Pump Reset Alarm")
  223. ),
  224. // TandemCore/Messages/CurrentStatus/AlarmStatus.swift:160
  225. Row(
  226. identifier: "AlarmResponseType.PUMP_RESET_ALARM",
  227. message: "The pump was reset. IOB has been reset to 0 and CGM may need to be re-activated.",
  228. role: "alertBody",
  229. taxonomy: "N1",
  230. expected: .other("The pump was reset. IOB has been reset to 0 and CGM may need to be re-activated.")
  231. ),
  232. // TandemCore/Messages/CurrentStatus/AlarmStatus.swift:166
  233. Row(
  234. identifier: "AlarmResponseType.temperatureGroup",
  235. message: "Temperature Alarm",
  236. role: "alertTitle",
  237. taxonomy: "N1",
  238. expected: .other("Temperature Alarm")
  239. ),
  240. // TandemCore/Messages/CurrentStatus/AlarmStatus.swift:166
  241. Row(
  242. identifier: "AlarmResponseType.temperatureGroup",
  243. message: "Pump temperature is out of range and insulin cannot be safely delivered.",
  244. role: "alertBody",
  245. taxonomy: "N1",
  246. expected: .other("Pump temperature is out of range and insulin cannot be safely delivered.")
  247. ),
  248. // TandemCore/Messages/CurrentStatus/AlarmStatus.swift:177
  249. Row(
  250. identifier: "AlarmResponseType.ALTITUDE_ALARM",
  251. message: "Altitude Alarm",
  252. role: "alertTitle",
  253. taxonomy: "N1",
  254. expected: .other("Altitude Alarm")
  255. ),
  256. // TandemCore/Messages/CurrentStatus/AlarmStatus.swift:181
  257. Row(
  258. identifier: "AlarmResponseType.ATMOSPHERIC_PRESSURE_OUT_OF_RANGE_ALARM",
  259. message: "Atmospheric Pressure Out Of Range Alarm",
  260. role: "alertTitle",
  261. taxonomy: "N1",
  262. expected: .other("Atmospheric Pressure Out Of Range Alarm")
  263. ),
  264. // TandemCore/Messages/CurrentStatus/AlarmStatus.swift:179
  265. Row(
  266. identifier: "AlarmResponseType.STUCK_BUTTON_ALARM",
  267. message: "Stuck Button Alarm",
  268. role: "alertTitle",
  269. taxonomy: "N1",
  270. expected: .other("Stuck Button Alarm")
  271. ),
  272. // TandemCore/Messages/CurrentStatus/AlarmStatus.swift:179
  273. Row(
  274. identifier: "AlarmResponseType.STUCK_BUTTON_ALARM",
  275. message: "The pump button may be stuck or has been pressed for too long a period of time.",
  276. role: "alertBody",
  277. taxonomy: "N1",
  278. expected: .other("The pump button may be stuck or has been pressed for too long a period of time.")
  279. ),
  280. // TandemCore/Messages/CurrentStatus/AlarmStatus.swift:172
  281. Row(
  282. identifier: "AlarmResponseType.INVALID_DATE_ALARM",
  283. message: "Invalid Date Alarm",
  284. role: "alertTitle",
  285. taxonomy: "N1",
  286. expected: .other("Invalid Date Alarm")
  287. ),
  288. // TandemCore/Messages/CurrentStatus/AlarmStatus.swift:172
  289. Row(
  290. identifier: "AlarmResponseType.INVALID_DATE_ALARM",
  291. message: "The pump's configured date is invalid.",
  292. role: "alertBody",
  293. taxonomy: "N1",
  294. expected: .other("The pump's configured date is invalid.")
  295. ),
  296. // TandemCore/Messages/CurrentStatus/HighestAam.swift:106
  297. Row(
  298. identifier: "HighestAamResponse.malfunction",
  299. message: "Malfunction",
  300. role: "alertTitle",
  301. taxonomy: "N1",
  302. expected: .other("Malfunction")
  303. ),
  304. // TandemCore/Messages/CurrentStatus/AlarmStatus.swift:174
  305. Row(
  306. identifier: "AlarmResponseType.RESUME_PUMP_ALARM",
  307. message: "Resume Pump Alarm",
  308. role: "alertTitle",
  309. taxonomy: "N2",
  310. expected: .other("Resume Pump Alarm")
  311. ),
  312. // TandemCore/Messages/CurrentStatus/AlarmStatus.swift:174
  313. Row(
  314. identifier: "AlarmResponseType.RESUME_PUMP_ALARM",
  315. message: "Insulin delivery is currently off. Please restart insulin delivery soon.",
  316. role: "alertBody",
  317. taxonomy: "N2",
  318. expected: .other("Insulin delivery is currently off. Please restart insulin delivery soon.")
  319. ),
  320. // TandemCore/Messages/CurrentStatus/AlarmStatus.swift:162
  321. Row(
  322. identifier: "AlarmResponseType.AUTO_OFF_ALARM",
  323. message: "Auto Off Alarm",
  324. role: "alertTitle",
  325. taxonomy: "N2",
  326. expected: .other("Auto Off Alarm")
  327. ),
  328. // TandemCore/Messages/CurrentStatus/AlarmStatus.swift:162
  329. Row(
  330. identifier: "AlarmResponseType.AUTO_OFF_ALARM",
  331. message: "Pump will stop delivering insulin automatically soon because no user activity has occurred and the auto-off setting is enabled.",
  332. role: "alertBody",
  333. taxonomy: "N2",
  334. expected: .other(
  335. "Pump will stop delivering insulin automatically soon because no user activity has occurred and the auto-off setting is enabled."
  336. )
  337. ),
  338. // TandemKitUI/TandemPumpManager+UI.swift:112
  339. Row(
  340. identifier: "PumpStatusHighlight.suspended",
  341. message: "Insulin Suspended",
  342. role: "notificationTitle",
  343. taxonomy: "N2",
  344. expected: .other("Insulin Suspended")
  345. ),
  346. // TandemKitUI/TandemPumpManager+UI.swift:123
  347. Row(
  348. identifier: "PumpStatusHighlight.noInsulin",
  349. message: "No Insulin",
  350. role: "notificationTitle",
  351. taxonomy: "N4",
  352. expected: .other("No Insulin")
  353. ),
  354. // TandemCore/Messages/CurrentStatus/AlarmStatus.swift:164
  355. Row(
  356. identifier: "AlarmResponseType.EMPTY_CARTRIDGE_ALARM",
  357. message: "Empty Cartridge Alarm",
  358. role: "alertTitle",
  359. taxonomy: "N4",
  360. expected: .other("Empty Cartridge Alarm")
  361. ),
  362. // TandemCore/Messages/CurrentStatus/AlarmStatus.swift:164
  363. Row(
  364. identifier: "AlarmResponseType.EMPTY_CARTRIDGE_ALARM",
  365. message: "Cartridge is out of insulin and insulin delivery cannot occur. Please fill a new cartridge.",
  366. role: "alertBody",
  367. taxonomy: "N4",
  368. expected: .other("Cartridge is out of insulin and insulin delivery cannot occur. Please fill a new cartridge.")
  369. ),
  370. // TandemCore/Messages/CurrentStatus/AlarmStatus.swift:170
  371. Row(
  372. identifier: "AlarmResponseType.BATTERY_SHUTDOWN_ALARM",
  373. message: "Battery Shutdown Alarm",
  374. role: "alertTitle",
  375. taxonomy: "N5",
  376. expected: .other("Battery Shutdown Alarm")
  377. ),
  378. // TandemCore/Messages/CurrentStatus/AlarmStatus.swift:170
  379. Row(
  380. identifier: "AlarmResponseType.BATTERY_SHUTDOWN_ALARM",
  381. message: "Pump battery level is critically low and the device will shut down. Please charge pump immediately.",
  382. role: "alertBody",
  383. taxonomy: "N5",
  384. expected: .other(
  385. "Pump battery level is critically low and the device will shut down. Please charge pump immediately."
  386. )
  387. ),
  388. // TandemCore/Messages/CurrentStatus/CGMAlertStatus.swift:135
  389. Row(
  390. identifier: "CGMAlert.SENSOR_FAILED",
  391. message: "Sensor Failed Cgm Alert",
  392. role: "alertTitle",
  393. taxonomy: "N7",
  394. expected: .other("Sensor Failed Cgm Alert")
  395. ),
  396. // TandemCore/Messages/CurrentStatus/CGMAlertStatus.swift:135
  397. Row(
  398. identifier: "CGMAlert.LOW",
  399. message: "Low Cgm Alert",
  400. role: "alertTitle",
  401. taxonomy: "N7",
  402. expected: .other("Low Cgm Alert")
  403. ),
  404. // TandemCore/Messages/CurrentStatus/CGMAlertStatus.swift:135
  405. Row(
  406. identifier: "CGMAlert.HIGH",
  407. message: "High Cgm Alert",
  408. role: "alertTitle",
  409. taxonomy: "N7",
  410. expected: .other("High Cgm Alert")
  411. ),
  412. // TandemCore/Messages/CurrentStatus/CGMAlertStatus.swift:135
  413. Row(
  414. identifier: "CGMAlert.FIXED_LOW",
  415. message: "Fixed Low Cgm Alert",
  416. role: "alertTitle",
  417. taxonomy: "N7",
  418. expected: .other("Fixed Low Cgm Alert")
  419. ),
  420. // TandemCore/Messages/CurrentStatus/CGMAlertStatus.swift:135
  421. Row(
  422. identifier: "CGMAlert.SENSOR_EXPIRING",
  423. message: "Sensor Expiring Cgm Alert",
  424. role: "alertTitle",
  425. taxonomy: "N7",
  426. expected: .other("Sensor Expiring Cgm Alert")
  427. ),
  428. // TandemCore/Messages/CurrentStatus/CGMAlertStatus.swift:135
  429. Row(
  430. identifier: "CGMAlert.SENSOR_EXPIRED",
  431. message: "Sensor Expired Cgm Alert",
  432. role: "alertTitle",
  433. taxonomy: "N7",
  434. expected: .other("Sensor Expired Cgm Alert")
  435. ),
  436. // TandemCore/Messages/CurrentStatus/CGMAlertStatus.swift:135
  437. Row(
  438. identifier: "CGMAlert.OUT_OF_RANGE",
  439. message: "Out Of Range Cgm Alert",
  440. role: "alertTitle",
  441. taxonomy: "N7",
  442. expected: .other("Out Of Range Cgm Alert")
  443. ),
  444. // TandemCore/Messages/CurrentStatus/CGMAlertStatus.swift:135
  445. Row(
  446. identifier: "CGMAlert.TRANSMITTER",
  447. message: "Transmitter Cgm Alert",
  448. role: "alertTitle",
  449. taxonomy: "N7",
  450. expected: .other("Transmitter Cgm Alert")
  451. ),
  452. // TandemCore/Messages/CurrentStatus/ReminderStatus.swift:129
  453. Row(
  454. identifier: "ReminderType.namedCases",
  455. message: "Reminder",
  456. role: "alertTitle",
  457. taxonomy: "N14",
  458. expected: .other("Reminder")
  459. ),
  460. // TandemKit/PumpManager/TandemPumpManager.swift:341
  461. Row(
  462. identifier: "PumpCommError.pumpNotConnected",
  463. message: "Pump not connected",
  464. role: "errorMessage",
  465. taxonomy: "N8",
  466. expected: .other("Pump not connected")
  467. ),
  468. // TandemKit/PumpManager/TandemPumpManager.swift:341
  469. Row(
  470. identifier: "PumpCommError.pumpNotConnected",
  471. message: "Make sure iPhone is nearby the active pump",
  472. role: "errorMessage",
  473. taxonomy: "N8",
  474. expected: .other("Make sure iPhone is nearby the active pump")
  475. ),
  476. // TandemKit/PumpManager/TandemPumpManager.swift:362
  477. Row(
  478. identifier: "PumpCommError.noResponse",
  479. message: "No response from pump",
  480. role: "errorMessage",
  481. taxonomy: "N8",
  482. expected: .other("No response from pump")
  483. ),
  484. // TandemKit/PumpManager/TandemPeripheralManager.swift:227
  485. Row(
  486. identifier: "PumpCommError.errorResponse",
  487. message: "Pump Error: Failed to discoverServices: <error>",
  488. role: "errorMessage",
  489. taxonomy: "N8",
  490. expected: .other("Pump Error: Failed to discoverServices: <error>")
  491. ),
  492. // TandemKit/PumpManager/TandemPeripheralManager.swift:235
  493. Row(
  494. identifier: "PumpCommError.errorResponse",
  495. message: "Pump Error: No service: <PUMP_SERVICE>",
  496. role: "errorMessage",
  497. taxonomy: "N8",
  498. expected: .other("Pump Error: No service: <PUMP_SERVICE>")
  499. ),
  500. // TandemKit/PumpManager/TandemPeripheralManager.swift:245
  501. Row(
  502. identifier: "PumpCommError.errorResponse",
  503. message: "Pump Error: <message>",
  504. role: "errorMessage",
  505. taxonomy: "N8",
  506. expected: .other("Pump Error: <message>")
  507. ),
  508. // TandemKit/PumpManager/TandemPeripheralManager.swift:254
  509. Row(
  510. identifier: "PumpCommError.errorResponse",
  511. message: "Pump Error: No service: <DIS_SERVICE>",
  512. role: "errorMessage",
  513. taxonomy: "N8",
  514. expected: .other("Pump Error: No service: <DIS_SERVICE>")
  515. ),
  516. // TandemKit/PumpManager/TandemBluetoothManager.swift:318
  517. Row(
  518. identifier: "PumpCommError.errorResponse",
  519. message: "Pump Error: Failed to connect: <error>",
  520. role: "errorMessage",
  521. taxonomy: "N8",
  522. expected: .other("Pump Error: Failed to connect: <error>")
  523. ),
  524. // TandemKitUI/Views/Settings/TandemKitSettingsView.swift:230-233
  525. Row(
  526. identifier: "TandemKitSettingsView.pumpDisconnectedBanner",
  527. message: "Pump Disconnected",
  528. role: "notificationTitle",
  529. taxonomy: "N8",
  530. expected: .other("Pump Disconnected")
  531. ),
  532. // TandemKit/PumpManager/TandemPumpManager.swift:346
  533. Row(
  534. identifier: "TandemPumpManagerValidationError.controlIQModeActive",
  535. message: "The operation couldn't be completed.",
  536. role: "errorMessage",
  537. taxonomy: "N9",
  538. expected: .other("The operation couldn't be completed.")
  539. ),
  540. // TandemKit/PumpManager/TandemPumpManager.swift:509
  541. Row(
  542. identifier: "PumpCommError.invalidScheduledBasalRate",
  543. message: "Invalid scheduled basal rate",
  544. role: "errorMessage",
  545. taxonomy: "N9",
  546. expected: .other("Invalid scheduled basal rate")
  547. ),
  548. // TandemKit/PumpManager/TandemPumpManager.swift:509
  549. Row(
  550. identifier: "PumpCommError.invalidScheduledBasalRate",
  551. message: "Temp basal cannot be performed while scheduled basal rate is 0 or no basal profile exists",
  552. role: "errorMessage",
  553. taxonomy: "N9",
  554. expected: .other("Temp basal cannot be performed while scheduled basal rate is 0 or no basal profile exists")
  555. ),
  556. // TandemKit/PumpManager/TandemPumpManager.swift:368
  557. Row(
  558. identifier: "PumpCommError.invalidPacket",
  559. message: "Other PumpCommError: invalidPacket",
  560. role: "errorMessage",
  561. taxonomy: "N9",
  562. expected: .other("Other PumpCommError: invalidPacket")
  563. ),
  564. // TandemKit/PumpManager/TandemPumpManager.swift:436
  565. Row(
  566. identifier: "PumpCommError.noActiveBolus",
  567. message: "No active bolus to cancel",
  568. role: "errorMessage",
  569. taxonomy: "N9",
  570. expected: .other("No active bolus to cancel")
  571. ),
  572. // TandemKit/PumpManager/TandemPumpManager.swift:436
  573. Row(
  574. identifier: "PumpCommError.noActiveBolus",
  575. message: "No bolus is running.",
  576. role: "errorMessage",
  577. taxonomy: "N9",
  578. expected: .other("No bolus is running.")
  579. ),
  580. // TandemKit/PumpManager/TandemPumpManager.swift:897
  581. Row(
  582. identifier: "PumpCommError.errorResponse",
  583. message: "Pump Error: Error syncing time with pump",
  584. role: "errorMessage",
  585. taxonomy: "N9",
  586. expected: .other("Pump Error: Error syncing time with pump")
  587. ),
  588. // TandemKit/PumpManager/TandemPumpManager+SyncBasal.swift:33
  589. Row(
  590. identifier: "PumpCommError.requestConstructionFailed",
  591. message: "Failed to build pump request: Invalid IDPManager state in syncBasalRateSchedule",
  592. role: "errorMessage",
  593. taxonomy: "N9",
  594. expected: .other("Failed to build pump request: Invalid IDPManager state in syncBasalRateSchedule")
  595. ),
  596. // TandemKit/PumpManager/TandemPumpManager+SyncBasal.swift:33
  597. Row(
  598. identifier: "PumpCommError.requestConstructionFailed",
  599. message: "Please try again. If the problem persists, re-pair your pump.",
  600. role: "errorMessage",
  601. taxonomy: "N9",
  602. expected: .other("Please try again. If the problem persists, re-pair your pump.")
  603. ),
  604. // TandemKit/PumpManager/TandemPumpManager+SyncBasal.swift:68
  605. Row(
  606. identifier: "PumpCommError.requestConstructionFailed",
  607. message: "Failed to build pump request: No active profile found and got unexpected response creating initial profile",
  608. role: "errorMessage",
  609. taxonomy: "N9",
  610. expected: .other(
  611. "Failed to build pump request: No active profile found and got unexpected response creating initial profile"
  612. )
  613. ),
  614. // TandemKit/PumpManager/TandemPumpManager+SyncBasal.swift:106
  615. Row(
  616. identifier: "PumpCommError.requestConstructionFailed",
  617. message: "Failed to build pump request: No active profile found",
  618. role: "errorMessage",
  619. taxonomy: "N9",
  620. expected: .other("Failed to build pump request: No active profile found")
  621. ),
  622. // TandemKit/PumpManager/TandemPumpManager+SyncBasal.swift:166
  623. Row(
  624. identifier: "PumpCommError.requestConstructionFailed",
  625. message: "Failed to build pump request: Pump rejected SetIDPSegment",
  626. role: "errorMessage",
  627. taxonomy: "N9",
  628. expected: .other("Failed to build pump request: Pump rejected SetIDPSegment")
  629. ),
  630. // TandemKit/PumpManager/TandemPumpManager+SyncBasal.swift:172
  631. Row(
  632. identifier: "PumpCommError.errorResponse",
  633. message: "Pump Error: Error syncing basal rates: Pump rejected SetIDPSegment",
  634. role: "errorMessage",
  635. taxonomy: "N9",
  636. expected: .other("Pump Error: Error syncing basal rates: Pump rejected SetIDPSegment")
  637. ),
  638. // TandemKitUI/Views/Settings/TandemKitSettingsView.swift:74-84
  639. Row(
  640. identifier: "CommandFailure.resume",
  641. message: "Could not resume insulin delivery",
  642. role: "alertTitle",
  643. taxonomy: "N9",
  644. expected: .other("Could not resume insulin delivery")
  645. ),
  646. // TandemKitUI/Views/Settings/TandemKitSettingsView.swift:74-84
  647. Row(
  648. identifier: "CommandFailure.resume",
  649. message: "The pump rejected starting insulin delivery. Check \"View Notifications\" for more details. The pump may be reporting an occlusion, empty cartridge, or not filling the tubing.",
  650. role: "alertBody",
  651. taxonomy: "N9",
  652. expected: .occlusion
  653. ),
  654. // TandemKitUI/Views/Settings/TandemKitSettingsView.swift:85-96
  655. Row(
  656. identifier: "CommandFailure.suspend",
  657. message: "Could not suspend insulin delivery",
  658. role: "alertTitle",
  659. taxonomy: "N9",
  660. expected: .other("Could not suspend insulin delivery")
  661. ),
  662. // TandemKitUI/Views/Settings/TandemKitSettingsView.swift:85-96
  663. Row(
  664. identifier: "CommandFailure.suspend",
  665. message: "The pump rejected suspending insulin delivery. To urgently suspend insulin delivery, detach from your site.",
  666. role: "alertBody",
  667. taxonomy: "N9",
  668. expected: .other(
  669. "The pump rejected suspending insulin delivery. To urgently suspend insulin delivery, detach from your site."
  670. )
  671. ),
  672. // TandemKitUI/Views/Settings/TandemKitSettingsView.swift:97-107
  673. Row(
  674. identifier: "CommandFailure.controlIQ",
  675. message: "Could not change Control-IQ setting",
  676. role: "alertTitle",
  677. taxonomy: "N9",
  678. expected: .other("Could not change Control-IQ setting")
  679. ),
  680. // TandemKitUI/Views/Settings/TandemKitSettingsView.swift:97-107
  681. Row(
  682. identifier: "CommandFailure.controlIQ",
  683. message: "The pump rejected the Control-IQ change. Check \"View Notifications\" for more details.",
  684. role: "alertBody",
  685. taxonomy: "N9",
  686. expected: .other("The pump rejected the Control-IQ change. Check \"View Notifications\" for more details.")
  687. ),
  688. // TandemKitUI/Views/Settings/TandemKitSettingsView.swift:108-118
  689. Row(
  690. identifier: "CommandFailure.exerciseMode",
  691. message: "Could not change exercise mode",
  692. role: "alertTitle",
  693. taxonomy: "N9",
  694. expected: .other("Could not change exercise mode")
  695. ),
  696. // TandemKitUI/Views/Settings/TandemKitSettingsView.swift:108-118
  697. Row(
  698. identifier: "CommandFailure.exerciseMode",
  699. message: "The pump rejected the exercise mode change.",
  700. role: "alertBody",
  701. taxonomy: "N9",
  702. expected: .other("The pump rejected the exercise mode change.")
  703. ),
  704. // TandemKitUI/Views/Settings/TandemKitSettingsView.swift:119-129
  705. Row(
  706. identifier: "CommandFailure.sleepMode",
  707. message: "Could not change sleep mode",
  708. role: "alertTitle",
  709. taxonomy: "N9",
  710. expected: .other("Could not change sleep mode")
  711. ),
  712. // TandemKitUI/Views/Settings/TandemKitSettingsView.swift:119-129
  713. Row(
  714. identifier: "CommandFailure.sleepMode",
  715. message: "The pump rejected the sleep mode change.",
  716. role: "alertBody",
  717. taxonomy: "N9",
  718. expected: .other("The pump rejected the sleep mode change.")
  719. ),
  720. // TandemKitUI/Views/Settings/TandemKitSettingsView.swift:130-140
  721. Row(
  722. identifier: "CommandFailure.stopTempRate",
  723. message: "Could not stop temp rate",
  724. role: "alertTitle",
  725. taxonomy: "N9",
  726. expected: .other("Could not stop temp rate")
  727. ),
  728. // TandemKitUI/Views/Settings/TandemKitSettingsView.swift:130-140
  729. Row(
  730. identifier: "CommandFailure.stopTempRate",
  731. message: "The pump rejected stopping the temp rate.",
  732. role: "alertBody",
  733. taxonomy: "N9",
  734. expected: .other("The pump rejected stopping the temp rate.")
  735. ),
  736. // TandemKitUI/ViewModels/Settings/TandemKitControlIQSettingsViewModel.swift:94
  737. Row(
  738. identifier: "TandemKitControlIQSettingsViewModel.saveError",
  739. message: "Failed to update Control-IQ settings. Check pump connection.",
  740. role: "errorMessage",
  741. taxonomy: "N9",
  742. expected: .other("Failed to update Control-IQ settings. Check pump connection.")
  743. ),
  744. // TandemKitUI/ViewModels/Settings/TandemKitControlIQSettingsViewModel.swift:115
  745. Row(
  746. identifier: "TandemKitControlIQSettingsViewModel.saveError",
  747. message: "Failed to update sleep schedule. Check pump connection.",
  748. role: "errorMessage",
  749. taxonomy: "N9",
  750. expected: .other("Failed to update sleep schedule. Check pump connection.")
  751. ),
  752. // TandemKitUI/ViewModels/Settings/TandemKitNotificationsViewModel.swift:54
  753. Row(
  754. identifier: "TandemKitNotificationsViewModel.errorMessage",
  755. message: "Failed to dismiss notification",
  756. role: "errorMessage",
  757. taxonomy: "N9",
  758. expected: .other("Failed to dismiss notification")
  759. ),
  760. // TandemKitUI/ViewModels/CGMs/TandemKitCGMSettingsViewModel.swift:143
  761. Row(
  762. identifier: "TandemKitCGMSettingsViewModel.commandError",
  763. message: "Failed to stop CGM sensor. Check pump connection.",
  764. role: "errorMessage",
  765. taxonomy: "N9",
  766. expected: .other("Failed to stop CGM sensor. Check pump connection.")
  767. ),
  768. // TandemKitUI/Views/Onboarding/TandemKitScanView.swift:71-106
  769. Row(
  770. identifier: "TandemKitScanView.pairingCodePrompt",
  771. message: "Enter Pairing code",
  772. role: "alertTitle",
  773. taxonomy: "N10",
  774. expected: .other("Enter Pairing code")
  775. ),
  776. // TandemKitUI/Views/Onboarding/TandemKitScanView.swift:71-106
  777. Row(
  778. identifier: "TandemKitScanView.pairingCodePrompt",
  779. message: "Enter the pairing code from your pump",
  780. role: "alertBody",
  781. taxonomy: "N10",
  782. expected: .other("Enter the pairing code from your pump")
  783. ),
  784. // TandemKitUI/Views/Onboarding/TandemKitScanView.swift:71-106
  785. Row(
  786. identifier: "TandemKitScanView.pairingCodePrompt",
  787. message: "The pairing code is located on the inside of the cartridge area labeled as 'PIN' and consists of six numbers.",
  788. role: "alertBody",
  789. taxonomy: "N10",
  790. expected: .other(
  791. "The pairing code is located on the inside of the cartridge area labeled as 'PIN' and consists of six numbers."
  792. )
  793. ),
  794. // TandemKitUI/ViewModels/CGMs/TandemKitCGMSettingsViewModel.swift:83-88
  795. Row(
  796. identifier: "TandemKitCGMSettingsViewModel.transmitterBatteryStatus",
  797. message: "OK",
  798. role: "validation",
  799. taxonomy: "N11",
  800. expected: .other("OK")
  801. ),
  802. // TandemKitUI/ViewModels/CGMs/TandemKitCGMSettingsViewModel.swift:83-88
  803. Row(
  804. identifier: "TandemKitCGMSettingsViewModel.transmitterBatteryStatus",
  805. message: "Error",
  806. role: "validation",
  807. taxonomy: "N11",
  808. expected: .other("Error")
  809. ),
  810. // TandemKitUI/ViewModels/CGMs/TandemKitCGMSettingsViewModel.swift:83-88
  811. Row(
  812. identifier: "TandemKitCGMSettingsViewModel.transmitterBatteryStatus",
  813. message: "Expired",
  814. role: "validation",
  815. taxonomy: "N11",
  816. expected: .other("Expired")
  817. ),
  818. // TandemKitUI/ViewModels/CGMs/TandemKitCGMSettingsViewModel.swift:83-88
  819. Row(
  820. identifier: "TandemKitCGMSettingsViewModel.transmitterBatteryStatus",
  821. message: "Out of Range",
  822. role: "validation",
  823. taxonomy: "N11",
  824. expected: .other("Out of Range")
  825. ),
  826. // TandemKitUI/ViewModels/CGMs/TandemKitCGMSettingsViewModel.swift:83-88
  827. Row(
  828. identifier: "TandemKitCGMSettingsViewModel.transmitterBatteryStatus",
  829. message: "Unavailable",
  830. role: "validation",
  831. taxonomy: "N11",
  832. expected: .other("Unavailable")
  833. ),
  834. // TandemKitUI/ViewModels/Onboarding/TandemKitScanViewModel.swift:52
  835. Row(
  836. identifier: "TandemKitScanViewModel.pinCodePromptError",
  837. message: "Please enter a pairing code",
  838. role: "validation",
  839. taxonomy: "N12",
  840. expected: .other("Please enter a pairing code")
  841. ),
  842. // TandemKitUI/ViewModels/Onboarding/TandemKitScanViewModel.swift:61
  843. Row(
  844. identifier: "TandemKitScanViewModel.connectionErrorMessage",
  845. message: "Pump manager not initialized",
  846. role: "errorMessage",
  847. taxonomy: "N12",
  848. expected: .other("Pump manager not initialized")
  849. ),
  850. // TandemKitUI/ViewModels/Onboarding/TandemKitScanViewModel.swift:67
  851. Row(
  852. identifier: "TandemKitScanViewModel.connectionErrorMessage",
  853. message: "No peripheral selected",
  854. role: "errorMessage",
  855. taxonomy: "N12",
  856. expected: .other("No peripheral selected")
  857. ),
  858. // TandemKitUI/Views/Onboarding/TandemKitScanView.swift:62-70
  859. Row(
  860. identifier: "TandemKitScanView.connectionErrorAlert",
  861. message: "Error while connecting to device",
  862. role: "alertTitle",
  863. taxonomy: "N12",
  864. expected: .other("Error while connecting to device")
  865. ),
  866. // TandemKitUI/ViewModels/CGMs/TandemKitG6SetupViewModel.swift:71
  867. Row(
  868. identifier: "TandemKitG6SetupViewModel.error",
  869. message: "Failed to set transmitter.",
  870. role: "errorMessage",
  871. taxonomy: "N12",
  872. expected: .other("Failed to set transmitter.")
  873. ),
  874. // TandemKitUI/ViewModels/CGMs/TandemKitG6SetupViewModel.swift:96
  875. Row(
  876. identifier: "TandemKitG6SetupViewModel.error",
  877. message: "Failed to start CGM sensor",
  878. role: "errorMessage",
  879. taxonomy: "N12",
  880. expected: .other("Failed to start CGM sensor")
  881. ),
  882. // TandemKitUI/ViewModels/CGMs/TandemKitG7SetupViewModel.swift:48
  883. Row(
  884. identifier: "TandemKitG7SetupViewModel.error",
  885. message: "Failed to set G7 pairing code",
  886. role: "errorMessage",
  887. taxonomy: "N12",
  888. expected: .other("Failed to set G7 pairing code")
  889. ),
  890. // TandemKitUI/Views/CGMs/TandemKitCGMSettingsView.swift:62-72
  891. Row(
  892. identifier: "TandemKitCGMSettingsView.stopConfirmation",
  893. message: "Stop CGM Sensor",
  894. role: "alertTitle",
  895. taxonomy: "N12",
  896. expected: .other("Stop CGM Sensor")
  897. ),
  898. // TandemKitUI/Views/CGMs/TandemKitCGMSettingsView.swift:62-72
  899. Row(
  900. identifier: "TandemKitCGMSettingsView.stopConfirmation",
  901. message: "Are you sure you want to stop the CGM sensor session on your pump?",
  902. role: "alertBody",
  903. taxonomy: "N12",
  904. expected: .other("Are you sure you want to stop the CGM sensor session on your pump?")
  905. ),
  906. // TandemKitUI/Views/CGMs/TandemKitG6SetupWizardView.swift:27-36
  907. Row(
  908. identifier: "TandemKitSetupWizard.errorAlert",
  909. message: "Error",
  910. role: "alertTitle",
  911. taxonomy: "N12",
  912. expected: .other("Error")
  913. ),
  914. // TandemKitUI/Views/Settings/TandemKitSettingsView.swift:492-516
  915. Row(
  916. identifier: "TandemKitSettingsView.switchToCIQ",
  917. message: "Disable %@ Algorithm",
  918. role: "alertTitle",
  919. taxonomy: "N13",
  920. expected: .other("Disable %@ Algorithm")
  921. ),
  922. // TandemKitUI/Views/Settings/TandemKitSettingsView.swift:492-516
  923. Row(
  924. identifier: "TandemKitSettingsView.switchToCIQ",
  925. message: "This will DISABLE %@ in favor of the Control-IQ algorithm built-in to your pump.\n\n%@ will stop issuing temp basals and boluses. Are you sure?",
  926. role: "alertBody",
  927. taxonomy: "N13",
  928. expected: .other(
  929. "This will DISABLE %@ in favor of the Control-IQ algorithm built-in to your pump.\n\n%@ will stop issuing temp basals and boluses. Are you sure?"
  930. )
  931. ),
  932. // TandemKitUI/Views/Settings/TandemKitSettingsView.swift:517-544
  933. Row(
  934. identifier: "TandemKitSettingsView.switchToDIY",
  935. message: "Enable %@ Algorithm",
  936. role: "alertTitle",
  937. taxonomy: "N13",
  938. expected: .other("Enable %@ Algorithm")
  939. ),
  940. // TandemKitUI/Views/Settings/TandemKitSettingsView.swift:517-544
  941. Row(
  942. identifier: "TandemKitSettingsView.switchToDIY",
  943. message: "This will disable Control-IQ on your pump and allow %@ to resume %@ closed-loop dosing.",
  944. role: "alertBody",
  945. taxonomy: "N13",
  946. expected: .other("This will disable Control-IQ on your pump and allow %@ to resume %@ closed-loop dosing.")
  947. ),
  948. // TandemKitUI/Views/Settings/TandemKitSettingsView.swift:567-584
  949. Row(
  950. identifier: "TandemKitSettingsView.stopTempRate",
  951. message: "Temp Basal Active",
  952. role: "alertTitle",
  953. taxonomy: "N13",
  954. expected: .other("Temp Basal Active")
  955. ),
  956. // TandemKitUI/Views/Settings/TandemKitSettingsView.swift:567-584
  957. Row(
  958. identifier: "TandemKitSettingsView.stopTempRate",
  959. message: "The current temp rate is <rate> for the next <duration>. Would you like to revert to your scheduled basal rate?",
  960. role: "alertBody",
  961. taxonomy: "N13",
  962. expected: .other(
  963. "The current temp rate is <rate> for the next <duration>. Would you like to revert to your scheduled basal rate?"
  964. )
  965. ),
  966. // TandemKitUI/Views/Settings/TandemKitSettingsView.swift:585-597
  967. Row(
  968. identifier: "TandemKitSettingsView.stopTempRateClosedLoopWarning",
  969. message: "WARNING",
  970. role: "alertTitle",
  971. taxonomy: "N13",
  972. expected: .other("WARNING")
  973. ),
  974. // TandemKitUI/Views/Settings/TandemKitSettingsView.swift:585-597
  975. Row(
  976. identifier: "TandemKitSettingsView.stopTempRateClosedLoopWarning",
  977. message: "Closed loop is currently enabled. Cancelling the active temp rate on the pump will NOT place you in open-loop mode, and the next run of the oref algorithm will set a new temp rate.",
  978. role: "alertBody",
  979. taxonomy: "N13",
  980. expected: .other(
  981. "Closed loop is currently enabled. Cancelling the active temp rate on the pump will NOT place you in open-loop mode, and the next run of the oref algorithm will set a new temp rate."
  982. )
  983. ),
  984. // TandemKitUI/Views/Settings/TandemKitSettingsView.swift:545-557
  985. Row(
  986. identifier: "TandemKitSettingsView.diyRestored",
  987. message: "%@ Mode Restored",
  988. role: "alertTitle",
  989. taxonomy: "N14",
  990. expected: .other("%@ Mode Restored")
  991. ),
  992. // TandemKitUI/Views/Settings/TandemKitSettingsView.swift:545-557
  993. Row(
  994. identifier: "TandemKitSettingsView.diyRestored",
  995. message: "Control-IQ has been disabled and you are currently in open-loop. To re-enable %@ closed-loop, go to your app's Algorithm settings.",
  996. role: "alertBody",
  997. taxonomy: "N14",
  998. expected: .other(
  999. "Control-IQ has been disabled and you are currently in open-loop. To re-enable %@ closed-loop, go to your app's Algorithm settings."
  1000. )
  1001. ),
  1002. // TandemKitUI/Views/Settings/TandemKitSettingsView.swift:558-566
  1003. Row(
  1004. identifier: "TandemKitSettingsView.noCGMConnected",
  1005. message: "No CGM Connected",
  1006. role: "alertTitle",
  1007. taxonomy: "N14",
  1008. expected: .other("No CGM Connected")
  1009. ),
  1010. // TandemKitUI/Views/Settings/TandemKitSettingsView.swift:558-566
  1011. Row(
  1012. identifier: "TandemKitSettingsView.noCGMConnected",
  1013. message: "Control-IQ was enabled but no CGM is connected to the pump.\n\nUntil a CGM sensor is connected, your configured profile rates will be used. To connect a sensor, go to CGM Settings below.",
  1014. role: "alertBody",
  1015. taxonomy: "N14",
  1016. expected: .other(
  1017. "Control-IQ was enabled but no CGM is connected to the pump.\n\nUntil a CGM sensor is connected, your configured profile rates will be used. To connect a sensor, go to CGM Settings below."
  1018. )
  1019. ),
  1020. // TandemKitUI/Views/Settings/TandemKitSettingsView.swift:33-52
  1021. Row(
  1022. identifier: "TandemKitSettingsView.removePump",
  1023. message: "Remove Pump",
  1024. role: "alertTitle",
  1025. taxonomy: "N14",
  1026. expected: .other("Remove Pump")
  1027. ),
  1028. // TandemKitUI/Views/Settings/TandemKitSettingsView.swift:33-52
  1029. Row(
  1030. identifier: "TandemKitSettingsView.removePump",
  1031. message: "Are you sure you want to remove the pump from %@? You will need to re-pair the device and have access to the charging pad.",
  1032. role: "alertBody",
  1033. taxonomy: "N14",
  1034. expected: .other(
  1035. "Are you sure you want to remove the pump from %@? You will need to re-pair the device and have access to the charging pad."
  1036. )
  1037. ),
  1038. // TandemKitUI/Views/Settings/TandemKitSettingsView.swift:33-52
  1039. Row(
  1040. identifier: "TandemKitSettingsView.removePump",
  1041. message: "Delete Pump",
  1042. role: "alertBody",
  1043. taxonomy: "N14",
  1044. expected: .other("Delete Pump")
  1045. ),
  1046. // TandemKitUI/Views/Settings/TandemKitSettingsView.swift:54-60
  1047. Row(
  1048. identifier: "TandemKitSettingsView.syncTime",
  1049. message: "Time Change Detected",
  1050. role: "alertTitle",
  1051. taxonomy: "N14",
  1052. expected: .other("Time Change Detected")
  1053. ),
  1054. // TandemKitUI/Views/Settings/TandemKitSettingsView.swift:54-60
  1055. Row(
  1056. identifier: "TandemKitSettingsView.syncTime",
  1057. 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?",
  1058. role: "alertBody",
  1059. taxonomy: "N14",
  1060. expected: .other(
  1061. "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?"
  1062. )
  1063. ),
  1064. // TandemKitUI/Views/Settings/TandemKitSettingsView.swift:234-245
  1065. Row(
  1066. identifier: "TandemKitSettingsView.noCGMBanner",
  1067. message: "No CGM connected to pump",
  1068. role: "notificationTitle",
  1069. taxonomy: "N14",
  1070. expected: .other("No CGM connected to pump")
  1071. ),
  1072. // TandemKitUI/Views/Settings/TandemKitSettingsView.swift:234-245
  1073. Row(
  1074. identifier: "TandemKitSettingsView.noCGMBanner",
  1075. message: "%@ closed-loop is disabled with your pump's Control-IQ algorithm on, but no CGM is connected, so you are in open-loop.",
  1076. role: "notificationBody",
  1077. taxonomy: "N14",
  1078. expected: .other(
  1079. "%@ closed-loop is disabled with your pump's Control-IQ algorithm on, but no CGM is connected, so you are in open-loop."
  1080. )
  1081. ),
  1082. // TandemKitUI/Views/Settings/TandemKitSettingsView.swift:246-254
  1083. Row(
  1084. identifier: "TandemKitSettingsView.controlIQBanner",
  1085. message: "Control-IQ pump algorithm enabled",
  1086. role: "notificationTitle",
  1087. taxonomy: "N14",
  1088. expected: .other("Control-IQ pump algorithm enabled")
  1089. ),
  1090. // TandemKitUI/Views/Settings/TandemKitSettingsView.swift:246-254
  1091. Row(
  1092. identifier: "TandemKitSettingsView.controlIQBanner",
  1093. message: "%@ closed-loop is not in use.",
  1094. role: "notificationBody",
  1095. taxonomy: "N14",
  1096. expected: .other("%@ closed-loop is not in use.")
  1097. ),
  1098. // TandemKitUI/Views/Settings/TandemKitSettingsView.swift:255-261
  1099. Row(
  1100. identifier: "TandemKitSettingsView.openLoopBanner",
  1101. message: "Open loop mode",
  1102. role: "notificationTitle",
  1103. taxonomy: "N14",
  1104. expected: .other("Open loop mode")
  1105. ),
  1106. // TandemKitUI/Views/Settings/TandemKitSettingsView.swift:263-273
  1107. Row(
  1108. identifier: "TandemKitSettingsView.timeSyncWarningBanner",
  1109. message: "The time on your pump is different from the current time. Your pump's time controls your scheduled therapy settings. Scroll down to Pump Time row to review the time difference and configure your pump.",
  1110. role: "notificationBody",
  1111. taxonomy: "N14",
  1112. expected: .other(
  1113. "The time on your pump is different from the current time. Your pump's time controls your scheduled therapy settings. Scroll down to Pump Time row to review the time difference and configure your pump."
  1114. )
  1115. ),
  1116. // TandemKitUI/ViewModels/CGMs/TandemKitCGMSettingsViewModel.swift:23-27
  1117. Row(
  1118. identifier: "CGMSessionState.displayText",
  1119. message: "Stopped",
  1120. role: "validation",
  1121. taxonomy: "N14",
  1122. expected: .other("Stopped")
  1123. ),
  1124. // TandemKitUI/ViewModels/CGMs/TandemKitCGMSettingsViewModel.swift:23-27
  1125. Row(
  1126. identifier: "CGMSessionState.displayText",
  1127. message: "Starting...",
  1128. role: "validation",
  1129. taxonomy: "N14",
  1130. expected: .other("Starting...")
  1131. ),
  1132. // TandemKitUI/ViewModels/CGMs/TandemKitCGMSettingsViewModel.swift:23-27
  1133. Row(
  1134. identifier: "CGMSessionState.displayText",
  1135. message: "Active",
  1136. role: "validation",
  1137. taxonomy: "N14",
  1138. expected: .other("Active")
  1139. ),
  1140. // TandemKitUI/ViewModels/CGMs/TandemKitCGMSettingsViewModel.swift:23-27
  1141. Row(
  1142. identifier: "CGMSessionState.displayText",
  1143. message: "Stopping...",
  1144. role: "validation",
  1145. taxonomy: "N14",
  1146. expected: .other("Stopping...")
  1147. ),
  1148. // TandemKitUI/ViewModels/CGMs/TandemKitCGMSettingsViewModel.swift:23-27
  1149. Row(
  1150. identifier: "CGMSessionState.displayText",
  1151. message: "Unknown",
  1152. role: "validation",
  1153. taxonomy: "N14",
  1154. expected: .other("Unknown")
  1155. ),
  1156. // TandemKitUI/Views/CGMs/TandemKitCGMSettingsView.swift:151-157
  1157. Row(
  1158. identifier: "TandemKitCGMSettingsView.gracePeriodRow",
  1159. message: "Grace Period",
  1160. role: "validation",
  1161. taxonomy: "N14",
  1162. expected: .other("Grace Period")
  1163. ),
  1164. // TandemKitUI/Views/CGMs/TandemKitCGMSettingsView.swift:133-140
  1165. Row(
  1166. identifier: "TandemKitCGMSettingsView.timeRemainingRow",
  1167. message: "Time Remaining",
  1168. role: "validation",
  1169. taxonomy: "N14",
  1170. expected: .other("Time Remaining")
  1171. )
  1172. ]
  1173. @Test(
  1174. "each (identifier, message) classifies as pinned",
  1175. arguments: rows
  1176. ) func eachMessageClassifiesAsPinned(row: Row) {
  1177. // Pins CURRENT behavior. Feeding the display string exercises the
  1178. // substring matcher over real prose; this MUST stay green.
  1179. #expect(TrioAlertClassifier.categorize(error: StubError(description: row.message)) == row.expected)
  1180. }
  1181. // MARK: - Coverage-gap ratchet
  1182. /// "identifier — message" keys for emissions whose taxonomy implies a
  1183. /// mappable bucket but the classifier returns `.other` because its
  1184. /// space-free tokens miss the spaced prose. Each line below names the
  1185. /// bucket the row SHOULD hit and why the tokens miss it. This set stays
  1186. /// green today and FAILS when the classifier is improved (forcing a
  1187. /// conscious update), ratcheting coverage upward.
  1188. ///
  1189. /// Per-gap rationale (bucket it SHOULD hit / why tokens miss):
  1190. /// - N1 alarm titles & bodies (cartridge, cartridge-removed, pump-reset,
  1191. /// temperature, altitude, atmospheric-pressure, stuck-button,
  1192. /// invalid-date) and `HighestAamResponse.malfunction` -> SHOULD be
  1193. /// `.hardwareFault`, but none contain the "fault" token; the prose says
  1194. /// "Cartridge Alarm" / "Malfunction" / etc.
  1195. /// (TandemCore/Messages/CurrentStatus/AlarmStatus.swift,
  1196. /// HighestAam.swift:106). NB: "Occlusion Alarm" is NOT a gap — it hits
  1197. /// `.occlusion` via the "occlusion" token (AlarmStatus.swift:157).
  1198. /// - N4 reservoir-empty prose ("No Insulin", "Empty Cartridge Alarm",
  1199. /// "Cartridge is out of insulin ...") -> SHOULD be `.reservoirEmpty`, but
  1200. /// the token is the space-free "reservoirempty"/"emptyreservoir" and the
  1201. /// prose never contains it (TandemKitUI/TandemPumpManager+UI.swift:123,
  1202. /// AlarmStatus.swift:164).
  1203. /// - N7 CGM alert titles render as space-separated formatted case names
  1204. /// ("Sensor Failed Cgm Alert", "Sensor Expired Cgm Alert", ...) -> SHOULD
  1205. /// be `.sensorFailure`, but "sensorfailed"/"sensorstopped" (and
  1206. /// "sensorexpired") are space-free and never appear in the formatted text
  1207. /// (TandemCore/Messages/CurrentStatus/CGMAlertStatus.swift:135).
  1208. /// - N8 connectivity strings ("Pump not connected", "Make sure iPhone is
  1209. /// nearby ...", "No response from pump", "Pump Error: ...",
  1210. /// "Pump Disconnected") -> SHOULD be `.commsTransient`, but
  1211. /// "notconnected"/"noresponse"/"comms"/"communication" are all space-free
  1212. /// or absent, so the spaced prose misses
  1213. /// (TandemKit/PumpManager/*.swift, TandemKitSettingsView.swift:230-233).
  1214. static let classifierCoverageGaps: Set<String> = [
  1215. "AlarmResponseType.cartridgeGroup — Cartridge Alarm",
  1216. "AlarmResponseType.cartridgeGroup — There is an issue with the cartridge and it needs to be replaced.",
  1217. "AlarmResponseType.CARTRIDGE_REMOVED_ALARM — Cartridge Removed Alarm",
  1218. "AlarmResponseType.CARTRIDGE_REMOVED_ALARM — The cartridge was removed from the pump. Please fill a new cartridge.",
  1219. "AlarmResponseType.PUMP_RESET_ALARM — Pump Reset Alarm",
  1220. "AlarmResponseType.PUMP_RESET_ALARM — The pump was reset. IOB has been reset to 0 and CGM may need to be re-activated.",
  1221. "AlarmResponseType.temperatureGroup — Temperature Alarm",
  1222. "AlarmResponseType.temperatureGroup — Pump temperature is out of range and insulin cannot be safely delivered.",
  1223. "AlarmResponseType.ALTITUDE_ALARM — Altitude Alarm",
  1224. "AlarmResponseType.ATMOSPHERIC_PRESSURE_OUT_OF_RANGE_ALARM — Atmospheric Pressure Out Of Range Alarm",
  1225. "AlarmResponseType.STUCK_BUTTON_ALARM — Stuck Button Alarm",
  1226. "AlarmResponseType.STUCK_BUTTON_ALARM — The pump button may be stuck or has been pressed for too long a period of time.",
  1227. "AlarmResponseType.INVALID_DATE_ALARM — Invalid Date Alarm",
  1228. "AlarmResponseType.INVALID_DATE_ALARM — The pump's configured date is invalid.",
  1229. "HighestAamResponse.malfunction — Malfunction",
  1230. "PumpStatusHighlight.noInsulin — No Insulin",
  1231. "AlarmResponseType.EMPTY_CARTRIDGE_ALARM — Empty Cartridge Alarm",
  1232. "AlarmResponseType.EMPTY_CARTRIDGE_ALARM — Cartridge is out of insulin and insulin delivery cannot occur. Please fill a new cartridge.",
  1233. "CGMAlert.SENSOR_FAILED — Sensor Failed Cgm Alert",
  1234. "CGMAlert.LOW — Low Cgm Alert",
  1235. "CGMAlert.HIGH — High Cgm Alert",
  1236. "CGMAlert.FIXED_LOW — Fixed Low Cgm Alert",
  1237. "CGMAlert.SENSOR_EXPIRING — Sensor Expiring Cgm Alert",
  1238. "CGMAlert.SENSOR_EXPIRED — Sensor Expired Cgm Alert",
  1239. "CGMAlert.OUT_OF_RANGE — Out Of Range Cgm Alert",
  1240. "CGMAlert.TRANSMITTER — Transmitter Cgm Alert",
  1241. "PumpCommError.pumpNotConnected — Pump not connected",
  1242. "PumpCommError.pumpNotConnected — Make sure iPhone is nearby the active pump",
  1243. "PumpCommError.noResponse — No response from pump",
  1244. "PumpCommError.errorResponse — Pump Error: Failed to discoverServices: <error>",
  1245. "PumpCommError.errorResponse — Pump Error: No service: <PUMP_SERVICE>",
  1246. "PumpCommError.errorResponse — Pump Error: <message>",
  1247. "PumpCommError.errorResponse — Pump Error: No service: <DIS_SERVICE>",
  1248. "PumpCommError.errorResponse — Pump Error: Failed to connect: <error>",
  1249. "TandemKitSettingsView.pumpDisconnectedBanner — Pump Disconnected"
  1250. ]
  1251. @Test("classifier coverage gaps are exactly as documented") func classifierCoverageGapsAreExactlyAsDocumented() {
  1252. // Recompute the gap set from the rows: a row is a coverage gap when the
  1253. // classifier returns `.other` for its message (so it is unbucketed)
  1254. // even though the taxonomy implies a mappable bucket (taxonomyBucket
  1255. // other than "other"). We detect "classifier returned .other" by
  1256. // comparing against `.other(message)`. This stays equal to the pinned
  1257. // set today and DIVERGES (failing this test) the moment the classifier
  1258. // learns to bucket any of these strings — the ratchet.
  1259. var computed = Set<String>()
  1260. for row in Self.rows {
  1261. let isOther: Bool
  1262. if case .other = row.expected { isOther = true } else { isOther = false }
  1263. if isOther {
  1264. computed.insert("\(row.identifier) — \(row.message)")
  1265. }
  1266. }
  1267. // `computed` is every `.other` row (mappable or not). Intersect with the
  1268. // documented gap keys to assert the documented set is exactly the
  1269. // taxonomy-mappable subset, and that none of them have started bucketing.
  1270. #expect(Self.classifierCoverageGaps.isSubset(of: computed))
  1271. #expect(Self.classifierCoverageGaps == Self.classifierCoverageGaps.intersection(computed))
  1272. #expect(Self.classifierCoverageGaps.count == 35)
  1273. }
  1274. }