Pulse/internal/ai/memory/memory_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

335 lines
9.3 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")
}
}
func TestRemediationLog_GetRecentRemediationStats(t *testing.T) {
r := NewRemediationLog(RemediationLogConfig{MaxRecords: 100})
// Log some remediations with different outcomes
now := time.Now()
r.Log(RemediationRecord{
Timestamp: now.Add(-1 * time.Hour),
Problem: "p1",
Action: "a1",
Outcome: OutcomeResolved,
Automatic: true,
})
r.Log(RemediationRecord{
Timestamp: now.Add(-2 * time.Hour),
Problem: "p2",
Action: "a2",
Outcome: OutcomePartial,
Automatic: false,
})
r.Log(RemediationRecord{
Timestamp: now.Add(-30 * time.Minute),
Problem: "p3",
Action: "a3",
Outcome: OutcomeFailed,
Automatic: true,
})
r.Log(RemediationRecord{
Timestamp: now.Add(-48 * time.Hour),
Problem: "old",
Action: "old",
Outcome: OutcomeResolved,
Automatic: false,
})
// Get stats for last 24 hours
since := now.Add(-24 * time.Hour)
stats := r.GetRecentRemediationStats(since)
if stats["total"] != 3 {
t.Errorf("Expected 3 total (last 24h), got %d", stats["total"])
}
if stats["resolved"] != 1 {
t.Errorf("Expected 1 resolved, got %d", stats["resolved"])
}
if stats["partial"] != 1 {
t.Errorf("Expected 1 partial, got %d", stats["partial"])
}
if stats["failed"] != 1 {
t.Errorf("Expected 1 failed, got %d", stats["failed"])
}
if stats["automatic"] != 2 {
t.Errorf("Expected 2 automatic, got %d", stats["automatic"])
}
if stats["manual"] != 1 {
t.Errorf("Expected 1 manual, got %d", stats["manual"])
}
}
func TestRemediationLog_AutomaticVsManual(t *testing.T) {
r := NewRemediationLog(RemediationLogConfig{MaxRecords: 100})
r.Log(RemediationRecord{
Problem: "auto problem",
Action: "auto action",
Outcome: OutcomeResolved,
Automatic: true,
})
r.Log(RemediationRecord{
Problem: "manual problem",
Action: "manual action",
Outcome: OutcomeResolved,
Automatic: false,
})
stats := r.GetRemediationStats()
// Verify both are counted
if stats["total"] != 2 {
t.Errorf("Expected 2 total, got %d", stats["total"])
}
}
func TestChangeDetector_Limit(t *testing.T) {
d := NewChangeDetector(ChangeDetectorConfig{MaxChanges: 5})
// Create many changes to exceed limit
for i := 0; i < 10; i++ {
d.DetectChanges([]ResourceSnapshot{
{ID: "vm-100", Name: "web", Type: "vm", Status: "running", Node: "node1"},
})
// Alternate status to create changes
d.DetectChanges([]ResourceSnapshot{
{ID: "vm-100", Name: "web", Type: "vm", Status: "stopped", Node: "node1"},
})
}
// Should have limited records
allChanges := d.GetRecentChanges(100, time.Time{})
if len(allChanges) > 5 {
t.Errorf("Expected max 5 changes due to limit, got %d", len(allChanges))
}
}