fix(alerts): skip read-only/small volumes in low-storage rule (Issue #33) #37
@@ -702,7 +702,9 @@ public final class SystemTelemetryStore {
|
||||
freeBytes: $0.freeBytes,
|
||||
freePercent: $0.totalBytes > 0
|
||||
? Double($0.freeBytes) / Double($0.totalBytes) * 100.0
|
||||
: 0
|
||||
: 0,
|
||||
totalBytes: $0.totalBytes,
|
||||
isReadOnly: $0.isReadOnly
|
||||
)
|
||||
}
|
||||
context.batteryLevel = self.batteryHealth.maxCapacity > 0
|
||||
|
||||
@@ -163,7 +163,10 @@ public final class AlertEngine {
|
||||
|
||||
// 4. Low storage space (per mounted volume)
|
||||
if cfg.storageEnabled {
|
||||
for volume in ctx.volumes {
|
||||
// Skip read-only mounts (DMGs, snapshots) and trivially small
|
||||
// volumes where free-space thresholds are meaningless.
|
||||
let minTotalBytes: UInt64 = 1_073_741_824 // 1 GB
|
||||
for volume in ctx.volumes where !volume.isReadOnly && volume.totalBytes >= minTotalBytes {
|
||||
let freeGB = Double(volume.freeBytes) / (1024 * 1024 * 1024)
|
||||
if freeGB < cfg.storageFreeGBThreshold || volume.freePercent < cfg.storageFreePercentThreshold {
|
||||
alerts.append(SystemAlert(
|
||||
|
||||
@@ -70,12 +70,18 @@ public struct StorageVolumeContext: Equatable, Sendable {
|
||||
public let volumeName: String
|
||||
public let freeBytes: UInt64
|
||||
public let freePercent: Double
|
||||
public let totalBytes: UInt64
|
||||
/// Read-only mounts (DMG images, snapshots) are excluded from alerting.
|
||||
public let isReadOnly: Bool
|
||||
|
||||
public init(mountPoint: String, volumeName: String, freeBytes: UInt64, freePercent: Double) {
|
||||
public init(mountPoint: String, volumeName: String, freeBytes: UInt64, freePercent: Double,
|
||||
totalBytes: UInt64 = 0, isReadOnly: Bool = false) {
|
||||
self.mountPoint = mountPoint
|
||||
self.volumeName = volumeName
|
||||
self.freeBytes = freeBytes
|
||||
self.freePercent = freePercent
|
||||
self.totalBytes = totalBytes
|
||||
self.isReadOnly = isReadOnly
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -128,8 +128,10 @@ final class MMAlertsTests: XCTestCase {
|
||||
|
||||
func testStorageRuleTriggersPerVolume() {
|
||||
let volumes = [
|
||||
StorageVolumeContext(mountPoint: "/", volumeName: "Macintosh HD", freeBytes: 5 * 1_073_741_824, freePercent: 5),
|
||||
StorageVolumeContext(mountPoint: "/Volumes/Data", volumeName: "Data", freeBytes: 500 * 1_073_741_824, freePercent: 50)
|
||||
StorageVolumeContext(mountPoint: "/", volumeName: "Macintosh HD", freeBytes: 5 * 1_073_741_824,
|
||||
freePercent: 5, totalBytes: 500 * 1_073_741_824),
|
||||
StorageVolumeContext(mountPoint: "/Volumes/Data", volumeName: "Data", freeBytes: 500 * 1_073_741_824,
|
||||
freePercent: 50, totalBytes: 1_000_000_000_000)
|
||||
]
|
||||
engine.evaluate(context(volumes: volumes))
|
||||
XCTAssertEqual(engine.activeAlerts.count, 1)
|
||||
@@ -137,11 +139,32 @@ final class MMAlertsTests: XCTestCase {
|
||||
}
|
||||
|
||||
func testStorageRuleTriggersOnLowPercent() {
|
||||
let volumes = [StorageVolumeContext(mountPoint: "/Volumes/Big", volumeName: "Big", freeBytes: 200 * 1_073_741_824, freePercent: 4)]
|
||||
let volumes = [StorageVolumeContext(mountPoint: "/Volumes/Big", volumeName: "Big",
|
||||
freeBytes: 200 * 1_073_741_824, freePercent: 4,
|
||||
totalBytes: 2_000_000_000_000)]
|
||||
engine.evaluate(context(volumes: volumes))
|
||||
XCTAssertEqual(engine.activeAlerts.count, 1)
|
||||
}
|
||||
|
||||
func testStorageRuleSkipsReadOnlyVolumes() {
|
||||
// A read-only DMG mount (e.g. an app running from a mounted image)
|
||||
// with ~0% free must not alert — issue #33 false positive.
|
||||
let volumes = [StorageVolumeContext(mountPoint: "/Volumes/Ghostty", volumeName: "Ghostty",
|
||||
freeBytes: 1_048_576, freePercent: 0.1,
|
||||
totalBytes: 500_000_000_000, isReadOnly: true)]
|
||||
engine.evaluate(context(volumes: volumes))
|
||||
XCTAssertTrue(engine.activeAlerts.isEmpty)
|
||||
XCTAssertTrue(engine.history.isEmpty)
|
||||
}
|
||||
|
||||
func testStorageRuleSkipsTinyVolumes() {
|
||||
let volumes = [StorageVolumeContext(mountPoint: "/Volumes/Small", volumeName: "Small",
|
||||
freeBytes: 1_048_576, freePercent: 0.5,
|
||||
totalBytes: 200_000_000, isReadOnly: false)]
|
||||
engine.evaluate(context(volumes: volumes))
|
||||
XCTAssertTrue(engine.activeAlerts.isEmpty)
|
||||
}
|
||||
|
||||
// MARK: Battery
|
||||
|
||||
func testBatteryRuleTriggersOnlyWhileDischarging() {
|
||||
|
||||
Reference in New Issue
Block a user