296 lines
8.8 KiB
Go
296 lines
8.8 KiB
Go
package easy_bee
|
|
|
|
import (
|
|
"testing"
|
|
"time"
|
|
|
|
"git.mchus.pro/mchus/logpile/internal/models"
|
|
"git.mchus.pro/mchus/logpile/internal/parser"
|
|
)
|
|
|
|
func TestDetectBeeSupportArchive(t *testing.T) {
|
|
p := &Parser{}
|
|
files := []parser.ExtractedFile{
|
|
{
|
|
Path: "bee-support-debian-20260325-162030/manifest.txt",
|
|
Content: []byte("bee_version=1.0.0\nhost=debian\ngenerated_at_utc=2026-03-25T16:20:30Z\nexport_dir=/appdata/bee/export\n"),
|
|
},
|
|
{
|
|
Path: "bee-support-debian-20260325-162030/export/bee-audit.json",
|
|
Content: []byte(`{"hardware":{"board":{"serial_number":"SN-BEE-001"}}}`),
|
|
},
|
|
{
|
|
Path: "bee-support-debian-20260325-162030/export/runtime-health.json",
|
|
Content: []byte(`{"status":"PARTIAL"}`),
|
|
},
|
|
}
|
|
|
|
if got := p.Detect(files); got < 90 {
|
|
t.Fatalf("expected high confidence detect score, got %d", got)
|
|
}
|
|
}
|
|
|
|
func TestCollectTopologyEvidenceSupportsCategorizedBundleAndPrefersFresh(t *testing.T) {
|
|
files := []parser.ExtractedFile{
|
|
{Path: "bundle/export/memory/dmidecode-type17.txt", Content: []byte("memory")},
|
|
{Path: "bundle/export/storage/storage-controllers.txt", Content: []byte("storage")},
|
|
{Path: "bundle/export/gpu/nvidia-smi-query.csv", Content: []byte("query")},
|
|
{Path: "bundle/export/gpu/nvidia-smi-topo.txt", Content: []byte("old")},
|
|
{Path: "bundle/export/gpu/nvidia-smi-topo-fresh.txt", Content: []byte("fresh")},
|
|
}
|
|
got := collectTopologyEvidence(files)
|
|
if got == nil {
|
|
t.Fatal("expected topology evidence")
|
|
}
|
|
if got.DIMMType17 != "memory" || got.StorageMap != "storage" || got.NVIDIAQueryCSV != "query" || got.NVIDIATopology != "fresh" {
|
|
t.Fatalf("unexpected topology evidence: %+v", got)
|
|
}
|
|
}
|
|
|
|
// TestDetectBeeSupportArchiveV12Layout covers the real bundle layout produced
|
|
// by bee-support v12.x, where bee-audit.json lives under tasks/_state/ and
|
|
// runtime-health.json under status/, not export/. See CLAUDE.md memory on
|
|
// the 2026-08-24 BEE-SP v12.84 bundle for the layout this guards against.
|
|
func TestDetectBeeSupportArchiveV12Layout(t *testing.T) {
|
|
p := &Parser{}
|
|
prefix := "bee-support-stage-easy-bee-nvidia-v12-84-20260824-220134"
|
|
files := []parser.ExtractedFile{
|
|
{
|
|
Path: prefix + "/manifest.txt",
|
|
Content: []byte("bee_version=12.84\nhost=easy-bee-nvidia-v12-84\ngenerated_at_utc=2026-08-24T22:02:12Z\nexport_dir=/appdata/bee/export\n"),
|
|
},
|
|
{
|
|
Path: prefix + "/tasks/_state/bee-audit.json",
|
|
Content: []byte(`{"hardware":{"board":{"serial_number":"SN-BEE-001"}}}`),
|
|
},
|
|
{
|
|
Path: prefix + "/status/runtime-health.json",
|
|
Content: []byte(`{"status":"OK"}`),
|
|
},
|
|
}
|
|
|
|
if got := p.Detect(files); got < 90 {
|
|
t.Fatalf("expected high confidence detect score for v12 layout, got %d", got)
|
|
}
|
|
|
|
snapshot := findSnapshotFile(files)
|
|
if snapshot == nil || snapshot.Path != prefix+"/tasks/_state/bee-audit.json" {
|
|
t.Fatalf("expected findSnapshotFile to pick up tasks/_state/bee-audit.json, got %+v", snapshot)
|
|
}
|
|
}
|
|
|
|
func TestDetectRejectsNonBeeArchive(t *testing.T) {
|
|
p := &Parser{}
|
|
files := []parser.ExtractedFile{
|
|
{
|
|
Path: "random/manifest.txt",
|
|
Content: []byte("host=test\n"),
|
|
},
|
|
{
|
|
Path: "random/export/runtime-health.json",
|
|
Content: []byte(`{"status":"OK"}`),
|
|
},
|
|
}
|
|
|
|
if got := p.Detect(files); got != 0 {
|
|
t.Fatalf("expected detect score 0, got %d", got)
|
|
}
|
|
}
|
|
|
|
func TestParseBeeAuditSnapshot(t *testing.T) {
|
|
p := &Parser{}
|
|
files := []parser.ExtractedFile{
|
|
{
|
|
Path: "bee-support-debian-20260325-162030/manifest.txt",
|
|
Content: []byte("bee_version=1.0.0\nhost=debian\ngenerated_at_utc=2026-03-25T16:20:30Z\nexport_dir=/appdata/bee/export\n"),
|
|
},
|
|
{
|
|
Path: "bee-support-debian-20260325-162030/export/bee-audit.json",
|
|
Content: []byte(`{
|
|
"source_type": "manual",
|
|
"target_host": "debian",
|
|
"collected_at": "2026-03-25T16:08:09Z",
|
|
"runtime": {
|
|
"status": "PARTIAL",
|
|
"checked_at": "2026-03-25T16:07:56Z",
|
|
"network_status": "OK",
|
|
"issues": [
|
|
{
|
|
"code": "nvidia_kernel_module_missing",
|
|
"severity": "warning",
|
|
"description": "NVIDIA kernel module is not loaded."
|
|
}
|
|
],
|
|
"services": [
|
|
{
|
|
"name": "bee-web",
|
|
"status": "inactive"
|
|
}
|
|
]
|
|
},
|
|
"hardware": {
|
|
"board": {
|
|
"manufacturer": "Supermicro",
|
|
"product_name": "AS-4124GQ-TNMI",
|
|
"serial_number": "S490387X4418273",
|
|
"part_number": "H12DGQ-NT6",
|
|
"uuid": "d868ae00-a61f-11ee-8000-7cc255e10309"
|
|
},
|
|
"firmware": [
|
|
{
|
|
"device_name": "BIOS",
|
|
"version": "2.8"
|
|
}
|
|
],
|
|
"cpus": [
|
|
{
|
|
"status": "OK",
|
|
"status_checked_at": "2026-03-25T16:08:09Z",
|
|
"socket": 1,
|
|
"model": "AMD EPYC 7763 64-Core Processor",
|
|
"cores": 64,
|
|
"threads": 128,
|
|
"frequency_mhz": 2450,
|
|
"max_frequency_mhz": 3525
|
|
}
|
|
],
|
|
"memory": [
|
|
{
|
|
"status": "OK",
|
|
"status_checked_at": "2026-03-25T16:08:09Z",
|
|
"slot": "P1-DIMMA1",
|
|
"location": "P0_Node0_Channel0_Dimm0",
|
|
"present": true,
|
|
"size_mb": 32768,
|
|
"type": "DDR4",
|
|
"max_speed_mhz": 3200,
|
|
"current_speed_mhz": 2933,
|
|
"manufacturer": "SK Hynix",
|
|
"serial_number": "80AD01224887286666",
|
|
"part_number": "HMA84GR7DJR4N-XN"
|
|
}
|
|
],
|
|
"storage": [
|
|
{
|
|
"status": "Unknown",
|
|
"status_checked_at": "2026-03-25T16:08:09Z",
|
|
"slot": "nvme0n1",
|
|
"type": "NVMe",
|
|
"model": "KCD6XLUL960G",
|
|
"serial_number": "2470A00XT5M8",
|
|
"interface": "NVMe",
|
|
"present": true
|
|
}
|
|
],
|
|
"pcie_devices": [
|
|
{
|
|
"status": "OK",
|
|
"status_checked_at": "2026-03-25T16:08:09Z",
|
|
"slot": "0000:05:00.0",
|
|
"vendor_id": 5555,
|
|
"device_id": 4123,
|
|
"device_class": "EthernetController",
|
|
"manufacturer": "Mellanox Technologies",
|
|
"model": "MT28908 Family [ConnectX-6]",
|
|
"link_width": 16,
|
|
"link_speed": "Gen4",
|
|
"max_link_width": 16,
|
|
"max_link_speed": "Gen4",
|
|
"mac_addresses": ["94:6d:ae:9a:75:4a"],
|
|
"present": true
|
|
}
|
|
],
|
|
"sensors": {
|
|
"power": [
|
|
{
|
|
"name": "PPT",
|
|
"location": "amdgpu-pci-1100",
|
|
"power_w": 95
|
|
}
|
|
],
|
|
"temperatures": [
|
|
{
|
|
"name": "Composite",
|
|
"location": "nvme-pci-0600",
|
|
"celsius": 28.85,
|
|
"threshold_warning_celsius": 72.85,
|
|
"threshold_critical_celsius": 81.85,
|
|
"status": "OK"
|
|
}
|
|
]
|
|
}
|
|
}
|
|
}`),
|
|
},
|
|
}
|
|
|
|
result, err := p.Parse(files)
|
|
if err != nil {
|
|
t.Fatalf("parse failed: %v", err)
|
|
}
|
|
|
|
if result.Hardware == nil {
|
|
t.Fatal("expected hardware to be populated")
|
|
}
|
|
if result.TargetHost != "debian" {
|
|
t.Fatalf("expected target host debian, got %q", result.TargetHost)
|
|
}
|
|
wantCollectedAt := time.Date(2026, 3, 25, 16, 8, 9, 0, time.UTC)
|
|
if !result.CollectedAt.Equal(wantCollectedAt) {
|
|
t.Fatalf("expected collected_at %s, got %s", wantCollectedAt, result.CollectedAt)
|
|
}
|
|
if result.Hardware.BoardInfo.SerialNumber != "S490387X4418273" {
|
|
t.Fatalf("unexpected board serial %q", result.Hardware.BoardInfo.SerialNumber)
|
|
}
|
|
if len(result.Hardware.CPUs) != 1 {
|
|
t.Fatalf("expected 1 cpu, got %d", len(result.Hardware.CPUs))
|
|
}
|
|
if len(result.Hardware.Memory) != 1 {
|
|
t.Fatalf("expected 1 dimm, got %d", len(result.Hardware.Memory))
|
|
}
|
|
if len(result.Hardware.Storage) != 1 {
|
|
t.Fatalf("expected 1 storage device, got %d", len(result.Hardware.Storage))
|
|
}
|
|
if len(result.Hardware.PCIeDevices) != 1 {
|
|
t.Fatalf("expected 1 pcie device, got %d", len(result.Hardware.PCIeDevices))
|
|
}
|
|
if result.Hardware.PCIeDevices[0].BDF != "0000:05:00.0" {
|
|
t.Fatalf("expected BDF to be normalized from slot, got %q", result.Hardware.PCIeDevices[0].BDF)
|
|
}
|
|
if len(result.Sensors) != 2 {
|
|
t.Fatalf("expected 2 flattened sensors, got %d", len(result.Sensors))
|
|
}
|
|
if len(result.Events) < 3 {
|
|
t.Fatalf("expected runtime events to be created, got %d", len(result.Events))
|
|
}
|
|
if len(result.FRU) == 0 {
|
|
t.Fatal("expected board FRU fallback to be populated")
|
|
}
|
|
}
|
|
|
|
func TestNormalizePSUSlots(t *testing.T) {
|
|
in := []models.PSU{
|
|
{Slot: "0", Firmware: "B"}, {Slot: "1", Firmware: "B"},
|
|
{Slot: "2", Firmware: "B"}, {Slot: "3", Firmware: "B"},
|
|
}
|
|
out := normalizePSUSlots(in)
|
|
for i, want := range []string{"1", "2", "3", "4"} {
|
|
if out[i].Slot != want {
|
|
t.Errorf("psu[%d].Slot = %q, want %q", i, out[i].Slot, want)
|
|
}
|
|
if out[i].Firmware != "" {
|
|
t.Errorf("psu[%d].Firmware = %q, want empty (bare FRU version leak)", i, out[i].Firmware)
|
|
}
|
|
}
|
|
|
|
// Already 1-based / non-numeric: leave untouched.
|
|
keep := []models.PSU{{Slot: "1"}, {Slot: "2"}}
|
|
if got := normalizePSUSlots(keep); got[0].Slot != "1" || got[1].Slot != "2" {
|
|
t.Errorf("1-based slots must be untouched, got %q,%q", got[0].Slot, got[1].Slot)
|
|
}
|
|
psuA := []models.PSU{{Slot: "PSU0"}}
|
|
if got := normalizePSUSlots(psuA); got[0].Slot != "PSU0" {
|
|
t.Errorf("non-numeric slot must be untouched, got %q", got[0].Slot)
|
|
}
|
|
}
|