d0bfae14c8
CI / Lint + build + test (push) Has been cancelled
Every supported host runs vetting-reporter in-OS and heartbeats every 30s. WoL was never the thing that started vetting — the heartbeat response's reboot_for_vetting command was. Firing WoL first only crowded the run log with misleading diagnostics when the real failure mode is "reporter isn't installed." - StartRun 409s if the host hasn't heartbeated within 60s, pointing the operator at /register/quick.sh. - Dispatcher re-checks LastSeenAt at dispatch time (run may sit in Queued long enough for the host to go offline); stale hosts mark the run Failed with failed_stage=dispatch instead of looping. - New StateWaitingReboot + TriggerRebootCommanded capture the actual semantics. StateWaitingWoL kept as the hook point for a future manual-override button. - Tile disables the Start button with a quick.sh tooltip when the host is offline, matching the server-side 409. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
203 lines
6.7 KiB
Go
203 lines
6.7 KiB
Go
package templates
|
|
|
|
import (
|
|
"testing"
|
|
|
|
"vetting/internal/model"
|
|
)
|
|
|
|
// node indexes for the default pipeline layout: pre-stages (3) + stage
|
|
// rows (9) + terminal Completed (1) = 13 nodes.
|
|
const (
|
|
idxQueued = 0
|
|
idxWaitingReboot = 1
|
|
idxBooting = 2
|
|
idxInventory = 3
|
|
idxSpecValidate = 4
|
|
idxSMART = 5
|
|
idxCPUStress = 6
|
|
idxStorage = 7
|
|
idxNetwork = 8
|
|
idxGPU = 9
|
|
idxPSU = 10
|
|
idxReporting = 11
|
|
idxCompleted = 12
|
|
)
|
|
|
|
// seedStages returns a fresh all-pending stage slice in the canonical order.
|
|
func seedStages() []model.Stage {
|
|
names := []string{"Inventory", "SpecValidate", "SMART", "CPUStress", "Storage", "Network", "GPU", "PSU", "Reporting"}
|
|
out := make([]model.Stage, len(names))
|
|
for i, n := range names {
|
|
out[i] = model.Stage{Name: n, Ordinal: i, State: model.StagePending}
|
|
}
|
|
return out
|
|
}
|
|
|
|
func TestBuildPipeline_NoRun(t *testing.T) {
|
|
nodes := BuildPipeline(nil, nil)
|
|
// Ghost pipeline: 3 pre-stages + 9 stage ghosts + 1 terminal = 13
|
|
// nodes, all pending.
|
|
if len(nodes) != 13 {
|
|
t.Fatalf("len = %d, want 13", len(nodes))
|
|
}
|
|
for i, n := range nodes {
|
|
if n.State != "pending" {
|
|
t.Errorf("node %d (%s) state = %q, want pending", i, n.Name, n.State)
|
|
}
|
|
}
|
|
}
|
|
|
|
// TestBuildPipeline_GhostStagesBeforeClaim models the real WaitingReboot
|
|
// case: the run exists but agent hasn't called /claim yet, so there are
|
|
// no stage rows. Pipeline must still render all 9 stage nodes as ghosts
|
|
// so the operator sees the full timeline ahead of them.
|
|
func TestBuildPipeline_GhostStagesBeforeClaim(t *testing.T) {
|
|
run := &model.Run{State: model.StateWaitingReboot}
|
|
nodes := BuildPipeline(run, nil)
|
|
if len(nodes) != 13 {
|
|
t.Fatalf("len = %d, want 13", len(nodes))
|
|
}
|
|
if nodes[idxQueued].State != "passed" {
|
|
t.Errorf("Queued = %q, want passed", nodes[idxQueued].State)
|
|
}
|
|
if nodes[idxWaitingReboot].State != "running" {
|
|
t.Errorf("WaitingReboot = %q, want running", nodes[idxWaitingReboot].State)
|
|
}
|
|
// All 9 stage ghosts must be pending — nothing has started yet.
|
|
for i := idxInventory; i <= idxReporting; i++ {
|
|
if nodes[i].State != "pending" {
|
|
t.Errorf("%s (ghost) = %q, want pending", nodes[i].Name, nodes[i].State)
|
|
}
|
|
}
|
|
if nodes[idxCompleted].State != "pending" {
|
|
t.Errorf("Completed = %q, want pending", nodes[idxCompleted].State)
|
|
}
|
|
}
|
|
|
|
// TestBuildPipeline_GhostStagesDuringStage models the in-flight case
|
|
// with only some stage rows seeded: later stages must still appear as
|
|
// pending ghosts rather than silently disappearing.
|
|
func TestBuildPipeline_GhostStagesDuringStage(t *testing.T) {
|
|
run := &model.Run{State: model.StateSMART}
|
|
// Only Inventory + SpecValidate seeded; SMART onwards are ghosts.
|
|
stages := []model.Stage{
|
|
{Name: "Inventory", Ordinal: 0, State: model.StagePassed},
|
|
{Name: "SpecValidate", Ordinal: 1, State: model.StagePassed},
|
|
}
|
|
nodes := BuildPipeline(run, stages)
|
|
if len(nodes) != 13 {
|
|
t.Fatalf("len = %d, want 13", len(nodes))
|
|
}
|
|
if nodes[idxSMART].State != "running" {
|
|
t.Errorf("SMART (ghost) = %q, want running", nodes[idxSMART].State)
|
|
}
|
|
for _, i := range []int{idxCPUStress, idxStorage, idxNetwork, idxGPU, idxPSU, idxReporting} {
|
|
if nodes[i].State != "pending" {
|
|
t.Errorf("%s (ghost) = %q, want pending", nodes[i].Name, nodes[i].State)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestBuildPipeline_Running(t *testing.T) {
|
|
run := &model.Run{State: model.StateSMART}
|
|
stages := seedStages()
|
|
stages[0].State = model.StagePassed
|
|
stages[1].State = model.StagePassed
|
|
stages[2].State = model.StageRunning
|
|
nodes := BuildPipeline(run, stages)
|
|
if len(nodes) != 13 {
|
|
t.Fatalf("len = %d, want 13", len(nodes))
|
|
}
|
|
// Pre-stages are all past for a run that has reached SMART.
|
|
for i := idxQueued; i <= idxBooting; i++ {
|
|
if nodes[i].State != "passed" {
|
|
t.Errorf("prestage %s = %q, want passed", nodes[i].Name, nodes[i].State)
|
|
}
|
|
}
|
|
if nodes[idxInventory].State != "passed" {
|
|
t.Errorf("Inventory = %q, want passed", nodes[idxInventory].State)
|
|
}
|
|
if nodes[idxSpecValidate].State != "passed" {
|
|
t.Errorf("SpecValidate = %q, want passed", nodes[idxSpecValidate].State)
|
|
}
|
|
if nodes[idxSMART].State != "running" {
|
|
t.Errorf("SMART = %q, want running", nodes[idxSMART].State)
|
|
}
|
|
if nodes[idxCPUStress].State != "pending" {
|
|
t.Errorf("CPUStress = %q, want pending", nodes[idxCPUStress].State)
|
|
}
|
|
if nodes[idxCompleted].State != "pending" {
|
|
t.Errorf("Completed = %q, want pending", nodes[idxCompleted].State)
|
|
}
|
|
}
|
|
|
|
func TestBuildPipeline_Failed(t *testing.T) {
|
|
run := &model.Run{State: model.StateFailedHolding, FailedStage: "Storage"}
|
|
stages := seedStages()
|
|
for i := 0; i <= 3; i++ {
|
|
stages[i].State = model.StagePassed
|
|
}
|
|
stages[4].State = model.StageFailed // Storage
|
|
nodes := BuildPipeline(run, stages)
|
|
// Pre-stages are past a run that reached Storage.
|
|
for i := idxQueued; i <= idxBooting; i++ {
|
|
if nodes[i].State != "passed" {
|
|
t.Errorf("prestage %s = %q, want passed", nodes[i].Name, nodes[i].State)
|
|
}
|
|
}
|
|
if nodes[idxStorage].State != "failed" {
|
|
t.Errorf("Storage = %q, want failed", nodes[idxStorage].State)
|
|
}
|
|
for _, i := range []int{idxNetwork, idxGPU, idxPSU, idxReporting} {
|
|
if nodes[i].State != "skipped" {
|
|
t.Errorf("%s = %q, want skipped", nodes[i].Name, nodes[i].State)
|
|
}
|
|
}
|
|
if nodes[idxCompleted].State != "pending" {
|
|
t.Errorf("Completed = %q, want pending on failure", nodes[idxCompleted].State)
|
|
}
|
|
}
|
|
|
|
func TestBuildPipeline_Completed(t *testing.T) {
|
|
run := &model.Run{State: model.StateCompleted}
|
|
stages := seedStages()
|
|
for i := range stages {
|
|
stages[i].State = model.StagePassed
|
|
}
|
|
nodes := BuildPipeline(run, stages)
|
|
for i, n := range nodes {
|
|
if n.State != "passed" {
|
|
t.Errorf("node %d (%s) state = %q, want passed", i, n.Name, n.State)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestBuildPipeline_QueuedNow(t *testing.T) {
|
|
run := &model.Run{State: model.StateQueued}
|
|
nodes := BuildPipeline(run, seedStages())
|
|
if nodes[idxQueued].State != "running" {
|
|
t.Errorf("Queued = %q, want running", nodes[idxQueued].State)
|
|
}
|
|
if nodes[idxWaitingReboot].State != "pending" {
|
|
t.Errorf("WaitingReboot = %q, want pending", nodes[idxWaitingReboot].State)
|
|
}
|
|
}
|
|
|
|
// TestBuildPipeline_PreStageRunning_WaitingReboot confirms the pre-stage
|
|
// node for WaitingReboot lights up while the run sits there — the new
|
|
// happy-path state must map onto its pipeline slot.
|
|
func TestBuildPipeline_PreStageRunning_WaitingReboot(t *testing.T) {
|
|
run := &model.Run{State: model.StateWaitingReboot}
|
|
nodes := BuildPipeline(run, seedStages())
|
|
if nodes[idxQueued].State != "passed" {
|
|
t.Errorf("Queued = %q, want passed", nodes[idxQueued].State)
|
|
}
|
|
if nodes[idxWaitingReboot].State != "running" {
|
|
t.Errorf("WaitingReboot = %q, want running", nodes[idxWaitingReboot].State)
|
|
}
|
|
if nodes[idxBooting].State != "pending" {
|
|
t.Errorf("Booting = %q, want pending", nodes[idxBooting].State)
|
|
}
|
|
}
|