feat(ui): distinguish cancel-from-hold as "Failed (cancelled)"
Before, a run that failed, held for operator review, and was then cancelled showed up on the tile and run header as plain "Cancelled" with an idle-grey mood — indistinguishable from a mid-stage cancel of a healthy run. That hides the actual failure from the dashboard. Now: when State=Cancelled with FailedStage still set (the hold-cancel signature the heartbeat handler already uses to pick reboot vs cancel_stage), the badge reads "Failed (cancelled)" with a fail- colored mood. Mid-stage cancels keep reading as plain "Cancelled".
This commit is contained in:
@@ -113,6 +113,9 @@ func tileStatus(r *model.Run) string {
|
||||
case model.StateWaitingReboot:
|
||||
return "Waiting for reboot"
|
||||
}
|
||||
if cancelledFromHold(r) {
|
||||
return "Failed (cancelled)"
|
||||
}
|
||||
return string(r.State)
|
||||
}
|
||||
|
||||
@@ -120,6 +123,9 @@ func tileMood(r *model.Run) string {
|
||||
if r == nil {
|
||||
return "idle"
|
||||
}
|
||||
if cancelledFromHold(r) {
|
||||
return "fail"
|
||||
}
|
||||
switch r.State {
|
||||
case model.StateCompleted:
|
||||
return "pass"
|
||||
@@ -131,6 +137,15 @@ func tileMood(r *model.Run) string {
|
||||
return "active"
|
||||
}
|
||||
|
||||
// cancelledFromHold is true when a FailedHolding run was later Cancelled
|
||||
// by the operator (tracked by State=Cancelled with FailedStage still
|
||||
// set — mid-stage cancels don't stamp FailedStage). These deserve a
|
||||
// fail-colored tile because the run did fail; the cancel was just the
|
||||
// operator choosing not to recover.
|
||||
func cancelledFromHold(r *model.Run) bool {
|
||||
return r != nil && r.State == model.StateCancelled && r.FailedStage != ""
|
||||
}
|
||||
|
||||
func sshInvocation(keyPath, ip string) string {
|
||||
if keyPath == "" {
|
||||
return "ssh root@" + ip + " (hold key not yet recorded)"
|
||||
|
||||
@@ -131,6 +131,48 @@ func TestHostTile_NoStageStrip(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
// TestTileStatusCancelledFromHold: a run that entered FailedHolding and
|
||||
// was later Cancelled by the operator should read as a failure, not a
|
||||
// plain cancel. The discriminator is FailedStage being set — mid-stage
|
||||
// cancels leave it empty.
|
||||
func TestTileStatusCancelledFromHold(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
run *model.Run
|
||||
wantStatus string
|
||||
wantMood string
|
||||
}{
|
||||
{
|
||||
name: "cancelled from hold shows failed",
|
||||
run: &model.Run{State: model.StateCancelled, FailedStage: "Storage"},
|
||||
wantStatus: "Failed (cancelled)",
|
||||
wantMood: "fail",
|
||||
},
|
||||
{
|
||||
name: "mid-stage cancel stays plain cancelled",
|
||||
run: &model.Run{State: model.StateCancelled},
|
||||
wantStatus: "Cancelled",
|
||||
wantMood: "idle",
|
||||
},
|
||||
{
|
||||
name: "failed-holding itself still reads as FailedHolding",
|
||||
run: &model.Run{State: model.StateFailedHolding, FailedStage: "Storage"},
|
||||
wantStatus: string(model.StateFailedHolding),
|
||||
wantMood: "fail",
|
||||
},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if got := tileStatus(tc.run); got != tc.wantStatus {
|
||||
t.Errorf("tileStatus = %q, want %q", got, tc.wantStatus)
|
||||
}
|
||||
if got := tileMood(tc.run); got != tc.wantMood {
|
||||
t.Errorf("tileMood = %q, want %q", got, tc.wantMood)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestLastSeenLabelAndClass(t *testing.T) {
|
||||
if got := lastSeenLabel(nil); got != "never" {
|
||||
t.Fatalf("label nil = %q, want never", got)
|
||||
|
||||
Reference in New Issue
Block a user