Просмотр исходного кода

Test: MinimedKit alert emissions — managerIdentifier mismatch gap

Table-driven test over MinimedKit's emitted alerts. Pins the current reality:
the plugin issues with managerIdentifier "Minimed500" (its pluginIdentifier),
but the registry keys its entries under "Minimed", so every lookup returns nil
and alerts fall back to .timeSensitive. Documented gap (ratchet): the intended
critical PumpReservoirEmpty entry is dead, so out-of-insulin never escalates.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
James Woglom недель назад: 2
Родитель
Сommit
9c672969ba
2 измененных файлов с 114 добавлено и 0 удалено
  1. 4 0
      Trio.xcodeproj/project.pbxproj
  2. 110 0
      TrioTests/MinimedKitAlertEmissionTests.swift

+ 4 - 0
Trio.xcodeproj/project.pbxproj

@@ -237,6 +237,7 @@
 		38FCF3D625E8FDF40078B0D1 /* MD5.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38FCF3D525E8FDF40078B0D1 /* MD5.swift */; };
 		38FCF3F925E902C20078B0D1 /* FileStorageTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = 38FCF3F825E902C20078B0D1 /* FileStorageTests.swift */; };
 		BD11A001000000000000A002 /* AlertMuterTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD11A000000000000000A002 /* AlertMuterTests.swift */; };
+		BD11A001000000000000A010 /* MinimedKitAlertEmissionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD11A000000000000000A010 /* MinimedKitAlertEmissionTests.swift */; };
 		BD11A001000000000000A00F /* MedtrumKitAlertEmissionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD11A000000000000000A00F /* MedtrumKitAlertEmissionTests.swift */; };
 		BD11A001000000000000A00E /* DanaKitAlertEmissionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD11A000000000000000A00E /* DanaKitAlertEmissionTests.swift */; };
 		BD11A001000000000000A00D /* AlertCatalogRegistryEntriesTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD11A000000000000000A00D /* AlertCatalogRegistryEntriesTests.swift */; };
@@ -1249,6 +1250,7 @@
 		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>"; };
 		BD11A000000000000000A002 /* AlertMuterTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertMuterTests.swift; sourceTree = "<group>"; };
+		BD11A000000000000000A010 /* MinimedKitAlertEmissionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MinimedKitAlertEmissionTests.swift; sourceTree = "<group>"; };
 		BD11A000000000000000A00F /* MedtrumKitAlertEmissionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MedtrumKitAlertEmissionTests.swift; sourceTree = "<group>"; };
 		BD11A000000000000000A00E /* DanaKitAlertEmissionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = DanaKitAlertEmissionTests.swift; sourceTree = "<group>"; };
 		BD11A000000000000000A00D /* AlertCatalogRegistryEntriesTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertCatalogRegistryEntriesTests.swift; sourceTree = "<group>"; };
@@ -3017,6 +3019,7 @@
 				3BAAE60B2DE776630049589B /* DynamicISFEnableTests.swift */,
 				38FCF3F825E902C20078B0D1 /* FileStorageTests.swift */,
 				BD11A000000000000000A002 /* AlertMuterTests.swift */,
+				BD11A000000000000000A010 /* MinimedKitAlertEmissionTests.swift */,
 				BD11A000000000000000A00F /* MedtrumKitAlertEmissionTests.swift */,
 				BD11A000000000000000A00E /* DanaKitAlertEmissionTests.swift */,
 				BD11A000000000000000A00D /* AlertCatalogRegistryEntriesTests.swift */,
@@ -5595,6 +5598,7 @@
 				3B1C5C482D68E269004E9273 /* IobHistoryTests.swift in Sources */,
 				38FCF3F925E902C20078B0D1 /* FileStorageTests.swift in Sources */,
 				BD11A001000000000000A002 /* AlertMuterTests.swift in Sources */,
+				BD11A001000000000000A010 /* MinimedKitAlertEmissionTests.swift in Sources */,
 				BD11A001000000000000A00F /* MedtrumKitAlertEmissionTests.swift in Sources */,
 				BD11A001000000000000A00E /* DanaKitAlertEmissionTests.swift in Sources */,
 				BD11A001000000000000A00D /* AlertCatalogRegistryEntriesTests.swift in Sources */,

+ 110 - 0
TrioTests/MinimedKitAlertEmissionTests.swift

