Files
pulse/internal/api/reporting_subject_alerts_test.go
T
pulse-triage[bot] 92eef9809b Attach subject report alerts with shared disk hardware ownership
Restore machine and other subject alert enrichment without path-based ownership. Reuse the existing PVE alert journal decision and cross-source hardware matching for serials, WWNs, placeholders, moved paths and ambiguous identities. Preserve machine isolation, handover counts, resolution windows and exact paid-boundary controls.

Adapt rcourtman's PR #2641 (de524e823d) and the boundary controls of rejected maintainer candidate b548e6dd3ae6d689a479254baf785d2bead7cf77. Correct its disk ownership defect against current main rather than retaining a parallel identity matcher. Imported and shared history is unchanged.

Change-source: pulse-maintainer
2026-10-07 12:36:24 +01:00

99 lines
6.1 KiB
Go

package api
import (
"context"
"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/reporting"
)
// Exercise the actual enrichment entry point with both open and resolved
// alerts. A matching path (including a metrics alias) must not override the
// journal's recorded-hardware ownership; an earlier path must not hide it.
func TestReportPhysicalDiskAlertsFollowRecordedHardware(t *testing.T) {
const fullWWN = "5000c500a1b2c3d4"
type diskSpec struct{ id, node, path, serial, wwn string }
base := diskSpec{"physical-disk-subject", "pve1", "/dev/sda", "SER-A", ""}
wwnOnly := diskSpec{"physical-disk-subject", "pve1", "/dev/sda", "", "naa." + fullWWN}
placeholder := wwnOnly
placeholder.serial = "unknown"
twin := diskSpec{"physical-disk-other", "pve2", "/dev/sdb", "SER-A", "naa.5000c500000000b2"}
for _, tc := range []struct {
name string
subject diskSpec
others []diskSpec
node, path, serial, wwn string
metricsAlias bool
want bool
}{
{name: "serial", subject: base, node: "pve1", path: "/dev/sda", serial: "ser-a", want: true},
{name: "replacement", subject: base, node: "pve1", path: "/dev/sda", serial: "SER-OLD"},
{name: "path metrics alias cannot bypass replacement", subject: base, node: "pve1", path: "/dev/sda", serial: "SER-OLD", metricsAlias: true},
{name: "earlier path", subject: base, node: "pve1", path: "/dev/sdz", serial: "SER-A", want: true},
{name: "earlier node", subject: base, node: "pve2", path: "/dev/sdz", serial: "SER-A", want: true},
{name: "WWN only framed", subject: wwnOnly, node: "pve1", path: "/dev/sda", wwn: "wwn-0x" + fullWWN, want: true},
{name: "WWN only moved", subject: wwnOnly, node: "pve2", path: "/dev/sdz", wwn: "eui." + fullWWN, want: true},
{name: "agent WWN fields", subject: wwnOnly, node: "pve1", path: "/dev/sda", wwn: "5-c50-a1b2c3d4", want: true},
{name: "serial to WWN cross source", subject: wwnOnly, node: "pve1", path: "/dev/sda", serial: fullWWN, want: true},
{name: "placeholder same WWN", subject: placeholder, node: "pve1", path: "/dev/sda", serial: "UNKNOWN", wwn: "0x" + fullWWN, want: true},
{name: "placeholder different WWN", subject: placeholder, node: "pve1", path: "/dev/sda", serial: "UNKNOWN", wwn: "0x5000c500000000b2"},
{name: "truncated WWN is not identity", subject: wwnOnly, node: "pve1", path: "/dev/sda", wwn: "0x5000c500a1b2"},
{name: "unknown cannot claim identified disk", subject: base, node: "pve1", path: "/dev/sda", serial: "unknown"},
{name: "zero cannot claim identified disk", subject: base, node: "pve1", path: "/dev/sda", serial: "00000000"},
{name: "shared serial is ambiguous even at path", subject: base, others: []diskSpec{twin}, node: "pve1", path: "/dev/sda", serial: "SER-A"},
{name: "WWN decides shared serial", subject: diskSpec{base.id, base.node, base.path, base.serial, "naa." + fullWWN}, others: []diskSpec{twin}, node: "pve2", path: "/dev/sdb", serial: "SER-A", wwn: "0x" + fullWWN, want: true},
{name: "WWN decides other twin despite subject path", subject: diskSpec{base.id, base.node, base.path, base.serial, "naa." + fullWWN}, others: []diskSpec{twin}, node: "pve1", path: "/dev/sda", serial: "SER-A", wwn: twin.wwn},
{name: "identity less exact path", subject: diskSpec{base.id, base.node, base.path, "unknown", ""}, node: "pve1", path: "/dev/sda", serial: "N/A", want: true},
{name: "identity less cannot move by path", subject: diskSpec{base.id, base.node, base.path, "", ""}, node: "pve1", path: "/dev/sdz"},
{name: "identity less path ambiguous", subject: diskSpec{base.id, base.node, base.path, "", ""}, others: []diskSpec{{twin.id, base.node, base.path, "", ""}}, node: "pve1", path: "/dev/sda"},
} {
t.Run(tc.name, func(t *testing.T) {
now := time.Date(2026, 10, 7, 12, 0, 0, 0, time.UTC)
start := now.Add(-24 * time.Hour)
snapshot := reportingEnrichmentSnapshot{}
for _, spec := range append([]diskSpec{tc.subject}, tc.others...) {
snapshot.Resources = append(snapshot.Resources, unifiedresources.Resource{
ID: spec.id, Type: unifiedresources.ResourceTypePhysicalDisk,
Proxmox: &unifiedresources.ProxmoxData{Instance: "lab:west", NodeName: spec.node},
PhysicalDisk: &unifiedresources.PhysicalDiskMeta{DevPath: spec.path, Serial: spec.serial, WWN: spec.wwn},
})
}
ref := unifiedresources.ProxmoxPhysicalDiskAlertResourceID("lab:west", tc.node, tc.path)
identifiers := unifiedresources.ProxmoxPhysicalDiskAlertIdentifiers(ref)
alert := models.Alert{ID: identifiers[0], ResourceID: ref, Type: "disk-health", Level: "critical", StartTime: start.Add(time.Hour),
Metadata: map[string]any{unifiedresources.MetadataDiskSerial: tc.serial, unifiedresources.MetadataDiskWWN: tc.wwn}}
snapshot.ActiveAlerts = []models.Alert{alert}
resolved := alert
resolved.ID, resolved.Type = identifiers[1], "disk-wearout"
snapshot.RecentlyResolved = []models.ResolvedAlert{
{Alert: resolved, ResolvedTime: start.Add(time.Hour)},
{Alert: resolved, ResolvedTime: start}, // exact boundaries excluded
{Alert: resolved, ResolvedTime: now},
}
// Even a path-shaped alert on the subject's metrics alias is not
// a PVE alert unless its canonical alert identifier agrees.
unrelated := alert
unrelated.ID = "unrelated-alert"
snapshot.ActiveAlerts = append(snapshot.ActiveAlerts, unrelated)
req := reporting.MetricReportRequest{ResourceType: "disk", ResourceID: tc.subject.id}
if tc.metricsAlias {
req.MetricsResourceID = ref
}
NewReportingHandlers(nil, nil).enrichReportRequest(context.Background(), "default", &req, snapshot, start, now)
wantCount := 0
if tc.want {
wantCount = 2
}
if len(req.Alerts) != wantCount {
t.Fatalf("alerts = %+v, want %d owned open/in-window rows", req.Alerts, wantCount)
}
if tc.want && (req.Alerts[0].ResolvedTime != nil || req.Alerts[1].ResolvedTime == nil || !req.Alerts[1].ResolvedTime.Equal(start.Add(time.Hour))) {
t.Fatalf("open/resolved state not preserved: %+v", req.Alerts)
}
})
}
}