Selaa lähdekoodia

Replace dead TrioAlertClassifier tests with catalog Omni-fault parser tests

trioneer 2 viikkoa sitten
vanhempi
commit
bd3b71ac0d

+ 4 - 4
Trio.xcodeproj/project.pbxproj

@@ -236,7 +236,6 @@
 		38F3B2EF25ED8E2A005C48AA /* TempTargetsStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38F3B2EE25ED8E2A005C48AA /* TempTargetsStorage.swift */; };
 		38FCF3D625E8FDF40078B0D1 /* MD5.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38FCF3D525E8FDF40078B0D1 /* MD5.swift */; };
 		38FCF3F925E902C20078B0D1 /* FileStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38FCF3F825E902C20078B0D1 /* FileStorageTests.swift */; };
-		BD11A001000000000000A001 /* TrioAlertClassifierTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD11A000000000000000A001 /* TrioAlertClassifierTests.swift */; };
 		BD11A001000000000000A002 /* AlertMuterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD11A000000000000000A002 /* AlertMuterTests.swift */; };
 		BD11A001000000000000A003 /* DeviceAlertsStoreTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD11A000000000000000A003 /* DeviceAlertsStoreTests.swift */; };
 		38FCF3FD25E997A80078B0D1 /* PumpHistoryStorage.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38FCF3FC25E997A80078B0D1 /* PumpHistoryStorage.swift */; };
@@ -521,6 +520,7 @@
 		BD8FC0642D6619EF00B95AED /* TempTargetStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD8FC0632D6619EF00B95AED /* TempTargetStorageTests.swift */; };
 		BD8FC0662D661A0000B95AED /* GlucoseStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD8FC0652D661A0000B95AED /* GlucoseStorageTests.swift */; };
 		BD8FC0712D661B0000B95AED /* TidepoolTherapySettingsTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD8FC0702D661B0000B95AED /* TidepoolTherapySettingsTests.swift */; };
+		CA03000000000000000010C2 /* AlertCatalogRegistryOmniFaultTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = CA03000000000000000010C1 /* AlertCatalogRegistryOmniFaultTests.swift */; };
 		BDA25EE42D260CD500035F34 /* AppleWatchManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDA25EE32D260CCF00035F34 /* AppleWatchManager.swift */; };
 		BDA25EE62D260D5E00035F34 /* WatchState.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDA25EE52D260D5800035F34 /* WatchState.swift */; };
 		BDA25EFD2D261C0000035F34 /* WatchState.swift in Sources */ = {isa = PBXBuildFile; fileRef = BDA25EFC2D261BF200035F34 /* WatchState.swift */; };
@@ -1238,7 +1238,6 @@
 		38FCF3ED25E9028E0078B0D1 /* TrioTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = TrioTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; };
 		38FCF3F125E9028E0078B0D1 /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = "<group>"; };
 		38FCF3F825E902C20078B0D1 /* FileStorageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = FileStorageTests.swift; sourceTree = "<group>"; };
-		BD11A000000000000000A001 /* TrioAlertClassifierTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TrioAlertClassifierTests.swift; sourceTree = "<group>"; };
 		BD11A000000000000000A002 /* AlertMuterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertMuterTests.swift; sourceTree = "<group>"; };
 		BD11A000000000000000A003 /* DeviceAlertsStoreTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DeviceAlertsStoreTests.swift; sourceTree = "<group>"; };
 		38FCF3FC25E997A80078B0D1 /* PumpHistoryStorage.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PumpHistoryStorage.swift; sourceTree = "<group>"; };
@@ -1497,6 +1496,7 @@
 		BD8FC0632D6619EF00B95AED /* TempTargetStorageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TempTargetStorageTests.swift; sourceTree = "<group>"; };
 		BD8FC0652D661A0000B95AED /* GlucoseStorageTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = GlucoseStorageTests.swift; sourceTree = "<group>"; };
 		BD8FC0702D661B0000B95AED /* TidepoolTherapySettingsTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TidepoolTherapySettingsTests.swift; sourceTree = "<group>"; };
+		CA03000000000000000010C1 /* AlertCatalogRegistryOmniFaultTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertCatalogRegistryOmniFaultTests.swift; sourceTree = "<group>"; };
 		BDA25EE32D260CCF00035F34 /* AppleWatchManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AppleWatchManager.swift; sourceTree = "<group>"; };
 		BDA25EE52D260D5800035F34 /* WatchState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchState.swift; sourceTree = "<group>"; };
 		BDA25EFC2D261BF200035F34 /* WatchState.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = WatchState.swift; sourceTree = "<group>"; };
@@ -2996,7 +2996,6 @@
 				CA02000000000000000010C1 /* DeliveryLimitsSyncTests.swift */,
 				3BAAE60B2DE776630049589B /* DynamicISFEnableTests.swift */,
 				38FCF3F825E902C20078B0D1 /* FileStorageTests.swift */,
