Pulse/internal/ai/patterns/detector_test.go
rcourtman 3fdf753a5b Enhance devcontainer and CI workflows
- Add persistent volume mounts for Go/npm caches (faster rebuilds)
- Add shell config with helpful aliases and custom prompt
- Add comprehensive devcontainer documentation
- Add pre-commit hooks for Go formatting and linting
- Use go-version-file in CI workflows instead of hardcoded versions
- Simplify docker compose commands with --wait flag
- Add gitignore entries for devcontainer auth files

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-01 22:29:15 +00:00

317 lines
7.3 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
}
func TestFormatDays(t *testing.T) {
tests := []struct {
name string
days float64
expected string
}{
{
name: "less than an hour",
days: 0.01, // about 14 minutes
expected: "less than an hour",
},
{
name: "a few hours",
days: 0.25, // 6 hours
expected: "6 hours",
},
{
name: "half a day",
days: 0.5, // 12 hours
expected: "12 hours",
},
{
name: "one day",
days: 1.0,
expected: "1 day",
},
{
name: "just under two days",
days: 1.9,
expected: "1 day",
},
{
name: "two days",
days: 2.0,
expected: "2 days",
},
{
name: "seven days",
days: 7.0,
expected: "7 days",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := formatDays(tt.days)
if result != tt.expected {
t.Errorf("formatDays(%.2f) = %q, want %q", tt.days, result, tt.expected)
}
})
}
}
func TestAverageDuration(t *testing.T) {
tests := []struct {
name string
durations []time.Duration
expected time.Duration
}{
{
name: "empty slice",
durations: []time.Duration{},
expected: 0,
},
{
name: "single duration",
durations: []time.Duration{10 * time.Hour},
expected: 10 * time.Hour,
},
{
name: "multiple durations",
durations: []time.Duration{6 * time.Hour, 12 * time.Hour, 18 * time.Hour},
expected: 12 * time.Hour, // average of 6, 12, 18 is 12
},
{
name: "mixed durations",
durations: []time.Duration{24 * time.Hour, 48 * time.Hour},
expected: 36 * time.Hour, // average of 1 and 2 days is 1.5 days
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
result := averageDuration(tt.durations)
if result != tt.expected {
t.Errorf("averageDuration() = %v, want %v", result, tt.expected)
}
})
}
}
func TestIntToStr(t *testing.T) {
tests := []struct {
input int
expected string
}{
{0, "0"},
{1, "1"},
{10, "10"},
{100, "100"},
{999, "999"},
}
for _, tt := range tests {
result := intToStr(tt.input)
if result != tt.expected {
t.Errorf("intToStr(%d) = %q, want %q", tt.input, result, tt.expected)
}
}
}