mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-10-27 05:57:27 +00:00
186ce504c8
The fleet sampler and coordinator had no production alert trigger and could repeat cached values as fresh incident evidence. Preserve saved recordings through explicit read-only lookups, propagate read failures and report the former live incident count as unmeasured. Keep historical status and duration units explicit without rewriting archived observations. Integrate main's alert dispatch wording and startup replay qualification. Canonical incident listing and real-model outcome qualification remain open.
239 lines
7.8 KiB
Go
239 lines
7.8 KiB
Go
package api
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/ai/baseline"
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/ai/patterns"
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/ai/proxmox"
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/config"
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/models"
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/monitoring"
|
|
unifiedresources "github.com/rcourtman/pulse-go-rewrite/internal/unifiedresources"
|
|
)
|
|
|
|
func TestForecastResourceIterator(t *testing.T) {
|
|
// Build a ResourceRegistry with known resources and use it as ReadState.
|
|
rr := unifiedresources.NewRegistry(nil)
|
|
rr.IngestSnapshot(models.StateSnapshot{
|
|
VMs: []models.VM{{ID: "vm-1", Name: "vm-one", Node: "n", Instance: "i"}},
|
|
Containers: []models.Container{{ID: "ct-1", Name: "ct-one", Node: "n", Instance: "i"}},
|
|
Nodes: []models.Node{{ID: "node-1", Name: "node-one", Instance: "i"}},
|
|
Storage: []models.Storage{{ID: "store-1", Name: "store-one", Node: "n", Instance: "i"}},
|
|
})
|
|
|
|
iter := &forecastResourceIterator{readState: rr}
|
|
|
|
vms := iter.ForecastVMs()
|
|
if len(vms) != 1 || vms[0].ID != "vm-1" || vms[0].Name != "vm-one" {
|
|
t.Fatalf("unexpected VMs: %#v", vms)
|
|
}
|
|
cts := iter.ForecastContainers()
|
|
if len(cts) != 1 || cts[0].ID != "ct-1" || cts[0].Name != "ct-one" {
|
|
t.Fatalf("unexpected containers: %#v", cts)
|
|
}
|
|
nodes := iter.ForecastNodes()
|
|
if len(nodes) != 1 || nodes[0].ID != "node-1" || nodes[0].Name != "node-one" {
|
|
t.Fatalf("unexpected nodes: %#v", nodes)
|
|
}
|
|
pools := iter.ForecastStoragePools()
|
|
if len(pools) != 1 || pools[0].ID != "store-1" || pools[0].Name != "store-one" {
|
|
t.Fatalf("unexpected storage pools: %#v", pools)
|
|
}
|
|
}
|
|
|
|
func TestForecastResourceIterator_NilReadState(t *testing.T) {
|
|
iter := &forecastResourceIterator{}
|
|
|
|
if len(iter.ForecastVMs()) != 0 || len(iter.ForecastContainers()) != 0 || len(iter.ForecastNodes()) != 0 || len(iter.ForecastStoragePools()) != 0 {
|
|
t.Fatalf("expected empty slices for nil readState")
|
|
}
|
|
}
|
|
|
|
func TestEventCorrelatorProviderWrapper(t *testing.T) {
|
|
now := time.Now().UTC()
|
|
corr := proxmox.EventCorrelation{
|
|
ID: "corr-1",
|
|
Event: proxmox.ProxmoxEvent{
|
|
ID: "evt-1",
|
|
Type: proxmox.EventBackupStart,
|
|
Timestamp: now,
|
|
ResourceID: "res-1",
|
|
ResourceName: "Resource",
|
|
},
|
|
Anomalies: []proxmox.MetricAnomaly{{ResourceID: "res-1", Metric: "cpu", Value: 10}},
|
|
Explanation: "Backup caused spike",
|
|
Confidence: 0.75,
|
|
ImpactedResources: []string{
|
|
"res-1",
|
|
},
|
|
}
|
|
|
|
correlator := proxmox.NewEventCorrelator(proxmox.EventCorrelatorConfig{})
|
|
setUnexportedField(t, correlator, "correlations", []proxmox.EventCorrelation{corr})
|
|
|
|
wrapper := &eventCorrelatorProviderWrapper{correlator: correlator}
|
|
results := wrapper.GetCorrelationsForResource("res-1", 30*time.Minute)
|
|
if len(results) != 1 {
|
|
t.Fatalf("expected 1 correlation, got %d", len(results))
|
|
}
|
|
|
|
result := results[0]
|
|
if result.EventType != string(corr.Event.Type) || result.ResourceID != "res-1" || result.Description == "" {
|
|
t.Fatalf("unexpected correlation: %#v", result)
|
|
}
|
|
if result.Metadata["event_id"] != "evt-1" {
|
|
t.Fatalf("expected metadata event_id, got %#v", result.Metadata)
|
|
}
|
|
}
|
|
|
|
func TestEventCorrelatorProviderWrapper_NilCorrelator(t *testing.T) {
|
|
wrapper := &eventCorrelatorProviderWrapper{}
|
|
if got := wrapper.GetCorrelationsForResource("res-1", time.Minute); got != nil {
|
|
t.Fatalf("expected nil correlations, got %#v", got)
|
|
}
|
|
}
|
|
|
|
func TestMetricsSourceWrapper(t *testing.T) {
|
|
history := monitoring.NewMetricsHistory(10, time.Hour)
|
|
now := time.Now()
|
|
history.AddGuestMetric("guest-1", "cpu", 0.5, now)
|
|
history.AddGuestMetric("guest-1", "memory", 0.7, now)
|
|
history.AddNodeMetric("node-1", "cpu", 0.2, now)
|
|
|
|
wrapper := &metricsSourceWrapper{history: history}
|
|
guestCPU := wrapper.GetGuestMetrics("guest-1", "cpu", time.Hour)
|
|
if len(guestCPU) != 1 || guestCPU[0].Value != 0.5 {
|
|
t.Fatalf("unexpected guest metrics: %#v", guestCPU)
|
|
}
|
|
|
|
nodeCPU := wrapper.GetNodeMetrics("node-1", "cpu", time.Hour)
|
|
if len(nodeCPU) != 1 || nodeCPU[0].Value != 0.2 {
|
|
t.Fatalf("unexpected node metrics: %#v", nodeCPU)
|
|
}
|
|
|
|
allGuest := wrapper.GetAllGuestMetrics("guest-1", time.Hour)
|
|
if len(allGuest) == 0 || len(allGuest["cpu"]) != 1 {
|
|
t.Fatalf("unexpected all guest metrics: %#v", allGuest)
|
|
}
|
|
}
|
|
|
|
func TestConvertMetricPoints(t *testing.T) {
|
|
now := time.Now()
|
|
points := []monitoring.MetricPoint{{Value: 1.23, Timestamp: now}}
|
|
|
|
got := convertMetricPoints(points)
|
|
if len(got) != 1 || got[0].Value != 1.23 || !got[0].Timestamp.Equal(now) {
|
|
t.Fatalf("unexpected metric points: %#v", got)
|
|
}
|
|
}
|
|
|
|
func TestBaselineSourceWrapper(t *testing.T) {
|
|
store := baseline.NewStore(baseline.StoreConfig{MinSamples: 1})
|
|
now := time.Now()
|
|
points := []baseline.MetricPoint{{Value: 1.0, Timestamp: now}}
|
|
if err := store.Learn("res-1", "vm", "cpu", points); err != nil {
|
|
t.Fatalf("learn baseline: %v", err)
|
|
}
|
|
|
|
wrapper := &baselineSourceWrapper{store: store}
|
|
mean, stddev, samples, ok := wrapper.GetBaseline("res-1", "cpu")
|
|
if !ok || samples != 1 || mean != 1.0 || stddev != 0 {
|
|
t.Fatalf("unexpected baseline: mean=%v stddev=%v samples=%d ok=%v", mean, stddev, samples, ok)
|
|
}
|
|
|
|
all := wrapper.GetAllBaselines()
|
|
if all == nil {
|
|
t.Fatalf("expected baselines map, got nil")
|
|
}
|
|
resourceBaselines, ok := all["res-1"]
|
|
if !ok {
|
|
t.Fatalf("expected baselines for res-1, got %#v", all)
|
|
}
|
|
cpuBaseline, ok := resourceBaselines["cpu"]
|
|
if !ok || cpuBaseline.SampleCount != 1 {
|
|
t.Fatalf("unexpected cpu baseline: %#v", cpuBaseline)
|
|
}
|
|
}
|
|
|
|
func TestBaselineSourceWrapper_NilStore(t *testing.T) {
|
|
wrapper := &baselineSourceWrapper{}
|
|
if _, _, _, ok := wrapper.GetBaseline("res-1", "cpu"); ok {
|
|
t.Fatalf("expected baseline lookup to fail")
|
|
}
|
|
if got := wrapper.GetAllBaselines(); got != nil {
|
|
t.Fatalf("expected nil baselines, got %#v", got)
|
|
}
|
|
}
|
|
|
|
func TestPatternSourceWrapper(t *testing.T) {
|
|
detector := patterns.NewDetector(patterns.DetectorConfig{MinOccurrences: 1})
|
|
now := time.Now()
|
|
pattern := &patterns.Pattern{
|
|
ResourceID: "res-1",
|
|
EventType: patterns.EventType("reboot"),
|
|
Occurrences: 2,
|
|
Confidence: 0.9,
|
|
LastOccurrence: now.Add(-time.Hour),
|
|
NextPredicted: now.Add(24 * time.Hour),
|
|
}
|
|
|
|
patternsMap := map[string]*patterns.Pattern{
|
|
"res-1:reboot": pattern,
|
|
"nil-pattern": nil,
|
|
}
|
|
setUnexportedField(t, detector, "patterns", patternsMap)
|
|
|
|
wrapper := &patternSourceWrapper{detector: detector}
|
|
gotPatterns := wrapper.GetPatterns()
|
|
if len(gotPatterns) != 1 {
|
|
t.Fatalf("expected 1 pattern, got %d", len(gotPatterns))
|
|
}
|
|
if gotPatterns[0].ResourceID != "res-1" || gotPatterns[0].PatternType != "reboot" {
|
|
t.Fatalf("unexpected pattern: %#v", gotPatterns[0])
|
|
}
|
|
|
|
predictions := wrapper.GetPredictions()
|
|
if len(predictions) != 1 || predictions[0].ResourceID != "res-1" {
|
|
t.Fatalf("unexpected predictions: %#v", predictions)
|
|
}
|
|
}
|
|
|
|
func TestPatternSourceWrapper_NilDetector(t *testing.T) {
|
|
wrapper := &patternSourceWrapper{}
|
|
if got := wrapper.GetPatterns(); got != nil {
|
|
t.Fatalf("expected nil patterns, got %#v", got)
|
|
}
|
|
if got := wrapper.GetPredictions(); got != nil {
|
|
t.Fatalf("expected nil predictions, got %#v", got)
|
|
}
|
|
}
|
|
|
|
func TestUpdatesConfigWrapper(t *testing.T) {
|
|
wrapper := &updatesConfigWrapper{}
|
|
if !wrapper.IsDockerUpdateActionsEnabled() {
|
|
t.Fatalf("expected docker update actions enabled by default")
|
|
}
|
|
|
|
wrapper = &updatesConfigWrapper{cfg: &config.Config{DisableDockerUpdateActions: true}}
|
|
if wrapper.IsDockerUpdateActionsEnabled() {
|
|
t.Fatalf("expected docker update actions disabled")
|
|
}
|
|
}
|
|
|
|
func containsStringSlice(values []string, target string) bool {
|
|
for _, value := range values {
|
|
if value == target {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func TestConvertMetricPoints_Empty(t *testing.T) {
|
|
if got := convertMetricPoints(nil); len(got) != 0 {
|
|
t.Fatalf("expected empty slice, got %#v", got)
|
|
}
|
|
}
|