-				BD11A000000000000000A001 /* TrioAlertClassifierTests.swift */,
 				BD11A000000000000000A002 /* AlertMuterTests.swift */,
 				BD11A000000000000000A003 /* DeviceAlertsStoreTests.swift */,
 				3B997DCE2DC00A3A006B6BB2 /* JSONImporterTests.swift */,
@@ -3005,6 +3004,7 @@
 				B3919BBB515547118D684CA2 /* SettingsSearchTests.swift */,
 				BD8FC0532D66186000B95AED /* TestError.swift */,
 				BD8FC0702D661B0000B95AED /* TidepoolTherapySettingsTests.swift */,
+				CA03000000000000000010C1 /* AlertCatalogRegistryOmniFaultTests.swift */,
 			);
 			path = TrioTests;
 			sourceTree = "<group>";
@@ -5564,7 +5564,6 @@
 				3B2CE68B2E24ADF7005EF782 /* IobGenerateTests.swift in Sources */,
 				3B1C5C482D68E269004E9273 /* IobHistoryTests.swift in Sources */,
 				38FCF3F925E902C20078B0D1 /* FileStorageTests.swift in Sources */,
-				BD11A001000000000000A001 /* TrioAlertClassifierTests.swift in Sources */,
 				BD11A001000000000000A002 /* AlertMuterTests.swift in Sources */,
 				BD11A001000000000000A003 /* DeviceAlertsStoreTests.swift in Sources */,
 				3B8221B22E5882E300585156 /* DetermineBasalEarlyExitTests.swift in Sources */,
@@ -5575,6 +5574,7 @@
 				41740E936552456AAC0EDAC3 /* SettingsSearchTests.swift in Sources */,
 				B015AFE52E500000000D7351 /* BolusSafetyValidatorTests.swift in Sources */,
 				BD8FC0712D661B0000B95AED /* TidepoolTherapySettingsTests.swift in Sources */,
+				CA03000000000000000010C2 /* AlertCatalogRegistryOmniFaultTests.swift in Sources */,
 			);
 			runOnlyForDeploymentPostprocessing = 0;
 		};

+ 60 - 0
TrioTests/AlertCatalogRegistryOmniFaultTests.swift

