Pulse/internal/ai/memory/memory_test.go
rcourtman 286edee8aa feat(ai): Add operational memory (Phase 3) - change detection and remediation logging
Phase 3 of Pulse AI differentiation:

Create internal/ai/memory package with:

1. Change Detection (changes.go):
   - Tracks infrastructure changes: creation, deletion, config changes
   - Detects status changes (started, stopped)
   - Detects VM/container migrations between nodes
   - Detects CPU/memory configuration changes
   - Detects backup completions
   - Persists change history to ai_changes.json
   - GetChangesSummary for AI context

2. Remediation Logging (remediation.go):
   - Records actions taken to fix problems
   - Tracks command, output, and outcome
   - Links to AI findings via findingID
   - GetSimilar finds past similar problems
   - GetSuccessfulRemediations for learning
   - Persists to ai_remediations.json

3. Type exports (memory_exports.go):
   - Clean re-exports from ai package

This enables the AI to say things like:
- 'This VM was migrated 2 hours ago'
- 'Memory was increased from 4GB to 8GB yesterday'
- 'Last time this happened, restarting nginx resolved it'

All tests passing.
2025-12-12 13:49:37 +00:00

232 lines
6.6 KiB
Go

package memory
import (
"testing"
"time"
)
func TestChangeDetector_DetectNew(t *testing.T) {
d := NewChangeDetector(ChangeDetectorConfig{MaxChanges: 100})
// First detection - should see new resource
snapshots := []ResourceSnapshot{
{ID: "vm-100", Name: "web-server", Type: "vm", Status: "running", Node: "node1"},
}
changes := d.DetectChanges(snapshots)
if len(changes) != 1 {
t.Errorf("Expected 1 change (creation), got %d", len(changes))
}
if changes[0].ChangeType != ChangeCreated {
t.Errorf("Expected ChangeCreated, got %s", changes[0].ChangeType)
}
}
func TestChangeDetector_DetectStatusChange(t *testing.T) {
d := NewChangeDetector(ChangeDetectorConfig{MaxChanges: 100})
// Initial state
d.DetectChanges([]ResourceSnapshot{
{ID: "vm-100", Name: "web-server", Type: "vm", Status: "running", Node: "node1"},
})
// Status change
changes := d.DetectChanges([]ResourceSnapshot{
{ID: "vm-100", Name: "web-server", Type: "vm", Status: "stopped", Node: "node1"},
})
if len(changes) != 1 {
t.Errorf("Expected 1 status change, got %d", len(changes))
}
if changes[0].ChangeType != ChangeStatus {
t.Errorf("Expected ChangeStatus, got %s", changes[0].ChangeType)
}
if changes[0].Before != "running" || changes[0].After != "stopped" {
t.Errorf("Expected running->stopped, got %v->%v", changes[0].Before, changes[0].After)
}
}
func TestChangeDetector_DetectMigration(t *testing.T) {
d := NewChangeDetector(ChangeDetectorConfig{MaxChanges: 100})
// Initial state
d.DetectChanges([]ResourceSnapshot{
{ID: "vm-100", Name: "web-server", Type: "vm", Status: "running", Node: "node1"},
})
// Migration
changes := d.DetectChanges([]ResourceSnapshot{
{ID: "vm-100", Name: "web-server", Type: "vm", Status: "running", Node: "node2"},
})
if len(changes) != 1 {
t.Errorf("Expected 1 migration change, got %d", len(changes))
}
if changes[0].ChangeType != ChangeMigrated {
t.Errorf("Expected ChangeMigrated, got %s", changes[0].ChangeType)
}
}
func TestChangeDetector_DetectDeleted(t *testing.T) {
d := NewChangeDetector(ChangeDetectorConfig{MaxChanges: 100})
// Initial state
d.DetectChanges([]ResourceSnapshot{
{ID: "vm-100", Name: "web-server", Type: "vm", Status: "running", Node: "node1"},
})
// Delete (empty snapshot)
changes := d.DetectChanges([]ResourceSnapshot{})
if len(changes) != 1 {
t.Errorf("Expected 1 deletion change, got %d", len(changes))
}
if changes[0].ChangeType != ChangeDeleted {
t.Errorf("Expected ChangeDeleted, got %s", changes[0].ChangeType)
}
}
func TestChangeDetector_NoChanges(t *testing.T) {
d := NewChangeDetector(ChangeDetectorConfig{MaxChanges: 100})
snapshot := []ResourceSnapshot{
{ID: "vm-100", Name: "web-server", Type: "vm", Status: "running", Node: "node1"},
}
// First time - creates
d.DetectChanges(snapshot)
// Second time - no changes
changes := d.DetectChanges(snapshot)
if len(changes) != 0 {
t.Errorf("Expected 0 changes, got %d", len(changes))
}
}
func TestChangeDetector_GetChangesForResource(t *testing.T) {
d := NewChangeDetector(ChangeDetectorConfig{MaxChanges: 100})
// Create and change a few times
d.DetectChanges([]ResourceSnapshot{
{ID: "vm-100", Name: "web", Type: "vm", Status: "stopped", Node: "node1"},
{ID: "vm-200", Name: "db", Type: "vm", Status: "running", Node: "node1"},
})
d.DetectChanges([]ResourceSnapshot{
{ID: "vm-100", Name: "web", Type: "vm", Status: "running", Node: "node1"},
{ID: "vm-200", Name: "db", Type: "vm", Status: "running", Node: "node1"},
})
// Get changes for vm-100 only
changes := d.GetChangesForResource("vm-100", 10)
for _, c := range changes {
if c.ResourceID != "vm-100" {
t.Errorf("Got change for wrong resource: %s", c.ResourceID)
}
}
}
func TestRemediationLog_LogAndRetrieve(t *testing.T) {
r := NewRemediationLog(RemediationLogConfig{MaxRecords: 100})
r.Log(RemediationRecord{
ResourceID: "vm-100",
Problem: "High memory usage",
Action: "systemctl restart nginx",
Outcome: OutcomeResolved,
})
records := r.GetForResource("vm-100", 10)
if len(records) != 1 {
t.Errorf("Expected 1 record, got %d", len(records))
}
if records[0].Action != "systemctl restart nginx" {
t.Errorf("Wrong action: %s", records[0].Action)
}
}
func TestRemediationLog_GetSimilar(t *testing.T) {
r := NewRemediationLog(RemediationLogConfig{MaxRecords: 100})
// Log some remediations
r.Log(RemediationRecord{
ResourceID: "vm-100",
Problem: "High memory usage causing OOM",
Action: "Restart service",
Outcome: OutcomeResolved,
})
r.Log(RemediationRecord{
ResourceID: "vm-200",
Problem: "Memory leak detected",
Action: "Cleared cache",
Outcome: OutcomePartial,
})
r.Log(RemediationRecord{
ResourceID: "vm-300",
Problem: "CPU spike from backup",
Action: "Rescheduled backup",
Outcome: OutcomeResolved,
})
// Search for similar memory issues
similar := r.GetSimilar("High memory usage causing slowdown", 5)
if len(similar) < 1 {
t.Errorf("Expected at least 1 similar record")
}
}
func TestRemediationLog_GetSuccessfulRemediations(t *testing.T) {
r := NewRemediationLog(RemediationLogConfig{MaxRecords: 100})
r.Log(RemediationRecord{
Problem: "Memory usage high",
Action: "Restart service",
Outcome: OutcomeResolved,
})
r.Log(RemediationRecord{
Problem: "Memory usage high",
Action: "Kill process",
Outcome: OutcomeFailed,
})
successful := r.GetSuccessfulRemediations("Memory usage issue", 5)
for _, rec := range successful {
if rec.Outcome != OutcomeResolved && rec.Outcome != OutcomePartial {
t.Errorf("Got unsuccessful remediation in successful list: %s", rec.Outcome)
}
}
}
func TestRemediationLog_Stats(t *testing.T) {
r := NewRemediationLog(RemediationLogConfig{MaxRecords: 100})
r.Log(RemediationRecord{Problem: "p1", Action: "a1", Outcome: OutcomeResolved})
r.Log(RemediationRecord{Problem: "p2", Action: "a2", Outcome: OutcomeResolved})
r.Log(RemediationRecord{Problem: "p3", Action: "a3", Outcome: OutcomeFailed})
stats := r.GetRemediationStats()
if stats["total"] != 3 {
t.Errorf("Expected 3 total, got %d", stats["total"])
}
if stats["resolved"] != 2 {
t.Errorf("Expected 2 resolved, got %d", stats["resolved"])
}
if stats["failed"] != 1 {
t.Errorf("Expected 1 failed, got %d", stats["failed"])
}
}
func TestChangeDetector_GetRecentChanges(t *testing.T) {
d := NewChangeDetector(ChangeDetectorConfig{MaxChanges: 100})
// Create some changes
d.DetectChanges([]ResourceSnapshot{
{ID: "vm-100", Name: "web", Type: "vm", Status: "running", Node: "node1"},
})
// Get recent changes
since := time.Now().Add(-1 * time.Hour)
changes := d.GetRecentChanges(10, since)
if len(changes) == 0 {
t.Error("Expected at least 1 recent change")
}
}