- HealthModel (pure): 0-100 score + plain-language diagnosis + rating - SystemAlert/AlertKind/AlertThresholds + pure AlertEvaluator (sustained-breach) - AlertCenter (@MainActor): per-cycle health + alert evaluation, de-dup by kind, inbox (capped), native UserNotifications on transition into a condition - Covers: sustained high CPU, critical memory pressure, disk full, thermal, runaway process, low battery - Wire evaluate() into SystemMonitor; request notif auth on launch - HealthCardView (score ring + diagnosis) + AlertsInboxView in Power & Storage; health score + alert badge in menu bar - Renamed model Alert->SystemAlert to avoid SwiftUI.Alert shadowing - 10 new tests incl. AlertCenter de-dup integration (52 total green) - Visual smoke: Energy + Disk I/O columns render with live data under load Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
111 sor
4.8 KiB
Swift
111 sor
4.8 KiB
Swift
import XCTest
|
|
@testable import TopManager
|
|
|
|
final class HealthAlertTests: XCTestCase {
|
|
|
|
private func healthy() -> HealthInput {
|
|
HealthInput(cpuUsage: 10, memoryPressure: .nominal, swapUsedBytes: 0,
|
|
thermal: .nominal, diskUsedFraction: 0.4)
|
|
}
|
|
|
|
// MARK: - Health score
|
|
|
|
func testHealthyScoreIsExcellent() {
|
|
let s = HealthModel.score(healthy())
|
|
XCTAssertEqual(s, 100)
|
|
XCTAssertEqual(HealthModel.rating(s), "Excellent")
|
|
}
|
|
|
|
func testHighCPULowersScore() {
|
|
var input = healthy()
|
|
input = HealthInput(cpuUsage: 100, memoryPressure: .nominal, swapUsedBytes: 0,
|
|
thermal: .nominal, diskUsedFraction: 0.4)
|
|
XCTAssertLessThan(HealthModel.score(input), HealthModel.score(healthy()))
|
|
}
|
|
|
|
func testCriticalMemoryAndThermalCompound() {
|
|
let input = HealthInput(cpuUsage: 95, memoryPressure: .critical, swapUsedBytes: 3_000_000_000,
|
|
thermal: .critical, diskUsedFraction: 0.97)
|
|
let s = HealthModel.score(input)
|
|
XCTAssertLessThan(s, 50)
|
|
XCTAssertEqual(HealthModel.rating(s), "Poor")
|
|
XCTAssertGreaterThanOrEqual(s, 0)
|
|
}
|
|
|
|
func testScoreClampedToRange() {
|
|
// Absolute worst case must not underflow below 0.
|
|
let worst = HealthInput(cpuUsage: 100, memoryPressure: .critical, swapUsedBytes: .max,
|
|
thermal: .critical, diskUsedFraction: 1.0)
|
|
let s = HealthModel.score(worst)
|
|
XCTAssertTrue((0...100).contains(s))
|
|
}
|
|
|
|
func testDiagnosisEmptyWhenHealthy() {
|
|
XCTAssertTrue(HealthModel.diagnosis(healthy()).isEmpty)
|
|
}
|
|
|
|
func testDiagnosisReportsProblems() {
|
|
let input = HealthInput(cpuUsage: 95, memoryPressure: .critical, swapUsedBytes: 3_000_000_000,
|
|
thermal: .serious, diskUsedFraction: 0.93)
|
|
let d = HealthModel.diagnosis(input)
|
|
XCTAssertFalse(d.isEmpty)
|
|
XCTAssertTrue(d.contains { $0.localizedCaseInsensitiveContains("CPU") })
|
|
XCTAssertTrue(d.contains { $0.localizedCaseInsensitiveContains("memory") })
|
|
XCTAssertTrue(d.contains { $0.localizedCaseInsensitiveContains("disk") })
|
|
}
|
|
|
|
// MARK: - Alert sustained evaluator
|
|
|
|
func testSustainedFiresOnlyAfterRequiredCycles() {
|
|
var count = 0
|
|
var result = AlertEvaluator.sustained(breached: true, previousCount: count, requiredCycles: 3)
|
|
count = result.count
|
|
XCTAssertFalse(result.firing) // 1
|
|
result = AlertEvaluator.sustained(breached: true, previousCount: count, requiredCycles: 3)
|
|
count = result.count
|
|
XCTAssertFalse(result.firing) // 2
|
|
result = AlertEvaluator.sustained(breached: true, previousCount: count, requiredCycles: 3)
|
|
count = result.count
|
|
XCTAssertTrue(result.firing) // 3 → fires
|
|
}
|
|
|
|
func testSustainedResetsWhenConditionClears() {
|
|
var count = 5
|
|
let result = AlertEvaluator.sustained(breached: false, previousCount: count, requiredCycles: 3)
|
|
count = result.count
|
|
XCTAssertFalse(result.firing)
|
|
XCTAssertEqual(count, 0)
|
|
}
|
|
|
|
func testSeverityOrdering() {
|
|
XCTAssertTrue(AlertSeverity.info < AlertSeverity.warning)
|
|
XCTAssertTrue(AlertSeverity.warning < AlertSeverity.critical)
|
|
}
|
|
|
|
// MARK: - AlertCenter integration (de-dup + inbox)
|
|
|
|
@MainActor
|
|
func testAlertCenterDedupesAndClearsActiveKinds() {
|
|
let center = AlertCenter.shared
|
|
center.clearInbox()
|
|
|
|
let mem = MemoryInfo(totalMemory: 16, usedMemory: 15, freeMemory: 1, activeMemory: 8,
|
|
inactiveMemory: 2, wiredMemory: 3, compressedMemory: 2, cachedMemory: 0,
|
|
swapUsed: 0, swapTotal: 0, memoryPressure: .critical)
|
|
|
|
let before = center.alerts.count
|
|
// Two consecutive critical-pressure evaluations → only ONE alert (de-dup).
|
|
center.evaluate(cpuUsage: 5, memory: mem, disk: nil, thermal: .nominal, topProcess: nil, power: nil)
|
|
center.evaluate(cpuUsage: 5, memory: mem, disk: nil, thermal: .nominal, topProcess: nil, power: nil)
|
|
XCTAssertEqual(center.alerts.count, before + 1, "critical memory pressure should fire exactly once")
|
|
XCTAssertTrue(center.activeKinds.contains(.memoryPressure))
|
|
|
|
// Pressure recovers → active kind clears, no new alert added.
|
|
let ok = MemoryInfo(totalMemory: 16, usedMemory: 4, freeMemory: 12, activeMemory: 2,
|
|
inactiveMemory: 1, wiredMemory: 1, compressedMemory: 0, cachedMemory: 0,
|
|
swapUsed: 0, swapTotal: 0, memoryPressure: .nominal)
|
|
center.evaluate(cpuUsage: 5, memory: ok, disk: nil, thermal: .nominal, topProcess: nil, power: nil)
|
|
XCTAssertFalse(center.activeKinds.contains(.memoryPressure))
|
|
}
|
|
}
|