@@ -0,0 +1,60 @@
+import Foundation
+import LoopKit
+import Testing
+
+@testable import Trio
+
+/// Pins `AlertCatalogRegistry.lookup` behavior for Omni pod-fault identifiers.
+/// `OmniPumpManager` builds `Alert.Identifier` from the formatted
+/// `FaultEventCode.description` string ("Fault Event Code 0xNN: ..."), so the
+/// registry has to parse the hex code out of that prefix at lookup time.
+@Suite("AlertCatalogRegistry — Omni pod-fault hex parser") struct AlertCatalogRegistryOmniFaultTests {
+    private func omniFaultID(_ desc: String) -> Alert.Identifier {
+        Alert.Identifier(managerIdentifier: "Omni:pumpFault", alertIdentifier: desc)
+    }
+
+    @Test("0x14 → occlusion concept, critical, Pod Occlusion title") func code0x14Occlusion() {
+        let entry = AlertCatalogRegistry.lookup(omniFaultID("Fault Event Code 0x14: Occluded"))
+        #expect(entry?.concept == .occlusion)
+        #expect(entry?.interruptionLevel == .critical)
+        #expect(entry?.title == "Pod Occlusion")
+    }
+
+    @Test("0x18 → reservoirEmpty concept, critical") func code0x18ReservoirEmpty() {
+        let entry = AlertCatalogRegistry.lookup(omniFaultID("Fault Event Code 0x18: Reservoir empty"))
+        #expect(entry?.concept == .reservoirEmpty)
+        #expect(entry?.interruptionLevel == .critical)
+    }
+
+    @Test("0x1C → deviceExpired concept, timeSensitive") func code0x1CExpired() {
+        let entry = AlertCatalogRegistry.lookup(
+            omniFaultID("Fault Event Code 0x1c: Exceeded maximum Pod life of 80 hours")
+        )
+        #expect(entry?.concept == .deviceExpired)
+        #expect(entry?.interruptionLevel == .timeSensitive)
+    }
+
+    @Test("Unmapped fault code falls back to generic hardwareFault") func unknownCodeFallback() {
+        let entry = AlertCatalogRegistry.lookup(omniFaultID("Fault Event Code 0x42: Some internal fault"))
+        #expect(entry?.concept == .hardwareFault)
+        #expect(entry?.interruptionLevel == .critical)
+        #expect(entry?.title == "Pod Fault")
+    }
+
+    @Test("Malformed hex (non-hex chars) falls back to hardwareFault") func malformedHex() {
+        let entry = AlertCatalogRegistry.lookup(omniFaultID("Fault Event Code 0xZZ: garbled"))
+        #expect(entry?.concept == .hardwareFault)
+    }
+
+    @Test("Truncated identifier still resolves to hardwareFault") func truncatedIdentifier() {
+        let entry = AlertCatalogRegistry.lookup(omniFaultID("Fault Event Code 0x"))
+        #expect(entry?.concept == .hardwareFault)
+    }
+
+    @Test("Non-Omni managerIdentifier is ignored by the fault parser") func wrongManagerIgnored() {
+        let id = Alert.Identifier(managerIdentifier: "Dana", alertIdentifier: "Fault Event Code 0x14: Occluded")
+        // Dana doesn't carry this slug in the catalog, so lookup is nil even
+        // though the alert text matches the Omni pattern.
+        #expect(AlertCatalogRegistry.lookup(id) == nil)
+    }
+}

+ 4 - 2
TrioTests/DeviceAlertsStoreTests.swift

@@ -10,11 +10,13 @@ import Testing
         let suiteName = "DeviceAlertsStoreTests.\(UUID().uuidString)"
         let defaults = UserDefaults(suiteName: suiteName)!
         defaults.removePersistentDomain(forName: suiteName)
+        let configsKey = "configs.\(suiteName)"
+        let snoozesKey = "snoozes.\(suiteName)"
         if let seed {
             let data = try? JSONEncoder().encode(seed)
-            defaults.set(data, forKey: "trio.deviceAlertSeverityConfigs.v1")
+            defaults.set(data, forKey: configsKey)
         }
