Pulse/internal/ai/patterns/detector_test.go
rcourtman d9d798084e feat(ai): Add failure pattern detection for predictive intelligence (Phase 5)
Create internal/ai/patterns package:

1. Pattern Detector (detector.go):
   - Records historical events (high memory, OOM, restarts, etc.)
   - Detects recurring failure patterns
   - Calculates average interval between occurrences
   - Computes confidence based on pattern consistency
   - Predicts when failures will occur again
   - Persists to ai_patterns.json

2. Event types tracked:
   - high_memory, high_cpu, disk_full
   - oom, restart, unresponsive
   - backup_failed

3. Integration:
   - Wire PatternDetector into router startup
   - Add to AI context in buildEnrichedContext
   - FormatForContext generates failure predictions

Example AI context now includes:
'OOM events typically occurs every ~10 days (next expected in ~3 days)'

This enables proactive alerts before problems recur.

All tests passing.
2025-12-12 14:11:28 +00:00

206 lines
5.2 KiB
Go

package patterns
import (
"testing"
"time"
)
func TestDetector_RecordEvent(t *testing.T) {
d := NewDetector(DetectorConfig{MinOccurrences: 2})
// Record first event
d.RecordEvent(HistoricalEvent{
ResourceID: "vm-100",
EventType: EventHighMemory,
Timestamp: time.Now().Add(-10 * 24 * time.Hour),
})
if len(d.events) != 1 {
t.Errorf("Expected 1 event, got %d", len(d.events))
}
}
func TestDetector_PatternDetection(t *testing.T) {
d := NewDetector(DetectorConfig{MinOccurrences: 3, PatternWindow: 365 * 24 * time.Hour})
// Record events with 10-day interval
now := time.Now()
for i := 5; i >= 0; i-- {
d.RecordEvent(HistoricalEvent{
ResourceID: "vm-100",
EventType: EventHighMemory,
Timestamp: now.Add(-time.Duration(i*10) * 24 * time.Hour),
})
}
// Check that pattern was detected
patterns := d.GetPatterns()
key := patternKey("vm-100", EventHighMemory)
pattern, ok := patterns[key]
if !ok {
t.Fatal("Expected pattern to be detected")
}
if pattern.Occurrences != 6 {
t.Errorf("Expected 6 occurrences, got %d", pattern.Occurrences)
}
// Average interval should be ~10 days
avgDays := pattern.AverageInterval.Hours() / 24
if avgDays < 9 || avgDays > 11 {
t.Errorf("Expected ~10 day interval, got %.1f days", avgDays)
}
}
func TestDetector_GetPredictions(t *testing.T) {
d := NewDetector(DetectorConfig{
MinOccurrences: 3,
PatternWindow: 365 * 24 * time.Hour,
PredictionLimit: 30 * 24 * time.Hour,
})
// Record events with regular interval
now := time.Now()
for i := 3; i >= 0; i-- {
d.RecordEvent(HistoricalEvent{
ResourceID: "vm-100",
EventType: EventOOM,
Timestamp: now.Add(-time.Duration(i*7) * 24 * time.Hour), // 7-day interval
})
}
predictions := d.GetPredictions()
// Should have a prediction for OOM
found := false
for _, p := range predictions {
if p.ResourceID == "vm-100" && p.EventType == EventOOM {
found = true
// Should predict in ~7 days
if p.DaysUntil < 5 || p.DaysUntil > 9 {
t.Errorf("Expected prediction in ~7 days, got %.1f days", p.DaysUntil)
}
break
}
}
if !found {
t.Error("Expected OOM prediction for vm-100")
}
}
func TestDetector_GetPredictionsForResource(t *testing.T) {
d := NewDetector(DetectorConfig{MinOccurrences: 3, PatternWindow: 365 * 24 * time.Hour})
now := time.Now()
// Add pattern for vm-100
for i := 3; i >= 0; i-- {
d.RecordEvent(HistoricalEvent{
ResourceID: "vm-100",
EventType: EventRestart,
Timestamp: now.Add(-time.Duration(i*14) * 24 * time.Hour),
})
}
// Add pattern for vm-200
for i := 3; i >= 0; i-- {
d.RecordEvent(HistoricalEvent{
ResourceID: "vm-200",
EventType: EventHighCPU,
Timestamp: now.Add(-time.Duration(i*5) * 24 * time.Hour),
})
}
// Get predictions for vm-100 only
predictions := d.GetPredictionsForResource("vm-100")
for _, p := range predictions {
if p.ResourceID != "vm-100" {
t.Errorf("Got prediction for wrong resource: %s", p.ResourceID)
}
}
}
func TestDetector_Confidence(t *testing.T) {
d := NewDetector(DetectorConfig{MinOccurrences: 3, PatternWindow: 365 * 24 * time.Hour})
now := time.Now()
// Add very consistent pattern (every 7 days exactly)
for i := 5; i >= 0; i-- {
d.RecordEvent(HistoricalEvent{
ResourceID: "consistent-vm",
EventType: EventHighMemory,
Timestamp: now.Add(-time.Duration(i*7*24) * time.Hour),
})
}
patterns := d.GetPatterns()
pattern := patterns[patternKey("consistent-vm", EventHighMemory)]
if pattern == nil {
t.Fatal("Expected pattern")
}
// Consistent pattern should have high confidence
if pattern.Confidence < 0.5 {
t.Errorf("Expected high confidence for consistent pattern, got %.2f", pattern.Confidence)
}
}
func TestDetector_FormatForContext(t *testing.T) {
d := NewDetector(DetectorConfig{MinOccurrences: 3, PatternWindow: 365 * 24 * time.Hour})
now := time.Now()
for i := 3; i >= 0; i-- {
d.RecordEvent(HistoricalEvent{
ResourceID: "vm-100",
EventType: EventOOM,
Timestamp: now.Add(-time.Duration(i*10) * 24 * time.Hour),
})
}
context := d.FormatForContext("vm-100")
if context == "" {
t.Error("Expected non-empty context")
}
if !contains(context, "OOM") && !contains(context, "oom") {
t.Errorf("Expected context to mention OOM: %s", context)
}
}
func TestMapAlertToEventType(t *testing.T) {
tests := []struct {
alertType string
expected EventType
}{
{"memory_warning", EventHighMemory},
{"memory_critical", EventHighMemory},
{"cpu_warning", EventHighCPU},
{"cpu_critical", EventHighCPU},
{"disk_warning", EventDiskFull},
{"disk_critical", EventDiskFull},
{"oom", EventOOM},
{"restart", EventRestart},
{"unresponsive", EventUnresponsive},
{"backup_failed", EventBackupFailed},
{"unknown_alert", ""},
}
for _, tc := range tests {
result := mapAlertToEventType(tc.alertType)
if result != tc.expected {
t.Errorf("mapAlertToEventType(%q) = %q, want %q", tc.alertType, result, tc.expected)
}
}
}
func contains(s, substr string) bool {
return len(s) >= len(substr) && (s == substr || len(s) > 0 && containsHelper(s, substr))
}
func containsHelper(s, substr string) bool {
for i := 0; i <= len(s)-len(substr); i++ {
if s[i:i+len(substr)] == substr {
return true
}
}
return false
}