Преглед изворни кода

Test: TandemKit alert emissions — structural no-Alert gap

Table-driven test for TandemKit, sourced solely from the synthesis audit
(TandemKit is not a Trio-dev submodule; other users add it for testing). The
audit's structural finding: TandemKit issues zero LoopKit Alerts, so no
Alert.Identifier ever reaches Trio and no registry entry can match. Pins that
absence and documents the structural escalation gap in prose.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
James Woglom пре 2 недеља
родитељ
комит
8759af7b18
2 измењених фајлова са 127 додато и 0 уклоњено
  1. 4 0
      Trio.xcodeproj/project.pbxproj
  2. 123 0
      TrioTests/TandemKitAlertEmissionTests.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 */; };
+		BD11A001000000000000A013 /* TandemKitAlertEmissionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD11A000000000000000A013 /* TandemKitAlertEmissionTests.swift */; };
 		BD11A001000000000000A012 /* RileyLinkKitAlertEmissionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD11A000000000000000A012 /* RileyLinkKitAlertEmissionTests.swift */; };
 		BD11A001000000000000A011 /* OmnipodKitAlertEmissionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD11A000000000000000A011 /* OmnipodKitAlertEmissionTests.swift */; };
 		BD11A001000000000000A010 /* MinimedKitAlertEmissionTests.swift in Sources */ = {isa = PBXBuildFile; fileRef = BD11A000000000000000A010 /* MinimedKitAlertEmissionTests.swift */; };
@@ -1252,6 +1253,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>"; };
+		BD11A000000000000000A013 /* TandemKitAlertEmissionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = TandemKitAlertEmissionTests.swift; sourceTree = "<group>"; };
 		BD11A000000000000000A012 /* RileyLinkKitAlertEmissionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RileyLinkKitAlertEmissionTests.swift; sourceTree = "<group>"; };
 		BD11A000000000000000A011 /* OmnipodKitAlertEmissionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = OmnipodKitAlertEmissionTests.swift; sourceTree = "<group>"; };
 		BD11A000000000000000A010 /* MinimedKitAlertEmissionTests.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MinimedKitAlertEmissionTests.swift; sourceTree = "<group>"; };
@@ -3023,6 +3025,7 @@
 				3BAAE60B2DE776630049589B /* DynamicISFEnableTests.swift */,
 				38FCF3F825E902C20078B0D1 /* FileStorageTests.swift */,
 				BD11A000000000000000A002 /* AlertMuterTests.swift */,
+				BD11A000000000000000A013 /* TandemKitAlertEmissionTests.swift */,
 				BD11A000000000000000A012 /* RileyLinkKitAlertEmissionTests.swift */,
 				BD11A000000000000000A011 /* OmnipodKitAlertEmissionTests.swift */,
 				BD11A000000000000000A010 /* MinimedKitAlertEmissionTests.swift */,
@@ -5604,6 +5607,7 @@
 				3B1C5C482D68E269004E9273 /* IobHistoryTests.swift in Sources */,
 				38FCF3F925E902C20078B0D1 /* FileStorageTests.swift in Sources */,
 				BD11A001000000000000A002 /* AlertMuterTests.swift in Sources */,
+				BD11A001000000000000A013 /* TandemKitAlertEmissionTests.swift in Sources */,
 				BD11A001000000000000A012 /* RileyLinkKitAlertEmissionTests.swift in Sources */,
 				BD11A001000000000000A011 /* OmnipodKitAlertEmissionTests.swift in Sources */,
 				BD11A001000000000000A010 /* MinimedKitAlertEmissionTests.swift in Sources */,

+ 123 - 0
TrioTests/TandemKitAlertEmissionTests.swift

