- 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%)
660 lines
18 KiB
Go
660 lines
18 KiB
Go
package memory
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/alerts"
|
|
)
|
|
|
|
func TestIncidentStore_RecordTimeline(t *testing.T) {
|
|
store := NewIncidentStore(IncidentStoreConfig{
|
|
MaxIncidents: 10,
|
|
MaxEventsPerIncident: 10,
|
|
MaxAgeDays: 30,
|
|
})
|
|
|
|
alert := &alerts.Alert{
|
|
ID: "alert-1",
|
|
Type: "cpu",
|
|
Level: alerts.AlertLevelWarning,
|
|
ResourceID: "res-1",
|
|
ResourceName: "vm-1",
|
|
StartTime: time.Now().Add(-5 * time.Minute),
|
|
Value: 92,
|
|
Threshold: 85,
|
|
}
|
|
|
|
store.RecordAlertFired(alert)
|
|
store.RecordAlertAcknowledged(alert, "admin")
|
|
store.RecordAnalysis(alert.ID, "analysis complete", map[string]interface{}{
|
|
"findings": 1,
|
|
})
|
|
store.RecordCommand(alert.ID, "systemctl restart nginx", true, "ok", nil)
|
|
store.RecordAlertResolved(alert, time.Now())
|
|
|
|
timeline := store.GetTimelineByAlertID(alert.ID)
|
|
if timeline == nil {
|
|
t.Fatalf("expected timeline, got nil")
|
|
}
|
|
if timeline.Status != IncidentStatusResolved {
|
|
t.Fatalf("expected status %q, got %q", IncidentStatusResolved, timeline.Status)
|
|
}
|
|
if timeline.AckUser != "admin" {
|
|
t.Fatalf("expected ack user admin, got %q", timeline.AckUser)
|
|
}
|
|
if len(timeline.Events) < 4 {
|
|
t.Fatalf("expected events recorded, got %d", len(timeline.Events))
|
|
}
|
|
|
|
if ok := store.RecordNote(alert.ID, "", "note text", ""); !ok {
|
|
t.Fatalf("expected note to be saved")
|
|
}
|
|
}
|
|
|
|
func TestIncidentStore_GetTimelineByAlertAt(t *testing.T) {
|
|
store := NewIncidentStore(IncidentStoreConfig{
|
|
MaxIncidents: 10,
|
|
MaxEventsPerIncident: 10,
|
|
MaxAgeDays: 30,
|
|
})
|
|
|
|
base := time.Now().UTC()
|
|
|
|
first := &alerts.Alert{
|
|
ID: "alert-2",
|
|
Type: "cpu",
|
|
Level: alerts.AlertLevelWarning,
|
|
ResourceID: "res-2",
|
|
ResourceName: "vm-2",
|
|
StartTime: base.Add(-2 * time.Hour),
|
|
}
|
|
|
|
second := &alerts.Alert{
|
|
ID: "alert-2",
|
|
Type: "cpu",
|
|
Level: alerts.AlertLevelWarning,
|
|
ResourceID: "res-2",
|
|
ResourceName: "vm-2",
|
|
StartTime: base.Add(-10 * time.Minute),
|
|
}
|
|
|
|
store.RecordAlertFired(first)
|
|
store.RecordAlertResolved(first, base.Add(-90*time.Minute))
|
|
store.RecordAlertFired(second)
|
|
|
|
timeline := store.GetTimelineByAlertAt(first.ID, first.StartTime)
|
|
if timeline == nil {
|
|
t.Fatalf("expected timeline for first incident, got nil")
|
|
}
|
|
if !timeline.OpenedAt.Equal(first.StartTime) {
|
|
t.Fatalf("expected openedAt %s, got %s", first.StartTime, timeline.OpenedAt)
|
|
}
|
|
|
|
timeline = store.GetTimelineByAlertAt(second.ID, second.StartTime)
|
|
if timeline == nil {
|
|
t.Fatalf("expected timeline for second incident, got nil")
|
|
}
|
|
if !timeline.OpenedAt.Equal(second.StartTime) {
|
|
t.Fatalf("expected openedAt %s, got %s", second.StartTime, timeline.OpenedAt)
|
|
}
|
|
|
|
timeline = store.GetTimelineByAlertAt(second.ID, base.Add(-45*time.Minute))
|
|
if timeline != nil {
|
|
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")
|
|
}
|
|
}
|
|
|