diff --git a/cmd/mesh-controller/plain_words.go b/cmd/mesh-controller/plain_words.go index 56803b12..846667ba 100644 --- a/cmd/mesh-controller/plain_words.go +++ b/cmd/mesh-controller/plain_words.go @@ -254,9 +254,10 @@ var plainWordings = map[string]func(conditions.Observation) words{ kindDataMissing: worded(func(o conditions.Observation) words { what, _ := dataWords(o) return words{Headline: conditions.Capital(what) + " is gone", - Needs: "restore it from a backup, or silence this if you removed it.", + // No silence from a click: data loss is never waved away from a notification (ADR 0258). + Needs: "restore it from a backup, or silence this " + FromMeshMCPServer, Explanation: fmt.Sprintf("Where %s is kept on %s, nothing exists any more.", what, machineOr(o, "its machine")), - Resolved: conditions.Capital(what) + " is back", Actions: []conditions.Action{conditions.SilenceAction(o.Key())}} + Resolved: conditions.Capital(what) + " is back"} }), kindDataShrank: worded(func(o conditions.Observation) words { what, _ := dataWords(o) @@ -265,9 +266,10 @@ var plainWordings = map[string]func(conditions.Observation) words{ what = "a dataset" } return words{Headline: conditions.Capital(what) + " shrank on " + m, - Needs: "if you meant it, silence this; if not, restore the last good copy from a backup.", + // No silence from a click: data loss is never waved away from a notification (ADR 0258). + Needs: "restore the last good copy from a backup, or silence this " + FromMeshMCPServer, Explanation: "More than half of what it held is gone within a week.", - Resolved: "Resolved: the shrinking on " + m + " is explained", Actions: []conditions.Action{conditions.SilenceAction(o.Key())}} + Resolved: "Resolved: the shrinking on " + m + " is explained"} }), kindDataQuiet: worded(func(o conditions.Observation) words { what, _ := dataWords(o) @@ -637,7 +639,7 @@ func walkWaitingWords(w waitFacts, in time.Duration, severity conditions.Severit // waitingNeeds is what the operator does about a walk waiting past its urgent bound: nothing before it. func waitingNeeds(severity conditions.Severity) string { if severity == conditions.Urgent { - return "start it, or stop it." + return "start it, or stop it, " + FromMeshMCPServer } return "" } @@ -656,11 +658,17 @@ func moduleNeeds(node string, rs []inventory.ResourceHealth) string { } } if unit != "" { - return fmt.Sprintf("restart its service %s on %s; if it fails again, the details say why.", unit, node) + return fmt.Sprintf("restart its service %s on %s %s", unit, node, FromMeshMCPServer) } return "" } +// FromMeshMCPServer ends what the operator needs when no notification can do it (ADR 0258), naming the mesh MCP +// server (the glossary's word; "console" is retired): the answer is not an +// acknowledgement, so it is given where the operator is known to be the one asking, until answers are +// authorised (to-be 46 phases 5 and 6). +const FromMeshMCPServer = "from the mesh MCP server; this notification cannot do it." + // reloginNeeds is what an account waiting for its groups needs (ADR 0252). func reloginNeeds(node string) string { return fmt.Sprintf("log out of every session on %s and log in again.", node) @@ -684,11 +692,11 @@ func stalledWords(l stalledLine, o conditions.Observation) (headline, explanatio // performs it (ADR 0258). switch held { case "held": - needs = "release it, or stop it." + needs = "release it, or stop it, " + FromMeshMCPServer case "ready", "checked": needs = "merge its pull request, or close it." default: - needs = "stop it, or read the details to see what it waits for." + needs = "stop it " + FromMeshMCPServer } } return fmt.Sprintf("Delivery of %s %s %s", name, held, long), diff --git a/cmd/mesh-controller/plain_words_test.go b/cmd/mesh-controller/plain_words_test.go index 16c6747e..abb8355e 100644 --- a/cmd/mesh-controller/plain_words_test.go +++ b/cmd/mesh-controller/plain_words_test.go @@ -68,7 +68,7 @@ func TestADeliveryWaitingNeedsNothingUntilItsBoundThenOffersStartAndStop(t *test f.waits[0].since = now.Add(-5 * time.Hour) got = watchWaits(f) plainExample(t, got[0], "openrazer delivery waiting to start", - "Needs you: start it, or stop it. The change to openrazer is merged and built, and mesh-delivery (the "+ + "Needs you: start it, or stop it, from the mesh MCP server; this notification cannot do it. The change to openrazer is merged and built, and mesh-delivery (the "+ "module that decides when a delivery goes out) has not let it start for 5 hours, so mesh-delivery may "+ "be stuck.") @@ -88,7 +88,7 @@ func TestAModuleUnhealthyAsksForARestartInWords(t *testing.T) { Resource: "openrazer-daemon", Target: "openrazer-daemon.service", Reason: "failed in the account's own service manager (exit-code)", Since: time.Now()}}) plainExample(t, o, "openrazer not working on g14", - "Needs you: restart its service openrazer-daemon on g14; if it fails again, the details say why. "+ + "Needs you: restart its service openrazer-daemon on g14 from the mesh MCP server; this notification cannot do it. "+ "openrazer on g14 is not healthy: its service openrazer-daemon stopped with an error. It clears as soon "+ "as it runs again.") // An account waiting for a new login (ADR 0252) asks for the login, held to the plain rule. @@ -149,7 +149,7 @@ func TestADeliveryHeldAsksForReleaseOrStopInWords(t *testing.T) { got := stalledObservations([]stalledLine{{ID: "novox/hq@055550802096", State: "held", For: "36h2m6s", Bound: "24h0m0s", H2: "none: the state is the operator's", Says: "it waits for the operator"}}) plainExample(t, got[0], "Delivery of hq held for 36 hours", - "Needs you: release it, or stop it. A delivery of hq has been held for 36 hours, past its limit.", + "Needs you: release it, or stop it, from the mesh MCP server; this notification cannot do it. A delivery of hq has been held for 36 hours, past its limit.", ) } @@ -204,3 +204,18 @@ func TestAnOperatorsAnswerIsNoHandRepair(t *testing.T) { t.Fatalf("a silence by hand stopped counting: %+v", got) } } + +// **Data loss is never silenced from a notification** (ADR 0258): data missing or shrunk offers no answer, +// and says where it is silenced. +func TestDataLossOffersNoSilence(t *testing.T) { + for _, kind := range []string{kindDataMissing, kindDataShrank} { + w := plainWordings[kind](conditions.Observation{Scope: conditions.ScopeMachine, ID: "ace.immich.library", + Machine: "ace", Kind: kind, Severity: conditions.Urgent}) + if len(w.Actions) != 0 || !strings.HasSuffix(w.Needs, FromMeshMCPServer) { + t.Errorf("%s: %+v", kind, w) + } + if why, ok := conditions.PlainWords(w, "ace"); !ok { + t.Errorf("%s: %s", kind, why) + } + } +}