mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-19 10:33:29 +00:00
Repair metric reducer governance proof
Record canonical metric resolution and reducer parity ownership in the alerts contract, move the stale-resolution regression into the recognized shared proof, and restore deterministic status evidence ordering. Change-source: pulse-maintainer
This commit is contained in:
parent
4223357449
commit
b1867a1de2
4 changed files with 55 additions and 48 deletions
|
|
@ -9806,12 +9806,12 @@
|
|||
"evidence": [
|
||||
{
|
||||
"repo": "pulse",
|
||||
"path": "docs/release-control/v6/internal/records/alert-lifecycle-contract-coverage-2026-08-26.md",
|
||||
"path": "docs/ALERT_ENGINE_EVOLUTION.md",
|
||||
"kind": "file"
|
||||
},
|
||||
{
|
||||
"repo": "pulse",
|
||||
"path": "docs/ALERT_ENGINE_EVOLUTION.md",
|
||||
"path": "docs/release-control/v6/internal/records/alert-lifecycle-contract-coverage-2026-08-26.md",
|
||||
"kind": "file"
|
||||
}
|
||||
]
|
||||
|
|
|
|||
|
|
@ -630,6 +630,21 @@ runtime must migrate the active alert, history entry, acknowledgment record,
|
|||
suppression/rate-limit/flapping tracking, and guest per-disk metric identity
|
||||
to the current canonical state instead of reopening a duplicate alert or
|
||||
resolving only the stale node-scoped identity.
|
||||
Metric alerts stored under a canonical state key must also clear through that
|
||||
same identity. Hysteresis resolution must derive the active-alert storage key
|
||||
from the existing alert and canonical state ID; removing only the legacy
|
||||
`<resourceID>-<metric>` ID can emit a resolved notification while leaving the
|
||||
canonical alert active to resolve again on every poll.
|
||||
|
||||
`internal/alerts/reducer` is the Phase 1 deterministic characterization of
|
||||
metric-threshold transitions. It accepts an explicit observation time and
|
||||
models enablement, hysteresis, sustained delay, and warning/critical severity
|
||||
without persistence or notification side effects. The alert manager remains
|
||||
the runtime owner and reference behavior while the parity harness drives both
|
||||
implementations through identical observations; a divergence is a reducer bug
|
||||
unless a focused manager regression first proves otherwise. Moving additional
|
||||
alert families or downstream delivery behavior into the reducer requires a
|
||||
separate contract slice.
|
||||
That same guest-threshold owner also governs guest-derived lifecycle and
|
||||
posture alerts. Snapshot age, backup age, powered-off state, and
|
||||
configuration-change reevaluation must all construct a canonical lightweight
|
||||
|
|
|
|||
|
|
@ -1,46 +0,0 @@
|
|||
package alerts
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
// Regression: checkMetric stores canonical-identity alerts under the
|
||||
// canonical state key, but its hysteresis resolution removed by the legacy
|
||||
// "<resourceID>-<metric>" ID, which is not registered as an alias. The
|
||||
// removal silently no-oped: a resolved notification went out while the
|
||||
// alert stayed active and re-resolved on every subsequent poll. Guest
|
||||
// per-disk usage alerts were the remaining production caller of this path.
|
||||
func TestCheckMetricResolveRemovesCanonicallyKeyedAlert(t *testing.T) {
|
||||
manager := NewManagerWithDataDir(t.TempDir(), WithoutPersistedAlertRestore())
|
||||
t.Cleanup(manager.Stop)
|
||||
|
||||
manager.mu.Lock()
|
||||
manager.config.Enabled = true
|
||||
manager.config.TimeThresholds = map[string]int{}
|
||||
manager.mu.Unlock()
|
||||
|
||||
threshold := &HysteresisThreshold{Trigger: 80, Clear: 75}
|
||||
resourceID := "guest-123/disk:root"
|
||||
|
||||
manager.checkMetric(resourceID, "test-vm", "node1", "qemu/123", "VM", "disk", 85, threshold, nil)
|
||||
|
||||
manager.mu.Lock()
|
||||
activeAfterFire := len(manager.activeAlerts)
|
||||
manager.mu.Unlock()
|
||||
if activeAfterFire != 1 {
|
||||
t.Fatalf("active alerts after fire = %d, want 1", activeAfterFire)
|
||||
}
|
||||
|
||||
manager.checkMetric(resourceID, "test-vm", "node1", "qemu/123", "VM", "disk", 70, threshold, nil)
|
||||
|
||||
manager.mu.Lock()
|
||||
activeAfterResolve := len(manager.activeAlerts)
|
||||
manager.mu.Unlock()
|
||||
if activeAfterResolve != 0 {
|
||||
t.Fatalf("active alerts after resolve = %d, want 0 (stale alert left behind)", activeAfterResolve)
|
||||
}
|
||||
|
||||
if resolved := manager.GetResolvedAlert(buildCanonicalStateID(resourceID, "metric-threshold:disk")); resolved == nil {
|
||||
t.Fatal("expected a recently-resolved entry for the cleared alert")
|
||||
}
|
||||
}
|
||||
|
|
@ -939,3 +939,41 @@ func TestGuestFilesystemThresholdOverrideOwnsMountEvaluation(t *testing.T) {
|
|||
t.Fatalf("/boot alert did not fire above its 99%% override")
|
||||
}
|
||||
}
|
||||
|
||||
// Regression: checkMetric stores canonical-identity alerts under the
|
||||
// canonical state key, so hysteresis resolution must not remove only the
|
||||
// unregistered legacy "<resourceID>-<metric>" ID.
|
||||
func TestCheckMetricResolveRemovesCanonicallyKeyedAlert(t *testing.T) {
|
||||
manager := NewManagerWithDataDir(t.TempDir(), WithoutPersistedAlertRestore())
|
||||
t.Cleanup(manager.Stop)
|
||||
|
||||
manager.mu.Lock()
|
||||
manager.config.Enabled = true
|
||||
manager.config.TimeThresholds = map[string]int{}
|
||||
manager.mu.Unlock()
|
||||
|
||||
threshold := &HysteresisThreshold{Trigger: 80, Clear: 75}
|
||||
resourceID := "guest-123/disk:root"
|
||||
|
||||
manager.checkMetric(resourceID, "test-vm", "node1", "qemu/123", "VM", "disk", 85, threshold, nil)
|
||||
|
||||
manager.mu.Lock()
|
||||
activeAfterFire := len(manager.activeAlerts)
|
||||
manager.mu.Unlock()
|
||||
if activeAfterFire != 1 {
|
||||
t.Fatalf("active alerts after fire = %d, want 1", activeAfterFire)
|
||||
}
|
||||
|
||||
manager.checkMetric(resourceID, "test-vm", "node1", "qemu/123", "VM", "disk", 70, threshold, nil)
|
||||
|
||||
manager.mu.Lock()
|
||||
activeAfterResolve := len(manager.activeAlerts)
|
||||
manager.mu.Unlock()
|
||||
if activeAfterResolve != 0 {
|
||||
t.Fatalf("active alerts after resolve = %d, want 0 (stale alert left behind)", activeAfterResolve)
|
||||
}
|
||||
|
||||
if resolved := manager.GetResolvedAlert(buildCanonicalStateID(resourceID, "metric-threshold:disk")); resolved == nil {
|
||||
t.Fatal("expected a recently-resolved entry for the cleared alert")
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue