- Add integration tests for Ollama provider (17 tests against real API) - Add unit tests for baseline, correlation, patterns, memory, knowledge, cost packages - Add context formatter and builder tests - Add factory tests for provider initialization - Add Makefile targets: test-integration, test-all - Clean up test theatre (removed struct field tests) Integration tests require Ollama at OLLAMA_URL (default: 192.168.0.124:11434) Run with: make test-integration
380 lines
8.9 KiB
Go
380 lines
8.9 KiB
Go
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
|
|
}
|