@@ -0,0 +1,123 @@
+import Foundation
+import LoopKit
+import Testing
+
+@testable import Trio
+
+/// Pins Trio's alert-routing behavior for the **TandemKit** pump plugin.
+///
+/// Rows are derived from the synthesis audit of the Tandem stack
+/// (`TandemCore` / `TandemKit` / `TandemKitUI` / `TandemKitPlugin`; pump:
+/// Tandem t:slim X2 / Mobi). The audit's central finding is STRUCTURAL:
+/// TandemKit issues **zero** LoopKit Alerts. There is no
+/// `issueAlert` / `retractAlert` / `Alert(identifier:)` / `DeviceAlert` /
+/// `UNUserNotificationCenter` usage anywhere in the shipping targets
+/// (audit.md:7-8, Notes:192), so TandemKit never constructs an
+/// `Alert.Identifier` and there is no `(managerIdentifier, alertIdentifier)`
+/// pair for Trio to route. The PRIMARY emitted-alert table is therefore empty
+/// and `AlertCatalogRegistry` has — and could match — no Tandem entries.
+///
+/// Every Tandem pump alarm the device surfaces (occlusion N1->Critical;
+/// empty/removed cartridge & "No Insulin" N4->Critical; battery-shutdown
+/// N5-pump->Critical; temperature/altitude/stuck-button/invalid-date/
+/// pump-reset & the non-dismissable Malfunction item N1->Critical;
+/// resume-pump / auto-off delivery-stopped N2->Critical; CGM-alert catalog
+/// N7->High) is rendered ONLY inside TandemKit's own in-app
+/// "Pump notifications" settings list
+/// (`NotificationBundle` -> `TandemKitNotificationsView`), which Trio never
+/// receives. The `AlertResponseType` description catalog is intercepted in
+/// `fetchNotifications` (audit:129,193) and is dead for display, and the lone
+/// `didError` call site (`TandemPumpManager.swift:218`) has no callers.
+///
+/// GAP SUMMARY: the effective interruption level of every taxonomy-Critical
+/// Tandem alarm is "never delivered" — strictly less severe than `.critical`.
+/// This is broader than a registry-key mismatch: no `Alert.Identifier` is
+/// ever issued, so no escalation is possible until upstream TandemKit issues
+/// LoopKit Alerts (and adopts `AlertCatalogVendor`, or Trio adds Tandem
+/// registry entries). The remediation gap is documented in this suite's prose
+/// rather than as table rows, because there are no emitted-alert rows to ratchet.
+///
+/// The only Tandem signals that DO reach Trio are `PumpManagerError`-wrapped
+/// `PumpCommError` / `TandemPumpManagerValidationError` values returned through
+/// `PumpManager` dosing/limit/time completion handlers (e.g. `enactTempBasal`
+/// -> `APSManager:703` `APSError.pumpError(error)` -> `processError` ->
+/// `TrioAlertClassifier.categorize`). These are error-STRING emissions, not
+/// Alerts. No classifier rows are pinned here: the TandemKit submodule is not
+/// present in Trio-dev and the audit recorded the rendered `errorDescription`
+/// text rather than the `String(describing:)` form, so the exact classifier
+/// input (a LoopKit `PumpManagerError` enum — not `CustomStringConvertible` —
+/// wrapping a `PumpCommError`/validation case) cannot be stated with
+/// certainty. Per the SCOPE rule those are omitted rather than guessed.
+@Suite("Manager Emissions: TandemKit") struct TandemKitAlertEmissionTests {
+    /// TandemKit issues no LoopKit Alerts, so it carries no real
+    /// `managerIdentifier`. We keep the audit's verbatim placeholder string so
+    /// the (empty) registry-lookup pin documents exactly what was searched.
+    private static let managerIdentifier =
+        "(none — TandemKit issues no LoopKit Alerts; it uses no Alert.Identifier at all)"
+
+    private func id(_ manager: String, _ alertID: String) -> Alert.Identifier {
+        Alert.Identifier(managerIdentifier: manager, alertIdentifier: alertID)
+    }
+
+    // MARK: - Emitted alerts (PRIMARY table)
+
+    /// `(alertIdentifier, expectedRegistryLevel)`.
+    ///
+    /// EMPTY: TandemKit issues no LoopKit Alerts (audit.md:7-8, Notes:192), so
+    /// there are no `(managerIdentifier, alertIdentifier)` pairs to pin. If this
+    /// table ever gains a row, the audit's structural finding has changed and
+    /// this suite's doc comment must be revisited.
+    static let alertRows: [(alertID: String, expectedLevel: Alert.InterruptionLevel?)] = []
+
+    @Test(
+        "registry behavior is pinned for every emitted alert",
+        arguments: alertRows
+    ) func registryBehaviorIsPinned(row: (alertID: String, expectedLevel: Alert.InterruptionLevel?)) {
+        let identifier = id(Self.managerIdentifier, row.alertID)
+        #expect(AlertCatalogRegistry.lookup(identifier)?.interruptionLevel == row.expectedLevel)
+    }
+
+    /// Backstop for the empty PRIMARY table: even the audit's verbatim
+    /// placeholder manager string resolves to no catalog entry. This pins the
+    /// structural fact that TandemKit has no escalatable surface in Trio today.
+    @Test("no Tandem alert identifier resolves in the catalog registry") func noTandemEntryInRegistry() {
+        #expect(Self.alertRows.isEmpty)
+        #expect(AlertCatalogRegistry.lookup(id(Self.managerIdentifier, "anything")) == nil)
+    }
+
+    // MARK: - Documented escalation gaps (ratchet)
+
+    /// `alertIdentifier`s of emitted alerts whose EFFECTIVE current level is
+    /// less severe than their taxonomy level.
+    ///
+    /// EMPTY by construction: a gap row requires an emitted alert with an
+    /// `Alert.Identifier`, and TandemKit emits none. The Tandem alarms that the
+    /// taxonomy rates Critical (occlusion N1, empty/removed cartridge & "No
+    /// Insulin" N4, battery-shutdown N5-pump, Malfunction/temperature/altitude/
+    /// stuck-button/invalid-date/pump-reset N1, resume-pump / auto-off N2) and
+    /// High (CGM-alert catalog N7) never reach Trio at all — they live only in
+    /// TandemKit's in-app "Pump notifications" list (`NotificationBundle` ->
+    /// `TandemKitNotificationsView`; audit.md:7-8, 129, 192-193). That is a
+    /// STRUCTURAL gap (no `Alert.Identifier` is ever issued), strictly broader
+    /// than a registry-key mismatch, and is documented in the suite prose
+    /// rather than as a ratchetable row. The fix is upstream: TandemKit must
+    /// issue LoopKit Alerts and adopt `AlertCatalogVendor` (or Trio must add
+    /// Tandem registry entries) before any escalation is possible.
+    static let knownEscalationGaps: Set<String> = []
+
+    @Test("known escalation gaps are exactly as documented") func knownEscalationGapsAreExactlyAsDocumented() {
+        // Recompute the gap set from the PRIMARY table: a row is a gap when its
+        // effective level (registry level if present, else unknown) is missing.
+        // With no rows this is empty and matches the documented (empty) set.
+        // Should TandemKit ever start issuing Alerts and a row be added with an
+        // unescalated level, this recomputed set will diverge from
+        // `knownEscalationGaps`, failing the test and prompting an update.
+        var computed = Set<String>()
+        for row in Self.alertRows where row.expectedLevel != .critical {
+            // Placeholder recompute: real gap math would compare against the
+            // row's taxonomy level. No rows exist, so the loop never executes.
+            computed.insert(row.alertID)
+        }
+        #expect(computed == Self.knownEscalationGaps)
+    }
+}