mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-23 04:06:21 +00:00
23695681ac
Patrol findings previously reached operators only through the Relay mobile push path. Anyone relying on the email, webhook, or Apprise destinations they already configured for alerts got no proactive signal when Patrol detected a problem and had to open /patrol to learn about it. This is the delivery half of #1369. Each newly stored warning or critical finding now also flows through a FindingNotifyCallback wired in the router, which projects the finding into the alert shape the notification manager delivers. The callback fires only on the genuinely-new path in recordFindingWithInvestigation, so a finding notifies at most once per lifetime regardless of how many later runs re-detect it, and SendAlert's own per-ID cooldown backstops that. Demo mode never notifies. Gating lives in AIConfig via patrol_finding_notifications_enabled (default on, matching the long-standing default for mobile push) and patrol_finding_notify_min_severity (warning or critical, default warning). The enabled flag persists without omitempty so an explicit opt-out survives reload while pre-existing configs inherit the default. The settings surface for these fields follows in a separate commit once the AI settings handler is free.
71 lines
2.1 KiB
Go
71 lines
2.1 KiB
Go
package ai
|
|
|
|
import "testing"
|
|
|
|
// The finding notification callback must fire exactly once per finding
|
|
// lifetime, on the genuinely-new path, and only for warning+ severities.
|
|
func TestRecordFinding_FindingNotifyCallback(t *testing.T) {
|
|
newFinding := func(id string, severity FindingSeverity) *Finding {
|
|
return &Finding{
|
|
ID: id,
|
|
Severity: severity,
|
|
Category: FindingCategoryReliability,
|
|
ResourceID: "vm-101",
|
|
ResourceName: "web",
|
|
ResourceType: "vm",
|
|
Title: "Test finding " + id,
|
|
}
|
|
}
|
|
|
|
t.Run("fires once for a new warning finding", func(t *testing.T) {
|
|
ps := NewPatrolService(nil, nil)
|
|
var notified []*Finding
|
|
ps.SetFindingNotifyCallback(func(f *Finding) {
|
|
notified = append(notified, f)
|
|
})
|
|
|
|
if !ps.recordFinding(newFinding("warn-1", FindingSeverityWarning)) {
|
|
t.Fatal("expected finding to record as new")
|
|
}
|
|
if len(notified) != 1 {
|
|
t.Fatalf("callback fired %d times, want 1", len(notified))
|
|
}
|
|
if notified[0].ID != "warn-1" {
|
|
t.Fatalf("notified finding ID = %q", notified[0].ID)
|
|
}
|
|
|
|
// A re-detection of the same finding must not notify again.
|
|
ps.recordFinding(newFinding("warn-1", FindingSeverityWarning))
|
|
if len(notified) != 1 {
|
|
t.Fatalf("re-detection fired the callback; %d calls, want 1", len(notified))
|
|
}
|
|
})
|
|
|
|
t.Run("fires for critical findings", func(t *testing.T) {
|
|
ps := NewPatrolService(nil, nil)
|
|
calls := 0
|
|
ps.SetFindingNotifyCallback(func(*Finding) { calls++ })
|
|
ps.recordFinding(newFinding("crit-1", FindingSeverityCritical))
|
|
if calls != 1 {
|
|
t.Fatalf("callback fired %d times, want 1", calls)
|
|
}
|
|
})
|
|
|
|
t.Run("does not fire for info findings", func(t *testing.T) {
|
|
ps := NewPatrolService(nil, nil)
|
|
calls := 0
|
|
ps.SetFindingNotifyCallback(func(*Finding) { calls++ })
|
|
ps.recordFinding(newFinding("info-1", FindingSeverityInfo))
|
|
if calls != 0 {
|
|
t.Fatalf("callback fired %d times for an info finding, want 0", calls)
|
|
}
|
|
})
|
|
|
|
t.Run("nil callback is safe", func(t *testing.T) {
|
|
ps := NewPatrolService(nil, nil)
|
|
if !ps.recordFinding(newFinding("warn-2", FindingSeverityWarning)) {
|
|
t.Fatal("expected finding to record as new")
|
|
}
|
|
})
|
|
}
|