Improve AI test coverage
- baseline/store_test.go: Add tests for CheckResourceAnomalies, formatAnomalyDescription, formatRatio, GetAllAnomalies, floatToStr (67.9% -> 92.2%) - memory/incidents_test.go: Add tests for RecordAlertUnacknowledged, RecordRunbook, ListIncidentsByResource, FormatForAlert, FormatForResource, FormatForPatrol (66.8% -> 81.1%) - intelligence_test.go: Add tests for SetStateProvider, FormatGlobalContext, RecordLearning, severityOrder, CheckBaselinesForResource with baselines (61.4% -> 63.1%)
This commit is contained in:
parent
0ed453c1aa
commit
324ea0ca92
3 changed files with 1115 additions and 2 deletions
|
|
@ -304,3 +304,275 @@ func TestFormatDays(t *testing.T) {
|
|||
}
|
||||
}
|
||||
|
||||
func TestCheckResourceAnomalies_Disk(t *testing.T) {
|
||||
store := NewStore(StoreConfig{MinSamples: 10})
|
||||
|
||||
// Create stable data with mean ~60% disk usage
|
||||
points := make([]MetricPoint, 100)
|
||||
for i := 0; i < 100; i++ {
|
||||
points[i] = MetricPoint{Value: 60 + float64(i%5) - 2} // 58-62
|
||||
}
|
||||
store.Learn("test-vm", "vm", "disk", points)
|
||||
|
||||
// Test: disk above 85% should be reported
|
||||
metrics := map[string]float64{"disk": 90}
|
||||
anomalies := store.CheckResourceAnomalies("test-vm", metrics)
|
||||
if len(anomalies) == 0 {
|
||||
t.Error("Expected disk anomaly to be reported for 90% usage")
|
||||
}
|
||||
|
||||
// Test: disk increase >15 points from baseline should be reported
|
||||
metrics = map[string]float64{"disk": 80}
|
||||
anomalies = store.CheckResourceAnomalies("test-vm", metrics)
|
||||
if len(anomalies) == 0 {
|
||||
t.Error("Expected disk anomaly to be reported for 20 point increase from baseline")
|
||||
}
|
||||
|
||||
// Test: disk at baseline should not be reported (no significant deviation)
|
||||
metrics = map[string]float64{"disk": 60}
|
||||
anomalies = store.CheckResourceAnomalies("test-vm", metrics)
|
||||
if len(anomalies) != 0 {
|
||||
t.Errorf("Expected no anomaly for disk at baseline, got %d", len(anomalies))
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckResourceAnomalies_CPU(t *testing.T) {
|
||||
store := NewStore(StoreConfig{MinSamples: 10})
|
||||
|
||||
// Create stable data with mean ~20% CPU usage
|
||||
points := make([]MetricPoint, 100)
|
||||
for i := 0; i < 100; i++ {
|
||||
points[i] = MetricPoint{Value: 20 + float64(i%3) - 1} // 19-21
|
||||
}
|
||||
store.Learn("test-vm", "vm", "cpu", points)
|
||||
|
||||
// Test: CPU at 80% (above 70% and >2x baseline) should be reported
|
||||
metrics := map[string]float64{"cpu": 80}
|
||||
anomalies := store.CheckResourceAnomalies("test-vm", metrics)
|
||||
if len(anomalies) == 0 {
|
||||
t.Error("Expected CPU anomaly to be reported for 80% (>70% and 4x baseline)")
|
||||
}
|
||||
|
||||
// Test: CPU at 50% should NOT be reported (below 70% threshold)
|
||||
metrics = map[string]float64{"cpu": 50}
|
||||
anomalies = store.CheckResourceAnomalies("test-vm", metrics)
|
||||
if len(anomalies) != 0 {
|
||||
t.Errorf("Expected no anomaly for CPU at 50%%, got %d", len(anomalies))
|
||||
}
|
||||
|
||||
// Test: CPU at 20% (baseline) should not be reported
|
||||
metrics = map[string]float64{"cpu": 20}
|
||||
anomalies = store.CheckResourceAnomalies("test-vm", metrics)
|
||||
if len(anomalies) != 0 {
|
||||
t.Errorf("Expected no anomaly for CPU at baseline, got %d", len(anomalies))
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckResourceAnomalies_Memory(t *testing.T) {
|
||||
store := NewStore(StoreConfig{MinSamples: 10})
|
||||
|
||||
// Create stable data with mean ~40% memory usage
|
||||
points := make([]MetricPoint, 100)
|
||||
for i := 0; i < 100; i++ {
|
||||
points[i] = MetricPoint{Value: 40 + float64(i%3) - 1} // 39-41
|
||||
}
|
||||
store.Learn("test-vm", "vm", "memory", points)
|
||||
|
||||
// Test: Memory at 85% should be reported (above 80% threshold)
|
||||
metrics := map[string]float64{"memory": 85}
|
||||
anomalies := store.CheckResourceAnomalies("test-vm", metrics)
|
||||
if len(anomalies) == 0 {
|
||||
t.Error("Expected memory anomaly to be reported for 85% (above 80%)")
|
||||
}
|
||||
|
||||
// Test: Memory at 70% with 1.75x baseline should be reported (>1.5x and >60%)
|
||||
metrics = map[string]float64{"memory": 70}
|
||||
anomalies = store.CheckResourceAnomalies("test-vm", metrics)
|
||||
if len(anomalies) == 0 {
|
||||
t.Error("Expected memory anomaly to be reported for 70% (1.75x baseline, >60%)")
|
||||
}
|
||||
|
||||
// Test: Memory at 50% should NOT be reported (not >1.5x enough or >80%)
|
||||
metrics = map[string]float64{"memory": 50}
|
||||
anomalies = store.CheckResourceAnomalies("test-vm", metrics)
|
||||
if len(anomalies) != 0 {
|
||||
t.Errorf("Expected no anomaly for memory at 50%%, got %d", len(anomalies))
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckResourceAnomalies_OtherMetrics(t *testing.T) {
|
||||
store := NewStore(StoreConfig{MinSamples: 10})
|
||||
|
||||
// Create stable network data with mean ~100
|
||||
points := make([]MetricPoint, 100)
|
||||
for i := 0; i < 100; i++ {
|
||||
points[i] = MetricPoint{Value: 100 + float64(i%5) - 2} // 98-102
|
||||
}
|
||||
store.Learn("test-vm", "vm", "network_in", points)
|
||||
|
||||
// Test: network_in at 2x baseline should be reported
|
||||
metrics := map[string]float64{"network_in": 250}
|
||||
anomalies := store.CheckResourceAnomalies("test-vm", metrics)
|
||||
if len(anomalies) == 0 {
|
||||
t.Error("Expected network anomaly to be reported for 2.5x baseline")
|
||||
}
|
||||
|
||||
// Test: network_in at 0.3x baseline should be reported (below 0.5x)
|
||||
metrics = map[string]float64{"network_in": 30}
|
||||
anomalies = store.CheckResourceAnomalies("test-vm", metrics)
|
||||
if len(anomalies) == 0 {
|
||||
t.Error("Expected network anomaly to be reported for 0.3x baseline")
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckResourceAnomalies_NoBaseline(t *testing.T) {
|
||||
store := NewStore(StoreConfig{MinSamples: 10})
|
||||
|
||||
// No baselines learned - should return empty
|
||||
metrics := map[string]float64{"cpu": 90, "memory": 85}
|
||||
anomalies := store.CheckResourceAnomalies("unknown-vm", metrics)
|
||||
if len(anomalies) != 0 {
|
||||
t.Errorf("Expected no anomalies for unknown resource, got %d", len(anomalies))
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatRatio(t *testing.T) {
|
||||
testCases := []struct {
|
||||
ratio float64
|
||||
expected string
|
||||
}{
|
||||
{0.005, "near zero"},
|
||||
{0.5, "significantly below"},
|
||||
{0.8, "significantly below"},
|
||||
{1.2, "slightly above"},
|
||||
{1.4, "slightly above"},
|
||||
{1.7, "1.5x"},
|
||||
{2.5, "2x"},
|
||||
{4.0, "3x"},
|
||||
{6.0, "~6x"},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
result := formatRatio(tc.ratio)
|
||||
if result != tc.expected {
|
||||
t.Errorf("formatRatio(%f): expected %q, got %q", tc.ratio, tc.expected, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatAnomalyDescription(t *testing.T) {
|
||||
testCases := []struct {
|
||||
metric string
|
||||
ratio float64
|
||||
direction string
|
||||
severity AnomalySeverity
|
||||
contains string
|
||||
}{
|
||||
{"cpu", 2.0, "above", AnomalyCritical, "Critical anomaly: CPU usage"},
|
||||
{"memory", 1.5, "above", AnomalyHigh, "High anomaly: Memory usage"},
|
||||
{"disk", 1.8, "above", AnomalyMedium, "Moderate anomaly: Disk usage"},
|
||||
{"network_in", 2.0, "below", AnomalyLow, "Minor anomaly: Network inbound"},
|
||||
{"network_out", 1.5, "above", AnomalyNone, "Network outbound"},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
result := formatAnomalyDescription(tc.metric, tc.ratio, tc.direction, tc.severity)
|
||||
if !contains(result, tc.contains) {
|
||||
t.Errorf("formatAnomalyDescription(%s, %f, %s, %s): expected to contain %q, got %q",
|
||||
tc.metric, tc.ratio, tc.direction, tc.severity, tc.contains, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 TestGetAllAnomalies(t *testing.T) {
|
||||
store := NewStore(StoreConfig{MinSamples: 10})
|
||||
|
||||
// Learn baselines for multiple resources
|
||||
points := make([]MetricPoint, 100)
|
||||
for i := 0; i < 100; i++ {
|
||||
points[i] = MetricPoint{Value: 20 + float64(i%3) - 1}
|
||||
}
|
||||
store.Learn("vm-1", "vm", "cpu", points)
|
||||
store.Learn("vm-2", "vm", "cpu", points)
|
||||
|
||||
diskPoints := make([]MetricPoint, 100)
|
||||
for i := 0; i < 100; i++ {
|
||||
diskPoints[i] = MetricPoint{Value: 50 + float64(i%3) - 1}
|
||||
}
|
||||
store.Learn("vm-1", "vm", "disk", diskPoints)
|
||||
|
||||
// Create a metrics provider that returns anomalous values
|
||||
metricsProvider := func(resourceID string) map[string]float64 {
|
||||
switch resourceID {
|
||||
case "vm-1":
|
||||
return map[string]float64{"cpu": 80, "disk": 90} // CPU 4x baseline, disk high
|
||||
case "vm-2":
|
||||
return map[string]float64{"cpu": 25} // Normal
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
}
|
||||
|
||||
anomalies := store.GetAllAnomalies(metricsProvider)
|
||||
|
||||
// Should have anomalies for vm-1 (cpu 4x baseline + disk at 90%)
|
||||
if len(anomalies) < 1 {
|
||||
t.Errorf("Expected at least 1 anomaly, got %d", len(anomalies))
|
||||
}
|
||||
|
||||
// vm-2 should not have anomalies
|
||||
for _, a := range anomalies {
|
||||
if a.ResourceID == "vm-2" {
|
||||
t.Errorf("Did not expect anomaly for vm-2 with normal metrics")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestGetAllAnomalies_EmptyStore(t *testing.T) {
|
||||
store := NewStore(StoreConfig{MinSamples: 10})
|
||||
|
||||
metricsProvider := func(resourceID string) map[string]float64 {
|
||||
return map[string]float64{"cpu": 90}
|
||||
}
|
||||
|
||||
anomalies := store.GetAllAnomalies(metricsProvider)
|
||||
if len(anomalies) != 0 {
|
||||
t.Errorf("Expected no anomalies from empty store, got %d", len(anomalies))
|
||||
}
|
||||
}
|
||||
|
||||
func TestFloatToStr(t *testing.T) {
|
||||
testCases := []struct {
|
||||
value float64
|
||||
precision int
|
||||
expected string
|
||||
}{
|
||||
{1.5, 1, "1.5"},
|
||||
{2.0, 1, "2"},
|
||||
{1.05, 2, "1.05"},
|
||||
{3.0, 2, "3"},
|
||||
{0.5, 1, "0.5"},
|
||||
{0.05, 2, "0.05"},
|
||||
}
|
||||
|
||||
for _, tc := range testCases {
|
||||
result := floatToStr(tc.value, tc.precision)
|
||||
if result != tc.expected {
|
||||
t.Errorf("floatToStr(%f, %d): expected %q, got %q", tc.value, tc.precision, tc.expected, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,10 @@ import (
|
|||
"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"
|
||||
)
|
||||
|
||||
|
|
@ -168,3 +172,288 @@ func TestIntelligence_CheckBaselinesForResource(t *testing.T) {
|
|||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
package memory
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
|
|
@ -9,7 +10,6 @@ import (
|
|||
|
||||
func TestIncidentStore_RecordTimeline(t *testing.T) {
|
||||
store := NewIncidentStore(IncidentStoreConfig{
|
||||
DataDir: t.TempDir(),
|
||||
MaxIncidents: 10,
|
||||
MaxEventsPerIncident: 10,
|
||||
MaxAgeDays: 30,
|
||||
|
|
@ -55,7 +55,6 @@ func TestIncidentStore_RecordTimeline(t *testing.T) {
|
|||
|
||||
func TestIncidentStore_GetTimelineByAlertAt(t *testing.T) {
|
||||
store := NewIncidentStore(IncidentStoreConfig{
|
||||
DataDir: t.TempDir(),
|
||||
MaxIncidents: 10,
|
||||
MaxEventsPerIncident: 10,
|
||||
MaxAgeDays: 30,
|
||||
|
|
@ -106,3 +105,556 @@ func TestIncidentStore_GetTimelineByAlertAt(t *testing.T) {
|
|||
t.Fatalf("expected no timeline for mismatched start time")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIncidentStore_RecordAlertUnacknowledged(t *testing.T) {
|
||||
store := NewIncidentStore(IncidentStoreConfig{
|
||||
MaxIncidents: 10,
|
||||
MaxEventsPerIncident: 10,
|
||||
MaxAgeDays: 30,
|
||||
})
|
||||
|
||||
alert := &alerts.Alert{
|
||||
ID: "alert-unack-1",
|
||||
Type: "memory",
|
||||
Level: alerts.AlertLevelWarning,
|
||||
ResourceID: "res-3",
|
||||
ResourceName: "vm-3",
|
||||
StartTime: time.Now().Add(-10 * time.Minute),
|
||||
}
|
||||
|
||||
// Fire alert and acknowledge it
|
||||
store.RecordAlertFired(alert)
|
||||
store.RecordAlertAcknowledged(alert, "admin")
|
||||
|
||||
timeline := store.GetTimelineByAlertID(alert.ID)
|
||||
if timeline == nil {
|
||||
t.Fatalf("expected timeline after ack")
|
||||
}
|
||||
if !timeline.Acknowledged {
|
||||
t.Fatal("expected acknowledged=true after acknowledgement")
|
||||
}
|
||||
if timeline.AckUser != "admin" {
|
||||
t.Errorf("expected ack user admin, got %q", timeline.AckUser)
|
||||
}
|
||||
|
||||
// Now unacknowledge
|
||||
store.RecordAlertUnacknowledged(alert, "operator")
|
||||
|
||||
timeline = store.GetTimelineByAlertID(alert.ID)
|
||||
if timeline == nil {
|
||||
t.Fatalf("expected timeline after unack")
|
||||
}
|
||||
if timeline.Acknowledged {
|
||||
t.Fatal("expected acknowledged=false after unacknowledgement")
|
||||
}
|
||||
if timeline.AckUser != "" {
|
||||
t.Errorf("expected empty ack user after unack, got %q", timeline.AckUser)
|
||||
}
|
||||
if timeline.AckTime != nil {
|
||||
t.Error("expected nil ack time after unack")
|
||||
}
|
||||
|
||||
// Check for the unacknowledge event
|
||||
foundUnackEvent := false
|
||||
for _, evt := range timeline.Events {
|
||||
if evt.Type == IncidentEventAlertUnacknowledged {
|
||||
foundUnackEvent = true
|
||||
if user, ok := evt.Details["user"].(string); !ok || user != "operator" {
|
||||
t.Errorf("expected user 'operator' in event details, got %v", evt.Details["user"])
|
||||
}
|
||||
}
|
||||
}
|
||||
if !foundUnackEvent {
|
||||
t.Error("expected to find unacknowledge event in timeline")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIncidentStore_RecordAlertUnacknowledged_NilAlert(t *testing.T) {
|
||||
store := NewIncidentStore(IncidentStoreConfig{
|
||||
MaxIncidents: 10,
|
||||
})
|
||||
|
||||
// Should not panic with nil alert
|
||||
store.RecordAlertUnacknowledged(nil, "admin")
|
||||
}
|
||||
|
||||
func TestIncidentStore_RecordRunbook(t *testing.T) {
|
||||
store := NewIncidentStore(IncidentStoreConfig{
|
||||
MaxIncidents: 10,
|
||||
MaxEventsPerIncident: 10,
|
||||
MaxAgeDays: 30,
|
||||
})
|
||||
|
||||
alert := &alerts.Alert{
|
||||
ID: "alert-runbook-1",
|
||||
Type: "disk",
|
||||
Level: alerts.AlertLevelCritical,
|
||||
ResourceID: "res-4",
|
||||
ResourceName: "storage-1",
|
||||
StartTime: time.Now().Add(-5 * time.Minute),
|
||||
}
|
||||
|
||||
store.RecordAlertFired(alert)
|
||||
|
||||
// Record a runbook execution
|
||||
store.RecordRunbook(alert.ID, "runbook-cleanup", "Disk Cleanup", "success", true, "Freed 10GB")
|
||||
|
||||
timeline := store.GetTimelineByAlertID(alert.ID)
|
||||
if timeline == nil {
|
||||
t.Fatal("expected timeline after runbook")
|
||||
}
|
||||
|
||||
// Find the runbook event
|
||||
foundRunbookEvent := false
|
||||
for _, evt := range timeline.Events {
|
||||
if evt.Type == IncidentEventRunbook {
|
||||
foundRunbookEvent = true
|
||||
if !strings.Contains(evt.Summary, "Disk Cleanup") {
|
||||
t.Errorf("expected summary to contain 'Disk Cleanup', got %q", evt.Summary)
|
||||
}
|
||||
if !strings.Contains(evt.Summary, "success") {
|
||||
t.Errorf("expected summary to contain 'success', got %q", evt.Summary)
|
||||
}
|
||||
if runbookID, ok := evt.Details["runbook_id"].(string); !ok || runbookID != "runbook-cleanup" {
|
||||
t.Errorf("expected runbook_id 'runbook-cleanup', got %v", evt.Details["runbook_id"])
|
||||
}
|
||||
if automatic, ok := evt.Details["automatic"].(bool); !ok || !automatic {
|
||||
t.Errorf("expected automatic=true, got %v", evt.Details["automatic"])
|
||||
}
|
||||
if message, ok := evt.Details["message"].(string); !ok || message != "Freed 10GB" {
|
||||
t.Errorf("expected message 'Freed 10GB', got %v", evt.Details["message"])
|
||||
}
|
||||
}
|
||||
}
|
||||
if !foundRunbookEvent {
|
||||
t.Error("expected to find runbook event in timeline")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIncidentStore_RecordRunbook_EmptyParams(t *testing.T) {
|
||||
store := NewIncidentStore(IncidentStoreConfig{
|
||||
MaxIncidents: 10,
|
||||
})
|
||||
|
||||
// Should not create incident with empty alertID
|
||||
store.RecordRunbook("", "runbook-1", "Test", "success", false, "")
|
||||
|
||||
// Should not create incident with empty runbookID
|
||||
store.RecordRunbook("alert-1", "", "Test", "success", false, "")
|
||||
}
|
||||
|
||||
func TestIncidentStore_RecordRunbook_CreatesIncident(t *testing.T) {
|
||||
store := NewIncidentStore(IncidentStoreConfig{
|
||||
MaxIncidents: 10,
|
||||
})
|
||||
|
||||
// RecordRunbook should create incident if none exists
|
||||
store.RecordRunbook("new-alert", "runbook-1", "Test Runbook", "completed", false, "")
|
||||
|
||||
timeline := store.GetTimelineByAlertID("new-alert")
|
||||
if timeline == nil {
|
||||
t.Fatal("expected timeline to be created by RecordRunbook")
|
||||
}
|
||||
if timeline.Status != IncidentStatusOpen {
|
||||
t.Errorf("expected status 'open', got %q", timeline.Status)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIncidentStore_ListIncidentsByResource(t *testing.T) {
|
||||
store := NewIncidentStore(IncidentStoreConfig{
|
||||
MaxIncidents: 20,
|
||||
MaxAgeDays: 30,
|
||||
})
|
||||
|
||||
// Create multiple incidents for the same resource
|
||||
for i := 0; i < 5; i++ {
|
||||
alert := &alerts.Alert{
|
||||
ID: "alert-list-" + string(rune('A'+i)),
|
||||
Type: "cpu",
|
||||
Level: alerts.AlertLevelWarning,
|
||||
ResourceID: "res-list-1",
|
||||
ResourceName: "vm-list-1",
|
||||
StartTime: time.Now().Add(-time.Duration(i) * time.Hour),
|
||||
}
|
||||
store.RecordAlertFired(alert)
|
||||
}
|
||||
|
||||
// Create incident for different resource
|
||||
otherAlert := &alerts.Alert{
|
||||
ID: "alert-other",
|
||||
Type: "memory",
|
||||
Level: alerts.AlertLevelCritical,
|
||||
ResourceID: "res-other",
|
||||
ResourceName: "vm-other",
|
||||
StartTime: time.Now(),
|
||||
}
|
||||
store.RecordAlertFired(otherAlert)
|
||||
|
||||
// List all incidents for res-list-1
|
||||
incidents := store.ListIncidentsByResource("res-list-1", 0)
|
||||
if len(incidents) != 5 {
|
||||
t.Errorf("expected 5 incidents for res-list-1, got %d", len(incidents))
|
||||
}
|
||||
|
||||
// List with limit
|
||||
incidents = store.ListIncidentsByResource("res-list-1", 3)
|
||||
if len(incidents) != 3 {
|
||||
t.Errorf("expected 3 incidents with limit, got %d", len(incidents))
|
||||
}
|
||||
|
||||
// List for non-existent resource
|
||||
incidents = store.ListIncidentsByResource("res-nonexistent", 0)
|
||||
if len(incidents) != 0 {
|
||||
t.Errorf("expected 0 incidents for non-existent resource, got %d", len(incidents))
|
||||
}
|
||||
|
||||
// Empty resource ID
|
||||
incidents = store.ListIncidentsByResource("", 0)
|
||||
if incidents != nil {
|
||||
t.Error("expected nil for empty resource ID")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIncidentStore_FormatForAlert(t *testing.T) {
|
||||
store := NewIncidentStore(IncidentStoreConfig{
|
||||
MaxIncidents: 10,
|
||||
MaxEventsPerIncident: 50,
|
||||
MaxAgeDays: 30,
|
||||
})
|
||||
|
||||
alert := &alerts.Alert{
|
||||
ID: "alert-format-1",
|
||||
Type: "cpu_high",
|
||||
Level: alerts.AlertLevelWarning,
|
||||
ResourceID: "res-format-1",
|
||||
ResourceName: "vm-format-1",
|
||||
StartTime: time.Now().Add(-10 * time.Minute),
|
||||
}
|
||||
|
||||
store.RecordAlertFired(alert)
|
||||
store.RecordAlertAcknowledged(alert, "admin")
|
||||
store.RecordAnalysis(alert.ID, "High CPU due to process X", nil)
|
||||
|
||||
result := store.FormatForAlert(alert.ID, 10)
|
||||
|
||||
if result == "" {
|
||||
t.Fatal("expected non-empty format result")
|
||||
}
|
||||
|
||||
// Check for expected content
|
||||
if !strings.Contains(result, "## Incident Memory") {
|
||||
t.Error("expected '## Incident Memory' header")
|
||||
}
|
||||
if !strings.Contains(result, "vm-format-1") {
|
||||
t.Error("expected resource name in output")
|
||||
}
|
||||
if !strings.Contains(result, "cpu_high") {
|
||||
t.Error("expected alert type in output")
|
||||
}
|
||||
if !strings.Contains(result, "Status: open") {
|
||||
t.Error("expected status in output")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIncidentStore_FormatForAlert_MaxEvents(t *testing.T) {
|
||||
store := NewIncidentStore(IncidentStoreConfig{
|
||||
MaxIncidents: 10,
|
||||
MaxEventsPerIncident: 100,
|
||||
})
|
||||
|
||||
alert := &alerts.Alert{
|
||||
ID: "alert-max-events",
|
||||
Type: "cpu",
|
||||
Level: alerts.AlertLevelWarning,
|
||||
ResourceID: "res-1",
|
||||
ResourceName: "vm-1",
|
||||
}
|
||||
|
||||
store.RecordAlertFired(alert)
|
||||
for i := 0; i < 10; i++ {
|
||||
store.RecordAnalysis(alert.ID, "Analysis "+string(rune('A'+i)), nil)
|
||||
}
|
||||
|
||||
// Request only 3 events
|
||||
result := store.FormatForAlert(alert.ID, 3)
|
||||
if result == "" {
|
||||
t.Fatal("expected non-empty result")
|
||||
}
|
||||
|
||||
// Should only have last 3 events in output (count occurrences of timestamps)
|
||||
lines := strings.Split(result, "\n")
|
||||
eventLines := 0
|
||||
for _, line := range lines {
|
||||
if strings.HasPrefix(strings.TrimSpace(line), "- 20") { // timestamp starts with year
|
||||
eventLines++
|
||||
}
|
||||
}
|
||||
if eventLines > 3 {
|
||||
t.Errorf("expected max 3 event lines, got %d", eventLines)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIncidentStore_FormatForAlert_NoIncident(t *testing.T) {
|
||||
store := NewIncidentStore(IncidentStoreConfig{
|
||||
MaxIncidents: 10,
|
||||
})
|
||||
|
||||
result := store.FormatForAlert("nonexistent-alert", 10)
|
||||
if result != "" {
|
||||
t.Errorf("expected empty string for non-existent alert, got %q", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIncidentStore_FormatForResource(t *testing.T) {
|
||||
store := NewIncidentStore(IncidentStoreConfig{
|
||||
MaxIncidents: 20,
|
||||
MaxAgeDays: 30,
|
||||
})
|
||||
|
||||
// Create multiple incidents for the same resource
|
||||
for i := 0; i < 3; i++ {
|
||||
alert := &alerts.Alert{
|
||||
ID: "alert-res-" + string(rune('A'+i)),
|
||||
Type: "disk",
|
||||
Level: alerts.AlertLevelWarning,
|
||||
ResourceID: "res-format-res",
|
||||
ResourceName: "storage-format",
|
||||
StartTime: time.Now().Add(-time.Duration(i) * time.Hour),
|
||||
}
|
||||
store.RecordAlertFired(alert)
|
||||
if i == 0 {
|
||||
store.RecordAlertAcknowledged(alert, "admin")
|
||||
}
|
||||
}
|
||||
|
||||
result := store.FormatForResource("res-format-res", 5)
|
||||
|
||||
if result == "" {
|
||||
t.Fatal("expected non-empty format result")
|
||||
}
|
||||
|
||||
if !strings.Contains(result, "## Incident Memory") {
|
||||
t.Error("expected '## Incident Memory' header")
|
||||
}
|
||||
if !strings.Contains(result, "Recent incidents for this resource") {
|
||||
t.Error("expected resource incidents header")
|
||||
}
|
||||
if !strings.Contains(result, "disk") {
|
||||
t.Error("expected alert type in output")
|
||||
}
|
||||
// First incident should show as acknowledged
|
||||
if !strings.Contains(result, "acknowledged") {
|
||||
t.Error("expected 'acknowledged' status for first incident")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIncidentStore_FormatForResource_NoIncidents(t *testing.T) {
|
||||
store := NewIncidentStore(IncidentStoreConfig{
|
||||
MaxIncidents: 10,
|
||||
})
|
||||
|
||||
result := store.FormatForResource("nonexistent-resource", 5)
|
||||
if result != "" {
|
||||
t.Errorf("expected empty string for resource with no incidents, got %q", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIncidentStore_FormatForPatrol(t *testing.T) {
|
||||
store := NewIncidentStore(IncidentStoreConfig{
|
||||
MaxIncidents: 20,
|
||||
MaxAgeDays: 30,
|
||||
})
|
||||
|
||||
// Create incidents for multiple resources
|
||||
resources := []string{"vm-1", "vm-2", "storage-1"}
|
||||
for i, resName := range resources {
|
||||
alert := &alerts.Alert{
|
||||
ID: "alert-patrol-" + string(rune('A'+i)),
|
||||
Type: "cpu",
|
||||
Level: alerts.AlertLevelWarning,
|
||||
ResourceID: "res-patrol-" + string(rune('1'+i)),
|
||||
ResourceName: resName,
|
||||
StartTime: time.Now().Add(-time.Duration(i) * time.Hour),
|
||||
Message: "High usage detected",
|
||||
}
|
||||
store.RecordAlertFired(alert)
|
||||
store.RecordAnalysis(alert.ID, "Analysis for "+resName, nil)
|
||||
}
|
||||
|
||||
result := store.FormatForPatrol(10)
|
||||
|
||||
if result == "" {
|
||||
t.Fatal("expected non-empty format result")
|
||||
}
|
||||
|
||||
if !strings.Contains(result, "## Incident Memory") {
|
||||
t.Error("expected '## Incident Memory' header")
|
||||
}
|
||||
if !strings.Contains(result, "Recent incidents across infrastructure") {
|
||||
t.Error("expected infrastructure-wide header")
|
||||
}
|
||||
// Should contain resource names
|
||||
for _, resName := range resources {
|
||||
if !strings.Contains(result, resName) {
|
||||
t.Errorf("expected resource name %q in output", resName)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIncidentStore_FormatForPatrol_WithLimit(t *testing.T) {
|
||||
store := NewIncidentStore(IncidentStoreConfig{
|
||||
MaxIncidents: 20,
|
||||
})
|
||||
|
||||
// Create 10 incidents
|
||||
for i := 0; i < 10; i++ {
|
||||
alert := &alerts.Alert{
|
||||
ID: "alert-limit-" + string(rune('A'+i)),
|
||||
Type: "memory",
|
||||
Level: alerts.AlertLevelWarning,
|
||||
ResourceID: "res-" + string(rune('A'+i)),
|
||||
ResourceName: "vm-" + string(rune('A'+i)),
|
||||
}
|
||||
store.RecordAlertFired(alert)
|
||||
}
|
||||
|
||||
// Request only 3
|
||||
result := store.FormatForPatrol(3)
|
||||
|
||||
// Count incident lines
|
||||
lines := strings.Split(result, "\n")
|
||||
incidentLines := 0
|
||||
for _, line := range lines {
|
||||
if strings.HasPrefix(strings.TrimSpace(line), "- 20") {
|
||||
incidentLines++
|
||||
}
|
||||
}
|
||||
if incidentLines > 3 {
|
||||
t.Errorf("expected max 3 incident lines, got %d", incidentLines)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIncidentStore_FormatForPatrol_Empty(t *testing.T) {
|
||||
store := NewIncidentStore(IncidentStoreConfig{
|
||||
MaxIncidents: 10,
|
||||
})
|
||||
|
||||
result := store.FormatForPatrol(10)
|
||||
if result != "" {
|
||||
t.Errorf("expected empty string for empty store, got %q", result)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIncidentStore_FormatForPatrol_DefaultLimit(t *testing.T) {
|
||||
store := NewIncidentStore(IncidentStoreConfig{
|
||||
MaxIncidents: 20,
|
||||
})
|
||||
|
||||
// Create 15 incidents
|
||||
for i := 0; i < 15; i++ {
|
||||
alert := &alerts.Alert{
|
||||
ID: "alert-default-" + string(rune('A'+i)),
|
||||
Type: "cpu",
|
||||
Level: alerts.AlertLevelWarning,
|
||||
ResourceID: "res-" + string(rune('A'+i)),
|
||||
ResourceName: "vm-" + string(rune('A'+i)),
|
||||
}
|
||||
store.RecordAlertFired(alert)
|
||||
}
|
||||
|
||||
// Pass 0 limit - should use default of 8
|
||||
result := store.FormatForPatrol(0)
|
||||
|
||||
// Count incident lines - should be at most 8
|
||||
lines := strings.Split(result, "\n")
|
||||
incidentLines := 0
|
||||
for _, line := range lines {
|
||||
if strings.HasPrefix(strings.TrimSpace(line), "- 20") {
|
||||
incidentLines++
|
||||
}
|
||||
}
|
||||
if incidentLines > 8 {
|
||||
t.Errorf("expected max 8 incident lines (default), got %d", incidentLines)
|
||||
}
|
||||
}
|
||||
|
||||
func TestIncidentStore_RecordNote_ByIncidentID(t *testing.T) {
|
||||
store := NewIncidentStore(IncidentStoreConfig{
|
||||
MaxIncidents: 10,
|
||||
})
|
||||
|
||||
alert := &alerts.Alert{
|
||||
ID: "alert-note-id",
|
||||
Type: "cpu",
|
||||
Level: alerts.AlertLevelWarning,
|
||||
ResourceID: "res-note",
|
||||
ResourceName: "vm-note",
|
||||
}
|
||||
store.RecordAlertFired(alert)
|
||||
|
||||
timeline := store.GetTimelineByAlertID(alert.ID)
|
||||
if timeline == nil {
|
||||
t.Fatal("expected timeline")
|
||||
}
|
||||
|
||||
// Add note by incident ID
|
||||
ok := store.RecordNote("", timeline.ID, "Test note by incident ID", "operator")
|
||||
if !ok {
|
||||
t.Fatal("expected note to be saved by incident ID")
|
||||
}
|
||||
|
||||
timeline = store.GetTimelineByAlertID(alert.ID)
|
||||
foundNoteEvent := false
|
||||
for _, evt := range timeline.Events {
|
||||
if evt.Type == IncidentEventNote {
|
||||
foundNoteEvent = true
|
||||
if note, ok := evt.Details["note"].(string); !ok || note != "Test note by incident ID" {
|
||||
t.Errorf("expected note text, got %v", evt.Details["note"])
|
||||
}
|
||||
}
|
||||
}
|
||||
if !foundNoteEvent {
|
||||
t.Error("expected to find note event")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIncidentStore_RecordNote_EmptyNote(t *testing.T) {
|
||||
store := NewIncidentStore(IncidentStoreConfig{
|
||||
MaxIncidents: 10,
|
||||
})
|
||||
|
||||
alert := &alerts.Alert{
|
||||
ID: "alert-empty-note",
|
||||
ResourceID: "res-1",
|
||||
}
|
||||
store.RecordAlertFired(alert)
|
||||
|
||||
// Empty note should return false
|
||||
ok := store.RecordNote(alert.ID, "", "", "admin")
|
||||
if ok {
|
||||
t.Error("expected false for empty note")
|
||||
}
|
||||
|
||||
// Whitespace-only note should return false
|
||||
ok = store.RecordNote(alert.ID, "", " ", "admin")
|
||||
if ok {
|
||||
t.Error("expected false for whitespace-only note")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIncidentStore_RecordNote_NonexistentIncident(t *testing.T) {
|
||||
store := NewIncidentStore(IncidentStoreConfig{
|
||||
MaxIncidents: 10,
|
||||
})
|
||||
|
||||
ok := store.RecordNote("nonexistent-alert", "", "Test note", "admin")
|
||||
if ok {
|
||||
t.Error("expected false for non-existent alert")
|
||||
}
|
||||
|
||||
ok = store.RecordNote("", "nonexistent-incident", "Test note", "admin")
|
||||
if ok {
|
||||
t.Error("expected false for non-existent incident")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue