Files
pulse/internal/api/router_wrappers_additional_test.go
T
rcourtman 186ce504c8 Retire disconnected incident recording and preserve archives
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.
2026-09-06 17:42:24 +01:00

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)
}
}