Files
josh 23c689aa5b
CI / Lint + build + test (push) Failing after 1m57s
Release / release (push) Has been cancelled
deep profile + threshold gating + firmware stage + Burn super-stage
Ships all five phases of the deep-profile overhaul together. Runs now
carry a profile (quick/deep/soak); every profile walks the same
11-stage order — Inventory → Firmware → SpecValidate → SMART →
CPUStress → Storage → Network → Burn → GPU → PSU → Reporting —
with only per-stage durations and concurrency scaled.

Phase 1: profiles.ProfileRegistry loaded from vetting.yaml; runs.profile
column + CreateWithProfile; threshold table + evaluator seeded per-run
from the shared vetting.thresholds block; breach flips result at
/sensor + /result.

Phase 2: upgraded CPUStress (stress-ng --cpu-method=all --verify +
EDAC/MCE poll), Storage (fio --verify=md5 + SMART start/end delta),
Network (sustained iperf + /proc/net/dev deltas) with per-profile
knobs from Deps.

Phase 3: Burn super-stage with goroutine fan-out for CPU + memory +
fio + iperf, PSU rails sampled across the Burn window, SensorMux
(2 s flush, 500-sample cap) to absorb backpressure.

Phase 4: Firmware stage + firmware_snapshots table; probes dmidecode
(BIOS), ipmitool (BMC), ethtool -i (NIC), nvme (sysfs + id-ctrl),
lspci (HBA), /proc/cpuinfo (microcode). spec.DiffFirmware folds into
SpecValidate with pin-by-identifier and fan-out-across-component
matching; mismatches park the run in FailedHolding.

Phase 5: profile radio on the host start form, profile chip on the
run header, Firmware section in the HTML report, coverage artifact
uploaded from CI, agent/tests/fakes/ scaffold with Deps.LookPath
seam + stress_ng and dmidecode example fakes.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-04-18 22:50:57 -04:00

244 lines
8.0 KiB
Go

