package context import ( "testing" "time" "github.com/rcourtman/pulse-go-rewrite/internal/models" ) func TestNewBuilder(t *testing.T) { builder := NewBuilder() if builder == nil { t.Fatal("Expected non-nil builder") } } func TestBuilder_WithMethods(t *testing.T) { builder := NewBuilder() // Test chaining methods return the builder result := builder. WithMetricsHistory(nil). WithKnowledge(nil). WithFindings(nil). WithBaseline(nil) if result != builder { t.Error("Expected method chaining to return same builder instance") } } func TestBuilder_BuildForInfrastructure_Empty(t *testing.T) { builder := NewBuilder() // Empty state state := models.StateSnapshot{} ctx := builder.BuildForInfrastructure(state) if ctx == nil { t.Fatal("Expected non-nil context") } // Empty state should have zero totals if ctx.TotalResources != 0 { t.Errorf("Expected 0 total resources for empty state, got %d", ctx.TotalResources) } // GeneratedAt should be set if ctx.GeneratedAt.IsZero() { t.Error("Expected GeneratedAt to be set") } } func TestBuilder_BuildForInfrastructure_WithNodes(t *testing.T) { builder := NewBuilder() state := models.StateSnapshot{ Nodes: []models.Node{ { ID: "node-1", Name: "pve-primary", Status: "online", CPU: 0.45, }, { ID: "node-2", Name: "pve-secondary", Status: "online", CPU: 0.25, }, }, } ctx := builder.BuildForInfrastructure(state) if len(ctx.Nodes) != 2 { t.Errorf("Expected 2 nodes, got %d", len(ctx.Nodes)) } } func TestBuilder_BuildForInfrastructure_WithGuests(t *testing.T) { builder := NewBuilder() state := models.StateSnapshot{ Nodes: []models.Node{ {ID: "node-1", Name: "pve-primary", Status: "online"}, }, VMs: []models.VM{ {ID: "vm-100", Name: "web-server", Node: "pve-primary", Status: "running", CPU: 0.30}, {ID: "vm-101", Name: "database", Node: "pve-primary", Status: "running", CPU: 0.50}, }, Containers: []models.Container{ {ID: "ct-200", Name: "nginx-proxy", Node: "pve-primary", Status: "running", CPU: 0.10}, }, } ctx := builder.BuildForInfrastructure(state) if len(ctx.VMs) != 2 { t.Errorf("Expected 2 VMs, got %d", len(ctx.VMs)) } if len(ctx.Containers) != 1 { t.Errorf("Expected 1 container, got %d", len(ctx.Containers)) } } func TestBuilder_BuildForInfrastructure_SkipsTemplates(t *testing.T) { builder := NewBuilder() state := models.StateSnapshot{ VMs: []models.VM{ {ID: "vm-100", Name: "web-server", Status: "running", Template: false}, {ID: "vm-101", Name: "template", Status: "stopped", Template: true}, }, Containers: []models.Container{ {ID: "ct-200", Name: "nginx", Status: "running", Template: false}, {ID: "ct-201", Name: "template", Status: "stopped", Template: true}, }, } ctx := builder.BuildForInfrastructure(state) // Should skip templates if len(ctx.VMs) != 1 { t.Errorf("Expected 1 VM (template skipped), got %d", len(ctx.VMs)) } if len(ctx.Containers) != 1 { t.Errorf("Expected 1 container (template skipped), got %d", len(ctx.Containers)) } } func TestBuilder_BuildForInfrastructure_WithStorage(t *testing.T) { builder := NewBuilder() state := models.StateSnapshot{ Storage: []models.Storage{ { ID: "storage-1", Name: "local-zfs", Type: "zfspool", Status: "available", }, { ID: "storage-2", Name: "nfs-share", Type: "nfs", Status: "available", }, }, } ctx := builder.BuildForInfrastructure(state) if len(ctx.Storage) != 2 { t.Errorf("Expected 2 storage, got %d", len(ctx.Storage)) } } func TestBuilder_BuildForInfrastructure_WithDocker(t *testing.T) { builder := NewBuilder() state := models.StateSnapshot{ DockerHosts: []models.DockerHost{ { ID: "docker-1", Hostname: "docker-host-1", Containers: []models.DockerContainer{ {ID: "container-1", Name: "nginx", State: "running"}, {ID: "container-2", Name: "redis", State: "running"}, }, }, }, } ctx := builder.BuildForInfrastructure(state) if len(ctx.DockerHosts) != 1 { t.Errorf("Expected 1 docker host, got %d", len(ctx.DockerHosts)) } } func TestBuilder_BuildForInfrastructure_WithHosts(t *testing.T) { builder := NewBuilder() state := models.StateSnapshot{ Hosts: []models.Host{ { ID: "host-1", Hostname: "server-1", Status: "online", CPUCount: 8, }, }, } ctx := builder.BuildForInfrastructure(state) if len(ctx.Hosts) != 1 { t.Errorf("Expected 1 host, got %d", len(ctx.Hosts)) } } func TestBuilder_BuildForInfrastructure_TotalResources(t *testing.T) { builder := NewBuilder() state := models.StateSnapshot{ Nodes: []models.Node{ {ID: "node-1", Name: "pve-1", Status: "online"}, }, VMs: []models.VM{ {ID: "vm-100", Name: "web", Status: "running"}, {ID: "vm-101", Name: "db", Status: "running"}, }, Containers: []models.Container{ {ID: "ct-200", Name: "nginx", Status: "running"}, }, Storage: []models.Storage{ {ID: "local", Name: "local", Status: "available"}, }, } ctx := builder.BuildForInfrastructure(state) // 1 node + 2 VMs + 1 container + 1 storage = 5 expected := 5 if ctx.TotalResources != expected { t.Errorf("Expected %d total resources, got %d", expected, ctx.TotalResources) } } func TestBuilder_BuildForInfrastructure_OCI(t *testing.T) { builder := NewBuilder() state := models.StateSnapshot{ Containers: []models.Container{ { ID: "ct-200", Name: "nginx-oci", Status: "running", IsOCI: true, OSTemplate: "docker.io/library/nginx:latest", }, }, } ctx := builder.BuildForInfrastructure(state) if len(ctx.Containers) != 1 { t.Fatalf("Expected 1 container, got %d", len(ctx.Containers)) } // OCI container should have type oci_container if ctx.Containers[0].ResourceType != "oci_container" { t.Errorf("Expected resource type 'oci_container', got '%s'", ctx.Containers[0].ResourceType) } // Should have metadata with image if ctx.Containers[0].Metadata == nil { t.Error("Expected metadata to be set for OCI container") } else if ctx.Containers[0].Metadata["oci_image"] != "docker.io/library/nginx:latest" { t.Errorf("Expected oci_image metadata, got %v", ctx.Containers[0].Metadata) } } func TestBuilder_MergeContexts(t *testing.T) { builder := NewBuilder() target := &ResourceContext{ ResourceID: "vm-100", ResourceType: "vm", ResourceName: "web-server", Status: "running", Node: "pve-1", } infra := &InfrastructureContext{ TotalResources: 10, GeneratedAt: time.Now(), VMs: []ResourceContext{ {ResourceID: "vm-100", ResourceName: "web-server", Node: "pve-1"}, {ResourceID: "vm-101", ResourceName: "database", Node: "pve-1"}, }, } result := builder.MergeContexts(target, infra) if result == "" { t.Error("Expected non-empty merged context") } // Should contain target resource section if !containsSubstring(result, "Target Resource") { t.Error("Expected merged context to contain 'Target Resource' section") } } func TestBuilder_MergeContexts_IncludesRelated(t *testing.T) { builder := NewBuilder() target := &ResourceContext{ ResourceID: "vm-100", ResourceType: "vm", ResourceName: "web-server", Node: "pve-1", } infra := &InfrastructureContext{ VMs: []ResourceContext{ {ResourceID: "vm-100", ResourceName: "web-server", Node: "pve-1"}, {ResourceID: "vm-101", ResourceName: "database", Node: "pve-1"}, {ResourceID: "vm-102", ResourceName: "other", Node: "pve-2"}, // Different node }, } result := builder.MergeContexts(target, infra) // Should include related resources section if !containsSubstring(result, "Related Resources") { t.Error("Expected merged context to contain 'Related Resources' section when target has a node") } } func TestResourceContext_Fields(t *testing.T) { ctx := ResourceContext{ ResourceID: "vm-100", ResourceType: "vm", ResourceName: "test-vm", Node: "node-1", CurrentCPU: 0.45, CurrentMemory: 0.65, CurrentDisk: 0.30, Status: "running", } if ctx.ResourceID != "vm-100" { t.Errorf("Expected ResourceID 'vm-100', got '%s'", ctx.ResourceID) } if ctx.CurrentCPU != 0.45 { t.Errorf("Expected CurrentCPU 0.45, got %f", ctx.CurrentCPU) } if ctx.Node != "node-1" { t.Errorf("Expected Node 'node-1', got '%s'", ctx.Node) } } func TestInfrastructureContext_Fields(t *testing.T) { now := time.Now() ctx := InfrastructureContext{ GeneratedAt: now, TotalResources: 25, ResourcesWithData: 20, } if ctx.TotalResources != 25 { t.Errorf("Expected 25 total resources, got %d", ctx.TotalResources) } if ctx.GeneratedAt != now { t.Error("Expected GeneratedAt to match") } if ctx.ResourcesWithData != 20 { t.Errorf("Expected 20 resources with data, got %d", ctx.ResourcesWithData) } } // Helper function func containsSubstring(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 TestFilterRecentPoints_Empty(t *testing.T) { points := []MetricPoint{} result := filterRecentPoints(points, time.Hour) if len(result) != 0 { t.Errorf("Expected empty result, got %d points", len(result)) } } func TestFilterRecentPoints_AllRecent(t *testing.T) { now := time.Now() points := []MetricPoint{ {Timestamp: now.Add(-30 * time.Minute), Value: 1.0}, {Timestamp: now.Add(-15 * time.Minute), Value: 2.0}, {Timestamp: now.Add(-5 * time.Minute), Value: 3.0}, } result := filterRecentPoints(points, time.Hour) if len(result) != 3 { t.Errorf("Expected 3 points, got %d", len(result)) } } func TestFilterRecentPoints_FilterOld(t *testing.T) { now := time.Now() points := []MetricPoint{ {Timestamp: now.Add(-3 * time.Hour), Value: 1.0}, // Old {Timestamp: now.Add(-2 * time.Hour), Value: 2.0}, // Old {Timestamp: now.Add(-30 * time.Minute), Value: 3.0}, // Recent } result := filterRecentPoints(points, time.Hour) if len(result) != 1 { t.Errorf("Expected 1 recent point, got %d", len(result)) } if result[0].Value != 3.0 { t.Errorf("Expected value 3.0, got %f", result[0].Value) } } func TestFormatAnomalyDescription(t *testing.T) { tests := []struct { name string metric string current float64 mean float64 stddev float64 severity string direction string wantContains []string }{ { name: "cpu high", metric: "cpu", current: 95.0, mean: 50.0, stddev: 10.0, severity: "significantly", direction: "above", wantContains: []string{"Cpu", "significantly", "above", "95%", "50%"}, }, { name: "memory low", metric: "memory", current: 20.0, mean: 60.0, stddev: 15.0, severity: "slightly", direction: "below", wantContains: []string{"Memory", "slightly", "below", "20%", "60%"}, }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { result := formatAnomalyDescription(tt.metric, tt.current, tt.mean, tt.stddev, tt.severity, tt.direction) for _, want := range tt.wantContains { if !containsSubstring(result, want) { t.Errorf("formatAnomalyDescription() = %q, want to contain %q", result, want) } } }) } } func TestFormatPredictionBasis(t *testing.T) { trend := Trend{ RatePerDay: 2.5, Period: 7 * 24 * time.Hour, // 7 days } result := formatPredictionBasis(trend) if !containsSubstring(result, "Growing") { t.Errorf("Expected 'Growing' in result, got %q", result) } if !containsSubstring(result, "based on") { t.Errorf("Expected 'based on' in result, got %q", result) } }