feat(cmd): add logpile-cpu-audit offline CPU identity audit CLI

Non-server entry point over the parser registry: recursively discovers
supported dumps, parses them with bounded concurrency, and writes one
path-mirrored CPU identity report per input plus a deterministic summary.
Failures are isolated as JSON. A -reanimator mode emits a flat CPU-only
dataset via the production exporter.

See ADL-059.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Mikhail Chusavitin
2026-08-27 17:05:52 +03:00
co-authored by Claude Sonnet 5
parent a430a8c46f
commit fc68134ed9
2 changed files with 612 additions and 0 deletions
+130
View File
@@ -0,0 +1,130 @@
package main
import (
"os"
"path/filepath"
"testing"
"git.mchus.pro/mchus/logpile/internal/exporter"
"git.mchus.pro/mchus/logpile/internal/models"
)
func TestIsAuditCandidate(t *testing.T) {
tests := []struct {
name string
path string
includePlain bool
want bool
}{
{name: "tar gzip", path: "dump.tar.gz", want: true},
{name: "zip", path: "dump.zip", want: true},
{name: "plain log excluded", path: "component.log", want: false},
{name: "plain log requested", path: "nvidia-bug-report.log", includePlain: true, want: true},
{name: "JSON report excluded", path: "report.json", want: false},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := isAuditCandidate(tt.path, tt.includePlain); got != tt.want {
t.Fatalf("isAuditCandidate(%q, %v) = %v, want %v", tt.path, tt.includePlain, got, tt.want)
}
})
}
}
func TestCPUOnlyReanimatorPayloadPreservesRequiredFields(t *testing.T) {
payload := &exporter.ReanimatorExport{
Filename: "dump.tar.gz",
CollectedAt: "2026-07-27T10:34:40Z",
Hardware: exporter.ReanimatorHardware{
Board: exporter.ReanimatorBoard{SerialNumber: "23E102624"},
CPUs: []exporter.ReanimatorCPU{{Socket: 0, SerialNumber: "D46E5D6B1D3E40E1"}},
Firmware: []exporter.ReanimatorFirmware{{DeviceName: "BIOS", Version: "1.0"}},
},
}
got := cpuOnlyReanimatorPayload(payload)
if got.CollectedAt == "" || got.Hardware.Board.SerialNumber == "" || len(got.Hardware.CPUs) != 1 {
t.Fatalf("required ingest fields were lost: %+v", got)
}
if len(got.Hardware.Firmware) != 0 {
t.Fatalf("expected non-CPU hardware to be removed: %+v", got.Hardware.Firmware)
}
}
func TestFlatReanimatorName(t *testing.T) {
got := flatReanimatorName("v1", "NL/example/dump.tar.gz")
if got != "v1__dump.tar.gz.reanimator.json" {
t.Fatalf("unexpected flat name %q", got)
}
}
func TestBuildCPUAudit(t *testing.T) {
tests := []struct {
name string
cpu models.CPU
finding string
promoted bool
expectedSer string
}{
{
name: "PPIN promoted",
cpu: models.CPU{Socket: 0, PPIN: "D46E5D6B1D3E40E1", SerialNumber: "D46E5D6B1D3E40E1"},
finding: "ppin_used_as_serial",
promoted: true,
expectedSer: "D46E5D6B1D3E40E1",
},
{
name: "regression detected",
cpu: models.CPU{Socket: 1, PPIN: "D44F8D6B9155EE0E"},
finding: "missing_serial_with_ppin",
expectedSer: "D44F8D6B9155EE0E",
},
{
name: "placeholder serial with PPIN",
cpu: models.CPU{Socket: 1, PPIN: "D44F8D6B9155EE0E", SerialNumber: "Unknown"},
finding: "missing_serial_with_ppin",
expectedSer: "D44F8D6B9155EE0E",
},
{
name: "native serial",
cpu: models.CPU{Socket: 0, SerialNumber: "CPU-SERIAL"},
finding: "source_serial_present",
expectedSer: "CPU-SERIAL",
},
{
name: "placeholder PPIN",
cpu: models.CPU{Socket: 0, PPIN: "N/A"},
finding: "cpu_identity_missing",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
got := buildCPUAudit(tt.cpu)
if got.Finding != tt.finding || got.PPINPromoted != tt.promoted || got.ExpectedSerialNumber != tt.expectedSer {
t.Fatalf("unexpected audit: %+v", got)
}
})
}
}
func TestRunAuditWritesFailureReportAndSummary(t *testing.T) {
input := t.TempDir()
output := t.TempDir()
badArchive := filepath.Join(input, "bad.zip")
if err := os.WriteFile(badArchive, []byte("not a zip"), 0o644); err != nil {
t.Fatalf("write fixture: %v", err)
}
summary, err := runAudit(input, output, 1, false)
if err != nil {
t.Fatalf("runAudit failed: %v", err)
}
if summary.FilesFound != 1 || summary.FilesFailed != 1 || summary.FilesParsed != 0 {
t.Fatalf("unexpected summary: %+v", summary)
}
for _, name := range []string{"bad.zip.cpu-audit.json", "_summary.json"} {
if _, err := os.Stat(filepath.Join(output, name)); err != nil {
t.Fatalf("expected %s: %v", name, err)
}
}
}