Click a tile to open /hosts/{id} — the canonical control surface per
host. Timeline renders every pre-stage, stage, and terminal node in
order, with the current one pulsing, failed ones flagged, and
downstream ones dimmed as skipped. Detail page shows summary, hold
card (when holding), all action buttons, spec diffs, a full-height
log pane, and a collapsed expected-spec YAML.
Tile slims to name, last-seen, status, and one primary action; a
CSS-overlay <a> makes the whole card clickable while buttons stay
receptive via z-index.
Runner.publishTileUpdate now also emits pipeline-{runID} fragments,
and CompleteStage wraps Stages.CompleteByName so stage completions
advance the timeline live — without this the dots only moved on
state transitions.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
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
|
||||
idxWaitingWoL = 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)
|
||||
if len(nodes) != len(preStageOrder)+1 {
|
||||
// No stage rows = just pre-stages + Completed.
|
||||
t.Fatalf("len = %d, want %d", len(nodes), len(preStageOrder)+1)
|
||||
}
|
||||
for i, n := range nodes {
|
||||
if n.State != "pending" {
|
||||
t.Errorf("node %d (%s) state = %q, want pending", i, n.Name, n.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[idxWaitingWoL].State != "pending" {
|
||||
t.Errorf("WaitingWoL = %q, want pending", nodes[idxWaitingWoL].State)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user