| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884 |
- import Foundation
- import LoopKit
- import Testing
- @testable import Trio
- /// Manager-emission pins for **MedtrumKit** (`managerIdentifier` family
- /// `"Medtrum"`). Rows come from the synthesis audit over the bundled pump
- /// managers (`managers`/`pump`/`MedtrumKit`).
- ///
- /// What this suite pins:
- /// - `registryBehaviorIsPinned`: the CURRENT `AlertCatalogRegistry.lookup`
- /// level for every alert identifier the Medtrum registry block keys on
- /// (the exact strings, including the misspelled double-s
- /// `patch-occlussion`). These are GREEN assertions of present behavior,
- /// not of the ideal.
- /// - `classifierCategoryIsPinned`: the CURRENT `TrioAlertClassifier`
- /// categorization for the one confidently-statable error that reaches
- /// `APSManager.processError` via a PumpManager completion handler
- /// (`PumpManagerError.uncertainDelivery`).
- /// - `knownEscalationGapsAreExactlyAsDocumented`: a ratchet over the
- /// documented gap set; FAILS when a gap is fixed, prompting an update.
- ///
- /// One-line gap summary: MedtrumKit issues NO LoopKit Alerts — every pump
- /// alarm is a `UNUserNotificationCenter` local notification, so
- /// `AlertCatalogRegistry.lookup` is never exercised by real emissions and
- /// the carefully-built Medtrum registry block escalates nothing today.
- /// Taxonomy-Critical conditions (occlusion/fault/reservoir-empty/daily &
- /// hourly suspend) reach the user only at the UN-notification default level
- /// (treated as `.timeSensitive` — unknown -> assume time-sensitive),
- /// below their `.critical` taxonomy mapping.
- @Suite("Trio Alert Emission: MedtrumKit") struct MedtrumKitAlertEmissionTests {
- private func id(_ manager: String, _ alertID: String) -> Alert.Identifier {
- Alert.Identifier(managerIdentifier: manager, alertIdentifier: alertID)
- }
- // MARK: - Registry behavior (CURRENT, must be green)
- /// `(alertIdentifier, currentRegistryLevel?)` exactly as the registry
- /// keys on them today. `nil` means no entry (pass-through).
- static let registryRows: [(alertID: String, level: Alert.InterruptionLevel?)] = [
- ("com.nightscout.medtrumkit.patch-occlussion", .critical), // NotificationManager.swift:61 / Registry:152 (double-s)
- ("com.nightscout.medtrumkit.patch-fault", .critical), // NotificationManager.swift:71 / Registry:154
- ("com.nightscout.medtrumkit.patch-empty", .critical), // NotificationManager.swift:81 / Registry:155
- ("com.nightscout.medtrumkit.patch-daily-limit", .timeSensitive), // NotificationManager.swift:38 / Registry:130
- ("com.nightscout.medtrumkit.patch-hourly-limit", .timeSensitive), // NotificationManager.swift:48 / Registry:138
- ("com.nightscout.medtrumkit.reservoir-low", .timeSensitive), // NotificationManager.swift:91 / Registry:156
- ("com.nightscout.medtrumkit.patch-expired", .active) // NotificationManager.swift:15 / Registry:122
- ]
- @Test(
- "registry behavior is pinned for every emitted alert",
- arguments: registryRows
- ) func registryBehaviorIsPinned(alertID: String, level: Alert.InterruptionLevel?) {
- #expect(AlertCatalogRegistry.lookup(id("Medtrum", alertID))?.interruptionLevel == level)
- }
- // MARK: - Classifier behavior (CURRENT, must be green)
- /// Errors handed back through a PumpManager completion handler feed
- /// `String(describing:)` into `TrioAlertClassifier.categorize`. A
- /// `CustomStringConvertible` stub reproduces that exact input.
- private struct StubError: Error, CustomStringConvertible {
- let description: String
- }
- @Test(
- "classifier category is pinned for completion-handler errors",
- arguments: [
- // MedtrumPumpManager.swift:992 emits PumpManagerError.uncertainDelivery;
- // String(describing:) == "uncertainDelivery" -> .deliveryUncertain (.critical).
- // Matches taxonomy N3 Uncertain Delivery -> Critical. Not a gap.
- ("uncertainDelivery", TrioAlertCategory.deliveryUncertain)
- ]
- ) func classifierCategoryIsPinned(describing: String, expected: TrioAlertCategory) {
- #expect(TrioAlertClassifier.categorize(error: StubError(description: describing)) == expected)
- }
- // MARK: - Known escalation gaps (ratchet)
- /// Alert identifiers where the EFFECTIVE current level is less severe
- /// than the taxonomy level (`isGap == true` in the audit). Every row
- /// is a gap today because MedtrumKit issues these as UN local
- /// notifications, never as LoopKit Alerts — so `AlertCatalogRegistry`
- /// is never consulted and the effective level is the UN-notification
- /// default (unknown -> assume `.timeSensitive`).
- ///
- /// SHOULD-have taxonomy levels (for when these are wired through
- /// `issueAlert`):
- /// - patch-occlussion: N1 Critical -> .critical (NotificationManager.swift:61)
- /// - patch-fault: N1 Critical -> .critical (NotificationManager.swift:71)
- /// - patch-empty: N4 Critical -> .critical (NotificationManager.swift:81)
- /// - patch-daily-limit: N2 Critical -> .critical; registry only .timeSensitive (NotificationManager.swift:38)
- /// - patch-hourly-limit:N2 Critical -> .critical; registry only .timeSensitive (NotificationManager.swift:48)
- /// - reservoir-low: F1 High -> .timeSensitive; registry already .timeSensitive,
- /// gap is ONLY the missing issueAlert wiring (NotificationManager.swift:91)
- /// - patch-expired: F3 Medium -> .timeSensitive; registry only .active (NotificationManager.swift:15)
- static let knownEscalationGaps: Set<String> = [
- "com.nightscout.medtrumkit.patch-occlussion",
- "com.nightscout.medtrumkit.patch-fault",
- "com.nightscout.medtrumkit.patch-empty",
- "com.nightscout.medtrumkit.patch-daily-limit",
- "com.nightscout.medtrumkit.patch-hourly-limit",
- "com.nightscout.medtrumkit.reservoir-low",
- "com.nightscout.medtrumkit.patch-expired"
- ]
- /// `(alertID, effectiveLevel, taxonomyLevel)` — `effectiveLevel` is the
- /// registry level when present, else (no Alert issued) the
- /// UN-notification default, which we treat as `.timeSensitive`
- /// (unknown -> assume time-sensitive). A gap exists when
- /// `effectiveLevel` is less severe than `taxonomyLevel`. Because no
- /// LoopKit Alert is issued, the effective level for every row is the
- /// UN default regardless of the registry value.
- static let gapTable: [(alertID: String, effective: Alert.InterruptionLevel, taxonomy: Alert.InterruptionLevel)] = [
- ("com.nightscout.medtrumkit.patch-occlussion", .timeSensitive, .critical),
- ("com.nightscout.medtrumkit.patch-fault", .timeSensitive, .critical),
- ("com.nightscout.medtrumkit.patch-empty", .timeSensitive, .critical),
- ("com.nightscout.medtrumkit.patch-daily-limit", .timeSensitive, .critical),
- ("com.nightscout.medtrumkit.patch-hourly-limit", .timeSensitive, .critical),
- ("com.nightscout.medtrumkit.reservoir-low", .timeSensitive, .timeSensitive),
- ("com.nightscout.medtrumkit.patch-expired", .timeSensitive, .timeSensitive)
- ]
- /// Severity rank for comparing interruption levels (higher == more severe).
- private static func severity(_ level: Alert.InterruptionLevel) -> Int {
- switch level {
- case .active: return 0
- case .timeSensitive: return 1
- case .critical: return 2
- @unknown default: return 0
- }
- }
- @Test("known escalation gaps are exactly as documented") func knownEscalationGapsAreExactlyAsDocumented() {
- // Recompute the gap set from the table: a gap is an emission whose
- // effective level is strictly less severe than its taxonomy level.
- // reservoir-low / patch-expired tie on level but remain documented
- // gaps because no Alert is issued (registry override never fires);
- // they are listed in `knownEscalationGaps` directly. Reconcile the
- // strictly-less-severe rows against the documented set, accounting
- // for the architectural (no-issueAlert) gaps.
- let strictlyLessSevere = Set(
- Self.gapTable
- .filter { Self.severity($0.effective) < Self.severity($0.taxonomy) }
- .map(\.alertID)
- )
- // Every strictly-less-severe row must be documented.
- #expect(strictlyLessSevere.isSubset(of: Self.knownEscalationGaps))
- // The full documented set is the audit's gap set (includes the
- // tie-level architectural gaps reservoir-low + patch-expired).
- let auditedGaps = Set(Self.gapTable.map(\.alertID))
- #expect(Self.knownEscalationGaps == auditedGaps)
- }
- }
- /// SPEC — Message-text classification catalog for **MedtrumKit**
- /// (`managers`/`pump`/`MedtrumKit`).
- ///
- /// MedtrumKit does NOT use the LoopKit `Alert`/`AlertIssuer` system at all:
- /// there are zero `issueAlert`/`Alert(identifier:)` calls. Every pump alarm
- /// surfaces via `UNUserNotificationCenter` local notifications, LoopKit
- /// status surfaces (`pumpStatusHighlight`, `didError`,
- /// `completion(.failure)`), and in-app SwiftUI text / published
- /// `errorMessage` fields. So there are no real LoopKit `alertIdentifier`s to
- /// attach — the `identifier` column below is the error enum case name /
- /// source symbol per the synthesis instructions.
- ///
- /// IMPORTANT: in production the classifier is fed `String(describing: error)`
- /// (i.e. the error *case name*, e.g. `"uncertainDelivery"`), NOT these
- /// user-facing display strings. This suite instead feeds each emission's
- /// EXACT display string through a `StubError` so we can pin how
- /// `TrioAlertClassifier`'s substring matcher handles the real natural-language
- /// text a user would see. Because the classifier matches compressed/
- /// concatenated tokens ("occlusion", "reservoirempty", "lowreservoir",
- /// "fault", "timeout", …) while the Medtrum strings are prose, almost every
- /// meaningful alarm string falls through to `.other`. Only a handful match:
- /// occlusion via PatchState "Occlusion", fault via "Fault"/"in Fault state",
- /// and the BLE "timeout" strings. Notably the occlusion notification body is
- /// misspelled "occlussion" (double-s), which does NOT contain "occlusion" and
- /// thus mis-routes to `.other`.
- @Suite("Trio Alert Emission: MedtrumKit — Classification") struct MedtrumKitMessageClassificationTests {
- /// `String(describing:)` returns `description` verbatim, reproducing the
- /// exact text the classifier's substring matcher sees.
- private struct StubError: Error, CustomStringConvertible {
- let description: String
- }
- struct Row {
- let identifier: String
- let message: String
- let role: String
- let taxonomy: String
- let expected: TrioAlertCategory
- }
- /// Every distinct user-facing emission, paired with the classifier
- /// category it currently resolves to. `currentCategory == "other"` pins
- /// `.other(message)` with the EXACT original (non-lowercased) string.
- static let rows: [Row] = [
- // MedtrumKit/PumpManager/NotificationManager.swift:15
- Row(
- identifier: "NotificationManager.Identifiers.patchExpiredNotification",
- message: "Your patch will expire soon! / Your patch has %lld hours left",
- role: "notificationBody",
- taxonomy: "F3",
- expected: .other("Your patch will expire soon! / Your patch has %lld hours left")
- ),
- // MedtrumKit/PumpManager/NotificationManager.swift:38
- Row(
- identifier: "NotificationManager.Identifiers.patchDailyMaxNotification",
- message: "Insulin has been suspended! / Your patch has reached its daily maximum!",
- role: "notificationBody",
- taxonomy: "N2",
- expected: .other("Insulin has been suspended! / Your patch has reached its daily maximum!")
- ),
- // MedtrumKit/PumpManager/NotificationManager.swift:48
- Row(
- identifier: "NotificationManager.Identifiers.patchHourlyMaxNotification",
- message: "Insulin has been suspended! / Your patch has reached its hourly maximum!",
- role: "notificationBody",
- taxonomy: "N2",
- expected: .other("Insulin has been suspended! / Your patch has reached its hourly maximum!")
- ),
- // MedtrumKit/PumpManager/NotificationManager.swift:61
- // Misspelled "occlussion" (double-s) does NOT contain "occlusion" -> .other.
- Row(
- identifier: "NotificationManager.Identifiers.occlusionNotification",
- message: "Replace your patch now! / Your patch has detected an occlussion!",
- role: "notificationBody",
- taxonomy: "N1",
- expected: .other("Replace your patch now! / Your patch has detected an occlussion!")
- ),
- // MedtrumKit/PumpManager/NotificationManager.swift:71
- // Contains "Fault" -> .hardwareFault.
- Row(
- identifier: "NotificationManager.Identifiers.patchFaultNotification",
- message: "Replace your patch now! / Your patch is in Fault state!",
- role: "notificationBody",
- taxonomy: "N1",
- expected: .hardwareFault
- ),
- // MedtrumKit/PumpManager/NotificationManager.swift:81
- Row(
- identifier: "NotificationManager.Identifiers.reservoirEmptyNotification",
- message: "Replace your patch now! / Your patch is out of insulin!",
- role: "notificationBody",
- taxonomy: "N4",
- expected: .other("Replace your patch now! / Your patch is out of insulin!")
- ),
- // MedtrumKit/PumpManager/NotificationManager.swift:91
- Row(
- identifier: "NotificationManager.Identifiers.reservoirEmptyNotification",
- message: "Reservoir low (%lld U) / Your patch is running out of insulin!",
- role: "notificationBody",
- taxonomy: "F1",
- expected: .other("Reservoir low (%lld U) / Your patch is running out of insulin!")
- ),
- // MedtrumKitUI/MedtrumKitPumpManager+UI.swift:76
- Row(
- identifier: "PumpStatusHighlight",
- message: "No patch",
- role: "errorMessage",
- taxonomy: "N14",
- expected: .other("No patch")
- ),
- // MedtrumKitUI/MedtrumKitPumpManager+UI.swift:85
- Row(
- identifier: "PumpStatusHighlight",
- message: "No Insulin",
- role: "errorMessage",
- taxonomy: "N4",
- expected: .other("No Insulin")
- ),
- // MedtrumKitUI/MedtrumKitPumpManager+UI.swift:91
- Row(
- identifier: "PumpStatusHighlight",
- message: "Insulin Suspended",
- role: "errorMessage",
- taxonomy: "N2",
- expected: .other("Insulin Suspended")
- ),
- // MedtrumKitUI/MedtrumKitPumpManager+UI.swift:100
- Row(
- identifier: "PumpStatusHighlight",
- message: "Patch expired. Basal only.",
- role: "errorMessage",
- taxonomy: "N6",
- expected: .other("Patch expired. Basal only.")
- ),
- // MedtrumKitUI/MedtrumKitPumpManager+UI.swift:109
- Row(
- identifier: "PumpStatusHighlight",
- message: "Signal Loss",
- role: "errorMessage",
- taxonomy: "N8",
- expected: .other("Signal Loss")
- ),
- // MedtrumKitUI/MedtrumKitPumpManager+UI.swift:118
- Row(
- identifier: "PumpStatusHighlight",
- message: "Patch Error",
- role: "errorMessage",
- taxonomy: "N1",
- expected: .other("Patch Error")
- ),
- // MedtrumKit/PumpManager/PeripheralManager.swift:126
- Row(
- identifier: "MedtrumConnectError.failedToCompleteAuthorizationFlow",
- message: "invalid response",
- role: "errorMessage",
- taxonomy: "N10",
- expected: .other("invalid response")
- ),
- // MedtrumKit/PumpManager/PeripheralManager.swift:209
- Row(
- identifier: "MedtrumConnectError.failedToDiscoverServices",
- message: "No Medtrum service found - <discovered service UUIDs>",
- role: "errorMessage",
- taxonomy: "N8",
- expected: .other("No Medtrum service found - <discovered service UUIDs>")
- ),
- // MedtrumKit/PumpManager/PeripheralManager.swift:231
- Row(
- identifier: "MedtrumConnectError.failedToDiscoverCharacteristics",
- message: "Failed to discover read, write or config characteristic - <UUIDs>",
- role: "errorMessage",
- taxonomy: "N8",
- expected: .other("Failed to discover read, write or config characteristic - <UUIDs>")
- ),
- // MedtrumKit/PumpManager/BluetoothManager.swift:76,162,333
- // Contains "Timeout" -> .commsTransient.
- Row(
- identifier: "MedtrumConnectError.failedToConnectToDevice",
- message: "Failed to connect to patch -> Timeout reached",
- role: "errorMessage",
- taxonomy: "N8",
- expected: .commsTransient
- ),
- // MedtrumKit/PumpManager/BluetoothManager.swift:114,128; MedtrumPumpManager.swift:1009
- Row(
- identifier: "MedtrumConnectError.failedToFindDevice",
- message: "Failed to connect to patch",
- role: "errorMessage",
- taxonomy: "N8",
- expected: .other("Failed to connect to patch")
- ),
- // MedtrumKit/PumpManager/BluetoothManager.swift:45
- Row(
- identifier: "MedtrumScanError.invalidBluetoothState",
- message: "Invalid Bluetooth state: <state>",
- role: "errorMessage",
- taxonomy: "N8",
- expected: .other("Invalid Bluetooth state: <state>")
- ),
- // MedtrumKit/PumpManager/PeripheralManager.swift:39,71
- Row(
- identifier: "MedtrumWriteError.noData",
- message: "No data",
- role: "errorMessage",
- taxonomy: "N8",
- expected: .other("No data")
- ),
- // MedtrumKit/PumpManager/PeripheralManager.swift:58
- Row(
- identifier: "MedtrumWriteError.noWriteCharacteristic",
- message: "No write characteristic. Device might be disconnected",
- role: "errorMessage",
- taxonomy: "N8",
- expected: .other("No write characteristic. Device might be disconnected")
- ),
- // MedtrumKit/PumpManager/PeripheralManager.swift:99
- // Contains "timeout" -> .commsTransient.
- Row(
- identifier: "MedtrumWriteError.timeout",
- message: "A command timeout is hit",
- role: "errorMessage",
- taxonomy: "N8",
- expected: .commsTransient
- ),
- // MedtrumKit/PumpManager/PeripheralManager.swift:314,321
- Row(
- identifier: "MedtrumWriteError.invalidData",
- message: "Invalid data received",
- role: "errorMessage",
- taxonomy: "N8",
- expected: .other("Invalid data received")
- ),
- // MedtrumKit/PumpManager/BluetoothManager.swift:170
- Row(
- identifier: "MedtrumWriteError.noManager",
- message: "No peripheral manager",
- role: "errorMessage",
- taxonomy: "N8",
- expected: .other("No peripheral manager")
- ),
- // MedtrumKit/PumpManager/MedtrumPumpManager.swift:992
- // "uncertain delivery" (with a space) != "uncertaindelivery" -> .other.
- Row(
- identifier: "PumpManagerError.uncertainDelivery",
- message: "uncertain delivery",
- role: "errorMessage",
- taxonomy: "N3",
- expected: .other("uncertain delivery")
- ),
- // MedtrumKit/PumpManager/MedtrumPumpManager.swift:281,405
- Row(
- identifier: "MedtrumConnectError.isBolussing",
- message: "Bolus issue. Patch is already bolussing",
- role: "errorMessage",
- taxonomy: "N9",
- expected: .other("Bolus issue. Patch is already bolussing")
- ),
- // MedtrumKit/PumpManager/MedtrumPumpManager.swift:287
- Row(
- identifier: "MedtrumConnectError.isSuspended",
- message: "Bolus issue. Patch is suspended. Resume delivery",
- role: "errorMessage",
- taxonomy: "N9",
- expected: .other("Bolus issue. Patch is suspended. Resume delivery")
- ),
- // MedtrumKit/PumpManager/PeripheralManager.swift:53
- Row(
- identifier: "MedtrumWriteError.alreadyRunning",
- message: "A command is already running",
- role: "errorMessage",
- taxonomy: "N9",
- expected: .other("A command is already running")
- ),
- // MedtrumKit/PumpManager/PeripheralManager.swift:311
- Row(
- identifier: "MedtrumWriteError.invalidResponse",
- message: "Invalid response code: <code>",
- role: "errorMessage",
- taxonomy: "N9",
- expected: .other("Invalid response code: <code>")
- ),
- // MedtrumKit/PumpManager/MedtrumPumpManager.swift:630
- Row(
- identifier: "MedtrumPrimePatchError.noKnownPumpBase",
- message: "No known pump base found.",
- role: "errorMessage",
- taxonomy: "N12",
- expected: .other("No known pump base found.")
- ),
- // MedtrumKit/PumpManager/MedtrumPumpManager.swift:645
- Row(
- identifier: "MedtrumPrimePatchError.connectionFailure",
- message: "Failed to connect to pump base: <reason>",
- role: "errorMessage",
- taxonomy: "N12",
- expected: .other("Failed to connect to pump base: <reason>")
- ),
- // MedtrumKit/PumpManager/MedtrumPumpManager.swift:659
- Row(
- identifier: "MedtrumPrimePatchError.unknownError",
- message: "Unknown error: <write error>",
- role: "errorMessage",
- taxonomy: "N12",
- expected: .other("Unknown error: <write error>")
- ),
- // MedtrumKit/PumpManager/MedtrumPumpManager.swift:674
- Row(
- identifier: "MedtrumActivatePatchError.connectionFailure",
- message: "Connection failure: <reason>",
- role: "errorMessage",
- taxonomy: "N12",
- expected: .other("Connection failure: <reason>")
- ),
- // MedtrumKit/PumpManager/MedtrumPumpManager.swift:698
- Row(
- identifier: "MedtrumActivatePatchError.unknownError",
- message: "Unknown error: <error description>",
- role: "errorMessage",
- taxonomy: "N12",
- expected: .other("Unknown error: <error description>")
- ),
- // MedtrumKit/PumpManager/MedtrumPumpManager.swift:704
- Row(
- identifier: "MedtrumActivatePatchError.unknownError",
- message: "Unknown error: Failed to parse response...",
- role: "errorMessage",
- taxonomy: "N12",
- expected: .other("Unknown error: Failed to parse response...")
- ),
- // MedtrumKit/PumpManager/BluetoothManager.swift:41
- Row(
- identifier: "MedtrumScanError.noSerialNumberAvailable",
- message: "No Serial number setup. Please complete activation flow...",
- role: "errorMessage",
- taxonomy: "N12",
- expected: .other("No Serial number setup. Please complete activation flow...")
- ),
- // MedtrumKitUI/ViewModels/Onboarding/PumpBaseSettingsViewModel.swift:44
- Row(
- identifier: "PumpBaseSettingsViewModel.errorMessage",
- message: "No pump manager available",
- role: "validation",
- taxonomy: "N12",
- expected: .other("No pump manager available")
- ),
- // MedtrumKitUI/ViewModels/Settings/PatchSettingsViewModel.swift:79; DeactivatePatchViewModel.swift:23,40,77
- Row(
- identifier: "PatchSettingsViewModel.errorMessage",
- message: "Authentication failure",
- role: "errorMessage",
- taxonomy: "N10",
- expected: .other("Authentication failure")
- ),
- // MedtrumKitUI/AuthorizeBiometrics.swift:11-13
- Row(
- identifier: "AuthorizeBiometrics.evaluatePolicyReason",
- message: "We need to unlock your data.",
- role: "notificationBody",
- taxonomy: "N10",
- expected: .other("We need to unlock your data.")
- ),
- // MedtrumKit/PumpManager/MedtrumPumpManager.swift:585
- Row(
- identifier: "NSError.syncBasalRateSchedule",
- message: "Basal schedule is empty...",
- role: "validation",
- taxonomy: "N13",
- expected: .other("Basal schedule is empty...")
- ),
- // MedtrumKitUI/ViewModels/Onboarding/PumpBaseSettingsViewModel.swift:32
- Row(
- identifier: "PumpBaseSettingsViewModel.errorMessage",
- message: "Serial Number is too short",
- role: "validation",
- taxonomy: "N13",
- expected: .other("Serial Number is too short")
- ),
- // MedtrumKitUI/ViewModels/Onboarding/PumpBaseSettingsViewModel.swift:38
- Row(
- identifier: "PumpBaseSettingsViewModel.errorMessage",
- message: "Serial Number is invalid hex format",
- role: "validation",
- taxonomy: "N13",
- expected: .other("Serial Number is invalid hex format")
- ),
- // MedtrumKitUI/ViewModels/Onboarding/PumpBaseSettingsViewModel.swift:55
- Row(
- identifier: "PumpBaseSettingsViewModel.errorMessage",
- message: "Incorrect serial number received",
- role: "validation",
- taxonomy: "N13",
- expected: .other("Incorrect serial number received")
- ),
- // MedtrumKit/Packets/Enums/PatchState.swift:73
- Row(
- identifier: "PatchState.noCalibration",
- message: "No Calibration",
- role: "errorMessage",
- taxonomy: "N11",
- expected: .other("No Calibration")
- ),
- // MedtrumKit/Packets/Enums/PatchState.swift:31
- Row(
- identifier: "PatchState.none",
- message: "None",
- role: "errorMessage",
- taxonomy: "N14",
- expected: .other("None")
- ),
- // MedtrumKit/Packets/Enums/PatchState.swift:33
- Row(
- identifier: "PatchState.idle",
- message: "Idle",
- role: "errorMessage",
- taxonomy: "N14",
- expected: .other("Idle")
- ),
- // MedtrumKit/Packets/Enums/PatchState.swift:35
- Row(
- identifier: "PatchState.filled",
- message: "Filled",
- role: "errorMessage",
- taxonomy: "N14",
- expected: .other("Filled")
- ),
- // MedtrumKit/Packets/Enums/PatchState.swift:37
- Row(
- identifier: "PatchState.priming",
- message: "Priming",
- role: "errorMessage",
- taxonomy: "N14",
- expected: .other("Priming")
- ),
- // MedtrumKit/Packets/Enums/PatchState.swift:39
- Row(
- identifier: "PatchState.primed",
- message: "Primed",
- role: "errorMessage",
- taxonomy: "N14",
- expected: .other("Primed")
- ),
- // MedtrumKit/Packets/Enums/PatchState.swift:41
- Row(
- identifier: "PatchState.ejecting",
- message: "Ejecting",
- role: "errorMessage",
- taxonomy: "N14",
- expected: .other("Ejecting")
- ),
- // MedtrumKit/Packets/Enums/PatchState.swift:43
- Row(
- identifier: "PatchState.ejected",
- message: "Ejected",
- role: "errorMessage",
- taxonomy: "N14",
- expected: .other("Ejected")
- ),
- // MedtrumKit/Packets/Enums/PatchState.swift:46
- Row(
- identifier: "PatchState.active",
- message: "Active",
- role: "errorMessage",
- taxonomy: "N14",
- expected: .other("Active")
- ),
- // MedtrumKit/Packets/Enums/PatchState.swift:49
- Row(
- identifier: "PatchState.lowBgSuspended",
- message: "Suspended - Low BG",
- role: "errorMessage",
- taxonomy: "N2",
- expected: .other("Suspended - Low BG")
- ),
- // MedtrumKit/Packets/Enums/PatchState.swift:51
- Row(
- identifier: "PatchState.autoSuspended",
- message: "Suspended - Auto",
- role: "errorMessage",
- taxonomy: "N2",
- expected: .other("Suspended - Auto")
- ),
- // MedtrumKit/Packets/Enums/PatchState.swift:53
- Row(
- identifier: "PatchState.hourlyMaxSuspended",
- message: "Suspended - Hourly Max",
- role: "errorMessage",
- taxonomy: "N2",
- expected: .other("Suspended - Hourly Max")
- ),
- // MedtrumKit/Packets/Enums/PatchState.swift:55
- Row(
- identifier: "PatchState.dailyMaxSuspended",
- message: "Suspended - Daily Max",
- role: "errorMessage",
- taxonomy: "N2",
- expected: .other("Suspended - Daily Max")
- ),
- // MedtrumKit/Packets/Enums/PatchState.swift:57
- Row(
- identifier: "PatchState.suspended",
- message: "Suspended",
- role: "errorMessage",
- taxonomy: "N2",
- expected: .other("Suspended")
- ),
- // MedtrumKit/Packets/Enums/PatchState.swift:59
- Row(
- identifier: "PatchState.paused",
- message: "Paused",
- role: "errorMessage",
- taxonomy: "N2",
- expected: .other("Paused")
- ),
- // MedtrumKit/Packets/Enums/PatchState.swift:61
- // Contains "Occlusion" -> .occlusion.
- Row(
- identifier: "PatchState.occlusion",
- message: "Occlusion",
- role: "errorMessage",
- taxonomy: "N1",
- expected: .occlusion
- ),
- // MedtrumKit/Packets/Enums/PatchState.swift:63
- Row(
- identifier: "PatchState.expired",
- message: "Expired",
- role: "errorMessage",
- taxonomy: "N6",
- expected: .other("Expired")
- ),
- // MedtrumKit/Packets/Enums/PatchState.swift:65
- Row(
- identifier: "PatchState.reservoirEmpty",
- message: "Reservoir Empty",
- role: "errorMessage",
- taxonomy: "N4",
- expected: .other("Reservoir Empty")
- ),
- // MedtrumKit/Packets/Enums/PatchState.swift:69
- // Contains "Fault" -> .hardwareFault.
- Row(
- identifier: "PatchState.baseFault",
- message: "Fault",
- role: "errorMessage",
- taxonomy: "N1",
- expected: .hardwareFault
- ),
- // MedtrumKit/Packets/Enums/PatchState.swift:71
- Row(
- identifier: "PatchState.batteryOut",
- message: "Battery Empty",
- role: "errorMessage",
- taxonomy: "N5",
- expected: .other("Battery Empty")
- ),
- // MedtrumKit/Packets/Enums/PatchState.swift:75
- Row(
- identifier: "PatchState.stopped",
- message: "Stopped",
- role: "errorMessage",
- taxonomy: "N14",
- expected: .other("Stopped")
- ),
- // MedtrumKitUI/Views/Settings/MedtrumKitSettings.swift:85-91
- Row(
- identifier: "MedtrumKitSettings.gracePeriodBanner",
- message: "Change your Patch now. Insulin delivery will stop in %@ or when no more insulin remains.",
- role: "alertBody",
- taxonomy: "F3",
- expected: .other("Change your Patch now. Insulin delivery will stop in %@ or when no more insulin remains.")
- ),
- // MedtrumKitUI/Views/Settings/MedtrumKitSettings.swift:96-107
- Row(
- identifier: "MedtrumKitSettings.hourlyMaxBanner",
- message: "Alert: Hourly max insulin / Patch is suspended. Limit of %lld U exceeded. If you increase the limit, you can clear the alert now. If you wait, patch will resume when enough time passes. / Clear alert",
- role: "alertBody",
- taxonomy: "N2",
- expected: .other(
- "Alert: Hourly max insulin / Patch is suspended. Limit of %lld U exceeded. If you increase the limit, you can clear the alert now. If you wait, patch will resume when enough time passes. / Clear alert"
- )
- ),
- // MedtrumKitUI/Views/Settings/MedtrumKitSettings.swift:126-137
- Row(
- identifier: "MedtrumKitSettings.dailyMaxBanner",
- message: "Alert: Daily max insulin / Patch is suspended. Limit of %lld U exceeded. If you increase the limit, you can clear the alert now. If you wait, patch will resume when enough time passes. / Clear alert",
- role: "alertBody",
- taxonomy: "N2",
- expected: .other(
- "Alert: Daily max insulin / Patch is suspended. Limit of %lld U exceeded. If you increase the limit, you can clear the alert now. If you wait, patch will resume when enough time passes. / Clear alert"
- )
- ),
- // MedtrumKitUI/Views/Onboarding/PatchDeactivationView.swift:43-48
- Row(
- identifier: "PatchDeactivationView.confirmAlert",
- message: "Are you sure? / It is recommended to deactivate first / Confirm",
- role: "alertBody",
- taxonomy: "N14",
- expected: .other("Are you sure? / It is recommended to deactivate first / Confirm")
- ),
- // MedtrumKitUI/Views/DeleteDriverActionSheet.swift:3-19
- Row(
- identifier: "DeleteDriverActionSheet",
- message: "Remove Pump / Are you sure you want to stop using Medtrum TouchCare Nano 200u/300u? / Delete Pump",
- role: "alertBody",
- taxonomy: "N14",
- expected: .other("Remove Pump / Are you sure you want to stop using Medtrum TouchCare Nano 200u/300u? / Delete Pump")
- ),
- // MedtrumKitUI/Views/Settings/MedtrumKitSettings.swift:14-28
- Row(
- identifier: "MedtrumKitSettings.timeSyncActionSheet",
- message: "Time Change Detected / 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? / Yes, Sync to Current Time / No, Keep Pump As Is",
- role: "alertBody",
- taxonomy: "N14",
- expected: .other(
- "Time Change Detected / 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? / Yes, Sync to Current Time / No, Keep Pump As Is"
- )
- ),
- // MedtrumKitUI/Views/Settings/MedtrumKitSettings.swift:30-45
- Row(
- identifier: "MedtrumKitSettings.suspendPickerActionSheet",
- message: "Suspend Insulin Delivery / How long you wish to suspend your patch maximum? It will resume automaticly after this time.",
- role: "alertBody",
- taxonomy: "N14",
- expected: .other(
- "Suspend Insulin Delivery / How long you wish to suspend your patch maximum? It will resume automaticly after this time."
- )
- ),
- // MedtrumKitUI/Views/Settings/MedtrumKitSettings.swift:74-81
- Row(
- identifier: "MedtrumKitSettings.timeSyncBanner",
- message: "Time Change Detected / 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...",
- role: "alertBody",
- taxonomy: "N14",
- expected: .other(
- "Time Change Detected / 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..."
- )
- )
- ]
- @Test("each (identifier, message) classifies as pinned", arguments: rows) func eachMessageClassifiesAsPinned(_ row: Row) {
- #expect(TrioAlertClassifier.categorize(error: StubError(description: row.message)) == row.expected)
- }
- // MARK: - Classifier coverage gaps (ratchet)
- /// `"identifier — message"` keys where the audit marks `isGap == true`:
- /// the message currently falls through to `.other` but SHOULD route to a
- /// meaningful bucket. This ratchet stays green today and FAILS when the
- /// classifier is improved to catch the real prose — prompting an update.
- ///
- /// Per gap: bucket it SHOULD hit, and why the substring tokens miss it.
- /// - occlusionNotification: SHOULD .occlusion. Body is misspelled
- /// "occlussion" (double-s) — does NOT contain "occlusion"
- /// (NotificationManager.swift:61).
- /// - reservoirEmptyNotification ("...out of insulin!"): SHOULD
- /// .reservoirEmpty. Prose has no "reservoirempty"/"emptyreservoir"
- /// token (NotificationManager.swift:81).
- /// - reservoirEmptyNotification ("Reservoir low..."): SHOULD
- /// .reservoirLow. "Reservoir low" (with space) != "lowreservoir"
- /// (NotificationManager.swift:91).
- /// - PumpStatusHighlight "No Insulin": SHOULD .reservoirEmpty. No
- /// "reservoirempty" token (MedtrumKitPumpManager+UI.swift:85).
- /// - PumpStatusHighlight "Patch expired. Basal only.": SHOULD
- /// .deviceExpired. Only "podexpired"/"sensorexpired" tokens match,
- /// not "patch expired" (MedtrumKitPumpManager+UI.swift:100).
- /// - PumpStatusHighlight "Signal Loss": SHOULD .commsTransient. No
- /// "communication"/"timeout"/etc. token (MedtrumKitPumpManager+UI.swift:109).
- /// - PumpStatusHighlight "Patch Error": SHOULD .hardwareFault. "Error"
- /// is not "fault" (MedtrumKitPumpManager+UI.swift:118).
- /// - failedToDiscoverServices: SHOULD .commsTransient. No comms token
- /// in the prose (PeripheralManager.swift:209).
- /// - failedToDiscoverCharacteristics: SHOULD .commsTransient. No comms
- /// token (PeripheralManager.swift:231).
- /// - failedToFindDevice "Failed to connect to patch": SHOULD
- /// .commsTransient. No "timeout"/"notconnected" token; "connect" is
- /// not "notconnected" (BluetoothManager.swift:114,128).
- /// - invalidBluetoothState: SHOULD .commsTransient. No comms token
- /// (BluetoothManager.swift:45).
- /// - noData "No data": SHOULD .commsTransient. No comms token
- /// (PeripheralManager.swift:39,71).
- /// - noWriteCharacteristic: SHOULD .commsTransient. "disconnected" !=
- /// "notconnected" (PeripheralManager.swift:58).
- /// - invalidData "Invalid data received": SHOULD .commsTransient. No
- /// comms token (PeripheralManager.swift:314,321).
- /// - noManager "No peripheral manager": SHOULD .commsTransient. No
- /// comms token (BluetoothManager.swift:170).
- /// - uncertainDelivery "uncertain delivery": SHOULD .deliveryUncertain.
- /// "uncertain delivery" (with space) != "uncertaindelivery"
- /// (MedtrumPumpManager.swift:992).
- static let classifierCoverageGaps: Set<String> = [
- "NotificationManager.Identifiers.occlusionNotification — Replace your patch now! / Your patch has detected an occlussion!",
- "NotificationManager.Identifiers.reservoirEmptyNotification — Replace your patch now! / Your patch is out of insulin!",
- "NotificationManager.Identifiers.reservoirEmptyNotification — Reservoir low (%lld U) / Your patch is running out of insulin!",
- "PumpStatusHighlight — No Insulin",
- "PumpStatusHighlight — Patch expired. Basal only.",
- "PumpStatusHighlight — Signal Loss",
- "PumpStatusHighlight — Patch Error",
- "MedtrumConnectError.failedToDiscoverServices — No Medtrum service found - <discovered service UUIDs>",
- "MedtrumConnectError.failedToDiscoverCharacteristics — Failed to discover read, write or config characteristic - <UUIDs>",
- "MedtrumConnectError.failedToFindDevice — Failed to connect to patch",
- "MedtrumScanError.invalidBluetoothState — Invalid Bluetooth state: <state>",
- "MedtrumWriteError.noData — No data",
- "MedtrumWriteError.noWriteCharacteristic — No write characteristic. Device might be disconnected",
- "MedtrumWriteError.invalidData — Invalid data received",
- "MedtrumWriteError.noManager — No peripheral manager",
- "PumpManagerError.uncertainDelivery — uncertain delivery"
- ]
- @Test("classifier coverage gaps are exactly as documented") func classifierCoverageGapsAreExactlyAsDocumented() {
- // A coverage gap = a row that currently resolves to `.other(message)`
- // even though the audit says it SHOULD route to a meaningful bucket
- // (taxonomyBucket != "other"). The bucket-vs-other distinction lives
- // in the audit, encoded here as `classifierCoverageGaps`. Recompute
- // the falls-through-to-other set among gap-flagged rows and confirm
- // it matches verbatim; this FAILS when the classifier is improved to
- // catch the prose, forcing the documented set to be updated.
- let fellThroughToOther = Set(
- Self.rows
- .filter { row in
- TrioAlertClassifier.categorize(error: StubError(description: row.message)) == .other(row.message)
- }
- .map { "\($0.identifier) — \($0.message)" }
- )
- let documentedGapsThatFellThrough = fellThroughToOther.intersection(Self.classifierCoverageGaps)
- #expect(documentedGapsThatFellThrough == Self.classifierCoverageGaps)
- }
- }
|