package api_test
import (
"context"
"fmt"
"net/http"
"net/http/httptest"
"regexp"
"strings"
"testing"
"github.com/go-chi/chi/v5"
"vetting/internal/model"
"vetting/internal/store"
)
func runReq(runID int64) *http.Request {
req := httptest.NewRequest(http.MethodGet, fmt.Sprintf("/runs/%d", runID), nil)
rctx := chi.NewRouteContext()
rctx.URLParams.Add("runID", fmt.Sprintf("%d", runID))
return req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx))
}
// defaultOpenStage returns the data-stage attr of the single
// `<details ... open data-stage="...">` rendered by ActiveStep.
// Rendered attribute order is fixed (class, then open?, then
// data-stage), so a tight substring match is safe.
func defaultOpenStage(body string) string {
re := regexp.MustCompile(`open data-stage="([^"]+)"`)
m := re.FindStringSubmatch(body)
if len(m) < 2 {
return ""
}
return m[1]
}
// TestRunPage_Pipeline: GET /runs/{id} emits the breadcrumb, run
// header, pipeline section, and active-step panels for every canonical
// stage. Sanity check for the run-page shell.
func TestRunPage_Pipeline(t *testing.T) {
ui, hosts, runs := setupPage(t)
ctx := context.Background()
id, err := hosts.Create(ctx, model.Host{
Name: "run-pipeline",
MAC: "aa:bb:cc:dd:ee:70",
WoLBroadcastIP: "10.0.0.255",
WoLPort: 9,
ExpectedSpecYAML: "memory:\n total_gib: 16\n",
})
if err != nil {
t.Fatalf("create host: %v", err)
}
runID, err := runs.Create(ctx, id, "rp", false)
if err != nil {
t.Fatalf("create run: %v", err)
}
if err := ui.Stages.Seed(ctx, runID); err != nil {
t.Fatalf("seed stages: %v", err)
}
rr := httptest.NewRecorder()
ui.RunPage(rr, runReq(runID))
if rr.Code != http.StatusOK {
t.Fatalf("status = %d, body = %q", rr.Code, rr.Body.String())
}
body := rr.Body.String()
wantPipelineID := fmt.Sprintf(`id="pipeline-%d"`, runID)
if !strings.Contains(body, wantPipelineID) {
t.Fatalf("body missing %s", wantPipelineID)
}
wantHeader := fmt.Sprintf(`id="run-header-%d"`, runID)
if !strings.Contains(body, wantHeader) {
t.Fatalf("body missing %s", wantHeader)
}
// Breadcrumb: Dashboard / host / run #N — three segments.
wantCrumb := fmt.Sprintf(`run #%d`, runID)
if !strings.Contains(body, wantCrumb) {
t.Fatalf("body missing breadcrumb %q", wantCrumb)
}
if !strings.Contains(body, `href="/hosts/`) {
t.Fatalf("body missing breadcrumb host link: %s", body)
}
// Every stage gets its own active-step panel with a log pane.
for _, s := range store.DefaultStageOrder {
wantPanel := fmt.Sprintf(`data-stage="%s"`, s)
if !strings.Contains(body, wantPanel) {
t.Fatalf("body missing active-step panel %s", wantPanel)
}
wantPane := fmt.Sprintf(`id="log-%d-%s"`, runID, s)
if !strings.Contains(body, wantPane) {
t.Fatalf("body missing log pane %s", wantPane)
}
}
}
// TestRunPage_DefaultStep_Running: while a stage is running, its
// <details open> opens by default so the operator's eye lands on the
// live work first.
func TestRunPage_DefaultStep_Running(t *testing.T) {
ui, hosts, runs := setupPage(t)
ctx := context.Background()
id, _ := hosts.Create(ctx, model.Host{
Name: "run-running",
MAC: "aa:bb:cc:dd:ee:71",
WoLBroadcastIP: "10.0.0.255",
WoLPort: 9,
ExpectedSpecYAML: "memory:\n total_gib: 16\n",
})
runID, _ := runs.Create(ctx, id, "rr", false)
_ = ui.Stages.Seed(ctx, runID)
for _, name := range []string{"Inventory", "Firmware", "SpecValidate"} {
_ = ui.Stages.StartByName(ctx, runID, name)
_ = ui.Stages.CompleteByName(ctx, runID, name, model.StagePassed, "")
}
_ = ui.Stages.StartByName(ctx, runID, "SMART")
rr := httptest.NewRecorder()
ui.RunPage(rr, runReq(runID))
if got := defaultOpenStage(rr.Body.String()); got != "SMART" {
t.Fatalf("default step = %q, want SMART", got)
}
}
// TestRunPage_DefaultStep_Failed: no stage running but one failed →
// default opens the failed stage so the operator reads the blocker.
func TestRunPage_DefaultStep_Failed(t *testing.T) {
ui, hosts, runs := setupPage(t)
ctx := context.Background()
id, _ := hosts.Create(ctx, model.Host{
Name: "run-failed",
MAC: "aa:bb:cc:dd:ee:72",
WoLBroadcastIP: "10.0.0.255",
WoLPort: 9,
ExpectedSpecYAML: "memory:\n total_gib: 16\n",
})
runID, _ := runs.Create(ctx, id, "rf", false)
_ = ui.Stages.Seed(ctx, runID)
for _, name := range []string{"Inventory", "Firmware", "SpecValidate", "SMART"} {
_ = ui.Stages.StartByName(ctx, runID, name)
_ = ui.Stages.CompleteByName(ctx, runID, name, model.StagePassed, "")
}
_ = ui.Stages.StartByName(ctx, runID, "CPUStress")
_ = ui.Stages.CompleteByName(ctx, runID, "CPUStress", model.StageFailed, `{"reason":"thermal"}`)
rr := httptest.NewRecorder()
ui.RunPage(rr, runReq(runID))
if got := defaultOpenStage(rr.Body.String()); got != "CPUStress" {
t.Fatalf("default step = %q, want CPUStress", got)
}
}
// TestRunPage_DefaultStep_Completed: all stages passed → default lands
// on Reporting (where the report link lives).
func TestRunPage_DefaultStep_Completed(t *testing.T) {
ui, hosts, runs := setupPage(t)
ctx := context.Background()
id, _ := hosts.Create(ctx, model.Host{
Name: "run-done",
MAC: "aa:bb:cc:dd:ee:73",
WoLBroadcastIP: "10.0.0.255",
WoLPort: 9,
ExpectedSpecYAML: "memory:\n total_gib: 16\n",
})
runID, _ := runs.Create(ctx, id, "rd", false)
_ = ui.Stages.Seed(ctx, runID)
for _, name := range store.DefaultStageOrder {
_ = ui.Stages.StartByName(ctx, runID, name)
_ = ui.Stages.CompleteByName(ctx, runID, name, model.StagePassed, "")
}
rr := httptest.NewRecorder()
ui.RunPage(rr, runReq(runID))
if got := defaultOpenStage(rr.Body.String()); got != "Reporting" {
t.Fatalf("default step = %q, want Reporting", got)
}
}
// TestRunPage_CancelForm: non-terminal run shows a Cancel button that
// posts to /hosts/{hostID}/cancel. Terminal runs omit Cancel and
// instead offer Start-new-run (posts to /hosts/{hostID}/start).
func TestRunPage_CancelForm(t *testing.T) {
ui, hosts, runs := setupPage(t)
ctx := context.Background()
id, _ := hosts.Create(ctx, model.Host{
Name: "run-cancel",
MAC: "aa:bb:cc:dd:ee:74",
WoLBroadcastIP: "10.0.0.255",
WoLPort: 9,
ExpectedSpecYAML: "memory:\n total_gib: 16\n",
})
runID, _ := runs.Create(ctx, id, "rc", false)
// Non-terminal (Queued): Cancel form present.
rr := httptest.NewRecorder()
ui.RunPage(rr, runReq(runID))
body := rr.Body.String()
wantCancel := fmt.Sprintf(`/hosts/%d/cancel`, id)
if !strings.Contains(body, wantCancel) {
t.Fatalf("non-terminal run missing Cancel form %s", wantCancel)
}
if strings.Contains(body, "Start new run") {
t.Fatalf("non-terminal run should not offer Start new run: %s", body)
}
// Now flip to Cancelled (terminal) and re-render.
if err := ui.Runs.SetState(ctx, runID, model.StateCancelled); err != nil {
t.Fatalf("cancel run: %v", err)
}
rr = httptest.NewRecorder()
ui.RunPage(rr, runReq(runID))
body = rr.Body.String()
if strings.Contains(body, wantCancel) {
t.Fatalf("terminal run should not show Cancel form: %s", body)
}
wantStart := fmt.Sprintf(`/hosts/%d/start`, id)
if !strings.Contains(body, wantStart) {
t.Fatalf("terminal run missing Start-new-run form %s", wantStart)
}
}
// TestRunPage_UnknownRun: /runs/999999 returns 404 rather than rendering
// an empty page.
func TestRunPage_UnknownRun(t *testing.T) {
ui, _, _ := setupPage(t)
rr := httptest.NewRecorder()
ui.RunPage(rr, runReq(999999))
if rr.Code != http.StatusNotFound {
t.Fatalf("status = %d, want 404", rr.Code)
}
}
// TestRunPage_BadID: a non-numeric runID returns 400.
func TestRunPage_BadID(t *testing.T) {
ui, _, _ := setupPage(t)
req := httptest.NewRequest(http.MethodGet, "/runs/bogus", nil)
rctx := chi.NewRouteContext()
rctx.URLParams.Add("runID", "bogus")
req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, rctx))
rr := httptest.NewRecorder()
ui.RunPage(rr, req)
if rr.Code != http.StatusBadRequest {
t.Fatalf("status = %d, want 400", rr.Code)
}
}