@@ -0,0 +1,110 @@
+import Foundation
+import LoopKit
+import Testing
+
+@testable import Trio
+
+/// Pins how MinimedKit's emitted LoopKit Alerts are routed through Trio's
+/// alert layer, as recorded by the synthesis audit over the managers / pump /
+/// MinimedKit sources (`MinimedKit/PumpManager/MinimedPumpManager.swift`).
+///
+/// What this suite pins:
+///  - The CURRENT (not ideal) registry behavior for every alert MinimedKit
+///    issues. MinimedKit issues all four alerts with managerIdentifier
+///    "Minimed500" (its pluginIdentifier), but `AlertCatalogRegistry` keys its
+///    Minimed entries under "Minimed" (AlertCatalogRegistry.swift:88-93). Since
+///    `lookup` matches the full `Alert.Identifier` and there is no
+///    "Minimed500"-style fallback (only "Omni:pumpFault" has one), every
+///    lookup of an actually-emitted identifier returns nil. Trio then falls
+///    back to the alert's own plugin level, which is LoopKit's default
+///    `.timeSensitive`.
+///  - The documented escalation gap, as a ratchet that fails when the gap is
+///    fixed (forcing this file to be updated).
+///
+/// One-line gap summary: PumpReservoirEmpty is taxonomy-Critical (N4 ->
+/// `.critical`) but its effective level is `.timeSensitive` because the
+/// intended registry entry ("Minimed","PumpReservoirEmpty"=.critical) is
+/// unreachable under the emitted managerIdentifier "Minimed500" — so an
+/// out-of-insulin condition never escalates to a critical interruption.
+@Suite("Manager Emissions: MinimedKit") struct MinimedKitAlertEmissionTests {
+    private func id(_ manager: String, _ alertID: String) -> Alert.Identifier {
+        Alert.Identifier(managerIdentifier: manager, alertIdentifier: alertID)
+    }
+
+    /// The managerIdentifier MinimedKit actually issues with (its
+    /// pluginIdentifier), NOT the "Minimed" key the registry uses.
+    private static let emittedManagerIdentifier = "Minimed500"
+
+    /// Emitted alerts from the synthesis audit. `currentRegistryLevel` is the
+    /// level `lookup(id("Minimed500", alertID))` returns TODAY (all nil due to
+    /// the managerIdentifier mismatch). `taxonomyLevel` is what the row should
+    /// be per taxonomy; `isGap` is true when the effective level
+    /// (registry-or-default `.timeSensitive`) is less severe than taxonomy.
+    struct Row {
+        let alertIdentifier: String
+        let currentRegistryLevel: Alert.InterruptionLevel?
+        let taxonomyLevel: Alert.InterruptionLevel
+        let isGap: Bool
+    }
+
+    private static let rows: [Row] = [
+        // F2 -> .timeSensitive. Registry intends ("Minimed","lowRLBattery")=
+        // .timeSensitive but it is unreachable; effective default
+        // .timeSensitive == taxonomy, not a gap.
+        // MinimedKit/PumpManager/MinimedPumpManager.swift:263-268
+        Row(alertIdentifier: "lowRLBattery", currentRegistryLevel: nil, taxonomyLevel: .timeSensitive, isGap: false),
+        // F2 -> .timeSensitive. ("Minimed","PumpBatteryLow")=.timeSensitive
+        // unreachable; effective default == taxonomy, not a gap.
+        // MinimedKit/PumpManager/MinimedPumpManager.swift:495-510
+        Row(alertIdentifier: "PumpBatteryLow", currentRegistryLevel: nil, taxonomyLevel: .timeSensitive, isGap: false),
+        // N4 -> .critical. GAP: ("Minimed","PumpReservoirEmpty")=.critical
+        // unreachable under "Minimed500"; effective default .timeSensitive is
+        // LESS severe than taxonomy .critical.
+        // MinimedKit/PumpManager/MinimedPumpManager.swift:560-600
+        Row(alertIdentifier: "PumpReservoirEmpty", currentRegistryLevel: nil, taxonomyLevel: .critical, isGap: true),
+        // F1 -> .timeSensitive. ("Minimed","PumpReservoirLow")=.timeSensitive
+        // unreachable; effective default == taxonomy, not a gap.
+        // MinimedKit/PumpManager/MinimedPumpManager.swift:571-610
+        Row(alertIdentifier: "PumpReservoirLow", currentRegistryLevel: nil, taxonomyLevel: .timeSensitive, isGap: false)
+    ]
+
+    // MARK: - Registry behavior (pinned to CURRENT, must be green)
+
+    @Test(
+        "registry behavior is pinned for every emitted alert",
+        arguments: rows
+    ) func registryBehaviorPinned(_ row: Row) {
+        // Asserts CURRENT behavior: lookup under the EMITTED managerIdentifier
+        // "Minimed500" returns the recorded level (nil today). This is not the
+        // ideal — it documents the managerIdentifier mismatch.
+        #expect(
+            AlertCatalogRegistry.lookup(
+                id(Self.emittedManagerIdentifier, row.alertIdentifier)
+            )?.interruptionLevel == row.currentRegistryLevel
+        )
+    }
+
+    // MARK: - Known escalation gaps (ratchet)
+
+    /// AlertIdentifiers whose effective level is LESS severe than their
+    /// taxonomy level today. Documented expectation per identifier:
+    ///
+    ///  - "PumpReservoirEmpty": SHOULD be `.critical` (taxonomy N4). The
+    ///    registry author intended this — ("Minimed","PumpReservoirEmpty") is
+    ///    registered at .critical (AlertCatalogRegistry.swift:90) — but
+    ///    MinimedKit issues with managerIdentifier "Minimed500", so the entry
+    ///    is dead and the alert falls back to .timeSensitive. Out-of-insulin
+    ///    never escalates to a critical interruption.
+    ///    Source: MinimedKit/PumpManager/MinimedPumpManager.swift:560-600
+    ///
+    /// This stays green now and FAILS (prompting an update here) once the
+    /// managerIdentifier mismatch is fixed and the gap closes.
+    private static let knownEscalationGaps: Set<String> = [
+        "PumpReservoirEmpty"
+    ]
+
+    @Test("known escalation gaps are exactly as documented") func knownEscalationGapsExact() {
+        let computed = Set(Self.rows.filter(\.isGap).map(\.alertIdentifier))
+        #expect(computed == Self.knownEscalationGaps)
+    }
+}