mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-04-28 03:20:11 +00:00
- 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>
796 lines
19 KiB
Go
796 lines
19 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, "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")
|
|
}
|
|
}
|