package server import ( "encoding/json" "net/http" "net/http/httptest" "testing" "git.mchus.pro/mchus/logpile/internal/models" ) func TestBuildHardwareDevices_DedupSerialThenBDF(t *testing.T) { hw := &models.HardwareConfig{ PCIeDevices: []models.PCIeDevice{ {Slot: "A1", SerialNumber: "SER-1", BDF: "0000:01:00.0", DeviceClass: "NetworkController"}, {Slot: "A2", SerialNumber: "SER-1", BDF: "0000:02:00.0", DeviceClass: "NetworkController"}, {Slot: "B1", SerialNumber: "", BDF: "0000:03:00.0", DeviceClass: "NetworkController"}, {Slot: "B2", SerialNumber: "", BDF: "0000:03:00.0", DeviceClass: "NetworkController"}, {Slot: "C1", SerialNumber: "", BDF: "", DeviceClass: "NetworkController"}, {Slot: "C2", SerialNumber: "", BDF: "", DeviceClass: "NetworkController"}, }, } devices := BuildHardwareDevices(hw) // 1 board + (SER-1 dedup -> 1) + (BDF 03 dedup -> 1) + (C1,C2 keep both) = 5 if len(devices) != 5 { t.Fatalf("expected 5 devices after dedupe, got %d", len(devices)) } bySlot := map[string]bool{} for _, d := range devices { bySlot[d.Slot] = true } if !bySlot["A1"] && !bySlot["A2"] { t.Fatalf("expected one serial-deduped A* device") } if bySlot["B1"] && bySlot["B2"] { t.Fatalf("expected B1/B2 to dedupe by bdf") } if !bySlot["C1"] || !bySlot["C2"] { t.Fatalf("expected C1 and C2 to remain without serial/bdf") } } func TestBuildHardwareDevices_SkipsEmptyMemorySlots(t *testing.T) { hw := &models.HardwareConfig{ Memory: []models.MemoryDIMM{ {Slot: "A1", Present: true, SizeMB: 32768, SerialNumber: "DIMM-1"}, {Slot: "A2", Present: false, SizeMB: 0, SerialNumber: "DIMM-2"}, }, } devices := BuildHardwareDevices(hw) memoryCount := 0 for _, d := range devices { if d.Kind == models.DeviceKindMemory { memoryCount++ if d.Slot == "A2" { t.Fatalf("empty memory slot should not be included") } } } if memoryCount != 1 { t.Fatalf("expected 1 installed memory record, got %d", memoryCount) } } func TestBuildHardwareDevices_MemorySameSerialDifferentSlots_NotDeduped(t *testing.T) { hw := &models.HardwareConfig{ Memory: []models.MemoryDIMM{ {Slot: "Node0_Dimm1", Location: "Node0_Bank0", Present: true, SizeMB: 16384, SerialNumber: "238F7649", PartNumber: "M393B2G70BH0-"}, {Slot: "Node0_Dimm3", Location: "Node0_Bank0", Present: true, SizeMB: 16384, SerialNumber: "238F7649", PartNumber: "M393B2G70BH0-"}, }, } devices := BuildHardwareDevices(hw) memorySlots := make(map[string]bool) for _, d := range devices { if d.Kind != models.DeviceKindMemory { continue } memorySlots[d.Slot] = true } if len(memorySlots) != 2 { t.Fatalf("expected 2 memory devices, got %d", len(memorySlots)) } if !memorySlots["Node0_Dimm1"] || !memorySlots["Node0_Dimm3"] { t.Fatalf("expected both Node0_Dimm1 and Node0_Dimm3 to remain") } } func TestBuildHardwareDevices_DuplicateSerials_AreAnnotated(t *testing.T) { hw := &models.HardwareConfig{ Memory: []models.MemoryDIMM{ {Slot: "A1", Location: "BANK0", Present: true, SizeMB: 16384, SerialNumber: "SN-1"}, {Slot: "A2", Location: "BANK1", Present: true, SizeMB: 16384, SerialNumber: "SN-1"}, }, } devices := BuildHardwareDevices(hw) var serials []string for _, d := range devices { if d.Kind == models.DeviceKindMemory { serials = append(serials, d.SerialNumber) } } if len(serials) != 2 { t.Fatalf("expected 2 memory devices, got %d", len(serials)) } if serials[0] != "SN-1 (DUP#1)" || serials[1] != "SN-1 (DUP#2)" { t.Fatalf("unexpected annotated serials: %+v", serials) } } func TestBuildHardwareDevices_DedupCrossKindByBDF(t *testing.T) { hw := &models.HardwareConfig{ PCIeDevices: []models.PCIeDevice{ { Slot: "SL0CP0_001", BDF: "02:00.0", DeviceClass: "DisplayController", VendorID: 0x1a03, DeviceID: 0x2000, PartNumber: "ASPEED Graphics Family", Manufacturer: "ASPEED Technology, Inc.", }, }, GPUs: []models.GPU{ { Slot: "SL0CP0_001", BDF: "02:00.0", Model: "ASPEED Graphics Family", Manufacturer: "ASPEED Technology, Inc.", VendorID: 0x1a03, DeviceID: 0x2000, }, }, } devices := BuildHardwareDevices(hw) count := 0 for _, d := range devices { if d.BDF == "02:00.0" { count++ } } if count != 1 { t.Fatalf("expected 1 canonical device for bdf 02:00.0, got %d", count) } } func TestBuildHardwareDevices_SkipsFirmwareOnlyNumericSlots(t *testing.T) { hw := &models.HardwareConfig{ PCIeDevices: []models.PCIeDevice{ {Slot: "0", DeviceClass: "Unknown", Manufacturer: "-", PartNumber: "-", Description: "-"}, {Slot: "1", DeviceClass: "Other", Manufacturer: "unknown", PartNumber: "N/A", Description: "NULL"}, }, } devices := BuildHardwareDevices(hw) for _, d := range devices { if d.Kind == models.DeviceKindPCIe && (d.Slot == "0" || d.Slot == "1") { t.Fatalf("firmware-only numeric-slot pcie record must be filtered, got slot %q", d.Slot) } } } func TestHandleGetConfig_ReturnsCanonicalHardware(t *testing.T) { srv := &Server{} srv.SetResult(&models.AnalysisResult{ Hardware: &models.HardwareConfig{ BoardInfo: models.BoardInfo{ProductName: "X", SerialNumber: "SN-1"}, CPUs: []models.CPU{{Socket: 0, Model: "CPU"}}, }, }) req := httptest.NewRequest(http.MethodGet, "/api/config", nil) w := httptest.NewRecorder() srv.handleGetConfig(w, req) if w.Code != http.StatusOK { t.Fatalf("expected 200, got %d", w.Code) } var payload map[string]any if err := json.NewDecoder(w.Body).Decode(&payload); err != nil { t.Fatalf("decode response: %v", err) } hardware, ok := payload["hardware"].(map[string]any) if !ok { t.Fatalf("expected hardware object") } if _, ok := hardware["devices"]; !ok { t.Fatalf("expected hardware.devices in config response") } if _, ok := hardware["cpus"]; ok { t.Fatalf("did not expect legacy hardware.cpus in config response") } }