| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217 |
- import Foundation
- import LoopKit
- import Swinject
- import Testing
- @testable import Trio
- @Suite("Trio Alerts: AlertColdStartReplay decisions") struct AlertColdStartReplayTests {
- private let now = Date()
- private func entry(
- manager: String = "Omni",
- alert: String = "userPodExpiration",
- triggerType: Int16 = 0,
- triggerInterval: Decimal? = nil,
- issuedAgo: TimeInterval = 60,
- level: Decimal? = 2,
- title: String? = "Title",
- body: String? = "Body"
- ) -> AlertEntry {
- AlertEntry(
- alertIdentifier: alert,
- primitiveInterruptionLevel: level,
- issuedDate: now.addingTimeInterval(-issuedAgo),
- managerIdentifier: manager,
- triggerType: triggerType,
- triggerInterval: triggerInterval,
- contentTitle: title,
- contentBody: body
- )
- }
- private func decide(
- _ entry: AlertEntry,
- tierSnoozed: Bool = false,
- muted: Bool = false
- ) -> AlertColdStartReplay.Decision {
- AlertColdStartReplay.decision(
- for: entry,
- now: now,
- isTierSnoozed: { _ in tierSnoozed },
- isMuted: muted
- )
- }
- @Test("Elapsed delayed trigger replays as immediate") func elapsedDelayedBecomesImmediate() {
- let decision = decide(entry(
- manager: "trio.aps", alert: "loop.notActive",
- triggerType: 1, triggerInterval: 1200, issuedAgo: 25 * 60
- ))
- guard case let .replay(alert) = decision else {
- Issue.record("expected replay, got \(decision)")
- return
- }
- #expect(alert.trigger == .immediate)
- }
- @Test("Pending delayed trigger re-arms with remaining interval") func pendingDelayedKeepsRemainder() {
- let decision = decide(entry(
- manager: "trio.aps", alert: "loop.notActive",
- triggerType: 1, triggerInterval: 1200, issuedAgo: 5 * 60
- ))
- guard case let .replay(alert) = decision, case let .delayed(interval) = alert.trigger else {
- Issue.record("expected delayed replay, got \(decision)")
- return
- }
- #expect(abs(interval - 900) < 2)
- }
- @Test("Repeating trigger replays with original interval") func repeatingKeepsInterval() {
- let decision = decide(entry(triggerType: 2, triggerInterval: 900, issuedAgo: 3600))
- guard case let .replay(alert) = decision else {
- Issue.record("expected replay, got \(decision)")
- return
- }
- #expect(alert.trigger == .repeating(repeatInterval: 900))
- }
- @Test("Delayed trigger without interval is unreconstructable") func corruptTriggerSkips() {
- #expect(decide(entry(triggerType: 1, triggerInterval: nil)) == .skip)
- }
- @Test("Glucose-family entries acknowledge silently") func glucoseAcknowledgesSilently() {
- let glucose = entry(manager: "Trio", alert: "glucose.urgentLow.ABC-123")
- #expect(decide(glucose) == .acknowledgeSilently)
- let carbs = entry(manager: "Trio", alert: "glucose.carbsRequired.DEF-456")
- #expect(decide(carbs) == .acknowledgeSilently)
- }
- @Test("Catalog level override wins over stored level") func catalogOverridesStoredLevel() {
- // userPodExpiration is catalogued .active; stored says critical (3).
- let decision = decide(entry(level: 3))
- guard case let .replay(alert) = decision else {
- Issue.record("expected replay, got \(decision)")
- return
- }
- #expect(alert.interruptionLevel == .active)
- }
- @Test("Unknown identifier falls back to stored level, then timeSensitive") func levelFallbacks() {
- guard case let .replay(stored) = decide(entry(manager: "x", alert: "y", level: 3)) else {
- Issue.record("expected replay")
- return
- }
- #expect(stored.interruptionLevel == .critical)
- guard case let .replay(unstored) = decide(entry(manager: "x", alert: "y", level: nil)) else {
- Issue.record("expected replay")
- return
- }
- #expect(unstored.interruptionLevel == .timeSensitive)
- }
- @Test("Tier snooze skips catalog non-critical, critical pierces") func tierSnoozeGate() {
- #expect(decide(entry(), tierSnoozed: true) == .skip)
- let critical = entry(manager: "trio.aps", alert: "loop.notActive", issuedAgo: 60)
- if case .replay = decide(critical, tierSnoozed: true) {} else {
- Issue.record("critical should pierce tier snooze")
- }
- }
- @Test("Mute skips non-critical, critical pierces") func muteGate() {
- #expect(decide(entry(), muted: true) == .skip)
- let critical = entry(manager: "trio.aps", alert: "loop.notActive", issuedAgo: 60)
- if case .replay = decide(critical, muted: true) {} else {
- Issue.record("critical should pierce mute")
- }
- }
- @Test("Rebuilt alert carries content on both surfaces, no sound") func rebuiltAlertShape() {
- guard case let .replay(alert) = decide(entry()) else {
- Issue.record("expected replay")
- return
- }
- #expect(alert.sound == nil)
- #expect(alert.foregroundContent?.title == "Title")
- #expect(alert.foregroundContent == alert.backgroundContent)
- }
- @Test("newestPerIdentifier keeps first (newest) entry per identifier") func dedupe() {
- let newer = entry(issuedAgo: 60)
- let older = entry(issuedAgo: 3600)
- let other = entry(alert: "lowReservoir", issuedAgo: 120)
- let result = AlertColdStartReplay.newestPerIdentifier([newer, other, older])
- #expect(result == [newer, other])
- }
- }
- @Suite("Trio Alerts: AlertHistoryStorage acknowledgement") struct AlertHistoryStorageAckTests {
- private func makeStorage() -> (BaseAlertHistoryStorage, UserDefaults) {
- let suiteName = "AlertHistoryStorageAckTests-\(UUID().uuidString)"
- let defaults = UserDefaults(suiteName: suiteName)!
- defaults.removePersistentDomain(forName: suiteName)
- let container = Container()
- container.register(FileStorage.self) { _ in BaseFileStorage() }
- container.register(Broadcaster.self) { _ in BaseBroadcaster() }
- let storage = BaseAlertHistoryStorage(resolver: container.synchronize(), userDefaults: defaults)
- return (storage, defaults)
- }
- private func entry(_ alert: String, issuedAgo: TimeInterval, manager: String = "Omni") -> AlertEntry {
- AlertEntry(
- alertIdentifier: alert,
- primitiveInterruptionLevel: 2,
- issuedDate: Date().addingTimeInterval(-issuedAgo),
- managerIdentifier: manager,
- triggerType: 0,
- triggerInterval: nil,
- contentTitle: "t",
- contentBody: "b"
- )
- }
- @Test("acknowledgeAllEntries marks every matching entry") func ackAllMatching() {
- let (storage, _) = makeStorage()
- storage.addAlert(entry("userPodExpiration", issuedAgo: 3600))
- storage.addAlert(entry("userPodExpiration", issuedAgo: 60))
- storage.addAlert(entry("lowReservoir", issuedAgo: 120))
- storage.acknowledgeAllEntries(managerIdentifier: "Omni", alertIdentifier: "userPodExpiration")
- let unacked = storage.unacknowledgedAlertsWithinLast24Hours()
- #expect(unacked.map(\.alertIdentifier) == ["lowReservoir"])
- }
- @Test("acknowledgeAllEntries matches manager identifier too") func ackScopedToManager() {
- let (storage, _) = makeStorage()
- storage.addAlert(entry("expiration", issuedAgo: 60, manager: "Omni"))
- storage.addAlert(entry("expiration", issuedAgo: 120, manager: "Medtrum"))
- storage.acknowledgeAllEntries(managerIdentifier: "Omni", alertIdentifier: "expiration")
- #expect(storage.unacknowledgedAlertsWithinLast24Hours().map(\.managerIdentifier) == ["Medtrum"])
- }
- @Test("removeAlert skips acknowledged entries") func removePreservesAcknowledged() {
- let (storage, defaults) = makeStorage()
- storage.addAlert(entry("userPodExpiration", issuedAgo: 60))
- storage.acknowledgeAllEntries(managerIdentifier: "Omni", alertIdentifier: "userPodExpiration")
- storage.removeAlert(identifier: "userPodExpiration")
- // Row survives as acknowledged history instead of being erased.
- let data = defaults.data(forKey: "openaps.monitor.alertHistory.data")!
- let persisted = try! JSONCoding.decoder.decode([AlertEntry].self, from: data)
- #expect(persisted.count == 1)
- #expect(persisted[0].acknowledgedDate != nil)
- }
- @Test("removeAlert deletes unacknowledged entry") func removeDeletesUnacknowledged() {
- let (storage, _) = makeStorage()
- storage.addAlert(entry("userPodExpiration", issuedAgo: 60))
- storage.removeAlert(identifier: "userPodExpiration")
- #expect(storage.unacknowledgedAlertsWithinLast24Hours().isEmpty)
- }
- }
|