Pulse/internal/ai/context/formatter_test.go
rcourtman 9e339957c6 fix: Update runtime config when toggling Docker update actions setting
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
2026-01-03 11:14:17 +00:00

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")
}
}