diff --git a/internal/cli/doctor.go b/internal/cli/doctor.go index 0b91ec5..8728365 100644 --- a/internal/cli/doctor.go +++ b/internal/cli/doctor.go @@ -9,6 +9,7 @@ import ( "github.com/spf13/cobra" + "github.com/abhijitkrm/cometcli/internal/config" "github.com/abhijitkrm/cometcli/internal/toolkit" ) @@ -35,11 +36,29 @@ func num(v any) int64 { return 0 } +// signsBlocks reports whether doctor should check signing and jailing: a +// validator, a profile with no role set, or any node saved with a valoper. +// Archive, RPC and sentry nodes don't sign, so those checks don't apply. +func signsBlocks(p *config.Profile) bool { + return p.IsValidator() || p.Role == "" || p.Metadata["valoper"] != "" +} + +// icon is a check's mark: a warning is "!" even when it has detail text. +func icon(ch Check) string { + switch { + case ch.Warn: + return "!" + case !ch.OK: + return "✗" + } + return "✓" +} + // DoctorCmd runs the full node health checklist. func DoctorCmd(reg *toolkit.Registry) *cobra.Command { return &cobra.Command{ Use: "doctor", - Short: "Full validator health checklist", + Short: "Full node health checklist", RunE: func(cmd *cobra.Command, _ []string) error { c, err := NewCtx(cmd, true) if err != nil { @@ -101,31 +120,33 @@ func DoctorCmd(reg *toolkit.Registry) *cobra.Command { bad := num(r.Data["bad"]) return Check{OK: bad == 0, Detail: fmt.Sprintf("%d problems", bad)} }) - run("signing health", "val.signing", nil, func(r *toolkit.Result, err error) Check { - if err != nil { - return Check{Warn: true, Detail: "skipped: " + err.Error()} - } - missed := num(r.Data["missed"]) - window := num(r.Data["window"]) - tomb, _ := r.Data["tombstoned"].(bool) - if tomb { - return Check{OK: false, Detail: "TOMBSTONED — double-sign slashed"} - } - if window > 0 && missed > window/10 { - return Check{OK: false, Detail: fmt.Sprintf("missing %d/%d blocks", missed, window)} - } - return Check{OK: true, Detail: fmt.Sprintf("missed %d/%d", missed, window)} - }) - run("jail status", "val.status", nil, func(r *toolkit.Result, err error) Check { - if err != nil { - return Check{Warn: true, Detail: "skipped: " + err.Error()} - } - j, _ := r.Data["jailed"].(bool) - if j { - return Check{OK: false, Detail: "JAILED — investigate + `cometcli val unjail`"} - } - return Check{OK: true, Detail: "not jailed"} - }) + if signsBlocks(c.Profile) { + run("signing health", "val.signing", nil, func(r *toolkit.Result, err error) Check { + if err != nil { + return Check{Warn: true, Detail: "skipped: " + err.Error()} + } + missed := num(r.Data["missed"]) + window := num(r.Data["window"]) + tomb, _ := r.Data["tombstoned"].(bool) + if tomb { + return Check{OK: false, Detail: "TOMBSTONED — double-sign slashed"} + } + if window > 0 && missed > window/10 { + return Check{OK: false, Detail: fmt.Sprintf("missing %d/%d blocks", missed, window)} + } + return Check{OK: true, Detail: fmt.Sprintf("missed %d/%d", missed, window)} + }) + run("jail status", "val.status", nil, func(r *toolkit.Result, err error) Check { + if err != nil { + return Check{Warn: true, Detail: "skipped: " + err.Error()} + } + j, _ := r.Data["jailed"].(bool) + if j { + return Check{OK: false, Detail: "JAILED — investigate + `cometcli val unjail`"} + } + return Check{OK: true, Detail: "not jailed"} + }) + } run("upgrade pending", "chain.upgrade-plan", nil, func(r *toolkit.Result, err error) Check { if err != nil { return Check{Warn: true, Detail: "skipped: " + err.Error()} @@ -203,16 +224,7 @@ func DoctorCmd(reg *toolkit.Registry) *cobra.Command { } var b strings.Builder for _, ch := range checks { - icon := "✓" - switch { - case !ch.OK && ch.Detail != "": - icon = "✗" - case ch.Warn: - icon = "!" - case !ch.OK: - icon = "✗" - } - fmt.Fprintf(&b, "%s %-18s %s\n", icon, ch.Name, ch.Detail) + fmt.Fprintf(&b, "%s %-18s %s\n", icon(ch), ch.Name, ch.Detail) } fmt.Fprintf(&b, "\n%d failures, %d warnings\n", fails, warns) fmt.Fprint(c.Out, b.String()) diff --git a/internal/cli/doctor_test.go b/internal/cli/doctor_test.go new file mode 100644 index 0000000..38a52ae --- /dev/null +++ b/internal/cli/doctor_test.go @@ -0,0 +1,45 @@ +package cli + +import ( + "testing" + + "github.com/abhijitkrm/cometcli/internal/config" +) + +func TestSignsBlocks(t *testing.T) { + for _, tc := range []struct { + role, valoper string + want bool + }{ + {"validator", "", true}, + {"", "", true}, // role unset: could be a validator + {"archive", "", false}, + {"rpc", "", false}, + {"sentry", "", false}, + {"archive", "cosmosvaloper1x", true}, // saved with a valoper + } { + p := &config.Profile{Role: tc.role, Metadata: map[string]string{}} + if tc.valoper != "" { + p.Metadata["valoper"] = tc.valoper + } + if got := signsBlocks(p); got != tc.want { + t.Errorf("role %q valoper %q: got %v, want %v", tc.role, tc.valoper, got, tc.want) + } + } +} + +func TestDoctorIcon(t *testing.T) { + for _, tc := range []struct { + ch Check + want string + }{ + {Check{OK: true}, "✓"}, + {Check{OK: false, Detail: "0 peers"}, "✗"}, + {Check{Warn: true, Detail: "skipped: no route"}, "!"}, // a warning with detail is not a failure + {Check{Warn: true}, "!"}, + } { + if got := icon(tc.ch); got != tc.want { + t.Errorf("%+v: got %s, want %s", tc.ch, got, tc.want) + } + } +}