mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-02-18 00:17:39 +01:00
553 lines
23 KiB
Go
553 lines
23 KiB
Go
package ai
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/ai/baseline"
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/ai/correlation"
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/ai/knowledge"
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/ai/memory"
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/ai/patterns"
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/alerts"
|
|
)
|
|
|
|
func TestIntelligence_formatBaselinesForContext(t *testing.T) {
|
|
intel := NewIntelligence(IntelligenceConfig{})
|
|
store := baseline.NewStore(baseline.StoreConfig{MinSamples: 1})
|
|
if err := store.Learn("res-1", "vm", "cpu", []baseline.MetricPoint{{Value: 12.5}}); err != nil {
|
|
t.Fatalf("Learn: %v", err)
|
|
}
|
|
intel.SetSubsystems(nil, nil, nil, store, nil, nil, nil, nil)
|
|
|
|
ctx := intel.formatBaselinesForContext("res-1")
|
|
if !strings.Contains(ctx, "Learned Baselines") {
|
|
t.Fatalf("expected baseline header, got %q", ctx)
|
|
}
|
|
if !strings.Contains(ctx, "cpu: mean") {
|
|
t.Fatalf("expected cpu baseline line, got %q", ctx)
|
|
}
|
|
|
|
if got := intel.formatBaselinesForContext("missing"); got != "" {
|
|
t.Errorf("expected empty context for missing baseline, got %q", got)
|
|
}
|
|
|
|
empty := NewIntelligence(IntelligenceConfig{})
|
|
if got := empty.formatBaselinesForContext("res-1"); got != "" {
|
|
t.Errorf("expected empty context with no baseline store, got %q", got)
|
|
}
|
|
}
|
|
|
|
func TestIntelligence_formatAnomaliesForContext(t *testing.T) {
|
|
intel := NewIntelligence(IntelligenceConfig{})
|
|
if got := intel.formatAnomaliesForContext(nil); got != "" {
|
|
t.Errorf("expected empty anomalies context, got %q", got)
|
|
}
|
|
|
|
anomalies := []AnomalyReport{{Metric: "cpu", Description: "CPU high"}}
|
|
ctx := intel.formatAnomaliesForContext(anomalies)
|
|
if !strings.Contains(ctx, "Current Anomalies") {
|
|
t.Fatalf("expected anomalies header, got %q", ctx)
|
|
}
|
|
if !strings.Contains(ctx, "CPU high") {
|
|
t.Fatalf("expected anomaly description, got %q", ctx)
|
|
}
|
|
}
|
|
|
|
func TestIntelligence_formatAnomalyDescription(t *testing.T) {
|
|
intel := NewIntelligence(IntelligenceConfig{})
|
|
bl := &baseline.MetricBaseline{Mean: 10}
|
|
|
|
above := intel.formatAnomalyDescription("cpu", 20, bl, 2.5)
|
|
if !strings.Contains(above, "above baseline") {
|
|
t.Errorf("expected above-baseline description, got %q", above)
|
|
}
|
|
|
|
below := intel.formatAnomalyDescription("cpu", 5, bl, -1.5)
|
|
if !strings.Contains(below, "below baseline") {
|
|
t.Errorf("expected below-baseline description, got %q", below)
|
|
}
|
|
}
|
|
|
|
func TestIntelligence_FormatContext_Anomalies(t *testing.T) {
|
|
intel := NewIntelligence(IntelligenceConfig{})
|
|
intel.anomalyDetector = func(resourceID string) []AnomalyReport {
|
|
return []AnomalyReport{{Metric: "cpu", Description: "CPU high"}}
|
|
}
|
|
|
|
ctx := intel.FormatContext("res-1")
|
|
if !strings.Contains(ctx, "Current Anomalies") {
|
|
t.Fatalf("expected anomalies context, got %q", ctx)
|
|
}
|
|
if !strings.Contains(ctx, "CPU high") {
|
|
t.Fatalf("expected anomaly description, got %q", ctx)
|
|
}
|
|
}
|
|
|
|
func TestIntelligence_getUpcomingRisks(t *testing.T) {
|
|
intel := NewIntelligence(IntelligenceConfig{})
|
|
predictions := []patterns.FailurePrediction{
|
|
{ResourceID: "r1", EventType: patterns.EventHighCPU, DaysUntil: 2, Confidence: 0.6},
|
|
{ResourceID: "r2", EventType: patterns.EventHighMemory, DaysUntil: 9, Confidence: 0.9},
|
|
{ResourceID: "r3", EventType: patterns.EventDiskFull, DaysUntil: 4, Confidence: 0.4},
|
|
{ResourceID: "r4", EventType: patterns.EventOOM, DaysUntil: 1, Confidence: 0.8},
|
|
{ResourceID: "r5", EventType: patterns.EventRestart, DaysUntil: 5, Confidence: 0.7},
|
|
}
|
|
|
|
risk := intel.getUpcomingRisks(predictions, 2)
|
|
if len(risk) != 2 {
|
|
t.Fatalf("expected 2 risks, got %d", len(risk))
|
|
}
|
|
if risk[0].ResourceID != "r4" || risk[1].ResourceID != "r1" {
|
|
t.Errorf("unexpected ordering: %v", []string{risk[0].ResourceID, risk[1].ResourceID})
|
|
}
|
|
}
|
|
|
|
func TestIntelligence_countFindings(t *testing.T) {
|
|
intel := NewIntelligence(IntelligenceConfig{})
|
|
findings := []*Finding{
|
|
nil,
|
|
{Severity: FindingSeverityCritical},
|
|
{Severity: FindingSeverityWarning},
|
|
{Severity: FindingSeverityWatch},
|
|
{Severity: FindingSeverityInfo},
|
|
}
|
|
counts := intel.countFindings(findings)
|
|
if counts.Total != 4 {
|
|
t.Errorf("expected total 4, got %d", counts.Total)
|
|
}
|
|
if counts.Critical != 1 || counts.Warning != 1 || counts.Watch != 1 || counts.Info != 1 {
|
|
t.Errorf("unexpected counts: %+v", counts)
|
|
}
|
|
}
|
|
|
|
func TestIntelligence_getTopFindings(t *testing.T) {
|
|
intel := NewIntelligence(IntelligenceConfig{})
|
|
base := time.Now()
|
|
findings := []*Finding{
|
|
{Severity: FindingSeverityWarning, DetectedAt: base.Add(-2 * time.Hour)},
|
|
{Severity: FindingSeverityCritical, DetectedAt: base.Add(-3 * time.Hour), Title: "older critical"},
|
|
{Severity: FindingSeverityCritical, DetectedAt: base.Add(-1 * time.Hour), Title: "newer critical"},
|
|
{Severity: FindingSeverityWatch, DetectedAt: base.Add(-30 * time.Minute)},
|
|
}
|
|
|
|
top := intel.getTopFindings(findings, 3)
|
|
if len(top) != 3 {
|
|
t.Fatalf("expected 3 findings, got %d", len(top))
|
|
}
|
|
if top[0].Title != "newer critical" || top[1].Title != "older critical" {
|
|
t.Errorf("unexpected ordering: %q, %q", top[0].Title, top[1].Title)
|
|
}
|
|
if top[2].Severity != FindingSeverityWarning {
|
|
t.Errorf("expected warning in third position, got %s", top[2].Severity)
|
|
}
|
|
}
|
|
|
|
func TestIntelligence_getTopFindings_Empty(t *testing.T) {
|
|
intel := NewIntelligence(IntelligenceConfig{})
|
|
if got := intel.getTopFindings(nil, 5); got != nil {
|
|
t.Errorf("expected nil for empty findings, got %v", got)
|
|
}
|
|
}
|
|
|
|
func TestIntelligence_getLearningStats(t *testing.T) {
|
|
intel := NewIntelligence(IntelligenceConfig{})
|
|
knowledgeStore, err := knowledge.NewStore(t.TempDir())
|
|
if err != nil {
|
|
t.Fatalf("NewStore: %v", err)
|
|
}
|
|
knowledgeStore.SaveNote("vm-1", "vm-1", "vm", "general", "Note", "Content")
|
|
knowledgeStore.SaveNote("vm-2", "vm-2", "vm", "general", "Note", "Content")
|
|
|
|
baselineStore := baseline.NewStore(baseline.StoreConfig{MinSamples: 1})
|
|
baselineStore.Learn("vm-1", "vm", "cpu", []baseline.MetricPoint{{Value: 10}})
|
|
|
|
patternDetector := patterns.NewDetector(patterns.DetectorConfig{
|
|
MinOccurrences: 2,
|
|
PatternWindow: 48 * time.Hour,
|
|
PredictionLimit: 30 * 24 * time.Hour,
|
|
})
|
|
patternStart := time.Now().Add(-90 * time.Minute)
|
|
patternDetector.RecordEvent(patterns.HistoricalEvent{ResourceID: "vm-1", EventType: patterns.EventHighCPU, Timestamp: patternStart})
|
|
patternDetector.RecordEvent(patterns.HistoricalEvent{ResourceID: "vm-1", EventType: patterns.EventHighCPU, Timestamp: patternStart.Add(60 * time.Minute)})
|
|
|
|
correlationDetector := correlation.NewDetector(correlation.Config{
|
|
MinOccurrences: 1,
|
|
CorrelationWindow: 2 * time.Hour,
|
|
RetentionWindow: 24 * time.Hour,
|
|
})
|
|
corrStart := time.Now().Add(-30 * time.Minute)
|
|
for i := 0; i < 2; i++ {
|
|
base := corrStart.Add(time.Duration(i) * 10 * time.Minute)
|
|
correlationDetector.RecordEvent(correlation.Event{ResourceID: "node-a", ResourceName: "node-a", ResourceType: "node", EventType: correlation.EventHighCPU, Timestamp: base})
|
|
correlationDetector.RecordEvent(correlation.Event{ResourceID: "vm-b", ResourceName: "vm-b", ResourceType: "vm", EventType: correlation.EventRestart, Timestamp: base.Add(1 * time.Minute)})
|
|
}
|
|
|
|
incidentStore := memory.NewIncidentStore(memory.IncidentStoreConfig{MaxIncidents: 5})
|
|
|
|
intel.SetSubsystems(nil, patternDetector, correlationDetector, baselineStore, incidentStore, knowledgeStore, nil, nil)
|
|
|
|
stats := intel.getLearningStats()
|
|
if stats.ResourcesWithKnowledge != 2 {
|
|
t.Errorf("expected 2 resources with knowledge, got %d", stats.ResourcesWithKnowledge)
|
|
}
|
|
if stats.TotalNotes != 2 {
|
|
t.Errorf("expected 2 total notes, got %d", stats.TotalNotes)
|
|
}
|
|
if stats.ResourcesWithBaselines != 1 {
|
|
t.Errorf("expected 1 resource with baseline, got %d", stats.ResourcesWithBaselines)
|
|
}
|
|
if stats.PatternsDetected != 1 {
|
|
t.Errorf("expected 1 pattern, got %d", stats.PatternsDetected)
|
|
}
|
|
if stats.CorrelationsLearned != 1 {
|
|
t.Errorf("expected 1 correlation, got %d", stats.CorrelationsLearned)
|
|
}
|
|
}
|
|
|
|
func TestIntelligence_getResourcesAtRisk(t *testing.T) {
|
|
intel := NewIntelligence(IntelligenceConfig{})
|
|
findings := NewFindingsStore()
|
|
findings.Add(&Finding{
|
|
ID: "crit-1",
|
|
Key: "crit-1",
|
|
Severity: FindingSeverityCritical,
|
|
Category: FindingCategoryReliability,
|
|
ResourceID: "res-critical",
|
|
ResourceName: "critical-vm",
|
|
ResourceType: "vm",
|
|
Title: "Critical outage",
|
|
DetectedAt: time.Now(),
|
|
LastSeenAt: time.Now(),
|
|
Source: "test",
|
|
})
|
|
findings.Add(&Finding{
|
|
ID: "warn-1",
|
|
Key: "warn-1",
|
|
Severity: FindingSeverityWarning,
|
|
Category: FindingCategoryPerformance,
|
|
ResourceID: "res-warning",
|
|
ResourceName: "warning-vm",
|
|
ResourceType: "vm",
|
|
Title: "Warning issue",
|
|
DetectedAt: time.Now(),
|
|
LastSeenAt: time.Now(),
|
|
Source: "test",
|
|
})
|
|
findings.Add(&Finding{
|
|
ID: "watch-1",
|
|
Key: "watch-1",
|
|
Severity: FindingSeverityWatch,
|
|
Category: FindingCategoryPerformance,
|
|
ResourceID: "res-warning",
|
|
ResourceName: "warning-vm",
|
|
ResourceType: "vm",
|
|
Title: "Watch issue",
|
|
DetectedAt: time.Now(),
|
|
LastSeenAt: time.Now(),
|
|
Source: "test",
|
|
})
|
|
findings.Add(&Finding{
|
|
ID: "info-1",
|
|
Key: "info-1",
|
|
Severity: FindingSeverityInfo,
|
|
Category: FindingCategoryPerformance,
|
|
ResourceID: "res-warning",
|
|
ResourceName: "warning-vm",
|
|
ResourceType: "vm",
|
|
Title: "Info issue",
|
|
DetectedAt: time.Now(),
|
|
LastSeenAt: time.Now(),
|
|
Source: "test",
|
|
})
|
|
|
|
intel.SetSubsystems(findings, nil, nil, nil, nil, nil, nil, nil)
|
|
risks := intel.getResourcesAtRisk(1)
|
|
if len(risks) != 1 {
|
|
t.Fatalf("expected 1 risk, got %d", len(risks))
|
|
}
|
|
if risks[0].ResourceID != "res-critical" {
|
|
t.Errorf("expected critical resource first, got %s", risks[0].ResourceID)
|
|
}
|
|
if risks[0].TopIssue != "Critical outage" {
|
|
t.Errorf("expected top issue to be critical, got %s", risks[0].TopIssue)
|
|
}
|
|
}
|
|
|
|
func TestIntelligence_calculateResourceHealth_ClampAndSeverities(t *testing.T) {
|
|
intel := NewIntelligence(IntelligenceConfig{})
|
|
|
|
resourceIntel := &ResourceIntelligence{
|
|
ResourceID: "test-vm",
|
|
ActiveFindings: []*Finding{
|
|
nil,
|
|
{Severity: FindingSeverityCritical, Title: "crit-1"},
|
|
{Severity: FindingSeverityCritical, Title: "crit-2"},
|
|
{Severity: FindingSeverityCritical, Title: "crit-3"},
|
|
{Severity: FindingSeverityCritical, Title: "crit-4"},
|
|
{Severity: FindingSeverityWatch, Title: "watch"},
|
|
{Severity: FindingSeverityInfo, Title: "info"},
|
|
},
|
|
Anomalies: []AnomalyReport{
|
|
{Metric: "cpu", Severity: baseline.AnomalyHigh, Description: "high"},
|
|
{Metric: "disk", Severity: baseline.AnomalyLow, Description: "low"},
|
|
},
|
|
}
|
|
|
|
health := intel.calculateResourceHealth(resourceIntel)
|
|
if health.Score != 0 {
|
|
t.Errorf("expected score clamped to 0, got %f", health.Score)
|
|
}
|
|
}
|
|
|
|
func TestIntelligence_calculateOverallHealth_Clamps(t *testing.T) {
|
|
intel := NewIntelligence(IntelligenceConfig{})
|
|
var predictions []patterns.FailurePrediction
|
|
for i := 0; i < 10; i++ {
|
|
predictions = append(predictions, patterns.FailurePrediction{EventType: patterns.EventHighCPU, DaysUntil: 1, Confidence: 0.9})
|
|
}
|
|
negative := intel.calculateOverallHealth(&IntelligenceSummary{
|
|
FindingsCount: FindingsCounts{Critical: 10, Warning: 10},
|
|
UpcomingRisks: predictions,
|
|
})
|
|
if negative.Score != 0 {
|
|
t.Errorf("expected score clamped to 0, got %f", negative.Score)
|
|
}
|
|
|
|
positive := intel.calculateOverallHealth(&IntelligenceSummary{
|
|
Learning: LearningStats{ResourcesWithKnowledge: 10},
|
|
})
|
|
if positive.Score != 100 {
|
|
t.Errorf("expected score clamped to 100, got %f", positive.Score)
|
|
}
|
|
if len(positive.Factors) == 0 {
|
|
t.Error("expected learning factor for positive health")
|
|
}
|
|
}
|
|
|
|
func TestIntelligence_generateHealthPrediction_Branches(t *testing.T) {
|
|
intel := NewIntelligence(IntelligenceConfig{})
|
|
|
|
gradeA := intel.generateHealthPrediction(HealthScore{Grade: HealthGradeA}, &IntelligenceSummary{})
|
|
if !strings.Contains(gradeA, "healthy") {
|
|
t.Errorf("expected healthy prediction, got %q", gradeA)
|
|
}
|
|
|
|
critical := intel.generateHealthPrediction(HealthScore{Grade: HealthGradeB}, &IntelligenceSummary{
|
|
FindingsCount: FindingsCounts{Critical: 2},
|
|
})
|
|
if !strings.Contains(critical, "Immediate attention") {
|
|
t.Errorf("expected critical prediction, got %q", critical)
|
|
}
|
|
|
|
risk := intel.generateHealthPrediction(HealthScore{Grade: HealthGradeB}, &IntelligenceSummary{
|
|
UpcomingRisks: []patterns.FailurePrediction{{EventType: patterns.EventHighCPU, DaysUntil: 2, Confidence: 0.8}},
|
|
})
|
|
if !strings.Contains(risk, "Predicted") {
|
|
t.Errorf("expected prediction text, got %q", risk)
|
|
}
|
|
|
|
stable := intel.generateHealthPrediction(HealthScore{Grade: HealthGradeC}, &IntelligenceSummary{})
|
|
if !strings.Contains(stable, "stable") {
|
|
t.Errorf("expected stable prediction, got %q", stable)
|
|
}
|
|
}
|
|
|
|
func TestIntelligence_FormatContext_AllSubsystems(t *testing.T) {
|
|
intel := NewIntelligence(IntelligenceConfig{})
|
|
knowledgeStore, _ := knowledge.NewStore(t.TempDir())
|
|
knowledgeStore.SaveNote("vm-ctx", "vm-ctx", "vm", "general", "Note", "Content")
|
|
|
|
baselineStore := baseline.NewStore(baseline.StoreConfig{MinSamples: 1})
|
|
baselineStore.Learn("vm-ctx", "vm", "cpu", []baseline.MetricPoint{{Value: 10}})
|
|
|
|
patternDetector := patterns.NewDetector(patterns.DetectorConfig{MinOccurrences: 2, PatternWindow: 48 * time.Hour, PredictionLimit: 30 * 24 * time.Hour})
|
|
patternStart := time.Now().Add(-90 * time.Minute)
|
|
patternDetector.RecordEvent(patterns.HistoricalEvent{ResourceID: "vm-ctx", EventType: patterns.EventHighCPU, Timestamp: patternStart})
|
|
patternDetector.RecordEvent(patterns.HistoricalEvent{ResourceID: "vm-ctx", EventType: patterns.EventHighCPU, Timestamp: patternStart.Add(60 * time.Minute)})
|
|
|
|
correlationDetector := correlation.NewDetector(correlation.Config{MinOccurrences: 1, CorrelationWindow: 2 * time.Hour, RetentionWindow: 24 * time.Hour})
|
|
corrBase := time.Now().Add(-30 * time.Minute)
|
|
correlationDetector.RecordEvent(correlation.Event{ResourceID: "vm-ctx", ResourceName: "vm-ctx", ResourceType: "vm", EventType: correlation.EventHighCPU, Timestamp: corrBase})
|
|
correlationDetector.RecordEvent(correlation.Event{ResourceID: "node-ctx", ResourceName: "node-ctx", ResourceType: "node", EventType: correlation.EventRestart, Timestamp: corrBase.Add(1 * time.Minute)})
|
|
|
|
incidentStore := memory.NewIncidentStore(memory.IncidentStoreConfig{MaxIncidents: 10})
|
|
incidentStore.RecordAlertFired(&alerts.Alert{ID: "alert-ctx", ResourceID: "vm-ctx", ResourceName: "vm-ctx", Type: "cpu", StartTime: time.Now()})
|
|
|
|
intel.SetSubsystems(nil, patternDetector, correlationDetector, baselineStore, incidentStore, knowledgeStore, nil, nil)
|
|
ctx := intel.FormatContext("vm-ctx")
|
|
if !strings.Contains(ctx, "Learned Baselines") {
|
|
t.Fatalf("expected baseline context, got %q", ctx)
|
|
}
|
|
if !strings.Contains(ctx, "Failure Predictions") {
|
|
t.Fatalf("expected predictions context, got %q", ctx)
|
|
}
|
|
if !strings.Contains(ctx, "Resource Correlations") {
|
|
t.Fatalf("expected correlations context, got %q", ctx)
|
|
}
|
|
if !strings.Contains(ctx, "Incident Memory") {
|
|
t.Fatalf("expected incident context, got %q", ctx)
|
|
}
|
|
}
|
|
|
|
func TestIntelligence_FormatGlobalContext_AllSubsystems(t *testing.T) {
|
|
intel := NewIntelligence(IntelligenceConfig{})
|
|
knowledgeStore, _ := knowledge.NewStore(t.TempDir())
|
|
knowledgeStore.SaveNote("vm-global", "vm-global", "vm", "general", "Note", "Content")
|
|
|
|
incidentStore := memory.NewIncidentStore(memory.IncidentStoreConfig{MaxIncidents: 10})
|
|
incidentStore.RecordAlertFired(&alerts.Alert{ID: "alert-global", ResourceID: "vm-global", ResourceName: "vm-global", Type: "cpu", StartTime: time.Now()})
|
|
|
|
correlationDetector := correlation.NewDetector(correlation.Config{MinOccurrences: 1, CorrelationWindow: 2 * time.Hour, RetentionWindow: 24 * time.Hour})
|
|
corrBase := time.Now().Add(-30 * time.Minute)
|
|
for i := 0; i < 2; i++ {
|
|
base := corrBase.Add(time.Duration(i) * 10 * time.Minute)
|
|
correlationDetector.RecordEvent(correlation.Event{ResourceID: "node-a", ResourceName: "node-a", ResourceType: "node", EventType: correlation.EventHighCPU, Timestamp: base})
|
|
correlationDetector.RecordEvent(correlation.Event{ResourceID: "vm-global", ResourceName: "vm-global", ResourceType: "vm", EventType: correlation.EventRestart, Timestamp: base.Add(1 * time.Minute)})
|
|
}
|
|
|
|
patternDetector := patterns.NewDetector(patterns.DetectorConfig{MinOccurrences: 2, PatternWindow: 48 * time.Hour, PredictionLimit: 30 * 24 * time.Hour})
|
|
patternStart := time.Now().Add(-90 * time.Minute)
|
|
patternDetector.RecordEvent(patterns.HistoricalEvent{ResourceID: "vm-global", EventType: patterns.EventHighCPU, Timestamp: patternStart})
|
|
patternDetector.RecordEvent(patterns.HistoricalEvent{ResourceID: "vm-global", EventType: patterns.EventHighCPU, Timestamp: patternStart.Add(60 * time.Minute)})
|
|
|
|
intel.SetSubsystems(nil, patternDetector, correlationDetector, nil, incidentStore, knowledgeStore, nil, nil)
|
|
ctx := intel.FormatGlobalContext()
|
|
if ctx == "" {
|
|
t.Fatal("expected non-empty global context")
|
|
}
|
|
if !strings.Contains(ctx, "Resource Correlations") {
|
|
t.Fatalf("expected correlations in global context, got %q", ctx)
|
|
}
|
|
}
|
|
|
|
func TestIntelligence_GetSummary_WithSubsystems(t *testing.T) {
|
|
intel := NewIntelligence(IntelligenceConfig{})
|
|
findings := NewFindingsStore()
|
|
findings.Add(&Finding{
|
|
ID: "f1",
|
|
Key: "f1",
|
|
Severity: FindingSeverityWarning,
|
|
Category: FindingCategoryPerformance,
|
|
ResourceID: "vm-sum",
|
|
ResourceName: "vm-sum",
|
|
ResourceType: "vm",
|
|
Title: "Warning",
|
|
DetectedAt: time.Now(),
|
|
LastSeenAt: time.Now(),
|
|
Source: "test",
|
|
})
|
|
|
|
patternDetector := patterns.NewDetector(patterns.DetectorConfig{MinOccurrences: 2, PatternWindow: 48 * time.Hour, PredictionLimit: 30 * 24 * time.Hour})
|
|
patternStart := time.Now().Add(-90 * time.Minute)
|
|
patternDetector.RecordEvent(patterns.HistoricalEvent{ResourceID: "vm-sum", EventType: patterns.EventHighCPU, Timestamp: patternStart})
|
|
patternDetector.RecordEvent(patterns.HistoricalEvent{ResourceID: "vm-sum", EventType: patterns.EventHighCPU, Timestamp: patternStart.Add(60 * time.Minute)})
|
|
|
|
changes := memory.NewChangeDetector(memory.ChangeDetectorConfig{MaxChanges: 10})
|
|
changes.DetectChanges([]memory.ResourceSnapshot{{ID: "vm-sum", Name: "vm-sum", Type: "vm", Status: "running", SnapshotTime: time.Now()}})
|
|
|
|
remediations := memory.NewRemediationLog(memory.RemediationLogConfig{MaxRecords: 10})
|
|
remediations.Log(memory.RemediationRecord{ResourceID: "vm-sum", Problem: "cpu", Action: "restart", Outcome: memory.OutcomeResolved})
|
|
|
|
intel.SetSubsystems(findings, patternDetector, nil, nil, nil, nil, changes, remediations)
|
|
|
|
summary := intel.GetSummary()
|
|
if summary.FindingsCount.Total == 0 {
|
|
t.Error("expected findings in summary")
|
|
}
|
|
if summary.PredictionsCount == 0 {
|
|
t.Error("expected predictions in summary")
|
|
}
|
|
if len(summary.UpcomingRisks) == 0 {
|
|
t.Error("expected upcoming risks in summary")
|
|
}
|
|
if summary.RecentChangesCount == 0 {
|
|
t.Error("expected recent changes in summary")
|
|
}
|
|
if len(summary.RecentRemediations) == 0 {
|
|
t.Error("expected recent remediations in summary")
|
|
}
|
|
if len(summary.ResourcesAtRisk) == 0 {
|
|
t.Error("expected resources at risk in summary")
|
|
}
|
|
}
|
|
|
|
func TestIntelligence_GetResourceIntelligence_WithAllSubsystems(t *testing.T) {
|
|
intel := NewIntelligence(IntelligenceConfig{})
|
|
findings := NewFindingsStore()
|
|
findings.Add(&Finding{
|
|
ID: "f1",
|
|
Key: "f1",
|
|
Severity: FindingSeverityWarning,
|
|
Category: FindingCategoryPerformance,
|
|
ResourceID: "vm-intel",
|
|
ResourceName: "vm-intel",
|
|
ResourceType: "vm",
|
|
Title: "Warning",
|
|
DetectedAt: time.Now(),
|
|
LastSeenAt: time.Now(),
|
|
Source: "test",
|
|
})
|
|
|
|
patternDetector := patterns.NewDetector(patterns.DetectorConfig{MinOccurrences: 2, PatternWindow: 48 * time.Hour, PredictionLimit: 30 * 24 * time.Hour})
|
|
patternStart := time.Now().Add(-90 * time.Minute)
|
|
patternDetector.RecordEvent(patterns.HistoricalEvent{ResourceID: "vm-intel", EventType: patterns.EventHighCPU, Timestamp: patternStart})
|
|
patternDetector.RecordEvent(patterns.HistoricalEvent{ResourceID: "vm-intel", EventType: patterns.EventHighCPU, Timestamp: patternStart.Add(60 * time.Minute)})
|
|
|
|
correlationDetector := correlation.NewDetector(correlation.Config{MinOccurrences: 1, CorrelationWindow: 2 * time.Hour, RetentionWindow: 24 * time.Hour})
|
|
corrBase := time.Now().Add(-30 * time.Minute)
|
|
correlationDetector.RecordEvent(correlation.Event{ResourceID: "node-intel", ResourceName: "node-intel", ResourceType: "node", EventType: correlation.EventHighCPU, Timestamp: corrBase})
|
|
correlationDetector.RecordEvent(correlation.Event{ResourceID: "vm-intel", ResourceName: "vm-intel", ResourceType: "vm", EventType: correlation.EventRestart, Timestamp: corrBase.Add(1 * time.Minute)})
|
|
correlationDetector.RecordEvent(correlation.Event{ResourceID: "vm-intel", ResourceName: "vm-intel", ResourceType: "vm", EventType: correlation.EventHighCPU, Timestamp: corrBase.Add(2 * time.Minute)})
|
|
correlationDetector.RecordEvent(correlation.Event{ResourceID: "vm-child", ResourceName: "vm-child", ResourceType: "vm", EventType: correlation.EventRestart, Timestamp: corrBase.Add(3 * time.Minute)})
|
|
|
|
baselineStore := baseline.NewStore(baseline.StoreConfig{MinSamples: 1})
|
|
baselineStore.Learn("vm-intel", "vm", "cpu", []baseline.MetricPoint{{Value: 10}})
|
|
|
|
incidentStore := memory.NewIncidentStore(memory.IncidentStoreConfig{MaxIncidents: 10})
|
|
incidentStore.RecordAlertFired(&alerts.Alert{ID: "alert-intel", ResourceID: "vm-intel", ResourceName: "vm-intel", Type: "cpu", StartTime: time.Now()})
|
|
|
|
knowledgeStore, _ := knowledge.NewStore(t.TempDir())
|
|
knowledgeStore.SaveNote("vm-intel", "vm-intel", "vm", "general", "Note", "Content")
|
|
|
|
intel.SetSubsystems(findings, patternDetector, correlationDetector, baselineStore, incidentStore, knowledgeStore, nil, nil)
|
|
res := intel.GetResourceIntelligence("vm-intel")
|
|
if len(res.ActiveFindings) == 0 {
|
|
t.Error("expected active findings")
|
|
}
|
|
if len(res.Predictions) == 0 {
|
|
t.Error("expected predictions")
|
|
}
|
|
if len(res.Correlations) == 0 {
|
|
t.Error("expected correlations")
|
|
}
|
|
if len(res.Dependents) == 0 || len(res.Dependencies) == 0 {
|
|
t.Error("expected dependencies and dependents")
|
|
}
|
|
if len(res.Baselines) == 0 {
|
|
t.Error("expected baselines")
|
|
}
|
|
if len(res.RecentIncidents) == 0 {
|
|
t.Error("expected incidents")
|
|
}
|
|
if res.Knowledge == nil || res.NoteCount == 0 {
|
|
t.Error("expected knowledge")
|
|
}
|
|
}
|
|
|
|
func TestIntelligence_GetResourceIntelligence_KnowledgeFallback(t *testing.T) {
|
|
intel := NewIntelligence(IntelligenceConfig{})
|
|
knowledgeStore, _ := knowledge.NewStore(t.TempDir())
|
|
knowledgeStore.SaveNote("vm-know", "knowledge-vm", "vm", "general", "Note", "Content")
|
|
|
|
intel.SetSubsystems(nil, nil, nil, nil, nil, knowledgeStore, nil, nil)
|
|
res := intel.GetResourceIntelligence("vm-know")
|
|
if res.ResourceName != "knowledge-vm" {
|
|
t.Errorf("expected resource name from knowledge, got %q", res.ResourceName)
|
|
}
|
|
if res.ResourceType != "vm" {
|
|
t.Errorf("expected resource type from knowledge, got %q", res.ResourceType)
|
|
}
|
|
}
|