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 = [ "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 - ", role: "errorMessage", taxonomy: "N8", expected: .other("No Medtrum service found - ") ), // MedtrumKit/PumpManager/PeripheralManager.swift:231 Row( identifier: "MedtrumConnectError.failedToDiscoverCharacteristics", message: "Failed to discover read, write or config characteristic - ", role: "errorMessage", taxonomy: "N8", expected: .other("Failed to discover read, write or config characteristic - ") ), // 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: ", role: "errorMessage", taxonomy: "N8", expected: .other("Invalid Bluetooth 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: ", role: "errorMessage", taxonomy: "N9", expected: .other("Invalid response 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: ", role: "errorMessage", taxonomy: "N12", expected: .other("Failed to connect to pump base: ") ), // MedtrumKit/PumpManager/MedtrumPumpManager.swift:659 Row( identifier: "MedtrumPrimePatchError.unknownError", message: "Unknown error: ", role: "errorMessage", taxonomy: "N12", expected: .other("Unknown error: ") ), // MedtrumKit/PumpManager/MedtrumPumpManager.swift:674 Row( identifier: "MedtrumActivatePatchError.connectionFailure", message: "Connection failure: ", role: "errorMessage", taxonomy: "N12", expected: .other("Connection failure: ") ), // MedtrumKit/PumpManager/MedtrumPumpManager.swift:698 Row( identifier: "MedtrumActivatePatchError.unknownError", message: "Unknown error: ", role: "errorMessage", taxonomy: "N12", expected: .other("Unknown error: ") ), // 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 = [ "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 - ", "MedtrumConnectError.failedToDiscoverCharacteristics — Failed to discover read, write or config characteristic - ", "MedtrumConnectError.failedToFindDevice — Failed to connect to patch", "MedtrumScanError.invalidBluetoothState — Invalid Bluetooth 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) } }