mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-04-28 11:30:15 +00:00
The DisableDockerUpdateActions setting was being saved to disk but not updated in h.config, causing the UI toggle to appear to revert on page refresh since the API returned the stale runtime value. Related to #1023
911 lines
22 KiB
Go
911 lines
22 KiB
Go
package context
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/rcourtman/pulse-go-rewrite/internal/models"
|
|
)
|
|
|
|
func TestFormatResourceContext_Basic(t *testing.T) {
|
|
ctx := ResourceContext{
|
|
ResourceID: "vm-100",
|
|
ResourceType: "vm",
|
|
ResourceName: "web-server",
|
|
Node: "pve-1",
|
|
Status: "running",
|
|
CurrentCPU: 45.5,
|
|
CurrentMemory: 65.2,
|
|
CurrentDisk: 30.0,
|
|
Uptime: 24 * time.Hour,
|
|
}
|
|
|
|
result := FormatResourceContext(ctx)
|
|
|
|
if result == "" {
|
|
t.Error("Expected non-empty result")
|
|
}
|
|
|
|
// Should contain resource name
|
|
if !containsStr(result, "web-server") {
|
|
t.Error("Expected result to contain resource name")
|
|
}
|
|
|
|
// Should contain node
|
|
if !containsStr(result, "pve-1") {
|
|
t.Error("Expected result to contain node name")
|
|
}
|
|
|
|
// Should contain status
|
|
if !containsStr(result, "running") {
|
|
t.Error("Expected result to contain status")
|
|
}
|
|
|
|
// Should contain metrics
|
|
if !containsStr(result, "CPU:") {
|
|
t.Error("Expected result to contain CPU metric")
|
|
}
|
|
|
|
// Should contain uptime
|
|
if !containsStr(result, "Uptime") {
|
|
t.Error("Expected result to contain uptime")
|
|
}
|
|
}
|
|
|
|
func TestFormatResourceContext_WithAnomalies(t *testing.T) {
|
|
ctx := ResourceContext{
|
|
ResourceID: "vm-100",
|
|
ResourceType: "vm",
|
|
ResourceName: "web-server",
|
|
Status: "running",
|
|
Anomalies: []Anomaly{
|
|
{
|
|
Metric: "cpu",
|
|
Description: "CPU is critically above normal",
|
|
},
|
|
},
|
|
}
|
|
|
|
result := FormatResourceContext(ctx)
|
|
|
|
if !containsStr(result, "ANOMALIES") {
|
|
t.Error("Expected result to contain ANOMALIES section")
|
|
}
|
|
|
|
if !containsStr(result, "critically above normal") {
|
|
t.Error("Expected result to contain anomaly description")
|
|
}
|
|
}
|
|
|
|
func TestFormatResourceContext_WithPredictions(t *testing.T) {
|
|
ctx := ResourceContext{
|
|
ResourceID: "storage-1",
|
|
ResourceType: "storage",
|
|
ResourceName: "local-zfs",
|
|
Status: "available",
|
|
Predictions: []Prediction{
|
|
{
|
|
Event: "storage_full",
|
|
DaysUntil: 14.5,
|
|
},
|
|
},
|
|
}
|
|
|
|
result := FormatResourceContext(ctx)
|
|
|
|
if !containsStr(result, "Predictions") {
|
|
t.Error("Expected result to contain Predictions section")
|
|
}
|
|
|
|
if !containsStr(result, "storage_full") {
|
|
t.Error("Expected result to contain prediction event")
|
|
}
|
|
}
|
|
|
|
func TestFormatResourceContext_WithUserNotes(t *testing.T) {
|
|
ctx := ResourceContext{
|
|
ResourceID: "vm-100",
|
|
ResourceType: "vm",
|
|
ResourceName: "web-server",
|
|
Status: "running",
|
|
UserNotes: []string{"Web frontend server", "Managed by team-alpha"},
|
|
}
|
|
|
|
result := FormatResourceContext(ctx)
|
|
|
|
if !containsStr(result, "User Notes") {
|
|
t.Error("Expected result to contain User Notes section")
|
|
}
|
|
|
|
if !containsStr(result, "Web frontend server") {
|
|
t.Error("Expected result to contain user note content")
|
|
}
|
|
}
|
|
|
|
func TestFormatResourceContext_WithHistory(t *testing.T) {
|
|
ctx := ResourceContext{
|
|
ResourceID: "vm-100",
|
|
ResourceType: "vm",
|
|
ResourceName: "web-server",
|
|
Status: "running",
|
|
LastRemediation: "Restarted service 2 days ago",
|
|
PastIssues: []string{"High CPU on 2024-01-01", "OOM on 2024-01-15"},
|
|
}
|
|
|
|
result := FormatResourceContext(ctx)
|
|
|
|
if !containsStr(result, "History") {
|
|
t.Error("Expected result to contain History section")
|
|
}
|
|
|
|
if !containsStr(result, "Restarted service") {
|
|
t.Error("Expected result to contain remediation info")
|
|
}
|
|
}
|
|
|
|
func TestFormatResourceContext_NodeWithoutNode(t *testing.T) {
|
|
ctx := ResourceContext{
|
|
ResourceID: "node/pve-1",
|
|
ResourceType: "node",
|
|
ResourceName: "pve-1",
|
|
Status: "online",
|
|
}
|
|
|
|
result := FormatResourceContext(ctx)
|
|
|
|
// Node type should NOT show "(on node)" since it IS the node
|
|
if containsStr(result, "(on ") {
|
|
t.Error("Node resource should not show itself as parent node")
|
|
}
|
|
}
|
|
|
|
func TestFormatResourceType(t *testing.T) {
|
|
tests := []struct {
|
|
input string
|
|
expected string
|
|
}{
|
|
{"node", "Node"},
|
|
{"vm", "VM"},
|
|
{"container", "Container"},
|
|
{"oci_container", "OCI Container"},
|
|
{"storage", "Storage"},
|
|
{"docker_host", "Docker Host"},
|
|
{"docker_container", "Docker Container"},
|
|
{"host", "Host"},
|
|
{"unknown", "Unknown"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
result := formatResourceType(tt.input)
|
|
if result != tt.expected {
|
|
t.Errorf("formatResourceType(%q) = %q, want %q", tt.input, result, tt.expected)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestFormatDuration(t *testing.T) {
|
|
tests := []struct {
|
|
input time.Duration
|
|
expected string
|
|
}{
|
|
{30 * time.Second, "30s"},
|
|
{5 * time.Minute, "5m"},
|
|
{2 * time.Hour, "2h"},
|
|
{2*time.Hour + 30*time.Minute, "2h30m"},
|
|
{24 * time.Hour, "1d"},
|
|
{25 * time.Hour, "1d1h"},
|
|
{48 * time.Hour, "2d"},
|
|
{72*time.Hour + 5*time.Hour, "3d5h"},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
result := formatDuration(tt.input)
|
|
if result != tt.expected {
|
|
t.Errorf("formatDuration(%v) = %q, want %q", tt.input, result, tt.expected)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestFormatRate(t *testing.T) {
|
|
tests := []struct {
|
|
input float64
|
|
expected string
|
|
}{
|
|
{5.5, "5.5/day"},
|
|
{1.0, "1.0/day"},
|
|
{0.5, "0.02/hr"},
|
|
{0.1, "slow"},
|
|
{0.0, "slow"},
|
|
{-2.5, "2.5/day"}, // Absolute value
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
result := formatRate(tt.input)
|
|
if result != tt.expected {
|
|
t.Errorf("formatRate(%f) = %q, want %q", tt.input, result, tt.expected)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestFormatTrendLine(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
metric string
|
|
trend Trend
|
|
expected string
|
|
}{
|
|
{
|
|
name: "insufficient data",
|
|
metric: "cpu",
|
|
trend: Trend{DataPoints: 2},
|
|
expected: "",
|
|
},
|
|
{
|
|
name: "growing trend",
|
|
metric: "cpu",
|
|
trend: Trend{
|
|
Direction: TrendGrowing,
|
|
RatePerDay: 5.0,
|
|
DataPoints: 10,
|
|
},
|
|
expected: "Cpu: (rising 5.0/day)",
|
|
},
|
|
{
|
|
name: "declining trend",
|
|
metric: "memory",
|
|
trend: Trend{
|
|
Direction: TrendDeclining,
|
|
RatePerDay: 2.0,
|
|
DataPoints: 10,
|
|
},
|
|
expected: "Memory: (falling 2.0/day)",
|
|
},
|
|
{
|
|
name: "stable trend",
|
|
metric: "disk",
|
|
trend: Trend{
|
|
Direction: TrendStable,
|
|
DataPoints: 10,
|
|
},
|
|
expected: "Disk: (stable)",
|
|
},
|
|
{
|
|
name: "volatile trend",
|
|
metric: "cpu",
|
|
trend: Trend{
|
|
Direction: TrendVolatile,
|
|
DataPoints: 10,
|
|
},
|
|
expected: "Cpu: (volatile)",
|
|
},
|
|
{
|
|
name: "with significant range",
|
|
metric: "cpu",
|
|
trend: Trend{
|
|
Direction: TrendStable,
|
|
DataPoints: 10,
|
|
Min: 20,
|
|
Max: 80,
|
|
},
|
|
expected: "Cpu: (stable) (20-80%)",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := formatTrendLine(tt.metric, tt.trend)
|
|
if result != tt.expected {
|
|
t.Errorf("formatTrendLine(%q, %+v) = %q, want %q", tt.metric, tt.trend, result, tt.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestFormatBackupStatus(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
input time.Time
|
|
contains string
|
|
}{
|
|
{
|
|
name: "never backed up",
|
|
input: time.Time{},
|
|
contains: "never",
|
|
},
|
|
{
|
|
name: "recent backup",
|
|
input: time.Now().Add(-2 * time.Hour),
|
|
contains: "h ago",
|
|
},
|
|
{
|
|
name: "old backup",
|
|
input: time.Now().Add(-72 * time.Hour),
|
|
contains: "d ago",
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := FormatBackupStatus(tt.input)
|
|
if !containsStr(result, tt.contains) {
|
|
t.Errorf("FormatBackupStatus() = %q, want to contain %q", result, tt.contains)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestFormatNodeForContext(t *testing.T) {
|
|
node := models.Node{
|
|
ID: "node/pve-1",
|
|
Name: "pve-1",
|
|
Status: "online",
|
|
CPU: 0.45, // 45%
|
|
Memory: models.Memory{
|
|
Used: 32 * 1024 * 1024 * 1024, // 32GB
|
|
Total: 64 * 1024 * 1024 * 1024, // 64GB
|
|
},
|
|
Uptime: 86400, // 1 day
|
|
}
|
|
|
|
trends := map[string]Trend{
|
|
"cpu_24h": {Direction: TrendStable, DataPoints: 24},
|
|
}
|
|
|
|
ctx := FormatNodeForContext(node, trends)
|
|
|
|
if ctx.ResourceID != "node/pve-1" {
|
|
t.Errorf("Expected ResourceID 'node/pve-1', got '%s'", ctx.ResourceID)
|
|
}
|
|
|
|
if ctx.ResourceType != "node" {
|
|
t.Errorf("Expected ResourceType 'node', got '%s'", ctx.ResourceType)
|
|
}
|
|
|
|
// CPU should be converted from 0-1 to percentage
|
|
if ctx.CurrentCPU != 45.0 {
|
|
t.Errorf("Expected CurrentCPU 45.0, got %f", ctx.CurrentCPU)
|
|
}
|
|
|
|
// Memory should be 50%
|
|
if ctx.CurrentMemory != 50.0 {
|
|
t.Errorf("Expected CurrentMemory 50.0, got %f", ctx.CurrentMemory)
|
|
}
|
|
|
|
if len(ctx.Trends) != 1 {
|
|
t.Errorf("Expected 1 trend, got %d", len(ctx.Trends))
|
|
}
|
|
}
|
|
|
|
func TestFormatGuestForContext(t *testing.T) {
|
|
trends := map[string]Trend{}
|
|
lastBackup := time.Now().Add(-24 * time.Hour)
|
|
|
|
ctx := FormatGuestForContext(
|
|
"vm-100",
|
|
"web-server",
|
|
"pve-1",
|
|
"vm",
|
|
"running",
|
|
100, // VMID
|
|
0.35, // CPU (0-1)
|
|
65.0, // Memory (0-100)
|
|
45.0, // Disk (0-100)
|
|
3600, // 1 hour uptime
|
|
lastBackup,
|
|
trends,
|
|
)
|
|
|
|
if ctx.ResourceID != "vm-100" {
|
|
t.Errorf("Expected ResourceID 'vm-100', got '%s'", ctx.ResourceID)
|
|
}
|
|
|
|
if ctx.ResourceType != "vm" {
|
|
t.Errorf("Expected ResourceType 'vm', got '%s'", ctx.ResourceType)
|
|
}
|
|
|
|
if ctx.Node != "pve-1" {
|
|
t.Errorf("Expected Node 'pve-1', got '%s'", ctx.Node)
|
|
}
|
|
|
|
// CPU should be converted from 0-1 to percentage
|
|
if ctx.CurrentCPU != 35.0 {
|
|
t.Errorf("Expected CurrentCPU 35.0, got %f", ctx.CurrentCPU)
|
|
}
|
|
|
|
// Memory and disk should pass through as-is
|
|
if ctx.CurrentMemory != 65.0 {
|
|
t.Errorf("Expected CurrentMemory 65.0, got %f", ctx.CurrentMemory)
|
|
}
|
|
|
|
if ctx.CurrentDisk != 45.0 {
|
|
t.Errorf("Expected CurrentDisk 45.0, got %f", ctx.CurrentDisk)
|
|
}
|
|
}
|
|
|
|
func TestFormatStorageForContext(t *testing.T) {
|
|
storage := models.Storage{
|
|
ID: "local-zfs",
|
|
Name: "local-zfs",
|
|
Node: "pve-1",
|
|
Status: "available",
|
|
Used: 500 * 1024 * 1024 * 1024, // 500GB
|
|
Total: 1000 * 1024 * 1024 * 1024, // 1TB
|
|
Usage: 0, // Will be calculated
|
|
}
|
|
|
|
trends := map[string]Trend{
|
|
"usage_7d": {Direction: TrendGrowing, RatePerDay: 1.5, DataPoints: 168},
|
|
}
|
|
|
|
ctx := FormatStorageForContext(storage, trends)
|
|
|
|
if ctx.ResourceID != "local-zfs" {
|
|
t.Errorf("Expected ResourceID 'local-zfs', got '%s'", ctx.ResourceID)
|
|
}
|
|
|
|
if ctx.ResourceType != "storage" {
|
|
t.Errorf("Expected ResourceType 'storage', got '%s'", ctx.ResourceType)
|
|
}
|
|
|
|
// Usage should be calculated as 50%
|
|
if ctx.CurrentDisk != 50.0 {
|
|
t.Errorf("Expected CurrentDisk 50.0, got %f", ctx.CurrentDisk)
|
|
}
|
|
}
|
|
|
|
func TestFormatInfrastructureContext_Empty(t *testing.T) {
|
|
ctx := &InfrastructureContext{
|
|
GeneratedAt: time.Now(),
|
|
TotalResources: 0,
|
|
}
|
|
|
|
result := FormatInfrastructureContext(ctx)
|
|
|
|
if result == "" {
|
|
t.Error("Expected non-empty result")
|
|
}
|
|
|
|
if !containsStr(result, "Infrastructure State") {
|
|
t.Error("Expected result to contain header")
|
|
}
|
|
|
|
if !containsStr(result, "0 resources") {
|
|
t.Error("Expected result to contain resource count")
|
|
}
|
|
}
|
|
|
|
func TestFormatInfrastructureContext_Full(t *testing.T) {
|
|
ctx := &InfrastructureContext{
|
|
GeneratedAt: time.Now(),
|
|
TotalResources: 5,
|
|
Nodes: []ResourceContext{
|
|
{ResourceID: "node-1", ResourceName: "pve-1", ResourceType: "node", Status: "online"},
|
|
},
|
|
VMs: []ResourceContext{
|
|
{ResourceID: "vm-100", ResourceName: "web", ResourceType: "vm", Status: "running"},
|
|
},
|
|
Containers: []ResourceContext{
|
|
{ResourceID: "ct-200", ResourceName: "nginx", ResourceType: "container", Status: "running"},
|
|
},
|
|
Storage: []ResourceContext{
|
|
{ResourceID: "local", ResourceName: "local-zfs", ResourceType: "storage", Status: "available"},
|
|
},
|
|
DockerHosts: []ResourceContext{
|
|
{ResourceID: "docker-1", ResourceName: "docker-host", ResourceType: "docker_host", Status: "online"},
|
|
},
|
|
}
|
|
|
|
result := FormatInfrastructureContext(ctx)
|
|
|
|
// Check for all sections
|
|
if !containsStr(result, "Proxmox Nodes") {
|
|
t.Error("Expected result to contain Proxmox Nodes section")
|
|
}
|
|
if !containsStr(result, "Virtual Machines") {
|
|
t.Error("Expected result to contain Virtual Machines section")
|
|
}
|
|
if !containsStr(result, "LXC/OCI Containers") {
|
|
t.Error("Expected result to contain Containers section")
|
|
}
|
|
if !containsStr(result, "Storage") {
|
|
t.Error("Expected result to contain Storage section")
|
|
}
|
|
if !containsStr(result, "Docker Hosts") {
|
|
t.Error("Expected result to contain Docker Hosts section")
|
|
}
|
|
}
|
|
|
|
func TestFormatInfrastructureContext_WithAnomalies(t *testing.T) {
|
|
ctx := &InfrastructureContext{
|
|
GeneratedAt: time.Now(),
|
|
TotalResources: 1,
|
|
Anomalies: []Anomaly{
|
|
{Metric: "cpu", Description: "High CPU cluster-wide"},
|
|
},
|
|
}
|
|
|
|
result := FormatInfrastructureContext(ctx)
|
|
|
|
if !containsStr(result, "Current Anomalies") {
|
|
t.Error("Expected result to contain Anomalies section")
|
|
}
|
|
}
|
|
|
|
func TestFormatInfrastructureContext_WithPredictions(t *testing.T) {
|
|
ctx := &InfrastructureContext{
|
|
GeneratedAt: time.Now(),
|
|
TotalResources: 1,
|
|
Predictions: []Prediction{
|
|
{
|
|
Event: "storage_full",
|
|
ResourceID: "local-zfs",
|
|
DaysUntil: 7,
|
|
Confidence: 0.85,
|
|
Basis: "Based on 7-day growth trend",
|
|
},
|
|
},
|
|
}
|
|
|
|
result := FormatInfrastructureContext(ctx)
|
|
|
|
if !containsStr(result, "Predictions") {
|
|
t.Error("Expected result to contain Predictions section")
|
|
}
|
|
if !containsStr(result, "85%") {
|
|
t.Error("Expected result to contain confidence percentage")
|
|
}
|
|
}
|
|
|
|
func TestFormatCompactSummary(t *testing.T) {
|
|
ctx := &InfrastructureContext{
|
|
GeneratedAt: time.Now(),
|
|
TotalResources: 10,
|
|
Nodes: []ResourceContext{
|
|
{ResourceID: "node-1", Status: "online"},
|
|
},
|
|
VMs: []ResourceContext{
|
|
{ResourceID: "vm-100", Status: "running"},
|
|
{ResourceID: "vm-101", Status: "running", Anomalies: []Anomaly{{Metric: "cpu"}}},
|
|
},
|
|
}
|
|
|
|
result := FormatCompactSummary(ctx)
|
|
|
|
if result == "" {
|
|
t.Error("Expected non-empty result")
|
|
}
|
|
|
|
if !containsStr(result, "10 resources") {
|
|
t.Error("Expected result to contain resource count")
|
|
}
|
|
|
|
if !containsStr(result, "Health:") {
|
|
t.Error("Expected result to contain health summary")
|
|
}
|
|
}
|
|
|
|
func TestFormatCompactSummary_WithPredictions(t *testing.T) {
|
|
ctx := &InfrastructureContext{
|
|
GeneratedAt: time.Now(),
|
|
TotalResources: 5,
|
|
Predictions: []Prediction{
|
|
{Event: "storage_full", DaysUntil: 14},
|
|
{Event: "memory_exhaustion", DaysUntil: 7}, // Nearest
|
|
{Event: "disk_warning", DaysUntil: 21},
|
|
},
|
|
}
|
|
|
|
result := FormatCompactSummary(ctx)
|
|
|
|
// Should show the nearest prediction (7 days)
|
|
if !containsStr(result, "Nearest prediction") {
|
|
t.Error("Expected result to contain nearest prediction")
|
|
}
|
|
if !containsStr(result, "memory_exhaustion") {
|
|
t.Error("Expected result to show the nearest prediction (memory_exhaustion)")
|
|
}
|
|
}
|
|
|
|
func TestHasGrowingTrend(t *testing.T) {
|
|
tests := []struct {
|
|
name string
|
|
resource ResourceContext
|
|
expected bool
|
|
}{
|
|
{
|
|
name: "no trends",
|
|
resource: ResourceContext{},
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "stable trend",
|
|
resource: ResourceContext{
|
|
Trends: map[string]Trend{
|
|
"cpu": {Direction: TrendStable, RatePerDay: 0.5},
|
|
},
|
|
},
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "slow growing trend",
|
|
resource: ResourceContext{
|
|
Trends: map[string]Trend{
|
|
"cpu": {Direction: TrendGrowing, RatePerDay: 0.5}, // < 1
|
|
},
|
|
},
|
|
expected: false,
|
|
},
|
|
{
|
|
name: "fast growing trend",
|
|
resource: ResourceContext{
|
|
Trends: map[string]Trend{
|
|
"cpu": {Direction: TrendGrowing, RatePerDay: 2.0}, // > 1
|
|
},
|
|
},
|
|
expected: true,
|
|
},
|
|
}
|
|
|
|
for _, tt := range tests {
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
result := hasGrowingTrend(tt.resource)
|
|
if result != tt.expected {
|
|
t.Errorf("hasGrowingTrend() = %v, want %v", result, tt.expected)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// Helper function
|
|
func containsStr(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 TestFormatMetricSamples_StepChange(t *testing.T) {
|
|
// Simulate a step change: stable at 26%, then jump to 31%, then stable at 31%
|
|
now := time.Now()
|
|
points := []MetricPoint{
|
|
{Value: 26.2, Timestamp: now.Add(-6 * time.Hour)},
|
|
{Value: 26.1, Timestamp: now.Add(-5 * time.Hour)},
|
|
{Value: 26.3, Timestamp: now.Add(-4 * time.Hour)},
|
|
{Value: 30.7, Timestamp: now.Add(-2 * time.Hour)}, // Jump
|
|
{Value: 30.8, Timestamp: now.Add(-1 * time.Hour)},
|
|
{Value: 30.7, Timestamp: now},
|
|
}
|
|
|
|
result := formatMetricSamples("disk", points)
|
|
|
|
// Should show the step change: 26→31 (deduped)
|
|
if !containsStr(result, "Disk:") {
|
|
t.Error("Expected result to contain 'Disk:'")
|
|
}
|
|
// Should show the progression, not just the rate
|
|
if !containsStr(result, "26") || !containsStr(result, "31") {
|
|
t.Errorf("Expected result to show both values (26 and 31), got: %s", result)
|
|
}
|
|
}
|
|
|
|
func TestFormatMetricSamples_Stable(t *testing.T) {
|
|
// All values the same
|
|
now := time.Now()
|
|
points := []MetricPoint{
|
|
{Value: 50.0, Timestamp: now.Add(-3 * time.Hour)},
|
|
{Value: 50.1, Timestamp: now.Add(-2 * time.Hour)},
|
|
{Value: 49.9, Timestamp: now.Add(-1 * time.Hour)},
|
|
{Value: 50.0, Timestamp: now},
|
|
}
|
|
|
|
result := formatMetricSamples("memory", points)
|
|
|
|
// All values round to 50, should show "stable at 50%"
|
|
if !containsStr(result, "stable at 50%") {
|
|
t.Errorf("Expected 'stable at 50%%' for consistent values, got: %s", result)
|
|
}
|
|
}
|
|
|
|
func TestFormatMetricSamples_InsufficientData(t *testing.T) {
|
|
points := []MetricPoint{
|
|
{Value: 50.0, Timestamp: time.Now()},
|
|
}
|
|
|
|
result := formatMetricSamples("cpu", points)
|
|
|
|
if result != "" {
|
|
t.Errorf("Expected empty string for insufficient data, got: %s", result)
|
|
}
|
|
}
|
|
|
|
func TestDownsampleMetrics(t *testing.T) {
|
|
now := time.Now()
|
|
|
|
// Create 100 points
|
|
points := make([]MetricPoint, 100)
|
|
for i := 0; i < 100; i++ {
|
|
points[i] = MetricPoint{
|
|
Value: float64(i),
|
|
Timestamp: now.Add(time.Duration(-100+i) * time.Minute),
|
|
}
|
|
}
|
|
|
|
// Downsample to 10
|
|
sampled := DownsampleMetrics(points, 10)
|
|
|
|
// Should have roughly 10-11 points (plus potentially the last one)
|
|
if len(sampled) < 10 || len(sampled) > 15 {
|
|
t.Errorf("Expected ~10-15 samples, got %d", len(sampled))
|
|
}
|
|
|
|
// Last point should be included
|
|
if sampled[len(sampled)-1].Timestamp != points[99].Timestamp {
|
|
t.Error("Expected last point to be included")
|
|
}
|
|
|
|
// First point should be included
|
|
if sampled[0].Timestamp != points[0].Timestamp {
|
|
t.Error("Expected first point to be included")
|
|
}
|
|
}
|
|
|
|
func TestDownsampleMetrics_SmallInput(t *testing.T) {
|
|
now := time.Now()
|
|
|
|
// Create 5 points - less than target
|
|
points := []MetricPoint{
|
|
{Value: 10, Timestamp: now.Add(-4 * time.Minute)},
|
|
{Value: 20, Timestamp: now.Add(-3 * time.Minute)},
|
|
{Value: 30, Timestamp: now.Add(-2 * time.Minute)},
|
|
{Value: 40, Timestamp: now.Add(-1 * time.Minute)},
|
|
{Value: 50, Timestamp: now},
|
|
}
|
|
|
|
// Downsample to 10 should return all 5
|
|
sampled := DownsampleMetrics(points, 10)
|
|
|
|
if len(sampled) != 5 {
|
|
t.Errorf("Expected all 5 points when target > input, got %d", len(sampled))
|
|
}
|
|
}
|
|
|
|
func TestFormatResourceContext_WithMetricSamples(t *testing.T) {
|
|
now := time.Now()
|
|
ctx := ResourceContext{
|
|
ResourceID: "ct-105",
|
|
ResourceType: "container",
|
|
ResourceName: "frigate",
|
|
Status: "running",
|
|
CurrentDisk: 30.7,
|
|
MetricSamples: map[string][]MetricPoint{
|
|
"disk": {
|
|
{Value: 26.2, Timestamp: now.Add(-3 * time.Hour)},
|
|
{Value: 30.7, Timestamp: now.Add(-1 * time.Hour)},
|
|
{Value: 30.7, Timestamp: now},
|
|
},
|
|
},
|
|
}
|
|
|
|
result := FormatResourceContext(ctx)
|
|
|
|
// Should contain the History section with sampled data
|
|
if !containsStr(result, "History") {
|
|
t.Error("Expected result to contain History section with metric samples")
|
|
}
|
|
}
|
|
func TestFormatResourceContext_Full(t *testing.T) {
|
|
ctx := ResourceContext{
|
|
ResourceID: "ct-105",
|
|
ResourceType: "docker_container",
|
|
ResourceName: "frigate",
|
|
Status: "running",
|
|
Uptime: 2 * time.Minute,
|
|
CurrentCPU: 10.5,
|
|
MetricSamples: map[string][]MetricPoint{
|
|
"cpu": {
|
|
{Value: 5.0, Timestamp: time.Now().Add(-1 * time.Hour)},
|
|
{Value: 10.0, Timestamp: time.Now().Add(-30 * time.Minute)},
|
|
{Value: 10.5, Timestamp: time.Now()},
|
|
},
|
|
},
|
|
Trends: map[string]Trend{
|
|
"cpu": {Direction: TrendGrowing, RatePerDay: 240.0, DataPoints: 10}, // 10%/hr = 240%/day
|
|
},
|
|
}
|
|
|
|
result := FormatResourceContext(ctx)
|
|
if !containsStr(result, "Docker Container") {
|
|
t.Error("Expected 'Docker Container' label")
|
|
}
|
|
if !containsStr(result, "History") {
|
|
t.Error("Expected History section")
|
|
}
|
|
if !containsStr(result, "rising 240.0/day") {
|
|
t.Errorf("Expected rising trend, got: %s", result)
|
|
}
|
|
}
|
|
|
|
func TestFormatInfrastructureContext_FullRich(t *testing.T) {
|
|
ctx := &InfrastructureContext{
|
|
GeneratedAt: time.Now(),
|
|
TotalResources: 1,
|
|
Changes: []Change{
|
|
{ResourceName: "web-server", Description: "Upgraded RAM"},
|
|
},
|
|
Hosts: []ResourceContext{
|
|
{ResourceID: "host-1", ResourceName: "server-1", ResourceType: "host", Status: "online"},
|
|
},
|
|
}
|
|
|
|
result := FormatInfrastructureContext(ctx)
|
|
if !containsStr(result, "Recent Changes") {
|
|
t.Error("Expected Recent Changes section")
|
|
}
|
|
if !containsStr(result, "Agent Hosts") {
|
|
t.Error("Expected Agent Hosts section")
|
|
}
|
|
}
|
|
|
|
func TestFormatCompactSummary_Full(t *testing.T) {
|
|
ctx := &InfrastructureContext{
|
|
GeneratedAt: time.Now(),
|
|
TotalResources: 10,
|
|
Nodes: []ResourceContext{
|
|
{
|
|
ResourceID: "node-1",
|
|
Trends: map[string]Trend{
|
|
"cpu": {Direction: TrendGrowing, RatePerDay: 5.0, DataPoints: 10},
|
|
},
|
|
},
|
|
},
|
|
}
|
|
|
|
result := FormatCompactSummary(ctx)
|
|
if !containsStr(result, "1 warning") {
|
|
t.Errorf("Expected 1 warning for growing trend, got: %s", result)
|
|
}
|
|
}
|
|
|
|
func TestFormatRate_PerHour(t *testing.T) {
|
|
// 4.8 per day = 0.2 per hour (but >= 1, so shows daily)
|
|
r1 := formatRate(4.8)
|
|
if r1 != "4.8/day" {
|
|
t.Errorf("Expected 4.8/day, got %s", r1)
|
|
}
|
|
|
|
// 0.48 per day = 0.02 per hour
|
|
r2 := formatRate(0.48)
|
|
if r2 != "0.02/hr" {
|
|
t.Errorf("Expected 0.02/hr, got %s", r2)
|
|
}
|
|
|
|
r3 := formatRate(0.01) // 0.01 / 24 < 0.01
|
|
if r3 != "slow" {
|
|
t.Errorf("Expected slow, got %s", r3)
|
|
}
|
|
}
|
|
|
|
func TestDownsampleMetrics_MinTarget(t *testing.T) {
|
|
points := make([]MetricPoint, 10)
|
|
sampled := DownsampleMetrics(points, 1)
|
|
if len(sampled) < 3 {
|
|
t.Errorf("Expected at least 3 samples, got %d", len(sampled))
|
|
}
|
|
}
|
|
|
|
func TestFormatDuration_Large(t *testing.T) {
|
|
d := 24*time.Hour + 2*time.Hour
|
|
result := formatDuration(d)
|
|
if result != "1d2h" {
|
|
t.Errorf("Expected 1d2h, got %s", result)
|
|
}
|
|
}
|
|
|
|
func TestDownsampleMetrics_NoSamples(t *testing.T) {
|
|
sampled := DownsampleMetrics([]MetricPoint{}, 10)
|
|
if len(sampled) != 0 {
|
|
t.Error("Expected 0 samples for empty input")
|
|
}
|
|
}
|