Files
bee/audit/internal/webui/page_topo_test.go
T
Mikhail ChusavitinandClaude Sonnet 5 59271fa674 feat(webui): break the topology PSU row into one card per supply
Each PSU is its own card, coloured by its own status — a failed unit
goes red on its own instead of dragging a single grouped card down —
and shows input voltage + draw (measured output/input, else nameplate
rating). Cards click through to the PSU detail modal and carry data-psu
so the shared topoLiveScript refreshes their wattage from
/api/metrics/latest on the same 5s poll as the fan tiles.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019VHG21rgTUiR1G3qFHTVmN
2026-09-04 12:32:56 +03:00

936 lines
31 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package webui
import (
"encoding/json"
"net/http"
"net/http/httptest"
"os"
"path/filepath"
"strconv"
"strings"
"testing"
"bee/audit/internal/schema"
)
func TestTopoPageNoAuditDataGracefulFallback(t *testing.T) {
handler := NewHandler(HandlerOptions{})
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/topo", nil))
if rec.Code != http.StatusOK {
t.Fatalf("status=%d", rec.Code)
}
body := rec.Body.String()
if !strings.Contains(body, "No audit data") {
t.Fatalf("topo page missing no-audit-data fallback: %s", body)
}
}
func TestTopoPageRendersCPUAndDegradedPCIeLink(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "audit.json")
socket := 0
numaNode := 0
gen3, gen4 := "Gen3", "Gen4"
deviceClass := "VideoController"
model := "NVIDIA H100 80GB HBM3"
cpuModel := "Intel Xeon 6530"
okStatus := "OK"
ingest := schema.HardwareIngestRequest{
CollectedAt: "2026-03-15T00:00:00Z",
Hardware: schema.HardwareSnapshot{
CPUs: []schema.HardwareCPU{
{
HardwareComponentStatus: schema.HardwareComponentStatus{Status: &okStatus},
Socket: &socket,
Model: &cpuModel,
},
},
PCIeDevices: []schema.HardwarePCIeDevice{
{
DeviceClass: &deviceClass,
Model: &model,
NUMANode: &numaNode,
LinkSpeed: &gen3,
MaxLinkSpeed: &gen4,
},
},
},
}
data, err := json.Marshal(ingest)
if err != nil {
t.Fatal(err)
}
if err := os.WriteFile(path, data, 0644); err != nil {
t.Fatal(err)
}
handler := NewHandler(HandlerOptions{AuditPath: path})
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/topo", nil))
if rec.Code != http.StatusOK {
t.Fatalf("status=%d", rec.Code)
}
body := rec.Body.String()
if !strings.Contains(body, "CPU 0") {
t.Fatalf("topo page missing CPU 0 box: %s", body)
}
if !strings.Contains(body, "GPU") {
t.Fatalf("topo page missing GPU box: %s", body)
}
if !strings.Contains(body, "var(--warn-fg)") {
t.Fatalf("topo page missing degraded-link warn edge color: %s", body)
}
}
func TestTopoPageRendersArbitraryPSUAndFirmwareCountsAsFlexRows(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "audit.json")
okStatus := "OK"
failStatus := "Critical"
watt := 3000
volt := 230.0
var psus []schema.HardwarePowerSupply
for i := 0; i < 6; i++ {
slot := strconv.Itoa(i)
st := okStatus
if i == 3 {
st = failStatus
}
psus = append(psus, schema.HardwarePowerSupply{
HardwareComponentStatus: schema.HardwareComponentStatus{Status: &st},
Slot: &slot,
WattageW: &watt,
InputVoltage: &volt,
})
}
ingest := schema.HardwareIngestRequest{
CollectedAt: "2026-03-15T00:00:00Z",
Hardware: schema.HardwareSnapshot{
Firmware: []schema.HardwareFirmwareRecord{
{DeviceName: "BIOS", Version: "2.1.0"},
{DeviceName: "BMC", Version: "5.17.00"},
},
PowerSupplies: psus,
},
}
data, err := json.Marshal(ingest)
if err != nil {
t.Fatal(err)
}
if err := os.WriteFile(path, data, 0644); err != nil {
t.Fatal(err)
}
handler := NewHandler(HandlerOptions{AuditPath: path})
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/topo", nil))
if rec.Code != http.StatusOK {
t.Fatalf("status=%d", rec.Code)
}
body := rec.Body.String()
// Firmware row must render every record (BIOS + BMC, not just BMC).
if !strings.Contains(body, "BIOS") || !strings.Contains(body, "BMC") {
t.Fatalf("topo page missing BIOS/BMC firmware boxes: %s", body)
}
// One card per PSU (6), each clickable, each showing voltage + power.
if n := strings.Count(body, `class="topo-psu-tile"`); n != 6 {
t.Fatalf("expected 6 per-PSU cards, got %d", n)
}
if n := strings.Count(body, `onclick="openComponentDetail('psu')"`) +
strings.Count(body, `onclick="openComponentDetail(&#39;psu&#39;)"`); n != 6 {
t.Fatalf("expected one clickable card per PSU (6), got %d", n)
}
if !strings.Contains(body, "230 V · 3000 W rated") {
t.Fatalf("PSU card missing voltage/power line: %s", body)
}
// The one failed PSU is coloured red on its own (crit token) and labelled.
if !strings.Contains(body, "var(--crit-bg)") || !strings.Contains(body, "CRITICAL") {
t.Fatalf("failed PSU should render individually as critical: %s", body)
}
// Firmware/PSU rows must be flex-wrap HTML (arbitrary count, no overlap),
// not absolutely-positioned SVG rects sharing fixed x/y coordinates.
if !strings.Contains(body, "flex-wrap:wrap") {
t.Fatalf("topo page missing flex-wrap layout for firmware/PSU rows: %s", body)
}
// Firmware row must come before the PSU row.
if strings.Index(body, "BIOS") > strings.Index(body, "Power Supplies") {
t.Fatalf("firmware row should render before PSU row: %s", body)
}
}
func TestTopoPageRendersCoolingFansAsFlexRow(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "audit.json")
ok, warn := "OK", "Warning"
rpm := func(v int) *int { return &v }
ingest := schema.HardwareIngestRequest{
CollectedAt: "2026-03-15T00:00:00Z",
Hardware: schema.HardwareSnapshot{
Sensors: &schema.HardwareSensors{
Fans: []schema.HardwareFanSensor{
{Name: "FAN1", RPM: rpm(4200), Status: &ok},
{Name: "FAN2", RPM: rpm(15000), Status: &warn},
{Name: "FAN2", RPM: rpm(15000), Status: &warn}, // dup name, first wins
},
},
},
}
data, err := json.Marshal(ingest)
if err != nil {
t.Fatal(err)
}
if err := os.WriteFile(path, data, 0644); err != nil {
t.Fatal(err)
}
handler := NewHandler(HandlerOptions{AuditPath: path})
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/topo", nil))
if rec.Code != http.StatusOK {
t.Fatalf("status=%d", rec.Code)
}
body := rec.Body.String()
// One clickable square per fan (2 after dedup by name), each with a
// spinning glyph, under a COOLING heading.
if !strings.Contains(body, "Cooling") {
t.Fatalf("topo page missing Cooling heading: %s", body)
}
if n := strings.Count(body, `onclick="openComponentDetail('fan')"`) +
strings.Count(body, `onclick="openComponentDetail(&#39;fan&#39;)"`); n != 2 {
t.Fatalf("expected one clickable square per fan (2), got %d: %s", n, body)
}
if n := strings.Count(body, `class="topo-fan-spin"`); n != 2 {
t.Fatalf("expected 2 spinning fan glyphs, got %d: %s", n, body)
}
if !strings.Contains(body, "FAN1 · 4200 RPM") || !strings.Contains(body, "FAN2 · 15000 RPM") {
t.Fatalf("topo page missing per-fan RPM tooltips: %s", body)
}
// No observed ceiling in this test → tiles show no duty fill and say so.
if !strings.Contains(body, "ceiling not measured") {
t.Fatalf("topo fan tooltip should note the ceiling is unmeasured: %s", body)
}
// Live-update: each tile is addressable and the poll script is present.
if n := strings.Count(body, `class="topo-fan-tile"`); n != 2 {
t.Fatalf("expected 2 addressable fan tiles, got %d", n)
}
if !strings.Contains(body, `data-fan="FAN1"`) || !strings.Contains(body, "/api/metrics/latest") {
t.Fatalf("topo fan row missing live-update wiring: %s", body)
}
// Component-detail fallback endpoint must resolve the "fan" type.
rec2 := httptest.NewRecorder()
handler.ServeHTTP(rec2, httptest.NewRequest(http.MethodGet, "/api/components/fan", nil))
if rec2.Code != http.StatusOK {
t.Fatalf("/api/components/fan status=%d", rec2.Code)
}
if b := rec2.Body.String(); !strings.Contains(b, "FAN1") || !strings.Contains(b, "FAN2") {
t.Fatalf("fan component detail missing fan names: %s", b)
}
}
func TestFanSpinPeriodSec(t *testing.T) {
// Clamped at both ends; monotonically faster (smaller period) with RPM.
if got := fanSpinPeriodSec(200); got != 2.2 {
t.Fatalf("low RPM: got %v want 2.2 (slowest visible)", got)
}
if got := fanSpinPeriodSec(25000); got != 0.35 {
t.Fatalf("high RPM: got %v want 0.35 (fastest visible)", got)
}
mid := fanSpinPeriodSec(7000)
if mid <= 0.35 || mid >= 2.2 {
t.Fatalf("mid RPM period %v out of band", mid)
}
if fanSpinPeriodSec(10000) >= fanSpinPeriodSec(3000) {
t.Fatalf("higher RPM must spin faster (shorter period)")
}
}
func TestTopoPageRendersStorageDisksGroupedByType(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "audit.json")
okStatus := "OK"
warnStatus := "WARNING"
ssd := "SSD"
nvme := "NVMe"
model := "SAMSUNG MZ7L3960HCJR-00B7C"
size := 960
ingest := schema.HardwareIngestRequest{
CollectedAt: "2026-03-15T00:00:00Z",
Hardware: schema.HardwareSnapshot{
Storage: []schema.HardwareStorage{
{HardwareComponentStatus: schema.HardwareComponentStatus{Status: &okStatus}, Type: &ssd, Model: &model, SizeGB: &size},
{HardwareComponentStatus: schema.HardwareComponentStatus{Status: &okStatus}, Type: &ssd, Model: &model, SizeGB: &size},
{HardwareComponentStatus: schema.HardwareComponentStatus{Status: &warnStatus}, Type: &nvme, SizeGB: &size},
},
},
}
data, err := json.Marshal(ingest)
if err != nil {
t.Fatal(err)
}
if err := os.WriteFile(path, data, 0644); err != nil {
t.Fatal(err)
}
handler := NewHandler(HandlerOptions{AuditPath: path})
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/topo", nil))
if rec.Code != http.StatusOK {
t.Fatalf("status=%d", rec.Code)
}
body := rec.Body.String()
if !strings.Contains(body, "SSD ×2") {
t.Fatalf("topo page missing grouped SSD x2 card: %s", body)
}
if !strings.Contains(body, "NVMe") {
t.Fatalf("topo page missing NVMe disk card: %s", body)
}
// SSD card totals capacity across both disks.
if !strings.Contains(body, "1.9 TB total") {
t.Fatalf("topo page missing SSD total capacity: %s", body)
}
// The degraded NVMe disk must escalate that card's status.
nvmeIdx := strings.Index(body, ">NVMe<")
if nvmeIdx < 0 || !strings.Contains(body[nvmeIdx:nvmeIdx+400], "Warning") {
t.Fatalf("topo page missing NVMe warning status: %s", body)
}
// With no storage-controller techdump, disks have no resolvable socket and
// land under the "Other" bar rather than being glued to a CPU.
if !strings.Contains(body, ">Other<") {
t.Fatalf("topo page missing Other bar for unattached disks: %s", body)
}
clicks := strings.Count(body, `openComponentDetail('storage')`) + strings.Count(body, `openComponentDetail(&#39;storage&#39;)`)
if clicks != 2 {
t.Fatalf("expected one clickable card per disk-type group, got %d: %s", clicks, body)
}
}
func TestParseStorageControllerMap(t *testing.T) {
raw := "sda hctl=2:0:0:0 ctrl=0000:00:17.0\n" +
"sdb hctl=3:0:0:0 ctrl=0000:00:17.0\n" +
"nvme0n1 hctl= ctrl=0000:65:00.0\n"
got := parseStorageControllerMap(raw)
if got["2:0:0:0"] != "0000:00:17.0" || got["3:0:0:0"] != "0000:00:17.0" {
t.Fatalf("SATA disks not mapped to controller: %#v", got)
}
if _, ok := got["nvme0n1"]; ok {
t.Fatalf("NVMe line with empty hctl must be skipped: %#v", got)
}
}
func TestTopoPageParentsDisksUnderTheirControllerSocket(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "audit.json")
techdump := filepath.Join(dir, "techdump")
if err := os.MkdirAll(techdump, 0755); err != nil {
t.Fatal(err)
}
// Both SSDs hang off the SATA controller at 0000:00:17.0, which is a
// NUMA-node-0 PCIe device -> they must render under CPU 0, not "Other".
if err := os.WriteFile(filepath.Join(techdump, "storage-controllers.txt"),
[]byte("sda hctl=2:0:0:0 ctrl=0000:00:17.0\nsdb hctl=3:0:0:0 ctrl=0000:00:17.0\n"), 0644); err != nil {
t.Fatal(err)
}
socket0, socket1 := 0, 1
numa0 := 0
okStatus := "OK"
sataClass := "SATA controller"
sataBDF := "0000:00:17.0"
hctlA, hctlB := "2:0:0:0", "3:0:0:0"
ssd := "SSD"
size := 960
ingest := schema.HardwareIngestRequest{
CollectedAt: "2026-03-15T00:00:00Z",
Hardware: schema.HardwareSnapshot{
CPUs: []schema.HardwareCPU{
{HardwareComponentStatus: schema.HardwareComponentStatus{Status: &okStatus}, Socket: &socket0},
{HardwareComponentStatus: schema.HardwareComponentStatus{Status: &okStatus}, Socket: &socket1},
},
PCIeDevices: []schema.HardwarePCIeDevice{
{HardwareComponentStatus: schema.HardwareComponentStatus{Status: &okStatus}, Slot: &sataBDF, DeviceClass: &sataClass, NUMANode: &numa0},
},
Storage: []schema.HardwareStorage{
{HardwareComponentStatus: schema.HardwareComponentStatus{Status: &okStatus}, Slot: &hctlA, Type: &ssd, SizeGB: &size},
{HardwareComponentStatus: schema.HardwareComponentStatus{Status: &okStatus}, Slot: &hctlB, Type: &ssd, SizeGB: &size},
},
},
}
data, _ := json.Marshal(ingest)
if err := os.WriteFile(path, data, 0644); err != nil {
t.Fatal(err)
}
handler := NewHandler(HandlerOptions{AuditPath: path, ExportDir: dir})
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/topo", nil))
body := rec.Body.String()
if strings.Contains(body, ">Other<") {
t.Fatalf("disks resolved to a socket — no Other bar expected: %s", body)
}
if !strings.Contains(body, "SATA ctrl") {
t.Fatalf("topo page missing the SATA controller branch node: %s", body)
}
if !strings.Contains(body, "SSD ×2") {
t.Fatalf("topo page missing SSD disk group under the controller: %s", body)
}
// controller branch must render before its SSD sub-node
if strings.Index(body, "SATA ctrl") > strings.Index(body, "SSD ×2") {
t.Fatalf("controller node should render before its disks: %s", body)
}
}
func TestTopoPageRendersOneBarPerSocketNoRoot(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "audit.json")
socket0, socket1 := 0, 1
okStatus := "OK"
board := "AS-4125GS-TNRT"
vendor := "Supermicro"
ingest := schema.HardwareIngestRequest{
CollectedAt: "2026-03-15T00:00:00Z",
Hardware: schema.HardwareSnapshot{
Board: schema.HardwareBoard{ProductName: &board, Manufacturer: &vendor},
CPUs: []schema.HardwareCPU{
{HardwareComponentStatus: schema.HardwareComponentStatus{Status: &okStatus}, Socket: &socket0},
{HardwareComponentStatus: schema.HardwareComponentStatus{Status: &okStatus}, Socket: &socket1},
},
},
}
data, _ := json.Marshal(ingest)
if err := os.WriteFile(path, data, 0644); err != nil {
t.Fatal(err)
}
handler := NewHandler(HandlerOptions{AuditPath: path})
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/topo", nil))
body := rec.Body.String()
if !strings.Contains(body, ">CPU 0<") || !strings.Contains(body, ">CPU 1<") {
t.Fatalf("topo diagram missing a bar per CPU socket: %s", body)
}
// No board/root node in the diagram itself (board identity is the
// Firmware row lower down).
svg := body[strings.Index(body, "<svg"):]
svg = svg[:strings.Index(svg, "</svg>")]
if strings.Contains(svg, board) {
t.Fatalf("board node must not render inside the topology diagram: %s", svg)
}
}
func TestTopoPageLinkedFromNav(t *testing.T) {
handler := NewHandler(HandlerOptions{})
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/", nil))
if rec.Code != http.StatusOK {
t.Fatalf("status=%d", rec.Code)
}
if !strings.Contains(rec.Body.String(), `href="/topo"`) {
t.Fatalf("nav missing /topo link")
}
}
func TestParseGPUPairAdjacencyRealTwoGPUDump(t *testing.T) {
// Real system/nvidia-smi-topo.txt from a support bundle for this exact
// server: two H100s directly bridged (NV17), spanning two NUMA nodes.
input := "\tGPU0\tGPU1\tNIC0\tNIC1\tCPU Affinity\tNUMA Affinity\tGPU NUMA ID\n" +
"GPU0\t X \tNV17\tSYS\tSYS\t0-23,48-71\t0\t\tN/A\n" +
"GPU1\tNV17\t X \tNODE\tNODE\t24-47,72-95\t1\t\tN/A\n" +
"NIC0\tSYS\tNODE\t X \tPIX\t\t\t\n" +
"NIC1\tSYS\tNODE\tPIX\t X \t\t\t\n"
pairs := parseGPUPairAdjacency(input)
if len(pairs) != 1 {
t.Fatalf("pairs=%d want 1 (%#v)", len(pairs), pairs)
}
if pairs[0].GPUA != 0 || pairs[0].GPUB != 1 || pairs[0].NVLinks != 17 {
t.Fatalf("pair=%#v want {0,1,17}", pairs[0])
}
}
func TestParseGPUPairAdjacencyDoesNotChainUnrelatedGPUs(t *testing.T) {
// 4 GPUs: only (0,1) and (2,3) are actually bonded. GPU1 and GPU2 must
// NOT get an edge just because they're adjacent in the layout.
input := "\tGPU0\tGPU1\tGPU2\tGPU3\n" +
"GPU0\t X \tNV18\tSYS\tSYS\n" +
"GPU1\tNV18\t X \tSYS\tSYS\n" +
"GPU2\tSYS\tSYS\t X \tNV18\n" +
"GPU3\tSYS\tSYS\tNV18\t X \n"
pairs := parseGPUPairAdjacency(input)
if len(pairs) != 2 {
t.Fatalf("pairs=%d want 2 (%#v)", len(pairs), pairs)
}
want := map[[2]int]bool{{0, 1}: true, {2, 3}: true}
for _, p := range pairs {
if !want[[2]int{p.GPUA, p.GPUB}] {
t.Fatalf("unexpected pair %#v (GPU1-GPU2 chaining bug?)", p)
}
}
}
func TestParseGPUPairAdjacencyANSIUnderlinedHeader(t *testing.T) {
// Real techdump capture: nvidia-smi underlines the header row with ANSI
// escapes even when writing to a file, so the header line starts with
// ESC[4m, not "GPU0".
input := "\x1b[4m\tGPU0\tGPU1\tGPU2\tGPU3\tNIC0\tNIC1\tCPU Affinity\x1b[0m\n" +
"GPU0\t X \tNV18\tPIX\tPIX\tNODE\tNODE\t0-31,64-95\t0\n" +
"GPU1\tNV18\t X \tPIX\tPIX\tNODE\tNODE\t0-31,64-95\t0\n" +
"GPU2\tPIX\tPIX\t X \tNV18\tNODE\tNODE\t0-31,64-95\t0\n" +
"GPU3\tPIX\tPIX\tNV18\t X \tNODE\tNODE\t0-31,64-95\t0\n" +
"NIC0\tNODE\tNODE\tNODE\tNODE\t X \tPIX\n" +
"NIC1\tNODE\tNODE\tNODE\tNODE\tPIX\t X \n"
pairs := parseGPUPairAdjacency(input)
if len(pairs) != 2 {
t.Fatalf("pairs=%d want 2 (%#v)", len(pairs), pairs)
}
want := map[[2]int]bool{{0, 1}: true, {2, 3}: true}
for _, p := range pairs {
if !want[[2]int{p.GPUA, p.GPUB}] || p.NVLinks != 18 {
t.Fatalf("unexpected pair %#v", p)
}
}
}
func TestParseGPUPairAdjacencyEmptyOnNoMatrix(t *testing.T) {
if pairs := parseGPUPairAdjacency("no gpus here"); pairs != nil {
t.Fatalf("pairs=%v want nil", pairs)
}
}
func TestPcieGenRank(t *testing.T) {
if pcieGenRank("Gen5") <= pcieGenRank("Gen4") {
t.Fatalf("Gen5 should rank higher than Gen4")
}
if pcieGenRank("bogus") != 0 {
t.Fatalf("unparseable gen should rank 0")
}
}
func TestTopoEdgeColorVar(t *testing.T) {
gen3, gen4 := "Gen3", "Gen4"
degraded := schema.HardwarePCIeDevice{LinkSpeed: &gen3, MaxLinkSpeed: &gen4}
if got := topoEdgeColorVar(degraded); got != "var(--warn-fg)" {
t.Fatalf("degraded color=%q want warn", got)
}
full := schema.HardwarePCIeDevice{LinkSpeed: &gen4, MaxLinkSpeed: &gen4}
if got := topoEdgeColorVar(full); got != "var(--ok-fg)" {
t.Fatalf("full-speed color=%q want ok", got)
}
unknown := schema.HardwarePCIeDevice{}
if got := topoEdgeColorVar(unknown); got != "var(--muted)" {
t.Fatalf("unknown color=%q want muted", got)
}
}
func TestBuildSocketIndex(t *testing.T) {
s0, s1 := 0, 1
cpus := []schema.HardwareCPU{{Socket: &s1}, {Socket: &s0}}
idx := buildSocketIndex(cpus)
if idx[0] != 1 || idx[1] != 0 {
t.Fatalf("idx=%#v want {0:1, 1:0}", idx)
}
}
func TestBuildSocketIndexOneIndexedSocketDesignation(t *testing.T) {
// dmidecode "Socket Designation" is frequently 1-indexed ("CPU1", "CPU2")
// while Linux NUMA nodes are always 0-indexed. NUMA node 0 must still
// resolve to the first CPU in Socket order, not fall through to the
// "unknown" column (the bug reported against the /topo page).
s1, s2 := 1, 2
cpus := []schema.HardwareCPU{{Socket: &s1}, {Socket: &s2}}
idx := buildSocketIndex(cpus)
if idx[0] != 0 || idx[1] != 1 {
t.Fatalf("idx=%#v want {0:0, 1:1}", idx)
}
}
func TestIsRAIDControllerClass(t *testing.T) {
if !isRAIDControllerClass("StorageController") || !isRAIDControllerClass("MassStorageController") {
t.Fatalf("expected known RAID/storage classes to match")
}
if isRAIDControllerClass("VideoController") {
t.Fatalf("GPU class should not match RAID classifier")
}
}
func TestIsNICDeviceClassDev(t *testing.T) {
class := "EthernetController"
nic := schema.HardwarePCIeDevice{DeviceClass: &class}
if !isNICDeviceClassDev(nic) {
t.Fatalf("expected EthernetController to classify as NIC")
}
withMac := schema.HardwarePCIeDevice{MacAddresses: []string{"aa:bb:cc:dd:ee:ff"}}
if !isNICDeviceClassDev(withMac) {
t.Fatalf("expected device with MAC address to classify as NIC")
}
other := schema.HardwarePCIeDevice{}
if isNICDeviceClassDev(other) {
t.Fatalf("expected empty device to not classify as NIC")
}
}
func TestClassifyTopoSeverityNilIsUnknown(t *testing.T) {
if sev := classifyTopoSeverity(nil); sev != 0 {
t.Fatalf("nil status severity=%d want 0 (unknown)", sev)
}
ok := "OK"
if sev := classifyTopoSeverity(&ok); sev != 1 {
t.Fatalf("OK status severity=%d want 1", sev)
}
warn := "Warning"
if sev := classifyTopoSeverity(&warn); sev != 2 {
t.Fatalf("Warning status severity=%d want 2", sev)
}
crit := "Critical"
if sev := classifyTopoSeverity(&crit); sev != 3 {
t.Fatalf("Critical status severity=%d want 3", sev)
}
}
func TestTopoStatusTallyLine(t *testing.T) {
var t1 topoStatusTally
t1.add(1)
if got := t1.line(); got != "OK" {
t.Fatalf("single-OK line=%q want %q", got, "OK")
}
var t2 topoStatusTally
t2.add(1)
t2.add(1)
t2.add(1)
t2.add(2)
if got := t2.line(); got != "1 Warning, 3 OK" {
t.Fatalf("mixed line=%q want %q", got, "1 Warning, 3 OK")
}
}
func TestTopoMainDiagramGroupsSameKindSameColumnIntoOneStackedCard(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "audit.json")
socket := 0
numaNode := 0
deviceClass := "VideoController"
model := "NVIDIA H100 80GB HBM3"
okStatus := "OK"
var gpus []schema.HardwarePCIeDevice
for i := 0; i < 4; i++ {
gpus = append(gpus, schema.HardwarePCIeDevice{
HardwareComponentStatus: schema.HardwareComponentStatus{Status: &okStatus},
DeviceClass: &deviceClass,
Model: &model,
NUMANode: &numaNode,
})
}
ingest := schema.HardwareIngestRequest{
CollectedAt: "2026-03-15T00:00:00Z",
Hardware: schema.HardwareSnapshot{
CPUs: []schema.HardwareCPU{
{HardwareComponentStatus: schema.HardwareComponentStatus{Status: &okStatus}, Socket: &socket},
},
PCIeDevices: gpus,
},
}
data, err := json.Marshal(ingest)
if err != nil {
t.Fatal(err)
}
if err := os.WriteFile(path, data, 0644); err != nil {
t.Fatal(err)
}
handler := NewHandler(HandlerOptions{AuditPath: path})
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/topo", nil))
body := rec.Body.String()
if !strings.Contains(body, "GPU ×4") {
t.Fatalf("expected one grouped GPU x4 card, got: %s", body)
}
if strings.Count(body, "openComponentDetail('gpu')") != 1 {
t.Fatalf("expected exactly one clickable GPU card, not one per GPU: %s", body)
}
if !strings.Contains(body, "4 OK") {
t.Fatalf("expected group status line '4 OK': %s", body)
}
}
func TestDimmRawNodeFromCPULocatorPrefix(t *testing.T) {
slot := "CPU1_DIMM_A1"
mem := schema.HardwareMemory{Slot: &slot}
node, ok := dimmRawNode(mem, nil)
if !ok || node != 1 {
t.Fatalf("dimmRawNode() = (%d, %v), want (1, true)", node, ok)
}
}
func TestDimmRawNodeFromBankLocatorFallback(t *testing.T) {
// Boards whose Locator carries no CPU number of its own (e.g.
// "DIMM000(A)") still need to be attached to a column via the node
// number in Bank Locator, read separately from techdump.
slot := "DIMM100(A)"
mem := schema.HardwareMemory{Slot: &slot}
bankNodes := map[string]int{"DIMM100(A)": 2}
node, ok := dimmRawNode(mem, bankNodes)
if !ok || node != 2 {
t.Fatalf("dimmRawNode() = (%d, %v), want (2, true)", node, ok)
}
}
func TestDimmRawNodeUnmatched(t *testing.T) {
slot := "SOMETHING_UNRECOGNIZED"
mem := schema.HardwareMemory{Slot: &slot}
if _, ok := dimmRawNode(mem, nil); ok {
t.Fatalf("expected no match for an unrecognized locator")
}
if _, ok := dimmRawNode(schema.HardwareMemory{}, nil); ok {
t.Fatalf("expected no match when Slot is nil")
}
}
func TestParseDIMMBankLocatorNodes(t *testing.T) {
// Real dmidecode -t 17 shape: "Locator" precedes "Bank Locator" within
// each "Memory Device" section.
raw := `Handle 0x0017, DMI type 17, 92 bytes
Memory Device
Size: 64 GB
Locator: DIMM000(A)
Bank Locator: _Node1_Channel0_Dimm0
Type: DDR5
Handle 0x0018, DMI type 17, 92 bytes
Memory Device
Size: No Module Installed
Locator: DIMM001(I)
Bank Locator: _Node1_Channel0_Dimm1
Handle 0x0019, DMI type 17, 92 bytes
Memory Device
Size: 64 GB
Locator: DIMM100(A)
Bank Locator: _Node2_Channel0_Dimm0
`
got := parseDIMMBankLocatorNodes(raw)
want := map[string]int{"DIMM000(A)": 1, "DIMM001(I)": 1, "DIMM100(A)": 2}
if len(got) != len(want) {
t.Fatalf("got=%#v want=%#v", got, want)
}
for k, v := range want {
if got[k] != v {
t.Fatalf("got[%q]=%d want %d (full: %#v)", k, got[k], v, got)
}
}
}
func TestBuildMemoryColumnIndex(t *testing.T) {
// Node numbers observed on real boards are not guaranteed 0-based (Bank
// Locator "NodeN" has been seen starting at 1) — this must rank by
// order, not treat the raw value as a column index.
idx := buildMemoryColumnIndex([]int{1, 2, 1, 2})
if idx[1] != 0 || idx[2] != 1 {
t.Fatalf("idx=%#v want {1:0, 2:1}", idx)
}
}
// TestTopoMainDiagramAttachesMemoryToItsCPUColumn is the regression test for
// the actual feature request: memory used to render as one unattached
// "MEMORY" row below the whole diagram regardless of which CPU it belonged
// to. DIMMs whose Locator carries a CPU number must now render as their own
// box directly under that CPU, wired to it with an edge, the same as
// GPU/NIC/RAID — and must NOT also show up in the leftover flex row.
func TestTopoMainDiagramAttachesMemoryToItsCPUColumn(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "audit.json")
socket0, socket1 := 0, 1
okStatus := "OK"
slot0, slot1 := "CPU0_DIMM_A1", "CPU1_DIMM_A1"
sizeMB := 98304
ingest := schema.HardwareIngestRequest{
CollectedAt: "2026-03-15T00:00:00Z",
Hardware: schema.HardwareSnapshot{
CPUs: []schema.HardwareCPU{
{HardwareComponentStatus: schema.HardwareComponentStatus{Status: &okStatus}, Socket: &socket0},
{HardwareComponentStatus: schema.HardwareComponentStatus{Status: &okStatus}, Socket: &socket1},
},
Memory: []schema.HardwareMemory{
{HardwareComponentStatus: schema.HardwareComponentStatus{Status: &okStatus}, Slot: &slot0, SizeMB: &sizeMB},
{HardwareComponentStatus: schema.HardwareComponentStatus{Status: &okStatus}, Slot: &slot1, SizeMB: &sizeMB},
},
},
}
data, err := json.Marshal(ingest)
if err != nil {
t.Fatal(err)
}
if err := os.WriteFile(path, data, 0644); err != nil {
t.Fatal(err)
}
handler := NewHandler(HandlerOptions{AuditPath: path})
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/topo", nil))
body := rec.Body.String()
if strings.Count(body, `>Memory<`) != 2 {
t.Fatalf("expected 2 attached Memory boxes (one per CPU column), got body: %s", body)
}
if strings.Contains(body, "Memory</div>") {
t.Fatalf("both DIMMs matched a CPU column — the leftover unattached Memory row must not render: %s", body)
}
if strings.Count(body, "openComponentDetail('memory')") != 2 {
t.Fatalf("expected 2 clickable Memory boxes, got body: %s", body)
}
}
// TestTopoMainDiagramUnattachableMemoryFallsBackToFlexRow guards that a DIMM
// whose Locator can't be parsed into a CPU/node number still shows up
// somewhere (the old unattached row) instead of silently disappearing.
func TestTopoMainDiagramUnattachableMemoryFallsBackToFlexRow(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "audit.json")
socket0 := 0
okStatus := "OK"
slot := "SOMETHING_UNRECOGNIZED"
sizeMB := 32768
ingest := schema.HardwareIngestRequest{
CollectedAt: "2026-03-15T00:00:00Z",
Hardware: schema.HardwareSnapshot{
CPUs: []schema.HardwareCPU{
{HardwareComponentStatus: schema.HardwareComponentStatus{Status: &okStatus}, Socket: &socket0},
},
Memory: []schema.HardwareMemory{
{HardwareComponentStatus: schema.HardwareComponentStatus{Status: &okStatus}, Slot: &slot, SizeMB: &sizeMB},
},
},
}
data, err := json.Marshal(ingest)
if err != nil {
t.Fatal(err)
}
if err := os.WriteFile(path, data, 0644); err != nil {
t.Fatal(err)
}
handler := NewHandler(HandlerOptions{AuditPath: path})
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/topo", nil))
body := rec.Body.String()
if !strings.Contains(body, "Memory</div>") {
t.Fatalf("unattachable DIMM should still appear in the leftover flex row: %s", body)
}
}
func TestParseTopoNVLinkStatus(t *testing.T) {
input := `GPU 0: NVIDIA H100 80GB HBM3 (UUID: GPU-a59f6931-c099-8fba-a0b3-08469d86f140)
Link 0: 26.562 GB/s
Link 15: <inactive>
GPU 1: NVIDIA H100 80GB HBM3 (UUID: GPU-603fe750-0516-9db5-86ec-ea61af3fce35)
Link 0: 26.562 GB/s
`
got := parseTopoNVLinkStatus(input)
if len(got[0]) != 2 || got[0][1].Active {
t.Fatalf("gpu0=%#v want link15 inactive", got[0])
}
if len(got[1]) != 1 || got[1][0].SpeedGBs == nil || *got[1][0].SpeedGBs != 26.562 {
t.Fatalf("gpu1=%#v want link0 26.562 GB/s", got[1])
}
}
// TestComponentDetailFallsBackToInventoryWhenStatusDBEmpty covers the bug where
// a /topo card shows "3 OK" (from schema.HardwareComponentStatus.Status in the
// audit snapshot) but clicking it opens a modal saying "No status data recorded
// yet" (because ComponentStatusDB has no pcie:gpu:* entries — nothing has run a
// SAT test on this boot yet). The modal must show the same 3 devices/status the
// card does, not an empty state.
func TestComponentDetailFallsBackToInventoryWhenStatusDBEmpty(t *testing.T) {
dir := t.TempDir()
path := filepath.Join(dir, "audit.json")
okStatus := "OK"
warnStatus := "Warning"
deviceClass := "VideoController"
var gpus []schema.HardwarePCIeDevice
for i, st := range []*string{&okStatus, &okStatus, &warnStatus} {
slot := "0000:c" + strconv.Itoa(i) + ":00.0"
gpus = append(gpus, schema.HardwarePCIeDevice{
HardwareComponentStatus: schema.HardwareComponentStatus{Status: st},
DeviceClass: &deviceClass,
Slot: &slot,
})
}
ingest := schema.HardwareIngestRequest{
Hardware: schema.HardwareSnapshot{PCIeDevices: gpus},
}
data, err := json.Marshal(ingest)
if err != nil {
t.Fatal(err)
}
if err := os.WriteFile(path, data, 0644); err != nil {
t.Fatal(err)
}
// No HandlerOptions.App / StatusDB set — matches a host where nothing has
// written to ComponentStatusDB yet.
handler := NewHandler(HandlerOptions{AuditPath: path})
rec := httptest.NewRecorder()
handler.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/api/components/gpu", nil))
if rec.Code != http.StatusOK {
t.Fatalf("status=%d body=%s", rec.Code, rec.Body.String())
}
body := rec.Body.String()
if strings.Contains(body, "No status data recorded yet") {
t.Fatalf("modal should not show empty state when inventory has GPUs: %s", body)
}
if strings.Count(body, "chip-ok") != 2 {
t.Fatalf("expected 2 OK chips from inventory fallback: %s", body)
}
if strings.Count(body, "chip-warn") != 1 {
t.Fatalf("expected 1 Warning chip from inventory fallback: %s", body)
}
if !strings.Contains(body, "No SAT-test history yet") {
t.Fatalf("expected fallback marker text: %s", body)
}
}
func TestParseTopoNVLinkErrors(t *testing.T) {
input := `GPU 0: NVIDIA H100 80GB HBM3 (UUID: GPU-a59f6931-c099-8fba-a0b3-08469d86f140)
Link 0: Replay Errors: 0
Link 0: Recovery Errors: 0
Link 0: CRC Errors: 0
Link 1: Replay Errors: 3
Link 1: Recovery Errors: 1
Link 1: CRC Errors: 2
`
got := parseTopoNVLinkErrors(input)
c := got[0][1]
if c[0] != 3 || c[1] != 1 || c[2] != 2 {
t.Fatalf("link1 counters=%#v want {3,1,2}", c)
}
}