From 8bc6aa3c867ce53fdd743f59a56bc0969890df4d Mon Sep 17 00:00:00 2001 From: "pulse-triage[bot]" <249995291+pulse-triage[bot]@users.noreply.github.com> Date: Sat, 19 Sep 2026 17:27:25 +0100 Subject: [PATCH] chore(monitoring): record agent-lifecycle contract and disk-life proof Follow-up to the physical-disk endurance precedence fix. Records the dependent agent-lifecycle contract section and moves the regression into the accepted physical-disk proof file so later per-commit canonical guards find both the contract and the verification artifact. Change-source: pulse-maintainer --- .../v6/internal/subsystems/agent-lifecycle.md | 16 +++++ .../v6/internal/subsystems/monitoring.md | 2 +- .../monitoring/monitor_integration_test.go | 67 ------------------ .../physical_disk_roundtrip_test.go | 69 +++++++++++++++++++ 4 files changed, 86 insertions(+), 68 deletions(-) diff --git a/docs/release-control/v6/internal/subsystems/agent-lifecycle.md b/docs/release-control/v6/internal/subsystems/agent-lifecycle.md index 3be657745e..f1a834635e 100644 --- a/docs/release-control/v6/internal/subsystems/agent-lifecycle.md +++ b/docs/release-control/v6/internal/subsystems/agent-lifecycle.md @@ -8105,3 +8105,19 @@ install token clears the block and heals to the base identity, while a token already bound to its own derived identity (an established distinct host sharing a hostname or machine ID, #1753) is left untouched. Focused proof lives in `internal/monitoring/monitor_host_agents_test.go`. + +### Agent endurance evidence cannot raise a disk's remaining life + +The physical-disk SMART merge treats a linked host agent's `PercentageUsed` as +more precise than the Proxmox inventory, but it must not make a disk look +healthier than Proxmox already reports. An intermittent NVMe endurance reading +of `PercentageUsed` 0 previously raised the merged remaining life to 100, which +resolved a genuine low-life disk-wearout alert and let it re-fire on the next +physical-disk poll with a fresh resolved notification every poll interval +(#2112). The merge now takes the agent value only when the Proxmox value is +unreported or more pessimistic, never raising a reported remaining-life value, +while still recording the agent's SMART attributes. This does not change agent +admission, token binding or removal-block behaviour. Focused proof lives in +`internal/monitoring/physical_disk_roundtrip_test.go` +(`TestMergeHostAgentSMARTIntoDisks_AgentWearoutDoesNotHideLowPVELife` and +`TestMergeHostAgentSMARTIntoDisks_AgentWearoutFillsUnreportedPVELife`). diff --git a/docs/release-control/v6/internal/subsystems/monitoring.md b/docs/release-control/v6/internal/subsystems/monitoring.md index 2afeefc878..5d8c706cc3 100644 --- a/docs/release-control/v6/internal/subsystems/monitoring.md +++ b/docs/release-control/v6/internal/subsystems/monitoring.md @@ -4116,7 +4116,7 @@ now takes the agent value when the Proxmox value is unreported (`-1`) or when the agent value is lower (more pessimistic); it never raises a reported remaining-life value. `PercentageUsed` and the other merged SMART attributes are still recorded. Focused proof lives in -`internal/monitoring/monitor_integration_test.go` +`internal/monitoring/physical_disk_roundtrip_test.go` (`TestMergeHostAgentSMARTIntoDisks_AgentWearoutDoesNotHideLowPVELife` and `TestMergeHostAgentSMARTIntoDisks_AgentWearoutFillsUnreportedPVELife`). This is synthetic merge evidence, not reporter acceptance or hardware confirmation. diff --git a/internal/monitoring/monitor_integration_test.go b/internal/monitoring/monitor_integration_test.go index dfd24180b5..db2dd0d5ad 100644 --- a/internal/monitoring/monitor_integration_test.go +++ b/internal/monitoring/monitor_integration_test.go @@ -217,73 +217,6 @@ func TestMergeHostAgentSMARTIntoDisks_SMARTFailureAndWearoutOverrideCoarsePVEVal } } -func TestMergeHostAgentSMARTIntoDisks_AgentWearoutDoesNotHideLowPVELife(t *testing.T) { - // A worn drive whose NVMe endurance log intermittently reads - // PercentageUsed 0 must not have its Proxmox-reported remaining life - // raised to 100. That resolved the low-life alert and let it re-fire on the - // next poll, producing repeated notifications (#2112). - disks := []models.PhysicalDisk{{ - ID: "d1", - Node: "pve1", - Serial: "SER1", - Type: "nvme", - Health: "PASSED", - Wearout: 0, - }} - nodes := []models.Node{{Name: "pve1", LinkedAgentID: "host-1"}} - used := 0 - hosts := []models.Host{{ - ID: "host-1", - Sensors: models.HostSensorSummary{ - SMART: []models.HostDiskSMART{{ - Device: "/dev/nvme0n1", - Serial: "SER1", - Health: "PASSED", - Attributes: &models.SMARTAttributes{ - PercentageUsed: &used, - }, - }}, - }, - }} - - result := mergeHostAgentSMARTIntoDisks(disks, nodes, hosts) - if result[0].Wearout != 0 { - t.Fatalf("agent endurance reading raised the remaining life of a worn disk: wearout=%d", result[0].Wearout) - } - if result[0].SmartAttributes == nil || result[0].SmartAttributes.PercentageUsed == nil || *result[0].SmartAttributes.PercentageUsed != 0 { - t.Fatalf("agent SMART attributes were not merged: %+v", result[0].SmartAttributes) - } -} - -func TestMergeHostAgentSMARTIntoDisks_AgentWearoutFillsUnreportedPVELife(t *testing.T) { - disks := []models.PhysicalDisk{{ - ID: "d1", - Node: "pve1", - Serial: "SER1", - Type: "nvme", - Wearout: -1, - }} - nodes := []models.Node{{Name: "pve1", LinkedAgentID: "host-1"}} - used := 40 - hosts := []models.Host{{ - ID: "host-1", - Sensors: models.HostSensorSummary{ - SMART: []models.HostDiskSMART{{ - Device: "/dev/nvme0n1", - Serial: "SER1", - Attributes: &models.SMARTAttributes{ - PercentageUsed: &used, - }, - }}, - }, - }} - - result := mergeHostAgentSMARTIntoDisks(disks, nodes, hosts) - if result[0].Wearout != 60 { - t.Fatalf("agent endurance did not fill unreported PVE life: wearout=%d", result[0].Wearout) - } -} - func TestMergeHostAgentSMARTIntoDisks_DoesNotReplaceFailureWithPassed(t *testing.T) { disks := []models.PhysicalDisk{{ID: "d1", Node: "pve1", Serial: "SER1", Health: "FAILED"}} nodes := []models.Node{{Name: "pve1", LinkedAgentID: "host-1"}} diff --git a/internal/monitoring/physical_disk_roundtrip_test.go b/internal/monitoring/physical_disk_roundtrip_test.go index 4f00d814ad..3d85327069 100644 --- a/internal/monitoring/physical_disk_roundtrip_test.go +++ b/internal/monitoring/physical_disk_roundtrip_test.go @@ -120,3 +120,72 @@ func TestPhysicalDiskReadbackSourceIDFallback(t *testing.T) { t.Fatal("nil resource has a source ID") } } + +// TestMergeHostAgentSMARTIntoDisks_AgentWearoutDoesNotHideLowPVELife pins the +// #2112 regression: a worn drive whose NVMe endurance log intermittently reads +// PercentageUsed 0 must not have its Proxmox-reported remaining life raised to +// 100, which resolved the low-life alert and let it re-fire on the next poll. +func TestMergeHostAgentSMARTIntoDisks_AgentWearoutDoesNotHideLowPVELife(t *testing.T) { + disks := []models.PhysicalDisk{{ + ID: "d1", + Node: "pve1", + Serial: "SER1", + Type: "nvme", + Health: "PASSED", + Wearout: 0, + }} + nodes := []models.Node{{Name: "pve1", LinkedAgentID: "host-1"}} + used := 0 + hosts := []models.Host{{ + ID: "host-1", + Sensors: models.HostSensorSummary{ + SMART: []models.HostDiskSMART{{ + Device: "/dev/nvme0n1", + Serial: "SER1", + Health: "PASSED", + Attributes: &models.SMARTAttributes{ + PercentageUsed: &used, + }, + }}, + }, + }} + + result := mergeHostAgentSMARTIntoDisks(disks, nodes, hosts) + if result[0].Wearout != 0 { + t.Fatalf("agent endurance reading raised the remaining life of a worn disk: wearout=%d", result[0].Wearout) + } + if result[0].SmartAttributes == nil || result[0].SmartAttributes.PercentageUsed == nil || *result[0].SmartAttributes.PercentageUsed != 0 { + t.Fatalf("agent SMART attributes were not merged: %+v", result[0].SmartAttributes) + } +} + +// TestMergeHostAgentSMARTIntoDisks_AgentWearoutFillsUnreportedPVELife confirms +// the agent still supplies endurance when the Proxmox inventory reports none. +func TestMergeHostAgentSMARTIntoDisks_AgentWearoutFillsUnreportedPVELife(t *testing.T) { + disks := []models.PhysicalDisk{{ + ID: "d1", + Node: "pve1", + Serial: "SER1", + Type: "nvme", + Wearout: -1, + }} + nodes := []models.Node{{Name: "pve1", LinkedAgentID: "host-1"}} + used := 40 + hosts := []models.Host{{ + ID: "host-1", + Sensors: models.HostSensorSummary{ + SMART: []models.HostDiskSMART{{ + Device: "/dev/nvme0n1", + Serial: "SER1", + Attributes: &models.SMARTAttributes{ + PercentageUsed: &used, + }, + }}, + }, + }} + + result := mergeHostAgentSMARTIntoDisks(disks, nodes, hosts) + if result[0].Wearout != 60 { + t.Fatalf("agent endurance did not fill unreported PVE life: wearout=%d", result[0].Wearout) + } +}