Host detail v2: full pipeline + per-stage logs + WoL diagnostics
CI / Lint + build + test (push) Has been cancelled
CI / Lint + build + test (push) Has been cancelled
Pipeline now always renders all 13 nodes (3 pre-stage + 9 stage +
Completed), synthesising ghosts from run state when stage rows
aren't seeded yet. Makes a WaitingWoL host show the full timeline
ahead of it instead of just 4 dots.
Agent tags each log line with its stage; logs.Hub fans out to both
log-{runID} and log-{runID}-{stage} SSE events so the detail page
can show per-stage tabs with a pure-CSS radio-sibling switch. Flat
run log prepends [stage] so grep still works.
Dispatcher writes picked/sent-WoL/heartbeat lines into the per-run
log — the operator opens the detail page, sees WaitingWoL stuck,
and reads exactly what the dispatcher did and why nothing's
progressing, instead of having to tail journalctl on the LXC.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -36,9 +36,10 @@ func seedStages() []model.Stage {
|
||||
|
||||
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)
|
||||
// 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" {
|
||||
@@ -47,6 +48,57 @@ func TestBuildPipeline_NoRun(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestBuildPipeline_GhostStagesBeforeClaim models the real WaitingWoL
|
||||
// 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.StateWaitingWoL}
|
||||
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[idxWaitingWoL].State != "running" {
|
||||
t.Errorf("WaitingWoL = %q, want running", nodes[idxWaitingWoL].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()
|
||||
|
||||
Reference in New Issue
Block a user