Increment 4: alerts engine, notifications & System Health score

- 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>
This commit is contained in:
hariel1985
2026-07-02 20:20:24 +02:00
szülő 9b92fb715a
commit bc494af363
10 fájl változott, egészen pontosan 608 új sor hozzáadva és 1 régi sor törölve

Fájl megtekintése

@@ -0,0 +1,110 @@
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))
}
}