package webui import ( "encoding/json" "net/http" "net/http/httptest" "os" "path/filepath" "strings" "testing" "time" "bee/audit/internal/platform" ) func TestChartLegendNumber(t *testing.T) { tests := []struct { in float64 want string }{ {in: 0.4, want: "0"}, {in: 61.5, want: "62"}, {in: 999.4, want: "999"}, {in: 1200, want: "1,2k"}, {in: 1250, want: "1,25k"}, {in: 1310, want: "1,31k"}, {in: 1500, want: "1,5k"}, {in: 2600, want: "2,6k"}, {in: 10200, want: "10k"}, } for _, tc := range tests { if got := chartLegendNumber(tc.in); got != tc.want { t.Fatalf("chartLegendNumber(%v)=%q want %q", tc.in, got, tc.want) } } } func TestRecoverMiddlewareReturns500OnPanic(t *testing.T) { handler := recoverMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { panic("boom") })) rec := httptest.NewRecorder() req := httptest.NewRequest(http.MethodGet, "/panic", nil) handler.ServeHTTP(rec, req) if rec.Code != http.StatusInternalServerError { t.Fatalf("status=%d want %d", rec.Code, http.StatusInternalServerError) } if !strings.Contains(rec.Body.String(), "internal server error") { t.Fatalf("body=%q", rec.Body.String()) } } func TestRecoverMiddlewarePreservesStreamingInterfaces(t *testing.T) { handler := recoverMiddleware(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if !sseStart(w) { return } if !sseWrite(w, "tick", "ok") { t.Fatal("expected sse write to succeed") } })) rec := httptest.NewRecorder() req := httptest.NewRequest(http.MethodGet, "/stream", nil) handler.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String()) } if got := rec.Header().Get("Content-Type"); got != "text/event-stream" { t.Fatalf("content-type=%q", got) } body := rec.Body.String() if !strings.Contains(body, "event: tick\n") || !strings.Contains(body, "data: ok\n\n") { t.Fatalf("body=%q", body) } } func TestChartDataFromSamplesUsesFullHistory(t *testing.T) { samples := []platform.LiveMetricSample{ { Timestamp: time.Now().Add(-3 * time.Minute), CPULoadPct: 10, MemLoadPct: 20, PowerW: 300, GPUs: []platform.GPUMetricRow{ {GPUIndex: 0, UsagePct: 90, MemUsagePct: 5, PowerW: 120, TempC: 50}, }, }, { Timestamp: time.Now().Add(-2 * time.Minute), CPULoadPct: 30, MemLoadPct: 40, PowerW: 320, GPUs: []platform.GPUMetricRow{ {GPUIndex: 0, UsagePct: 95, MemUsagePct: 7, PowerW: 125, TempC: 51}, }, }, { Timestamp: time.Now().Add(-1 * time.Minute), CPULoadPct: 50, MemLoadPct: 60, PowerW: 340, GPUs: []platform.GPUMetricRow{ {GPUIndex: 0, UsagePct: 97, MemUsagePct: 9, PowerW: 130, TempC: 52}, }, }, } datasets, names, labels, title, _, _, ok := chartDataFromSamples("gpu-all-power", samples) if !ok { t.Fatal("chartDataFromSamples returned ok=false") } if title != "GPU Power" { t.Fatalf("title=%q", title) } if len(names) != 1 || names[0] != "GPU 0" { t.Fatalf("names=%v", names) } if len(labels) != len(samples) { t.Fatalf("labels len=%d want %d", len(labels), len(samples)) } if len(datasets) != 1 || len(datasets[0]) != len(samples) { t.Fatalf("datasets shape=%v", datasets) } if got := datasets[0][0]; got != 120 { t.Fatalf("datasets[0][0]=%v want 120", got) } if got := datasets[0][2]; got != 130 { t.Fatalf("datasets[0][2]=%v want 130", got) } } func TestChartDataFromSamplesKeepsStableGPUSeriesOrder(t *testing.T) { samples := []platform.LiveMetricSample{ { Timestamp: time.Now().Add(-2 * time.Minute), GPUs: []platform.GPUMetricRow{ {GPUIndex: 7, PowerW: 170}, {GPUIndex: 2, PowerW: 120}, {GPUIndex: 0, PowerW: 100}, }, }, { Timestamp: time.Now().Add(-1 * time.Minute), GPUs: []platform.GPUMetricRow{ {GPUIndex: 0, PowerW: 101}, {GPUIndex: 7, PowerW: 171}, {GPUIndex: 2, PowerW: 121}, }, }, } datasets, names, _, title, _, _, ok := chartDataFromSamples("gpu-all-power", samples) if !ok { t.Fatal("chartDataFromSamples returned ok=false") } if title != "GPU Power" { t.Fatalf("title=%q", title) } wantNames := []string{"GPU 0", "GPU 2", "GPU 7"} if len(names) != len(wantNames) { t.Fatalf("names len=%d want %d: %v", len(names), len(wantNames), names) } for i := range wantNames { if names[i] != wantNames[i] { t.Fatalf("names[%d]=%q want %q; full=%v", i, names[i], wantNames[i], names) } } if got := datasets[0]; len(got) != 2 || got[0] != 100 || got[1] != 101 { t.Fatalf("GPU 0 dataset=%v want [100 101]", got) } if got := datasets[1]; len(got) != 2 || got[0] != 120 || got[1] != 121 { t.Fatalf("GPU 2 dataset=%v want [120 121]", got) } if got := datasets[2]; len(got) != 2 || got[0] != 170 || got[1] != 171 { t.Fatalf("GPU 7 dataset=%v want [170 171]", got) } } func TestChartDataFromSamplesIncludesGPUClockCharts(t *testing.T) { samples := []platform.LiveMetricSample{ { Timestamp: time.Now().Add(-2 * time.Minute), GPUs: []platform.GPUMetricRow{ {GPUIndex: 0, ClockMHz: 1400}, {GPUIndex: 3, ClockMHz: 1500}, }, }, { Timestamp: time.Now().Add(-1 * time.Minute), GPUs: []platform.GPUMetricRow{ {GPUIndex: 0, ClockMHz: 1410}, {GPUIndex: 3, ClockMHz: 1510}, }, }, } datasets, names, _, title, _, _, ok := chartDataFromSamples("gpu-all-clock", samples) if !ok { t.Fatal("gpu-all-clock returned ok=false") } if title != "GPU Core Clock" { t.Fatalf("title=%q", title) } if len(names) != 2 || names[0] != "GPU 0" || names[1] != "GPU 3" { t.Fatalf("names=%v", names) } if got := datasets[1][1]; got != 1510 { t.Fatalf("GPU 3 core clock=%v want 1510", got) } } func TestNormalizePowerSeriesHoldsLastPositive(t *testing.T) { got := normalizePowerSeries([]float64{0, 480, 0, 0, 510, 0}) want := []float64{0, 480, 480, 480, 510, 510} if len(got) != len(want) { t.Fatalf("len=%d want %d", len(got), len(want)) } for i := range want { if got[i] != want[i] { t.Fatalf("got[%d]=%v want %v", i, got[i], want[i]) } } } func TestRenderMetricsUsesBufferedChartRefresh(t *testing.T) { body := renderMetrics() if !strings.Contains(body, "const probe = new Image();") { t.Fatalf("metrics page should preload chart images before swap: %s", body) } if !strings.Contains(body, "el.dataset.loading === '1'") { t.Fatalf("metrics page should avoid overlapping chart reloads: %s", body) } if !strings.Contains(body, `id="gpu-metrics-section" style="display:none`) { t.Fatalf("metrics page should keep gpu charts in a hidden dedicated section until GPUs are detected: %s", body) } if !strings.Contains(body, `id="gpu-chart-toggle"`) { t.Fatalf("metrics page should render GPU chart mode toggle: %s", body) } if !strings.Contains(body, `/api/metrics/chart/gpu-all-clock.svg`) { t.Fatalf("metrics page should include GPU core clock chart: %s", body) } if strings.Contains(body, `/api/metrics/chart/gpu-all-memclock.svg`) { t.Fatalf("metrics page should not include GPU memory clock chart: %s", body) } if !strings.Contains(body, `renderGPUOverviewCards(indices, names)`) { t.Fatalf("metrics page should build per-GPU chart cards dynamically: %s", body) } } func TestChartLegendVisible(t *testing.T) { if !chartLegendVisible(8) { t.Fatal("legend should stay visible for charts with up to 8 series") } if chartLegendVisible(9) { t.Fatal("legend should be hidden for charts with more than 8 series") } } func TestChartYAxisNumber(t *testing.T) { tests := []struct { in float64 want string }{ {in: 999, want: "999"}, {in: 1000, want: "1к"}, {in: 1370, want: "1,4к"}, {in: 1500, want: "1,5к"}, {in: 1700, want: "1,7к"}, {in: 2000, want: "2к"}, {in: 9999, want: "10к"}, {in: 10200, want: "10к"}, {in: -1500, want: "-1,5к"}, } for _, tc := range tests { if got := chartYAxisNumber(tc.in); got != tc.want { t.Fatalf("chartYAxisNumber(%v)=%q want %q", tc.in, got, tc.want) } } } func TestChartCanvasHeight(t *testing.T) { if got := chartCanvasHeight(4); got != 360 { t.Fatalf("chartCanvasHeight(4)=%d want 360", got) } if got := chartCanvasHeight(12); got != 288 { t.Fatalf("chartCanvasHeight(12)=%d want 288", got) } } func TestChartTimelineSegmentsForRangeMergesActiveSpansAndIdleGaps(t *testing.T) { start := time.Date(2026, 4, 5, 12, 0, 0, 0, time.UTC) end := start.Add(10 * time.Minute) taskWindow := func(offsetStart, offsetEnd time.Duration) Task { s := start.Add(offsetStart) e := start.Add(offsetEnd) return Task{ Name: "task", Status: TaskDone, StartedAt: &s, DoneAt: &e, } } segments := chartTimelineSegmentsForRange(start, end, end, []Task{ taskWindow(1*time.Minute, 3*time.Minute), taskWindow(2*time.Minute, 5*time.Minute), taskWindow(7*time.Minute, 8*time.Minute), }) if len(segments) != 5 { t.Fatalf("segments=%d want 5: %#v", len(segments), segments) } wantActive := []bool{false, true, false, true, false} wantMinutes := [][2]int{{0, 1}, {1, 5}, {5, 7}, {7, 8}, {8, 10}} for i, segment := range segments { if segment.Active != wantActive[i] { t.Fatalf("segment[%d].Active=%v want %v", i, segment.Active, wantActive[i]) } if got := int(segment.Start.Sub(start).Minutes()); got != wantMinutes[i][0] { t.Fatalf("segment[%d] start=%d want %d", i, got, wantMinutes[i][0]) } if got := int(segment.End.Sub(start).Minutes()); got != wantMinutes[i][1] { t.Fatalf("segment[%d] end=%d want %d", i, got, wantMinutes[i][1]) } } } func TestRenderMetricChartSVGIncludesTimelineOverlay(t *testing.T) { start := time.Date(2026, 4, 5, 12, 0, 0, 0, time.UTC) labels := []string{"12:00", "12:01", "12:02"} times := []time.Time{start, start.Add(time.Minute), start.Add(2 * time.Minute)} svg, err := renderMetricChartSVG( "System Power", labels, times, [][]float64{{300, 320, 310}}, []string{"Power W"}, floatPtr(0), floatPtr(400), 360, []chartTimelineSegment{ {Start: start, End: start.Add(time.Minute), Active: false}, {Start: start.Add(time.Minute), End: start.Add(2 * time.Minute), Active: true}, }, ) if err != nil { t.Fatal(err) } body := string(svg) if !strings.Contains(body, `data-role="timeline-overlay"`) { t.Fatalf("svg missing timeline overlay: %s", body) } if !strings.Contains(body, `opacity="0.10"`) { t.Fatalf("svg missing idle overlay opacity: %s", body) } if !strings.Contains(body, `System Power`) { t.Fatalf("svg missing chart title: %s", body) } } func TestHandleMetricsChartSVGRendersCustomSVG(t *testing.T) { dir := t.TempDir() db, err := openMetricsDB(filepath.Join(dir, "metrics.db")) if err != nil { t.Fatal(err) } t.Cleanup(func() { _ = db.db.Close() }) start := time.Date(2026, 4, 5, 12, 0, 0, 0, time.UTC) for i, sample := range []platform.LiveMetricSample{ {Timestamp: start, PowerW: 300}, {Timestamp: start.Add(time.Minute), PowerW: 320}, {Timestamp: start.Add(2 * time.Minute), PowerW: 310}, } { if err := db.Write(sample); err != nil { t.Fatalf("write sample %d: %v", i, err) } } globalQueue.mu.Lock() prevTasks := globalQueue.tasks s := start.Add(30 * time.Second) e := start.Add(90 * time.Second) globalQueue.tasks = []*Task{{Name: "Burn", Status: TaskDone, StartedAt: &s, DoneAt: &e}} globalQueue.mu.Unlock() t.Cleanup(func() { globalQueue.mu.Lock() globalQueue.tasks = prevTasks globalQueue.mu.Unlock() }) h := &handler{opts: HandlerOptions{ExportDir: dir}, metricsDB: db} rec := httptest.NewRecorder() req := httptest.NewRequest(http.MethodGet, "/api/metrics/chart/server-power.svg", nil) h.handleMetricsChartSVG(rec, req) if rec.Code != http.StatusOK { t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String()) } body := rec.Body.String() if !strings.Contains(body, `data-role="timeline-overlay"`) { t.Fatalf("custom svg response missing timeline overlay: %s", body) } if !strings.Contains(body, `stroke-linecap="round"`) { t.Fatalf("custom svg response missing custom polyline styling: %s", body) } } func TestNormalizeFanSeriesHoldsLastPositive(t *testing.T) { got := normalizeFanSeries([]float64{4200, 0, 0, 4300, 0}) want := []float64{4200, 4200, 4200, 4300, 4300} if len(got) != len(want) { t.Fatalf("len=%d want %d", len(got), len(want)) } for i := range want { if got[i] != want[i] { t.Fatalf("got[%d]=%v want %v", i, got[i], want[i]) } } } func TestSnapshotFanRingsUsesTimelineLabels(t *testing.T) { r1 := newMetricsRing(4) r2 := newMetricsRing(4) r1.push(1000) r1.push(1100) r2.push(1200) r2.push(1300) datasets, names, labels := snapshotFanRings([]*metricsRing{r1, r2}, []string{"FAN_A", "FAN_B"}) if len(datasets) != 2 { t.Fatalf("datasets=%d want 2", len(datasets)) } if len(names) != 2 || names[0] != "FAN_A RPM" || names[1] != "FAN_B RPM" { t.Fatalf("names=%v", names) } if len(labels) != 2 { t.Fatalf("labels=%v want 2 entries", labels) } if labels[0] == "" || labels[1] == "" { t.Fatalf("labels should contain timeline values, got %v", labels) } } func TestRenderNetworkInlineSyncsPendingState(t *testing.T) { body := renderNetworkInline() if !strings.Contains(body, "d.pending_change") { t.Fatalf("network UI should read pending network state from API: %s", body) } if !strings.Contains(body, "setInterval(loadNetwork, 5000)") { t.Fatalf("network UI should periodically refresh network state: %s", body) } if !strings.Contains(body, "showNetPending(NET_ROLLBACK_SECS)") { t.Fatalf("network UI should show pending confirmation immediately on apply: %s", body) } } func TestRootRendersDashboard(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "audit.json") exportDir := filepath.Join(dir, "export") if err := os.MkdirAll(exportDir, 0755); err != nil { t.Fatal(err) } if err := os.WriteFile(path, []byte(`{"collected_at":"2026-03-15T00:00:00Z","hardware":{"board":{"serial_number":"SERIAL-OLD"}}}`), 0644); err != nil { t.Fatal(err) } handler := NewHandler(HandlerOptions{ Title: "Bee Hardware Audit", BuildLabel: "1.2.3", AuditPath: path, ExportDir: exportDir, }) first := httptest.NewRecorder() handler.ServeHTTP(first, httptest.NewRequest(http.MethodGet, "/", nil)) if first.Code != http.StatusOK { t.Fatalf("first status=%d", first.Code) } // Dashboard should contain the audit nav link and hardware summary if !strings.Contains(first.Body.String(), `href="/audit"`) { t.Fatalf("first body missing audit nav link: %s", first.Body.String()) } if !strings.Contains(first.Body.String(), `/viewer`) { t.Fatalf("first body missing viewer link: %s", first.Body.String()) } versionIdx := strings.Index(first.Body.String(), `Version 1.2.3`) navIdx := strings.Index(first.Body.String(), `href="/"`) if versionIdx == -1 || navIdx == -1 || versionIdx > navIdx { t.Fatalf("version should render near top of sidebar before nav links: %s", first.Body.String()) } if got := first.Header().Get("Cache-Control"); got != "no-store" { t.Fatalf("first cache-control=%q", got) } if err := os.WriteFile(path, []byte(`{"collected_at":"2026-03-15T00:05:00Z","hardware":{"board":{"serial_number":"SERIAL-NEW"}}}`), 0644); err != nil { t.Fatal(err) } second := httptest.NewRecorder() handler.ServeHTTP(second, httptest.NewRequest(http.MethodGet, "/", nil)) if second.Code != http.StatusOK { t.Fatalf("second status=%d", second.Code) } if !strings.Contains(second.Body.String(), `Hardware Summary`) { t.Fatalf("second body missing hardware summary: %s", second.Body.String()) } } func TestRootShowsRunAuditButtonWhenSnapshotMissing(t *testing.T) { dir := t.TempDir() exportDir := filepath.Join(dir, "export") if err := os.MkdirAll(exportDir, 0755); err != nil { t.Fatal(err) } handler := NewHandler(HandlerOptions{ Title: "Bee Hardware Audit", AuditPath: filepath.Join(dir, "missing-audit.json"), ExportDir: exportDir, }) rec := httptest.NewRecorder() handler.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/", nil)) if rec.Code != http.StatusOK { t.Fatalf("status=%d", rec.Code) } body := rec.Body.String() if !strings.Contains(body, `onclick="auditModalRun()">Run audit`) { t.Fatalf("dashboard missing run audit button: %s", body) } if strings.Contains(body, `No audit data`) { t.Fatalf("dashboard still shows empty audit badge: %s", body) } } func TestReadyIsOKWhenAuditPathIsUnset(t *testing.T) { handler := NewHandler(HandlerOptions{}) rec := httptest.NewRecorder() handler.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/api/ready", nil)) if rec.Code != http.StatusOK { t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String()) } if strings.TrimSpace(rec.Body.String()) != "ready" { t.Fatalf("body=%q want ready", rec.Body.String()) } } func TestAuditPageRendersViewerFrameAndActions(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "audit.json") if err := os.WriteFile(path, []byte(`{"collected_at":"2026-03-15T00:00:00Z"}`), 0644); err != nil { t.Fatal(err) } handler := NewHandler(HandlerOptions{AuditPath: path}) rec := httptest.NewRecorder() handler.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/audit", nil)) if rec.Code != http.StatusOK { t.Fatalf("status=%d", rec.Code) } body := rec.Body.String() if !strings.Contains(body, `iframe class="viewer-frame" src="/viewer"`) { t.Fatalf("audit page missing viewer frame: %s", body) } if !strings.Contains(body, `openAuditModal()`) { t.Fatalf("audit page missing action modal trigger: %s", body) } } func TestTasksPageRendersOpenLinksAndPaginationControls(t *testing.T) { handler := NewHandler(HandlerOptions{}) rec := httptest.NewRecorder() handler.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/tasks", nil)) if rec.Code != http.StatusOK { t.Fatalf("status=%d", rec.Code) } body := rec.Body.String() if !strings.Contains(body, `Open a task to view its saved logs and charts.`) { t.Fatalf("tasks page missing task report hint: %s", body) } if !strings.Contains(body, `_taskPageSize = 50`) { t.Fatalf("tasks page missing pagination size config: %s", body) } if !strings.Contains(body, `Previous`) || !strings.Contains(body, `Next`) { t.Fatalf("tasks page missing pagination controls: %s", body) } } func TestToolsPageRendersNvidiaSelfHealSection(t *testing.T) { handler := NewHandler(HandlerOptions{}) rec := httptest.NewRecorder() handler.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/tools", nil)) if rec.Code != http.StatusOK { t.Fatalf("status=%d", rec.Code) } body := rec.Body.String() if !strings.Contains(body, `NVIDIA Self Heal`) { t.Fatalf("tools page missing nvidia self heal section: %s", body) } if !strings.Contains(body, `Restart GPU Drivers`) { t.Fatalf("tools page missing restart gpu drivers button: %s", body) } if !strings.Contains(body, `nvidiaRestartDrivers()`) { t.Fatalf("tools page missing nvidiaRestartDrivers action: %s", body) } if !strings.Contains(body, `/api/gpu/nvidia-status`) { t.Fatalf("tools page missing nvidia status api usage: %s", body) } if !strings.Contains(body, `nvidiaResetGPU(`) { t.Fatalf("tools page missing nvidiaResetGPU action: %s", body) } if !strings.Contains(body, `id="boot-source-text"`) { t.Fatalf("tools page missing boot source field: %s", body) } if !strings.Contains(body, `Export to USB`) { t.Fatalf("tools page missing export to usb section: %s", body) } if !strings.Contains(body, `Support Bundle`) { t.Fatalf("tools page missing support bundle usb button: %s", body) } } func TestBenchmarkPageRendersGPUSelectionControls(t *testing.T) { handler := NewHandler(HandlerOptions{}) rec := httptest.NewRecorder() handler.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/benchmark", nil)) if rec.Code != http.StatusOK { t.Fatalf("status=%d", rec.Code) } body := rec.Body.String() for _, needle := range []string{ `href="/benchmark"`, `id="benchmark-gpu-list"`, `/api/gpu/nvidia`, `/api/benchmark/nvidia/run`, `benchmark-run-nccl`, } { if !strings.Contains(body, needle) { t.Fatalf("benchmark page missing %q: %s", needle, body) } } } func TestBenchmarkPageRendersSavedResultsTable(t *testing.T) { dir := t.TempDir() exportDir := filepath.Join(dir, "export") runDir := filepath.Join(exportDir, "bee-benchmark", "gpu-benchmark-20260406-120000") if err := os.MkdirAll(runDir, 0755); err != nil { t.Fatal(err) } result := platform.NvidiaBenchmarkResult{ GeneratedAt: time.Date(2026, time.April, 6, 12, 0, 0, 0, time.UTC), BenchmarkProfile: "standard", OverallStatus: "OK", GPUs: []platform.BenchmarkGPUResult{ { Index: 0, Name: "NVIDIA H100 PCIe", Scores: platform.BenchmarkScorecard{ CompositeScore: 1176.25, }, }, { Index: 1, Name: "NVIDIA H100 PCIe", Scores: platform.BenchmarkScorecard{ CompositeScore: 1168.50, }, }, }, } raw, err := json.Marshal(result) if err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(runDir, "result.json"), raw, 0644); err != nil { t.Fatal(err) } handler := NewHandler(HandlerOptions{ExportDir: exportDir}) rec := httptest.NewRecorder() handler.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/benchmark", nil)) if rec.Code != http.StatusOK { t.Fatalf("status=%d", rec.Code) } body := rec.Body.String() wantTime := result.GeneratedAt.Local().Format("2006-01-02 15:04:05") for _, needle := range []string{ `Benchmark Results`, `Composite score by saved benchmark run and GPU.`, `GPU #0 — NVIDIA H100 PCIe`, `GPU #1 — NVIDIA H100 PCIe`, `#1`, wantTime, `1176.25`, `1168.50`, } { if !strings.Contains(body, needle) { t.Fatalf("benchmark page missing %q: %s", needle, body) } } } func TestValidatePageRendersNvidiaTargetedStressCard(t *testing.T) { handler := NewHandler(HandlerOptions{}) rec := httptest.NewRecorder() handler.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/validate", nil)) if rec.Code != http.StatusOK { t.Fatalf("status=%d", rec.Code) } body := rec.Body.String() for _, needle := range []string{ `NVIDIA GPU Targeted Stress`, `nvidia-targeted-stress`, `controlled NVIDIA DCGM load`, `dcgmi diag targeted_stress`, `NVIDIA GPU Selection`, `All NVIDIA validate tasks use only the GPUs selected here.`, `Select All`, `id="sat-gpu-list"`, } { if !strings.Contains(body, needle) { t.Fatalf("validate page missing %q: %s", needle, body) } } } func TestBurnPageRendersGoalBasedNVIDIACards(t *testing.T) { handler := NewHandler(HandlerOptions{}) rec := httptest.NewRecorder() handler.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/burn", nil)) if rec.Code != http.StatusOK { t.Fatalf("status=%d", rec.Code) } body := rec.Body.String() for _, needle := range []string{ `NVIDIA Max Compute Load`, `dcgmproftester`, `NCCL`, `Validate → Stress mode`, `id="burn-gpu-list"`, } { if !strings.Contains(body, needle) { t.Fatalf("burn page missing %q: %s", needle, body) } } } func TestTaskDetailPageRendersSavedReport(t *testing.T) { dir := t.TempDir() exportDir := filepath.Join(dir, "export") reportDir := filepath.Join(exportDir, "tasks", "task-1_cpu_sat_done") if err := os.MkdirAll(reportDir, 0755); err != nil { t.Fatal(err) } reportPath := filepath.Join(reportDir, "report.html") if err := os.WriteFile(reportPath, []byte(`
Task Report
saved report
`), 0644); err != nil { t.Fatal(err) } globalQueue.mu.Lock() origTasks := globalQueue.tasks globalQueue.tasks = []*Task{{ ID: "task-1", Name: "CPU SAT", Target: "cpu", Status: TaskDone, CreatedAt: time.Now(), ArtifactsDir: reportDir, ReportHTMLPath: reportPath, }} globalQueue.mu.Unlock() t.Cleanup(func() { globalQueue.mu.Lock() globalQueue.tasks = origTasks globalQueue.mu.Unlock() }) handler := NewHandler(HandlerOptions{Title: "Bee Hardware Audit", ExportDir: exportDir}) rec := httptest.NewRecorder() handler.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/tasks/task-1", nil)) if rec.Code != http.StatusOK { t.Fatalf("status=%d", rec.Code) } body := rec.Body.String() if !strings.Contains(body, `saved report`) { t.Fatalf("task detail page missing saved report: %s", body) } if !strings.Contains(body, `Back to Tasks`) { t.Fatalf("task detail page missing back link: %s", body) } } func TestTaskDetailPageRendersCancelForRunningTask(t *testing.T) { globalQueue.mu.Lock() origTasks := globalQueue.tasks globalQueue.tasks = []*Task{{ ID: "task-live-1", Name: "CPU SAT", Target: "cpu", Status: TaskRunning, CreatedAt: time.Now(), }} globalQueue.mu.Unlock() t.Cleanup(func() { globalQueue.mu.Lock() globalQueue.tasks = origTasks globalQueue.mu.Unlock() }) handler := NewHandler(HandlerOptions{Title: "Bee Hardware Audit"}) rec := httptest.NewRecorder() handler.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/tasks/task-live-1", nil)) if rec.Code != http.StatusOK { t.Fatalf("status=%d", rec.Code) } body := rec.Body.String() if !strings.Contains(body, `Cancel`) { t.Fatalf("task detail page missing cancel button: %s", body) } if !strings.Contains(body, `function cancelTaskDetail(id)`) { t.Fatalf("task detail page missing cancel handler: %s", body) } if !strings.Contains(body, `/api/tasks/' + id + '/cancel`) { t.Fatalf("task detail page missing cancel endpoint: %s", body) } if !strings.Contains(body, `id="task-live-charts"`) { t.Fatalf("task detail page missing live charts container: %s", body) } if !strings.Contains(body, `/api/tasks/' + taskId + '/charts`) { t.Fatalf("task detail page missing live charts index endpoint: %s", body) } } func TestTaskChartSVGUsesTaskTimeWindow(t *testing.T) { dir := t.TempDir() metricsPath := filepath.Join(dir, "metrics.db") prevMetricsPath := taskReportMetricsDBPath taskReportMetricsDBPath = metricsPath t.Cleanup(func() { taskReportMetricsDBPath = prevMetricsPath }) db, err := openMetricsDB(metricsPath) if err != nil { t.Fatalf("openMetricsDB: %v", err) } base := time.Now().UTC() samples := []platform.LiveMetricSample{ {Timestamp: base.Add(-3 * time.Minute), PowerW: 100}, {Timestamp: base.Add(-2 * time.Minute), PowerW: 200}, {Timestamp: base.Add(-1 * time.Minute), PowerW: 300}, } for _, sample := range samples { if err := db.Write(sample); err != nil { t.Fatalf("Write: %v", err) } } _ = db.Close() started := base.Add(-2*time.Minute - 5*time.Second) done := base.Add(-1*time.Minute + 5*time.Second) globalQueue.mu.Lock() origTasks := globalQueue.tasks globalQueue.tasks = []*Task{{ ID: "task-chart-1", Name: "Power Window", Target: "cpu", Status: TaskDone, CreatedAt: started.Add(-10 * time.Second), StartedAt: &started, DoneAt: &done, }} globalQueue.mu.Unlock() t.Cleanup(func() { globalQueue.mu.Lock() globalQueue.tasks = origTasks globalQueue.mu.Unlock() }) handler := NewHandler(HandlerOptions{Title: "Bee Hardware Audit"}) req := httptest.NewRequest(http.MethodGet, "/api/tasks/task-chart-1/chart/server-power.svg", nil) req.SetPathValue("id", "task-chart-1") rec := httptest.NewRecorder() handler.ServeHTTP(rec, req) if rec.Code != http.StatusOK { t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String()) } body := rec.Body.String() if !strings.Contains(body, "System Power") { t.Fatalf("task chart missing expected title: %s", body) } if !strings.Contains(body, "min 200") { t.Fatalf("task chart stats should start from in-window sample: %s", body) } if strings.Contains(body, "min 100") { t.Fatalf("task chart should not include pre-task sample in stats: %s", body) } } func TestViewerRendersLatestSnapshot(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "audit.json") if err := os.WriteFile(path, []byte(`{"collected_at":"2026-03-15T00:00:00Z","hardware":{"board":{"serial_number":"SERIAL-OLD"}}}`), 0644); err != nil { t.Fatal(err) } handler := NewHandler(HandlerOptions{AuditPath: path}) first := httptest.NewRecorder() handler.ServeHTTP(first, httptest.NewRequest(http.MethodGet, "/viewer", nil)) if first.Code != http.StatusOK { t.Fatalf("first status=%d", first.Code) } if !strings.Contains(first.Body.String(), "SERIAL-OLD") { t.Fatalf("viewer body missing old serial: %s", first.Body.String()) } if err := os.WriteFile(path, []byte(`{"collected_at":"2026-03-15T00:05:00Z","hardware":{"board":{"serial_number":"SERIAL-NEW"}}}`), 0644); err != nil { t.Fatal(err) } second := httptest.NewRecorder() handler.ServeHTTP(second, httptest.NewRequest(http.MethodGet, "/viewer", nil)) if second.Code != http.StatusOK { t.Fatalf("second status=%d", second.Code) } if !strings.Contains(second.Body.String(), "SERIAL-NEW") { t.Fatalf("viewer body missing new serial: %s", second.Body.String()) } if strings.Contains(second.Body.String(), "SERIAL-OLD") { t.Fatalf("viewer body still contains old serial: %s", second.Body.String()) } } func TestAuditJSONServesLatestSnapshot(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "audit.json") body := `{"hardware":{"board":{"serial_number":"SERIAL-API"}}}` if err := os.WriteFile(path, []byte(body), 0644); err != nil { t.Fatal(err) } handler := NewHandler(HandlerOptions{AuditPath: path}) rec := httptest.NewRecorder() handler.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/audit.json", nil)) if rec.Code != http.StatusOK { t.Fatalf("status=%d", rec.Code) } if !strings.Contains(rec.Body.String(), "SERIAL-API") { t.Fatalf("body missing expected serial: %s", rec.Body.String()) } if got := rec.Header().Get("Content-Type"); !strings.Contains(got, "application/json") { t.Fatalf("content-type=%q", got) } } func TestMissingAuditJSONReturnsNotFound(t *testing.T) { handler := NewHandler(HandlerOptions{AuditPath: "/missing/audit.json"}) rec := httptest.NewRecorder() handler.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/audit.json", nil)) if rec.Code != http.StatusNotFound { t.Fatalf("status=%d want %d", rec.Code, http.StatusNotFound) } } func TestSupportBundleEndpointReturnsArchive(t *testing.T) { dir := t.TempDir() exportDir := filepath.Join(dir, "export") if err := os.MkdirAll(exportDir, 0755); err != nil { t.Fatal(err) } if err := os.WriteFile(filepath.Join(exportDir, "bee-audit.log"), []byte("audit log"), 0644); err != nil { t.Fatal(err) } archive, err := os.CreateTemp(os.TempDir(), "bee-support-server-test-*.tar.gz") if err != nil { t.Fatal(err) } t.Cleanup(func() { _ = os.Remove(archive.Name()) }) if _, err := archive.WriteString("support-bundle"); err != nil { t.Fatal(err) } if err := archive.Close(); err != nil { t.Fatal(err) } handler := NewHandler(HandlerOptions{ExportDir: exportDir}) rec := httptest.NewRecorder() handler.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/export/support.tar.gz", nil)) if rec.Code != http.StatusOK { t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String()) } if got := rec.Header().Get("Content-Disposition"); !strings.Contains(got, "attachment;") { t.Fatalf("content-disposition=%q", got) } if rec.Body.Len() == 0 { t.Fatal("empty archive body") } } func TestRuntimeHealthEndpointReturnsJSON(t *testing.T) { dir := t.TempDir() exportDir := filepath.Join(dir, "export") if err := os.MkdirAll(exportDir, 0755); err != nil { t.Fatal(err) } body := `{"status":"PARTIAL","checked_at":"2026-03-16T10:00:00Z"}` if err := os.WriteFile(filepath.Join(exportDir, "runtime-health.json"), []byte(body), 0644); err != nil { t.Fatal(err) } handler := NewHandler(HandlerOptions{ExportDir: exportDir}) rec := httptest.NewRecorder() handler.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/runtime-health.json", nil)) if rec.Code != http.StatusOK { t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String()) } if strings.TrimSpace(rec.Body.String()) != body { t.Fatalf("body=%q want %q", strings.TrimSpace(rec.Body.String()), body) } } func TestDashboardRendersRuntimeHealthTable(t *testing.T) { dir := t.TempDir() path := filepath.Join(dir, "audit.json") exportDir := filepath.Join(dir, "export") if err := os.MkdirAll(exportDir, 0755); err != nil { t.Fatal(err) } if err := os.WriteFile(path, []byte(`{"collected_at":"2026-03-15T00:00:00Z","hardware":{"board":{"serial_number":"SERIAL-1"}}}`), 0644); err != nil { t.Fatal(err) } health := `{ "status":"PARTIAL", "checked_at":"2026-03-16T10:00:00Z", "export_dir":"/tmp/export", "driver_ready":true, "cuda_ready":false, "network_status":"PARTIAL", "issues":[ {"code":"dhcp_partial","description":"At least one interface did not obtain IPv4 connectivity."}, {"code":"cuda_runtime_not_ready","description":"CUDA runtime is not ready for GPU SAT."} ], "tools":[ {"name":"dmidecode","ok":true}, {"name":"nvidia-smi","ok":false} ], "services":[ {"name":"bee-web","status":"active"}, {"name":"bee-nvidia","status":"inactive"} ] }` if err := os.WriteFile(filepath.Join(exportDir, "runtime-health.json"), []byte(health), 0644); err != nil { t.Fatal(err) } componentStatus := `[ { "component_key":"cpu:all", "status":"Warning", "error_summary":"cpu SAT: FAILED", "history":[{"at":"2026-03-16T10:00:00Z","status":"Warning","source":"sat:cpu","detail":"cpu SAT: FAILED"}] }, { "component_key":"memory:all", "status":"OK", "history":[{"at":"2026-03-16T10:01:00Z","status":"OK","source":"sat:memory","detail":"memory SAT: OK"}] }, { "component_key":"storage:nvme0n1", "status":"Critical", "error_summary":"storage SAT: FAILED", "history":[{"at":"2026-03-16T10:02:00Z","status":"Critical","source":"sat:storage","detail":"storage SAT: FAILED"}] }, { "component_key":"pcie:gpu:nvidia", "status":"Warning", "error_summary":"nvidia SAT: FAILED", "history":[{"at":"2026-03-16T10:03:00Z","status":"Warning","source":"sat:nvidia","detail":"nvidia SAT: FAILED"}] } ]` if err := os.WriteFile(filepath.Join(exportDir, "component-status.json"), []byte(componentStatus), 0644); err != nil { t.Fatal(err) } handler := NewHandler(HandlerOptions{AuditPath: path, ExportDir: exportDir}) rec := httptest.NewRecorder() handler.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/", nil)) if rec.Code != http.StatusOK { t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String()) } body := rec.Body.String() for _, needle := range []string{ `Runtime Health`, `CheckStatusSourceIssue`, `Export Directory`, `Network`, `NVIDIA/AMD Driver`, `CUDA / ROCm`, `Required Utilities`, `Bee Services`, `CPU`, `Memory`, `Storage`, `GPU`, `CUDA runtime is not ready for GPU SAT.`, `Missing: nvidia-smi`, `bee-nvidia=inactive`, `cpu SAT: FAILED`, `storage SAT: FAILED`, `sat:nvidia`, } { if !strings.Contains(body, needle) { t.Fatalf("dashboard missing %q: %s", needle, body) } } }