-        return DeviceAlertsStore(defaults: defaults, configsKey: "trio.deviceAlertSeverityConfigs.v1")
+        return DeviceAlertsStore(defaults: defaults, configsKey: configsKey, snoozesKey: snoozesKey)
     }
 
     @Test("Fresh store seeds one .always config per severity") func freshSeed() {

+ 0 - 429
TrioTests/TrioAlertClassifierTests.swift

@@ -1,429 +0,0 @@
-import Foundation
-import Testing
-
-@testable import Trio
-
-@Suite("Trio Alerts: TrioAlertClassifier — alertIdentifier round-trip") struct TrioAlertClassifierIdentifierTests {
-    /// Ground-truth slugs emitted by bundled pump managers. The classifier is
-    /// substring-matched and case-insensitive, so the test inputs reproduce the
-    /// real source-of-truth casing.
-
-    // MARK: - OmnipodKit (covers Eros + DASH)
-
-    @Test("Omnipod: lowReservoir → reservoirLow") func omnipodLowReservoir() {
-        #expect(TrioAlertClassifier.categorize(alertIdentifier: "lowReservoir") == .reservoirLow)
-    }
-
-    @Test("Omnipod: podExpiring is a reminder, not an expiration") func omnipodPodExpiring() {
-        #expect(TrioAlertClassifier.categorize(alertIdentifier: "podExpiring") == .deviceExpirationReminder)
-    }
-
-    @Test("Omnipod: userPodExpiration → reminder") func omnipodUserPodExpiration() {
-        #expect(TrioAlertClassifier.categorize(alertIdentifier: "userPodExpiration") == .deviceExpirationReminder)
-    }
-
-    @Test("Omnipod: podExpireImminent → shutdown imminent") func omnipodExpireImminent() {
-        #expect(TrioAlertClassifier.categorize(alertIdentifier: "podExpireImminent") == .podShutdownImminent)
-    }
-
-    @Test("Omnipod: suspendEnded → suspendTimeExpired") func omnipodSuspendEnded() {
-        #expect(TrioAlertClassifier.categorize(alertIdentifier: "suspendEnded") == .suspendTimeExpired)
-    }
-
-    @Test("Omnipod: suspendEnded-repeating → suspendTimeExpired") func omnipodSuspendEndedRepeating() {
-        #expect(TrioAlertClassifier.categorize(alertIdentifier: "suspendEnded-repeating") == .suspendTimeExpired)
-    }
-
-    @Test("Omnipod: unexpectedAlert → hardwareFault") func omnipodUnexpectedAlert() {
-        #expect(TrioAlertClassifier.categorize(alertIdentifier: "unexpectedAlert") == .hardwareFault)
-    }
-
-    @Test("Omnipod: timeOffsetChangeDetected → other") func omnipodTimeOffset() {
-        // Not a clinically actionable category — falls through to .other.
-        if case .other = TrioAlertClassifier.categorize(alertIdentifier: "timeOffsetChangeDetected") {
-            // ok
-        } else {
-            Issue.record("Expected .other for timeOffsetChangeDetected")
-        }
-    }
-
-    @Test("Omnipod: lowRLBattery → batteryLow") func omnipodLowRLBattery() {
-        #expect(TrioAlertClassifier.categorize(alertIdentifier: "lowRLBattery") == .batteryLow)
-    }
-
-    @Test("Omnipod: finishSetupReminder → other") func omnipodFinishSetup() {
-        if case .other = TrioAlertClassifier.categorize(alertIdentifier: "finishSetupReminder") {
-            // ok
-        } else {
-            Issue.record("Expected .other for finishSetupReminder")
-        }
-    }
-
-    @Test("Omnipod: suspendInProgress → other") func omnipodSuspendInProgress() {
-        if case .other = TrioAlertClassifier.categorize(alertIdentifier: "suspendInProgress") {
-            // ok
-        } else {
-            Issue.record("Expected .other for suspendInProgress")
-        }
-    }
-
-    // MARK: - DanaKit
-
-    @Test("Dana: batteryZeroPercent → batteryEmpty") func danaBatteryZero() {
-        #expect(TrioAlertClassifier.categorize(alertIdentifier: "batteryZeroPercent") == .batteryEmpty)
-    }
-
-    @Test("Dana: pumpError → hardwareFault") func danaPumpError() {
-        #expect(TrioAlertClassifier.categorize(alertIdentifier: "pumpError") == .hardwareFault)
-    }
-
-    @Test("Dana: occlusion") func danaOcclusion() {
-        #expect(TrioAlertClassifier.categorize(alertIdentifier: "occlusion") == .occlusion)
-    }
-
-    @Test("Dana: lowBattery → batteryLow") func danaLowBattery() {
-        #expect(TrioAlertClassifier.categorize(alertIdentifier: "lowBattery") == .batteryLow)
-    }
-
-    @Test("Dana: shutdown → hardwareFault") func danaShutdown() {
-        // The classifier matches the exact lowercased "shutdown" to hardwareFault.
-        #expect(TrioAlertClassifier.categorize(alertIdentifier: "shutdown") == .hardwareFault)
-    }
-
-    @Test("Dana: emptyReservoir → reservoirEmpty") func danaEmptyReservoir() {
-        #expect(TrioAlertClassifier.categorize(alertIdentifier: "emptyReservoir") == .reservoirEmpty)
-    }
-
-    @Test("Dana: remainingInsulinLevel → reservoirLow") func danaRemainingInsulin() {
-        #expect(TrioAlertClassifier.categorize(alertIdentifier: "remainingInsulinLevel") == .reservoirLow)
-    }
-
-    @Test("Dana: checkShaft → hardwareFault") func danaCheckShaft() {
-        #expect(TrioAlertClassifier.categorize(alertIdentifier: "checkShaft") == .hardwareFault)
-    }
-
-    @Test("Dana: ble5InvalidKeys → other") func danaInvalidKeys() {
-        if case .other = TrioAlertClassifier.categorize(alertIdentifier: "ble5InvalidKeys") {
-            // ok
-        } else {
-            Issue.record("Expected .other for ble5InvalidKeys")
-        }
-    }
-
-    // MARK: - MedtrumKit (jbr7rr/MedtrumKit PR #147)
-
-    @Test("Medtrum: patch-expired → reminder (misnamed, fires PRE-expiry via .delayed)") func medtrumPatchExpiredIsReminder() {
-        #expect(
-            TrioAlertClassifier
-                .categorize(alertIdentifier: "com.nightscout.medtrumkit.patch-expired") == .deviceExpirationReminder
-        )
-    }
-
-    @Test("Medtrum: patch-occlusion → occlusion") func medtrumOcclusion() {
-        #expect(
-            TrioAlertClassifier
-                .categorize(alertIdentifier: "com.nightscout.medtrumkit.patch-occlusion") == .occlusion
-        )
-    }
-
-    @Test("Medtrum: patch-fault → hardwareFault") func medtrumPatchFault() {
-        #expect(
-            TrioAlertClassifier
-                .categorize(alertIdentifier: "com.nightscout.medtrumkit.patch-fault") == .hardwareFault
-        )
-    }
-
-    @Test("Medtrum: patch-empty → reservoirEmpty") func medtrumPatchEmpty() {
-        #expect(
-            TrioAlertClassifier
-                .categorize(alertIdentifier: "com.nightscout.medtrumkit.patch-empty") == .reservoirEmpty
-        )
-    }
-
-    @Test("Medtrum: reservoir-low → reservoirLow") func medtrumReservoirLow() {
-        #expect(
-            TrioAlertClassifier
-                .categorize(alertIdentifier: "com.nightscout.medtrumkit.reservoir-low") == .reservoirLow
-        )
-    }
-
-    @Test("Medtrum: patch-daily-limit → suspendTimeExpired (auto-suspend on cap)") func medtrumPatchDailyLimit() {
-        #expect(
-            TrioAlertClassifier
-                .categorize(alertIdentifier: "com.nightscout.medtrumkit.patch-daily-limit") == .suspendTimeExpired
-        )
-    }
-
-    @Test("Medtrum: patch-hourly-limit → suspendTimeExpired") func medtrumPatchHourlyLimit() {
-        #expect(
-            TrioAlertClassifier
-                .categorize(alertIdentifier: "com.nightscout.medtrumkit.patch-hourly-limit") == .suspendTimeExpired
-        )
-    }
-
-    // MARK: - MinimedKit
-
-    @Test("Minimed: PumpBatteryLow → batteryLow") func minimedBatteryLow() {
-        #expect(TrioAlertClassifier.categorize(alertIdentifier: "PumpBatteryLow") == .batteryLow)
-    }
-
-    @Test("Minimed: PumpReservoirEmpty → reservoirEmpty") func minimedReservoirEmpty() {
-        #expect(TrioAlertClassifier.categorize(alertIdentifier: "PumpReservoirEmpty") == .reservoirEmpty)
-    }
-
-    @Test("Minimed: PumpReservoirLow → reservoirLow") func minimedReservoirLow() {
-        #expect(TrioAlertClassifier.categorize(alertIdentifier: "PumpReservoirLow") == .reservoirLow)
-    }
-
-    // MARK: - Trio internal slugs (APSManager + NotLoopingMonitor + GlucoseAlertCoordinator)
-
-    @Test("Trio: occlusion") func trioOcclusion() {
-        #expect(TrioAlertClassifier.categorize(alertIdentifier: "occlusion") == .occlusion)
-    }
-
-    @Test("Trio: reservoirLow") func trioReservoirLow() {
-        #expect(TrioAlertClassifier.categorize(alertIdentifier: "reservoirLow") == .reservoirLow)
-    }
-
-    @Test("Trio: reservoirEmpty") func trioReservoirEmpty() {
-        #expect(TrioAlertClassifier.categorize(alertIdentifier: "reservoirEmpty") == .reservoirEmpty)
-    }
-
-    @Test("Trio: batteryLow") func trioBatteryLow() {
-        #expect(TrioAlertClassifier.categorize(alertIdentifier: "batteryLow") == .batteryLow)
-    }
-
-    @Test("Trio: batteryEmpty") func trioBatteryEmpty() {
-        #expect(TrioAlertClassifier.categorize(alertIdentifier: "batteryEmpty") == .batteryEmpty)
-    }
-
-    @Test("Trio: hardwareFault") func trioHardwareFault() {
-        #expect(TrioAlertClassifier.categorize(alertIdentifier: "hardwareFault") == .hardwareFault)
-    }
-
-    @Test("Trio: deliveryUncertain") func trioDeliveryUncertain() {
-        #expect(TrioAlertClassifier.categorize(alertIdentifier: "deliveryUncertain") == .deliveryUncertain)
-    }
-
-    @Test("Trio: deviceExpirationReminder") func trioReminder() {
-        #expect(TrioAlertClassifier.categorize(alertIdentifier: "deviceExpirationReminder") == .deviceExpirationReminder)
-    }
-
-    @Test("Trio: deviceExpired") func trioExpired() {
-        #expect(TrioAlertClassifier.categorize(alertIdentifier: "deviceExpired") == .deviceExpired)
-    }
-
-    @Test("Trio: podShutdownImminent") func trioShutdownImminent() {
-        #expect(TrioAlertClassifier.categorize(alertIdentifier: "podShutdownImminent") == .podShutdownImminent)
-    }
-
-    @Test("Trio: suspendTimeExpired") func trioSuspendTimeExpired() {
-        #expect(TrioAlertClassifier.categorize(alertIdentifier: "suspendTimeExpired") == .suspendTimeExpired)
-    }
-
-    @Test("Trio: bolusFailed") func trioBolusFailed() {
-        #expect(TrioAlertClassifier.categorize(alertIdentifier: "bolusFailed") == .bolusFailed)
-    }
-
-    @Test("Trio: manualTempBasalActive") func trioManualTempBasal() {
-        #expect(TrioAlertClassifier.categorize(alertIdentifier: "manualTempBasalActive") == .manualTempBasalActive)
-    }
-
-    @Test("Trio: notLooping") func trioNotLooping() {
-        #expect(TrioAlertClassifier.categorize(alertIdentifier: "notLooping") == .notLooping)
-    }
-
-    @Test("Trio: loop.notActive (NotLoopingMonitor slug)") func trioLoopNotActive() {
-        #expect(TrioAlertClassifier.categorize(alertIdentifier: "loop.notActive") == .notLooping)
-    }
-
-    @Test("Trio: sensorFailure") func trioSensorFailure() {
-        #expect(TrioAlertClassifier.categorize(alertIdentifier: "sensorFailure") == .sensorFailure)
-    }
-
-    @Test("Trio: algorithmError") func trioAlgorithmError() {
-        #expect(TrioAlertClassifier.categorize(alertIdentifier: "algorithmError") == .algorithmError)
-    }
-
-    @Test("Trio: glucose.urgentLow.<uuid>") func trioGlucoseUrgentLow() {
-        #expect(
-            TrioAlertClassifier
-                .categorize(alertIdentifier: "glucose.urgentLow.5C9C3D2A-1234-4321-9876-ABCDEF012345") == .glucoseUrgentLow
-        )
-    }
-
-    @Test("Trio: glucose.low.<uuid>") func trioGlucoseLow() {
-        #expect(TrioAlertClassifier.categorize(alertIdentifier: "glucose.low.uuid-1") == .glucoseLow)
-    }
-
-    @Test("Trio: glucose.forecastedLow.<uuid>") func trioGlucoseForecastedLow() {
-        #expect(TrioAlertClassifier.categorize(alertIdentifier: "glucose.forecastedLow.uuid-2") == .glucoseForecastedLow)
-    }
-
-    @Test("Trio: glucose.high.<uuid>") func trioGlucoseHigh() {
-        #expect(TrioAlertClassifier.categorize(alertIdentifier: "glucose.high.uuid-3") == .glucoseHigh)
-    }
-
-    // MARK: - CGM driver slugs the classifier should recognize when bridges land
-
-    @Test("CGM: sensorFailed → sensorFailure") func cgmSensorFailed() {
-        #expect(TrioAlertClassifier.categorize(alertIdentifier: "sensorFailed") == .sensorFailure)
-    }
-
-    @Test("CGM: sensorStopped → sensorFailure") func cgmSensorStopped() {
-        #expect(TrioAlertClassifier.categorize(alertIdentifier: "sensorStopped") == .sensorFailure)
-    }
-
-    @Test("CGM: transmitterDisconnected → sensorFailure") func cgmTransmitterDisconnected() {
-        #expect(TrioAlertClassifier.categorize(alertIdentifier: "transmitterDisconnected") == .sensorFailure)
-    }
-
-    @Test("CGM: sensorExpired → deviceExpired") func cgmSensorExpired() {
-        #expect(TrioAlertClassifier.categorize(alertIdentifier: "sensorExpired") == .deviceExpired)
-    }
-
-    @Test("CGM: transmitterEoL → deviceExpired") func cgmTransmitterEoL() {
-        #expect(TrioAlertClassifier.categorize(alertIdentifier: "transmitterEoL") == .deviceExpired)
-    }
-
-    @Test("CGM: sensorGrace → reminder, not expired") func cgmSensorGrace() {
-        #expect(TrioAlertClassifier.categorize(alertIdentifier: "sensorGrace") == .deviceExpirationReminder)
-    }
-
-    @Test("CGM: gracePeriod → reminder") func cgmGracePeriod() {
-        #expect(TrioAlertClassifier.categorize(alertIdentifier: "gracePeriod") == .deviceExpirationReminder)
-    }
-
-    @Test("CGM: transmitterError → hardwareFault") func cgmTransmitterError() {
-        #expect(TrioAlertClassifier.categorize(alertIdentifier: "transmitterError") == .hardwareFault)
-    }
-
-    @Test("CGM: criticalFault → hardwareFault") func cgmCriticalFault() {
-        #expect(TrioAlertClassifier.categorize(alertIdentifier: "criticalFault") == .hardwareFault)
-    }
-
-    // MARK: - Glucose family wins over substring overlap
-
-    @Test("Glucose stale beats the generic 'glucose' fallthrough") func glucoseDataStale() {
-        #expect(TrioAlertClassifier.categorize(alertIdentifier: "glucoseDataStale") == .glucoseDataStale)
-    }
-
-    // MARK: - Unrecognized slug
-
-    @Test("Unrecognized slug → .other with original identifier") func unrecognizedFallsThrough() {
-        let category = TrioAlertClassifier.categorize(alertIdentifier: "totallyMadeUpSlug")
-        if case let .other(id) = category {
-            #expect(id == "totallyMadeUpSlug")
-        } else {
-            Issue.record("Expected .other(\"totallyMadeUpSlug\")")
-        }
-    }
-}
-
-@Suite("Trio Alerts: TrioAlertClassifier — error round-trip") struct TrioAlertClassifierErrorTests {
-    @Test("APSError.invalidPumpState → hardwareFault") func invalidPumpState() {
-        let err = APSError.invalidPumpState(message: "x")
-        #expect(TrioAlertClassifier.categorize(error: err) == .hardwareFault)
-    }
-
-    @Test("APSError.glucoseError → glucoseDataStale") func glucoseError() {
-        let err = APSError.glucoseError(message: "x")
-        #expect(TrioAlertClassifier.categorize(error: err) == .glucoseDataStale)
-    }
-
-    @Test("APSError.apsError → algorithmError") func apsErrorCase() {
-        let err = APSError.apsError(message: "x")
-        #expect(TrioAlertClassifier.categorize(error: err) == .algorithmError)
-    }
-
-    @Test("APSError.manualBasalTemp → manualTempBasalActive") func manualBasalTemp() {
-        let err = APSError.manualBasalTemp(message: "x")
-        #expect(TrioAlertClassifier.categorize(error: err) == .manualTempBasalActive)
-    }
-
-    /// `categorize(pumpError:)` keys on `String(describing:)`, not
-    /// `localizedDescription`. Match that contract with `CustomStringConvertible`
-    /// so the fake errors actually expose the substring the classifier looks for.
-    private struct PumpFake: Error, CustomStringConvertible {
-        let description: String
-    }
-
-    @Test("APSError.pumpError(occlusion) descends into pumpError categorize") func pumpErrorOcclusion() {
-        let err = APSError.pumpError(PumpFake(description: "Pump occlusion detected"))
-        #expect(TrioAlertClassifier.categorize(error: err) == .occlusion)
-    }
-
-    @Test("APSError.pumpError(uncertain) → deliveryUncertain") func pumpErrorUncertain() {
-        let err = APSError.pumpError(PumpFake(description: "UncertainDelivery: bolus may have failed"))
-        #expect(TrioAlertClassifier.categorize(error: err) == .deliveryUncertain)
-    }
-
-    @Test("APSError.pumpError(timeout) → commsTransient") func pumpErrorComms() {
-        let err = APSError.pumpError(PumpFake(description: "Communication timeout, no response"))
-        #expect(TrioAlertClassifier.categorize(error: err) == .commsTransient)
-    }
-
-    @Test("Generic error → .other") func genericError() {
-        struct OddError: Error {}
-        let category = TrioAlertClassifier.categorize(error: OddError())
-        if case .other = category {
-            // ok
-        } else {
-            Issue.record("Expected .other for unrecognized error")
-        }
-    }
-}
-
-@Suite("Trio Alerts: TrioAlertClassifier — surfacing rules") struct TrioAlertCategorySurfacingTests {
-    @Test("commsTransient is the only category dwell-gated at both boundaries") func commsTransientNotImmediate() {
-        #expect(!TrioAlertCategory.commsTransient.shouldFireImmediately)
-        // Spot-check that everything else surfaces immediately.
-        let immediate: [TrioAlertCategory] = [
-            .occlusion, .reservoirLow, .reservoirEmpty, .batteryLow, .batteryEmpty,
-            .hardwareFault, .deliveryUncertain, .deviceExpirationReminder, .deviceExpired,
-            .podShutdownImminent, .suspendTimeExpired, .bolusFailed, .manualTempBasalActive,
-            .notLooping, .sensorFailure, .glucoseUrgentLow, .glucoseLow, .glucoseForecastedLow,
-            .glucoseHigh, .glucoseDataStale, .algorithmError, .other("any")
-        ]
-        for category in immediate {
-            #expect(category.shouldFireImmediately, "Expected \(category) to surface immediately")
-        }
-    }
-
-    @Test("sensorFailure default tier is Critical") func sensorFailureCritical() {
-        #expect(PumpAlertCategory.sensorFailure.defaultSeverity == .critical)
-    }
-
-    @Test("algorithmError defaults to Normal") func algorithmErrorNormal() {
-        #expect(PumpAlertCategory.algorithmError.defaultSeverity == .normal)
-    }
-
-    @Test("Identifier slug round-trips through the classifier for every category") func slugRoundTrip() {
-        for category in PumpAlertCategory.allCases {
-            // Build the canonical slug from the matching TrioAlertCategory if available.
-            let trioCategory = TrioAlertCategory.allCasesForRoundTripTest
-                .first(where: { PumpAlertCategory(trioCategory: $0) == category })
-            guard let trioCategory else { continue }
-            let slug = trioCategory.alertIdentifier
-            let reclassified = TrioAlertClassifier.categorize(alertIdentifier: slug)
-            #expect(
-                reclassified == trioCategory,
-                "Slug \"\(slug)\" should reclassify as \(trioCategory) but got \(reclassified)"
-            )
-        }
-    }
-}
-
-// Helper: TrioAlertCategory is not CaseIterable because of `.other(String)`,
-// so we enumerate the round-trippable cases explicitly for the round-trip test.
-extension TrioAlertCategory {
-    static var allCasesForRoundTripTest: [TrioAlertCategory] {
-        [
-            .occlusion, .reservoirLow, .reservoirEmpty, .batteryLow, .batteryEmpty,
-            .hardwareFault, .deliveryUncertain, .deviceExpirationReminder, .deviceExpired,
-            .podShutdownImminent, .suspendTimeExpired, .bolusFailed, .manualTempBasalActive,
-            .notLooping, .sensorFailure, .glucoseUrgentLow, .glucoseLow, .glucoseForecastedLow,
-            .glucoseHigh, .glucoseDataStale, .algorithmError, .commsTransient
-        ]
-    }
-}