Keep a disk heat finding open without a current reading

Patrol verifies a disk-high finding as recovered once the disk's
temperature cools to its alert clear value. A disk in standby, or one
whose host agent stopped reporting, has no current reading, and a
missing reading counted as cooled. A finding raised for a SATA disk at
56C would close as soon as the disk spun down, while its temperature
alert stayed open.

When a disk has no current reading and its last known reading is still
at or above the clear value, verification now reports the recovery as
unknown instead of proven. A disk last seen cool still recovers.
This commit is contained in:
rcourtman
2026-10-07 00:17:32 +01:00
parent 35bf55af23
commit 396bb5d517
3 changed files with 40 additions and 9 deletions
@@ -315,7 +315,8 @@ configuration applies. A disk is hot from its trigger: a triage warning, a disk
issue and an AI chat "needing attention" entry, as its temperature alert and
Physical Disks Running Hot verdict are. A disk-high finding recovers at or
below the clear value (under the trigger when there is no band below it), as
the alert does. Scoped runs keep the provider, and a
the alert does. With no current reading, a disk last seen hot leaves that
verification unknown rather than recovered. Scoped runs keep the provider, and a
provider set before Patrol starts is handed to it. Below the trigger Patrol flags nothing, so an NVMe at 63C stays
quiet and a SATA disk at 56C is flagged. Proofs:
`internal/ai/patrol_disk_temperature_test.go` and
@@ -1,12 +1,15 @@
package ai
import (
"errors"
"strings"
"testing"
"time"
"github.com/rcourtman/pulse-go-rewrite/internal/models"
"github.com/rcourtman/pulse-go-rewrite/internal/unifiedresources"
"github.com/rcourtman/pulse-go-rewrite/pkg/aicontracts"
"github.com/rcourtman/pulse-go-rewrite/pkg/diskinventory"
)
// raisedNVMeThresholds is a user who raised the NVMe disk temperature trigger
@@ -226,3 +229,21 @@ func TestDiskHeatRecoversAtTheClearValue(t *testing.T) {
t.Fatalf("no-band limits %+v: want 70C hot and not cooled, 69C cooled", noBand)
}
}
// With no current reading (standby, a silent agent) a disk last seen hot has
// not shown that it cooled, so heat recovery is unknown rather than proven.
func TestDiskHeatRecoveryNeedsACurrentReading(t *testing.T) {
retained := &diskinventory.CollectionStatus{Temperature: diskinventory.Unavailable("smartctl", "disk is in standby")}
state := newPatrolRuntimeState(models.StateSnapshot{PhysicalDisks: []models.PhysicalDisk{
{ID: "sata-retained-hot", DevPath: "/dev/sda", Type: "sata", Health: "PASSED", Wearout: -1, Temperature: 56, Collection: retained},
{ID: "sata-retained-cool", DevPath: "/dev/sdb", Type: "sata", Health: "PASSED", Wearout: -1, Temperature: 40, Collection: retained},
}})
if _, err := verifyMetricRecoveredState(state, PatrolThresholds{}, "disk-high", "sata-retained-hot", "physical_disk"); !errors.Is(err, aicontracts.ErrVerificationUnknown) {
t.Fatalf("retained 56C: err = %v, want ErrVerificationUnknown", err)
}
recovered, err := verifyMetricRecoveredState(state, PatrolThresholds{}, "disk-high", "sata-retained-cool", "physical_disk")
if err != nil || !recovered {
t.Fatalf("retained 40C: recovered = %v, err = %v, want recovered", recovered, err)
}
}
+17 -8
View File
@@ -2296,6 +2296,11 @@ func verifyMetricRecoveredState(snap patrolRuntimeState, thresholds PatrolThresh
if !disk.temperatureLimits.cooled(disk.temperature) {
return false, nil
}
// A disk last seen hot with no current reading has not
// shown that it cooled, so its recovery is unknown.
if disk.temperature <= 0 && !disk.temperatureLimits.cooled(disk.lastKnownTemperature) {
return false, fmt.Errorf("%w: no current temperature reading for %s", aicontracts.ErrVerificationUnknown, rid)
}
return true, nil
}
break
@@ -2317,10 +2322,13 @@ func verifyMetricRecoveredState(snap patrolRuntimeState, thresholds PatrolThresh
}
type patrolPhysicalDiskVerification struct {
health string
wearout int
temperature int
temperatureLimits diskTemperatureLimits
health string
wearout int
temperature int
// lastKnownTemperature is a retained reading the current observation
// did not collect (a disk in standby, a silent agent).
lastKnownTemperature int
temperatureLimits diskTemperatureLimits
}
type patrolPhysicalDiskVisitor func(identifiers []string, verification patrolPhysicalDiskVerification) bool
@@ -2464,10 +2472,11 @@ func patrolVisitPhysicalDiskVerification(snap patrolRuntimeState, visit patrolPh
rows := patrolPhysicalDiskRows(snap, nil)
for _, disk := range rows {
if !visit([]string{disk.id, disk.name, disk.devPath, disk.model}, patrolPhysicalDiskVerification{
health: strings.TrimSpace(disk.health),
wearout: disk.wearout,
temperature: disk.temperature.Collected,
temperatureLimits: disk.temperatureLimits,
health: strings.TrimSpace(disk.health),
wearout: disk.wearout,
temperature: disk.temperature.Collected,
lastKnownTemperature: disk.temperature.LastKnown,
temperatureLimits: disk.temperatureLimits,
}) {
return true
}