HPL 2.3 from netlib compiled against OpenBLAS with a minimal single-process MPI stub — no MPI package required in the ISO. Matrix size is auto-sized to 80% of total RAM at runtime. Build: - VERSIONS: HPL_VERSION=2.3, HPL_SHA256=32c5c17d… - build-hpl.sh: downloads HPL + OpenBLAS from Debian 12 repo, compiles xhpl with a self-contained mpi_stub.c - build.sh: step 80-hpl, injects xhpl + libopenblas into overlay Runtime: - bee-hpl: generates HPL.dat (N auto from /proc/meminfo, NB=256, P=1 Q=1), runs xhpl, prints standard WR... Gflops output - platform/hpl.go: RunHPL(), parses WR line → GFlops + PASSED/FAILED - tasks.go: target "hpl" - pages.go: LINPACK (HPL) card in validate/stress grid (stress-only) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1121 lines
34 KiB
Go
1121 lines
34 KiB
Go
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</button>`) {
|
|
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</button>`) || !strings.Contains(body, `Next</button>`) {
|
|
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</button>`) {
|
|
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`,
|
|
`<code>dcgmi diag targeted_stress</code>`,
|
|
`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(`<div class="card"><div class="card-head">Task Report</div><div class="card-body">saved report</div></div>`), 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</button>`) {
|
|
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`,
|
|
`<th>Check</th><th>Status</th><th>Source</th><th>Issue</th>`,
|
|
`Export Directory`,
|
|
`Network`,
|
|
`NVIDIA/AMD Driver`,
|
|
`CUDA / ROCm`,
|
|
`Required Utilities`,
|
|
`Bee Services`,
|
|
`<td>CPU</td>`,
|
|
`<td>Memory</td>`,
|
|
`<td>Storage</td>`,
|
|
`<td>GPU</td>`,
|
|
`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)
|
|
}
|
|
}
|
|
}
|