The standalone "confidential-computing" SAT target only ever checked CC readiness, which most fleets never opt into (a NOT_READY verdict there isn't a fault). Meanwhile DCGM diag never asserts GPU config compliance (ECC/MIG/power-limit vs factory default) or NVLink topology (per NVIDIA's own DGX BasePOD deployment guide, this needs a separate validation step) — gaps confirmed against public DCGM docs and a real NV17-vs-expected-NV18 bonded pair found on a live bundle. Repurposes the routine into "nvidia-config": reuses the existing ListNvidiaGPUSettings() (already backing the GPU-settings page) to flag ECC disabled, a MIG mode change stuck pending a reset/reboot, and a power limit capped >5% below default; parses "nvidia-smi topo -m" bonded pairs against "nvlink -s/-e" to flag any inactive lane or nonzero replay/ recovery/CRC counter on an otherwise-active bond. CC readiness is folded in as one informational field (does not gate overall_status) rather than a dedicated test. Reports under the same pcie:gpu:nvidia severity key as every other nvidia-* SAT target instead of an isolated key, so a config/NVLink FAILED result isn't invisible next to stress-test results. Also fixes ApplySATResultToDB silently dropping any target with no matching switch case (exactly what the old confidential-computing target did) with a new coverage test enumerating every real SAT target. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
258 lines
9.9 KiB
Go
258 lines
9.9 KiB
Go
package app
|
|
|
|
import (
|
|
"os"
|
|
"path/filepath"
|
|
"testing"
|
|
|
|
"bee/audit/internal/schema"
|
|
)
|
|
|
|
func TestExtractArchivePath(t *testing.T) {
|
|
cases := map[string]string{
|
|
"/appdata/bee/export/bee-sat/gpu-nvidia-20260706-174722": "/appdata/bee/export/bee-sat/gpu-nvidia-20260706-174722",
|
|
"Archive written to /appdata/bee/export/bee-sat/gpu-nvidia-20260706-174722": "/appdata/bee/export/bee-sat/gpu-nvidia-20260706-174722",
|
|
"Archive written to /path/with spaces/foo.tar.gz": "/path/with spaces/foo.tar.gz",
|
|
" Archive written to /path/foo.tar.gz ": "/path/foo.tar.gz",
|
|
}
|
|
for in, want := range cases {
|
|
if got := ExtractArchivePath(in); got != want {
|
|
t.Errorf("ExtractArchivePath(%q) = %q, want %q", in, got, want)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestReadSATOverallStatus_HandlesActionResultPrefix(t *testing.T) {
|
|
runDir := t.TempDir()
|
|
summary := "run_at_utc=2026-07-06T17:47:22Z\noverall_status=FAILED\n"
|
|
if err := os.WriteFile(filepath.Join(runDir, "summary.txt"), []byte(summary), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// Regression: RunNvidiaAcceptancePackWithOptions wraps the bare run dir as
|
|
// "Archive written to <dir>" before it reaches ReadSATOverallStatus. If the
|
|
// prefix isn't stripped, the summary.txt lookup silently fails and a FAILED
|
|
// sub-job never surfaces as a task failure.
|
|
wrapped := "Archive written to " + runDir
|
|
if got := ReadSATOverallStatus(ExtractArchivePath(wrapped)); got != "FAILED" {
|
|
t.Errorf("ReadSATOverallStatus(wrapped) = %q, want FAILED", got)
|
|
}
|
|
|
|
if got := ReadSATOverallStatus(ExtractArchivePath(runDir)); got != "FAILED" {
|
|
t.Errorf("ReadSATOverallStatus(bare) = %q, want FAILED", got)
|
|
}
|
|
}
|
|
|
|
func writeSATSummary(t *testing.T, overall string) string {
|
|
t.Helper()
|
|
runDir := t.TempDir()
|
|
summary := "run_at_utc=2026-07-06T17:47:22Z\noverall_status=" + overall + "\n"
|
|
if err := os.WriteFile(filepath.Join(runDir, "summary.txt"), []byte(summary), 0644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return runDir
|
|
}
|
|
|
|
func TestApplySATResultToDBNormalizesGPUKeyByVendor(t *testing.T) {
|
|
// "nvidia" (Check tier) and "nvidia-stress" (Load/Burn tier) exercise the
|
|
// same physical GPUs and must collapse onto one component key so a later
|
|
// clean Check run can't erase an earlier Load-tier failure.
|
|
db, err := OpenComponentStatusDB(filepath.Join(t.TempDir(), "component-status.json"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
ApplySATResultToDB(db, "nvidia-stress", writeSATSummary(t, "FAILED"))
|
|
ApplySATResultToDB(db, "nvidia", writeSATSummary(t, "OK"))
|
|
|
|
rec, ok := db.Get("pcie:gpu:nvidia")
|
|
if !ok {
|
|
t.Fatalf("expected pcie:gpu:nvidia record to exist")
|
|
}
|
|
if rec.Status != "Warning" {
|
|
t.Fatalf("status=%q, want Warning (FAILED) to survive the later OK Check run", rec.Status)
|
|
}
|
|
}
|
|
|
|
func TestApplyComponentStatusDBMatchesGPUByVendor(t *testing.T) {
|
|
db, err := OpenComponentStatusDB(filepath.Join(t.TempDir(), "component-status.json"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
db.Record("pcie:gpu:nvidia", "sat:nvidia-stress", "Critical", "nvidia-stress SAT: FAILED")
|
|
|
|
class := "VideoController"
|
|
vendor := 0x10de // collector.NvidiaVendorID
|
|
snap := &schema.HardwareSnapshot{
|
|
PCIeDevices: []schema.HardwarePCIeDevice{
|
|
{DeviceClass: &class, VendorID: &vendor, BDF: strPtr("0000:c8:00.0")},
|
|
},
|
|
}
|
|
|
|
applyComponentStatusDB(snap, db)
|
|
|
|
if snap.PCIeDevices[0].Status == nil || *snap.PCIeDevices[0].Status != "Critical" {
|
|
t.Fatalf("expected GPU device status Critical, got %v", snap.PCIeDevices[0].Status)
|
|
}
|
|
}
|
|
|
|
func strPtr(s string) *string { return &s }
|
|
|
|
// TestApplySATResultToDBCoversAllHealthCheckTargets guards against a target
|
|
// silently falling through ApplySATResultToDB's switch with no matching
|
|
// case — exactly what happened to the old "confidential-computing" target
|
|
// before it was folded into "nvidia-config" (task ran, produced a valid
|
|
// summary.txt, but no component-status.json record was ever written for it,
|
|
// so a /topo card or any other consumer had no way to know the check had
|
|
// even run). Every target below is a real health/acceptance check target
|
|
// dispatched by task_runner.go's executeTaskWithOptions that produces a
|
|
// summary.txt with overall_status; each must land somewhere in the DB.
|
|
func TestApplySATResultToDBCoversAllHealthCheckTargets(t *testing.T) {
|
|
cases := []struct {
|
|
target string
|
|
wantKey string
|
|
}{
|
|
{"nvidia", "pcie:gpu:nvidia"},
|
|
{"nvidia-stress", "pcie:gpu:nvidia"},
|
|
{"nvidia-targeted-stress", "pcie:gpu:nvidia"},
|
|
{"nvidia-compute", "pcie:gpu:nvidia"},
|
|
{"nvidia-targeted-power", "pcie:gpu:nvidia"},
|
|
{"nvidia-pulse", "pcie:gpu:nvidia"},
|
|
{"nvidia-interconnect", "pcie:gpu:nvidia"},
|
|
{"nvidia-bandwidth", "pcie:gpu:nvidia"},
|
|
{"nvidia-config", "pcie:gpu:nvidia"},
|
|
{"amd", "pcie:gpu:amd"},
|
|
{"amd-stress", "pcie:gpu:amd"},
|
|
{"amd-mem", "pcie:gpu:amd"},
|
|
{"amd-bandwidth", "pcie:gpu:amd"},
|
|
{"memory", "memory:all"},
|
|
{"memory-stress", "memory:all"},
|
|
{"sat-stress", "memory:all"},
|
|
{"cpu", "cpu:all"},
|
|
{"platform-stress", "cpu:all"},
|
|
{"storage", "storage:all"},
|
|
}
|
|
for _, tc := range cases {
|
|
t.Run(tc.target, func(t *testing.T) {
|
|
db, err := OpenComponentStatusDB(filepath.Join(t.TempDir(), "component-status.json"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
ApplySATResultToDB(db, tc.target, writeSATSummary(t, "OK"))
|
|
rec, ok := db.Get(tc.wantKey)
|
|
if !ok {
|
|
t.Fatalf("target %q wrote no record under key %q — falls through the switch silently", tc.target, tc.wantKey)
|
|
}
|
|
if rec.Status != "OK" {
|
|
t.Fatalf("target %q key %q status=%q want OK", tc.target, tc.wantKey, rec.Status)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// TestApplySATResultToDBNvidiaConfigSharesGPUKeyWithOtherNvidiaTargets
|
|
// confirms "nvidia-config" (GPU config/NVLink/CC check — confidential
|
|
// computing readiness folded in here rather than run as its own standalone
|
|
// SAT target) reports under the same pcie:gpu:nvidia key as every other
|
|
// nvidia-* target, so a config/NVLink FAILED result isn't invisible next to
|
|
// the plain stress-test results, and a clean run afterward doesn't silently
|
|
// erase an earlier failure (severity merge, same as
|
|
// TestApplySATResultToDBNormalizesGPUKeyByVendor above).
|
|
func TestApplySATResultToDBNvidiaConfigSharesGPUKeyWithOtherNvidiaTargets(t *testing.T) {
|
|
db, err := OpenComponentStatusDB(filepath.Join(t.TempDir(), "component-status.json"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
ApplySATResultToDB(db, "nvidia-config", writeSATSummary(t, "FAILED"))
|
|
ApplySATResultToDB(db, "nvidia", writeSATSummary(t, "OK"))
|
|
|
|
rec, ok := db.Get("pcie:gpu:nvidia")
|
|
if !ok {
|
|
t.Fatalf("expected pcie:gpu:nvidia record to exist")
|
|
}
|
|
if rec.Status != "Warning" {
|
|
t.Fatalf("status=%q, want Warning (nvidia-config FAILED must survive the later OK nvidia run)", rec.Status)
|
|
}
|
|
}
|
|
|
|
|
|
// TestRecordDeduplicatesRepeatedIdenticalStatusFromSameSource guards the
|
|
// hardware-ingest-contract.md rule that status_history is a transition log
|
|
// ("История переходов статусов"), not a per-poll journal. A component
|
|
// checked continuously (the PSU watchdog, every 60s indefinitely) must not
|
|
// grow one History entry per poll while its status stays unchanged — that
|
|
// is exactly what made component-status.json grow without bound in
|
|
// practice.
|
|
func TestRecordDeduplicatesRepeatedIdenticalStatusFromSameSource(t *testing.T) {
|
|
db, err := OpenComponentStatusDB(filepath.Join(t.TempDir(), "component-status.json"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for i := 0; i < 50; i++ {
|
|
db.Record("psu:0", "watchdog:psu", "OK", "")
|
|
}
|
|
rec, ok := db.Get("psu:0")
|
|
if !ok {
|
|
t.Fatalf("expected psu:0 record")
|
|
}
|
|
if len(rec.History) != 1 {
|
|
t.Fatalf("history len=%d want 1 after 50 identical polls (dedup by transition)", len(rec.History))
|
|
}
|
|
|
|
// A real transition must still be recorded, and dedup resumes at the
|
|
// new status.
|
|
db.Record("psu:0", "watchdog:psu", "Critical", "PSU sensor reported non-OK state")
|
|
db.Record("psu:0", "watchdog:psu", "Critical", "PSU sensor reported non-OK state")
|
|
rec, _ = db.Get("psu:0")
|
|
if len(rec.History) != 2 {
|
|
t.Fatalf("history len=%d want 2 (OK, then Critical, second Critical deduped)", len(rec.History))
|
|
}
|
|
if rec.Status != "Critical" {
|
|
t.Fatalf("status=%q want Critical", rec.Status)
|
|
}
|
|
}
|
|
|
|
// TestRecordDoesNotClobberConcurrentWriterFromAnotherProcess reproduces the
|
|
// bug behind a real support bundle where many GPU/CPU/memory SAT tasks had
|
|
// completed successfully but component-status.json only ever held PSU
|
|
// records: bee-web keeps one ComponentStatusDB open for its whole process
|
|
// lifetime (writing PSU/kmsg watchdog records ~every 60s via a long-running
|
|
// health poller), while each SAT task runs as a short-lived "bee bee-worker"
|
|
// subprocess that opens its own separate ComponentStatusDB instance backed
|
|
// by the same file. Without a reload-before-write, the long-lived process's
|
|
// stale in-memory snapshot (which never learned about the subprocess's
|
|
// write) overwrites the whole file on its next save and erases it.
|
|
func TestRecordDoesNotClobberConcurrentWriterFromAnotherProcess(t *testing.T) {
|
|
path := filepath.Join(t.TempDir(), "component-status.json")
|
|
|
|
// bee-web's long-lived DB instance, opened once at process start.
|
|
webDB, err := OpenComponentStatusDB(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
// A "bee bee-worker" subprocess for a completed GPU SAT task opens its
|
|
// own instance backed by the same file and records the result.
|
|
workerDB, err := OpenComponentStatusDB(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
workerDB.Record("pcie:gpu:nvidia", "sat:nvidia", "OK", "nvidia SAT: OK")
|
|
|
|
// bee-web's health poller ticks next, using its own (older) in-memory
|
|
// view, and records a PSU status it polled independently.
|
|
webDB.Record("psu:0", "watchdog:psu", "OK", "")
|
|
|
|
// The GPU record written by the worker subprocess must survive.
|
|
onDisk, err := OpenComponentStatusDB(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if rec, ok := onDisk.Get("pcie:gpu:nvidia"); !ok || rec.Status != "OK" {
|
|
t.Fatalf("pcie:gpu:nvidia record lost after concurrent PSU write: ok=%v rec=%+v", ok, rec)
|
|
}
|
|
if _, ok := onDisk.Get("psu:0"); !ok {
|
|
t.Fatalf("psu:0 record missing")
|
|
}
|
|
}
|