Files
Pulse/internal/ai/intelligence_test.go
rcourtman 31c704c7a7 refactor: fix lint issues in internal/ai package
- Remove redundant nil checks before len() calls
- Mark unused parameters with underscore
- Convert if/else chains to switch statements for cleaner code
- Add test assertions to resolve unused write warnings in patrol_test.go
2026-01-02 19:53:01 +00:00

754 lines
21 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"
)
func TestNewIntelligence(t *testing.T) {
intel := NewIntelligence(IntelligenceConfig{DataDir: "/tmp/test"})
if intel == nil {
t.Fatal("Expected non-nil Intelligence")
}
}
func TestIntelligence_GetSummary_NoSubsystems(t *testing.T) {
intel := NewIntelligence(IntelligenceConfig{})
summary := intel.GetSummary()
if summary == nil {
t.Fatal("Expected non-nil summary")
}
// Should have default healthy state
if summary.OverallHealth.Score != 100 {
t.Errorf("Expected health score 100, got %f", summary.OverallHealth.Score)
}
if summary.OverallHealth.Grade != HealthGradeA {
t.Errorf("Expected grade A, got %s", summary.OverallHealth.Grade)
}
if summary.Timestamp.IsZero() {
t.Error("Expected non-zero timestamp")
}
}
func TestIntelligence_GetResourceIntelligence_NoSubsystems(t *testing.T) {
intel := NewIntelligence(IntelligenceConfig{})
resourceIntel := intel.GetResourceIntelligence("test-resource")
if resourceIntel == nil {
t.Fatal("Expected non-nil resource intelligence")
}
if resourceIntel.ResourceID != "test-resource" {
t.Errorf("Expected resource ID 'test-resource', got %s", resourceIntel.ResourceID)
}
// Should have default healthy state
if resourceIntel.Health.Score != 100 {
t.Errorf("Expected health score 100, got %f", resourceIntel.Health.Score)
}
}
func TestIntelligence_FormatContext_NoSubsystems(t *testing.T) {
intel := NewIntelligence(IntelligenceConfig{})
// With no subsystems, context should be empty
ctx := intel.FormatContext("test-resource")
if ctx != "" {
t.Errorf("Expected empty context with no subsystems, got: %s", ctx)
}
}
func TestIntelligence_CreatePredictionFinding(t *testing.T) {
intel := NewIntelligence(IntelligenceConfig{})
pred := patterns.FailurePrediction{
ResourceID: "vm-100",
EventType: patterns.EventHighCPU, // Use the constant instead of cast
DaysUntil: 0.5, // Less than 1 day
Confidence: 0.85, // High confidence
Basis: "Pattern detected",
}
finding := intel.CreatePredictionFinding(pred)
if finding == nil {
t.Fatal("Expected non-nil finding")
}
// High confidence + < 1 day should be critical
if finding.Severity != FindingSeverityCritical {
t.Errorf("Expected critical severity for imminent high-confidence prediction, got %s", finding.Severity)
}
if finding.ResourceID != "vm-100" {
t.Errorf("Expected resource ID 'vm-100', got %s", finding.ResourceID)
}
}
func TestIntelligence_SetSubsystems(t *testing.T) {
intel := NewIntelligence(IntelligenceConfig{})
// Create a findings store
findings := NewFindingsStore()
// Add a finding
findings.Add(&Finding{
ID: "test-finding",
Key: "test:finding",
Severity: FindingSeverityWarning,
Category: FindingCategoryPerformance,
ResourceID: "vm-100",
ResourceName: "test-vm",
ResourceType: "vm",
Title: "Test Finding",
DetectedAt: time.Now(),
LastSeenAt: time.Now(),
Source: "test",
})
// Set subsystems with just findings
intel.SetSubsystems(findings, nil, nil, nil, nil, nil, nil, nil)
// Get summary
summary := intel.GetSummary()
// Should have 1 warning
if summary.FindingsCount.Warning != 1 {
t.Errorf("Expected 1 warning, got %d", summary.FindingsCount.Warning)
}
if summary.FindingsCount.Total != 1 {
t.Errorf("Expected 1 total finding, got %d", summary.FindingsCount.Total)
}
// Health should be reduced due to warning
if summary.OverallHealth.Score >= 100 {
t.Error("Expected health score < 100 due to warning finding")
}
}
func TestIntelligence_HealthGrades(t *testing.T) {
tests := []struct {
score float64
grade HealthGrade
}{
{100, HealthGradeA},
{95, HealthGradeA},
{90, HealthGradeA},
{85, HealthGradeB},
{75, HealthGradeB},
{70, HealthGradeC},
{60, HealthGradeC},
{55, HealthGradeD},
{40, HealthGradeD},
{30, HealthGradeF},
{0, HealthGradeF},
}
for _, tt := range tests {
grade := scoreToGrade(tt.score)
if grade != tt.grade {
t.Errorf("scoreToGrade(%f) = %s, expected %s", tt.score, grade, tt.grade)
}
}
}
func TestIntelligence_CheckBaselinesForResource(t *testing.T) {
intel := NewIntelligence(IntelligenceConfig{})
// With no baseline store, should return nil
anomalies := intel.CheckBaselinesForResource("vm-100", map[string]float64{
"cpu": 85.0,
"memory": 90.0,
})
if anomalies != nil {
t.Error("Expected nil anomalies when baseline store not configured")
}
}
// Note: mockStateProvider is already defined in alert_triggered_test.go
func TestIntelligence_SetStateProvider(t *testing.T) {
intel := NewIntelligence(IntelligenceConfig{})
mockSP := &mockStateProvider{}
intel.SetStateProvider(mockSP)
// State provider should be set (we can't directly access it, but no panic = success)
}
func TestIntelligence_FormatGlobalContext(t *testing.T) {
intel := NewIntelligence(IntelligenceConfig{})
// With no subsystems, should return empty
ctx := intel.FormatGlobalContext()
if ctx != "" {
t.Errorf("Expected empty context with no subsystems, got: %s", ctx)
}
// Set up knowledge store
knowledgeStore, err := knowledge.NewStore(t.TempDir())
if err != nil {
t.Fatalf("Failed to create knowledge store: %v", err)
}
intel.SetSubsystems(nil, nil, nil, nil, nil, knowledgeStore, nil, nil)
// Should still be empty (no knowledge saved)
ctx = intel.FormatGlobalContext()
// Empty or with headers only is fine
}
func TestIntelligence_FormatGlobalContext_WithSubsystems(t *testing.T) {
intel := NewIntelligence(IntelligenceConfig{})
// Set up incident store with some data
incidentStore := memory.NewIncidentStore(memory.IncidentStoreConfig{
MaxIncidents: 10,
})
// Create some incidents to format
incidentStore.RecordAnalysis("alert-1", "Test analysis", nil)
intel.SetSubsystems(nil, nil, nil, nil, incidentStore, nil, nil, nil)
ctx := intel.FormatGlobalContext()
// Having set incidents, there should be some context
// (may be empty if no actual incidents, but shouldn't panic)
_ = ctx
}
func TestIntelligence_RecordLearning(t *testing.T) {
intel := NewIntelligence(IntelligenceConfig{})
// Without knowledge store, should return nil
err := intel.RecordLearning("vm-100", "test-vm", "vm", "Test Title", "Test content")
if err != nil {
t.Errorf("Expected nil error without knowledge store, got: %v", err)
}
// With knowledge store
knowledgeStore, err := knowledge.NewStore(t.TempDir())
if err != nil {
t.Fatalf("Failed to create knowledge store: %v", err)
}
intel.SetSubsystems(nil, nil, nil, nil, nil, knowledgeStore, nil, nil)
err = intel.RecordLearning("vm-100", "test-vm", "vm", "Test Title", "Test content")
if err != nil {
t.Errorf("Expected nil error, got: %v", err)
}
}
func TestSeverityOrder(t *testing.T) {
tests := []struct {
severity FindingSeverity
expected int
}{
{FindingSeverityCritical, 0},
{FindingSeverityWarning, 1},
{FindingSeverityWatch, 2},
{FindingSeverityInfo, 3},
{FindingSeverity("unknown"), 4},
}
for _, tt := range tests {
result := severityOrder(tt.severity)
if result != tt.expected {
t.Errorf("severityOrder(%s) = %d, expected %d", tt.severity, result, tt.expected)
}
}
}
func TestIntelligence_CheckBaselinesForResource_WithBaselines(t *testing.T) {
intel := NewIntelligence(IntelligenceConfig{})
// Create baseline store with learned data
baselineStore := baseline.NewStore(baseline.StoreConfig{MinSamples: 10})
// Learn baseline for CPU at ~20%
points := make([]baseline.MetricPoint, 100)
for i := 0; i < 100; i++ {
points[i] = baseline.MetricPoint{Value: 20 + float64(i%3) - 1} // 19-21
}
baselineStore.Learn("vm-100", "vm", "cpu", points)
intel.SetSubsystems(nil, nil, nil, baselineStore, nil, nil, nil, nil)
// Check with anomalous value (80% is 4x baseline)
anomalies := intel.CheckBaselinesForResource("vm-100", map[string]float64{
"cpu": 80.0,
})
// Should detect anomaly for CPU (80% with baseline of 20% is 4x = anomalous)
if len(anomalies) == 0 {
t.Error("Expected anomaly for CPU at 80% with baseline of 20%")
}
}
func TestIntelligence_GetResourceIntelligence_WithSubsystems(t *testing.T) {
intel := NewIntelligence(IntelligenceConfig{})
// Create findings store with a finding for the resource
findings := NewFindingsStore()
findings.Add(&Finding{
ID: "f1",
Key: "test:f1",
Severity: FindingSeverityCritical,
Category: FindingCategoryPerformance,
ResourceID: "vm-200",
ResourceName: "critical-vm",
ResourceType: "vm",
Title: "Critical CPU",
DetectedAt: time.Now(),
LastSeenAt: time.Now(),
Source: "test",
})
// Create correlation detector
correlationDetector := correlation.NewDetector(correlation.DefaultConfig())
intel.SetSubsystems(findings, nil, correlationDetector, nil, nil, nil, nil, nil)
resourceIntel := intel.GetResourceIntelligence("vm-200")
if len(resourceIntel.ActiveFindings) != 1 {
t.Errorf("Expected 1 active finding, got %d", len(resourceIntel.ActiveFindings))
}
if resourceIntel.ResourceName != "critical-vm" {
t.Errorf("Expected resource name 'critical-vm', got %s", resourceIntel.ResourceName)
}
// Health should be reduced due to critical finding
if resourceIntel.Health.Score >= 100 {
t.Error("Expected reduced health score due to critical finding")
}
// Grade should be less than A
if resourceIntel.Health.Grade == HealthGradeA {
t.Error("Expected health grade less than A due to critical finding")
}
}
func TestIntelligence_FormatContext_WithKnowledge(t *testing.T) {
intel := NewIntelligence(IntelligenceConfig{})
// Create knowledge store with data
knowledgeStore, err := knowledge.NewStore(t.TempDir())
if err != nil {
t.Fatalf("Failed to create knowledge store: %v", err)
}
knowledgeStore.SaveNote("vm-300", "test-vm", "vm", "general", "Test Note", "This is test content")
intel.SetSubsystems(nil, nil, nil, nil, nil, knowledgeStore, nil, nil)
ctx := intel.FormatContext("vm-300")
// Should contain the knowledge context
if ctx == "" {
t.Error("Expected non-empty context with knowledge")
}
}
func TestIntelligence_CreatePredictionFinding_LowSeverity(t *testing.T) {
intel := NewIntelligence(IntelligenceConfig{})
// Prediction far in the future with low confidence
pred := patterns.FailurePrediction{
ResourceID: "vm-100",
EventType: patterns.EventHighMemory,
DaysUntil: 14, // Far away
Confidence: 0.3, // Low confidence
Basis: "Pattern detected",
}
finding := intel.CreatePredictionFinding(pred)
// Should be watch severity (not critical or warning)
if finding.Severity != FindingSeverityWatch {
t.Errorf("Expected watch severity for far-off low-confidence prediction, got %s", finding.Severity)
}
}
func TestIntelligence_CreatePredictionFinding_WarningSeverity(t *testing.T) {
intel := NewIntelligence(IntelligenceConfig{})
// Prediction soon but low confidence
pred := patterns.FailurePrediction{
ResourceID: "vm-100",
EventType: patterns.EventHighCPU,
DaysUntil: 0.5, // Soon
Confidence: 0.6, // Medium confidence (not > 0.8)
Basis: "Pattern detected",
}
finding := intel.CreatePredictionFinding(pred)
// Should be warning (soon but not high confidence)
if finding.Severity != FindingSeverityWarning {
t.Errorf("Expected warning severity for imminent medium-confidence prediction, got %s", finding.Severity)
}
}
func TestIntelligence_GetSummary_WithCriticalFindings(t *testing.T) {
intel := NewIntelligence(IntelligenceConfig{})
findings := NewFindingsStore()
// Add multiple critical findings
for i := 0; i < 3; i++ {
findings.Add(&Finding{
ID: "crit-" + string(rune('A'+i)),
Key: "critical:" + string(rune('A'+i)),
Severity: FindingSeverityCritical,
Category: FindingCategoryReliability,
ResourceID: "vm-crit-" + string(rune('A'+i)),
Title: "Critical Issue",
DetectedAt: time.Now(),
LastSeenAt: time.Now(),
Source: "test",
})
}
intel.SetSubsystems(findings, nil, nil, nil, nil, nil, nil, nil)
summary := intel.GetSummary()
// Should have 3 critical
if summary.FindingsCount.Critical != 3 {
t.Errorf("Expected 3 critical findings, got %d", summary.FindingsCount.Critical)
}
// Health should be significantly reduced
// Note: critical impact is capped at 40 points, so score = 100 - 40 = 60
if summary.OverallHealth.Score > 60 {
t.Errorf("Expected health score <= 60 with 3 critical findings, got %f", summary.OverallHealth.Score)
}
// Prediction text should mention critical issues
if summary.OverallHealth.Prediction == "" {
t.Error("Expected non-empty prediction text")
}
}
func TestAbsFloatIntel(t *testing.T) {
tests := []struct {
input float64
expected float64
}{
{5.5, 5.5},
{-5.5, 5.5},
{0, 0},
{-0, 0},
}
for _, tt := range tests {
result := absFloatIntel(tt.input)
if result != tt.expected {
t.Errorf("absFloatIntel(%f) = %f, expected %f", tt.input, result, tt.expected)
}
}
}
func TestIntelligence_GetSummary_WithPatterns(t *testing.T) {
intel := NewIntelligence(IntelligenceConfig{})
// Create pattern detector with predictions
patternDetector := patterns.NewDetector(patterns.DefaultConfig())
// Set up the subsystems
intel.SetSubsystems(nil, patternDetector, nil, nil, nil, nil, nil, nil)
summary := intel.GetSummary()
// Predictions count should be available
if summary.PredictionsCount < 0 {
t.Error("PredictionsCount should not be negative")
}
}
func TestIntelligence_GetResourceIntelligence_WithBaselines(t *testing.T) {
intel := NewIntelligence(IntelligenceConfig{})
// Create baseline store with learned data
baselineStore := baseline.NewStore(baseline.StoreConfig{MinSamples: 10})
// Learn baseline for CPU
points := make([]baseline.MetricPoint, 100)
for i := 0; i < 100; i++ {
points[i] = baseline.MetricPoint{Value: 30 + float64(i%5) - 2}
}
baselineStore.Learn("vm-with-baseline", "vm", "cpu", points)
baselineStore.Learn("vm-with-baseline", "vm", "memory", points)
intel.SetSubsystems(nil, nil, nil, baselineStore, nil, nil, nil, nil)
resourceIntel := intel.GetResourceIntelligence("vm-with-baseline")
// Should have baselines
if len(resourceIntel.Baselines) == 0 {
t.Error("Expected baselines to be populated")
}
// Check CPU baseline exists
if _, ok := resourceIntel.Baselines["cpu"]; !ok {
t.Error("Expected CPU baseline")
}
if _, ok := resourceIntel.Baselines["memory"]; !ok {
t.Error("Expected memory baseline")
}
}
func TestIntelligence_GetResourceIntelligence_WithIncidents(t *testing.T) {
intel := NewIntelligence(IntelligenceConfig{})
// Create incident store with some incidents
incidentStore := memory.NewIncidentStore(memory.IncidentStoreConfig{
MaxIncidents: 10,
})
// Record an incident for the resource
incidentStore.RecordAnalysis("alert-vm-500", "Analysis for vm-500", nil)
intel.SetSubsystems(nil, nil, nil, nil, incidentStore, nil, nil, nil)
resourceIntel := intel.GetResourceIntelligence("vm-500")
// Should have the resource ID
if resourceIntel.ResourceID != "vm-500" {
t.Errorf("Expected resource ID 'vm-500', got %s", resourceIntel.ResourceID)
}
}
func TestIntelligence_calculateResourceHealth_WithAnomalies(t *testing.T) {
intel := NewIntelligence(IntelligenceConfig{})
// Create resource intelligence with anomalies
resourceIntel := &ResourceIntelligence{
ResourceID: "test-vm",
Anomalies: []AnomalyReport{
{
Metric: "cpu",
CurrentValue: 90,
BaselineMean: 30,
ZScore: 5.0,
Severity: baseline.AnomalyCritical,
Description: "CPU is 3x baseline",
},
{
Metric: "memory",
CurrentValue: 80,
BaselineMean: 50,
ZScore: 3.0,
Severity: baseline.AnomalyMedium,
Description: "Memory is elevated",
},
},
}
health := intel.calculateResourceHealth(resourceIntel)
// Health should be reduced due to anomalies
if health.Score >= 100 {
t.Error("Expected reduced health score due to anomalies")
}
// Should have factors for anomalies
hasAnomalyFactor := false
for _, f := range health.Factors {
if f.Category == "baseline" {
hasAnomalyFactor = true
break
}
}
if !hasAnomalyFactor {
t.Error("Expected baseline/anomaly factor in health factors")
}
}
func TestIntelligence_calculateResourceHealth_WithPredictions(t *testing.T) {
intel := NewIntelligence(IntelligenceConfig{})
// Create resource intelligence with predictions
resourceIntel := &ResourceIntelligence{
ResourceID: "test-vm",
Predictions: []patterns.FailurePrediction{
{
ResourceID: "test-vm",
EventType: patterns.EventHighCPU,
DaysUntil: 2.0,
Confidence: 0.8,
Basis: "Pattern detected",
},
},
}
health := intel.calculateResourceHealth(resourceIntel)
// Health should be reduced due to predictions
if health.Score >= 100 {
t.Error("Expected reduced health score due to predictions")
}
// Should have a prediction factor
hasPredictionFactor := false
for _, f := range health.Factors {
if f.Category == "prediction" {
hasPredictionFactor = true
break
}
}
if !hasPredictionFactor {
t.Error("Expected prediction factor in health factors")
}
}
func TestIntelligence_calculateResourceHealth_WithNotes(t *testing.T) {
intel := NewIntelligence(IntelligenceConfig{})
// Create resource intelligence with notes (bonus for documentation)
resourceIntel := &ResourceIntelligence{
ResourceID: "test-vm",
NoteCount: 3,
}
health := intel.calculateResourceHealth(resourceIntel)
// Health should have a bonus for having notes
if health.Score < 100 {
t.Error("Expected health score >= 100 with only notes (bonus)")
}
// Should have a learning factor
hasLearningFactor := false
for _, f := range health.Factors {
if f.Category == "learning" {
hasLearningFactor = true
break
}
}
if !hasLearningFactor {
t.Error("Expected learning factor for documented resource")
}
}
func TestIntelligence_GetSummary_WithLearningBonus(t *testing.T) {
intel := NewIntelligence(IntelligenceConfig{})
// Create knowledge store with many resources
knowledgeStore, err := knowledge.NewStore(t.TempDir())
if err != nil {
t.Fatalf("Failed to create knowledge store: %v", err)
}
// Add knowledge for 6+ resources to trigger learning bonus
for i := 0; i < 7; i++ {
resourceID := "vm-" + string(rune('A'+i))
knowledgeStore.SaveNote(resourceID, "VM "+string(rune('A'+i)), "vm", "general", "Note", "Content")
}
intel.SetSubsystems(nil, nil, nil, nil, nil, knowledgeStore, nil, nil)
summary := intel.GetSummary()
// With 6+ resources learned, should have learning bonus factor
hasLearningFactor := false
for _, f := range summary.OverallHealth.Factors {
if f.Category == "learning" {
hasLearningFactor = true
break
}
}
if !hasLearningFactor {
t.Error("Expected learning factor with 6+ resources learned")
}
}
func TestIntelligence_FormatContext_WithCorrelation(t *testing.T) {
intel := NewIntelligence(IntelligenceConfig{})
// Create correlation detector
correlationDetector := correlation.NewDetector(correlation.DefaultConfig())
intel.SetSubsystems(nil, nil, correlationDetector, nil, nil, nil, nil, nil)
// Should not panic even without correlations
ctx := intel.FormatContext("vm-test")
_ = ctx
}
func TestIntelligence_FormatContext_WithPatterns(t *testing.T) {
intel := NewIntelligence(IntelligenceConfig{})
// Create pattern detector
patternDetector := patterns.NewDetector(patterns.DefaultConfig())
intel.SetSubsystems(nil, patternDetector, nil, nil, nil, nil, nil, nil)
// Should not panic
ctx := intel.FormatContext("vm-test")
_ = ctx
}
func TestIntelligence_FormatContext_WithIncidents(t *testing.T) {
intel := NewIntelligence(IntelligenceConfig{})
// Create incident store
incidentStore := memory.NewIncidentStore(memory.IncidentStoreConfig{
MaxIncidents: 10,
})
intel.SetSubsystems(nil, nil, nil, nil, incidentStore, nil, nil, nil)
ctx := intel.FormatContext("vm-test")
_ = ctx
}
func TestIntelligence_FormatGlobalContext_Full(t *testing.T) {
intel := NewIntelligence(IntelligenceConfig{})
// Set up all context-contributing subsystems
knowledgeStore, _ := knowledge.NewStore(t.TempDir())
incidentStore := memory.NewIncidentStore(memory.IncidentStoreConfig{MaxIncidents: 10})
correlationDetector := correlation.NewDetector(correlation.DefaultConfig())
patternDetector := patterns.NewDetector(patterns.DefaultConfig())
intel.SetSubsystems(nil, patternDetector, correlationDetector, nil, incidentStore, knowledgeStore, nil, nil)
ctx := intel.FormatGlobalContext()
// May be empty if no data, but shouldn't panic
_ = ctx
}
func TestIntelligence_generateHealthPrediction_Warnings(t *testing.T) {
intel := NewIntelligence(IntelligenceConfig{})
health := HealthScore{
Score: 80,
Grade: HealthGradeB,
}
summary := &IntelligenceSummary{
FindingsCount: FindingsCounts{
Warning: 3,
},
}
prediction := intel.generateHealthPrediction(health, summary)
if prediction == "" {
t.Error("Expected non-empty prediction")
}
if !strings.Contains(prediction, "warning") && !strings.Contains(prediction, "Warning") {
t.Errorf("Expected prediction to mention warnings, got: %s", prediction)
}
}