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) } }