From 51d7bbfdc88c64ff9d8073e042bd2a2f2e85647e Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 8 Oct 2026 14:25:20 +0200 Subject: [PATCH] Give relogin-needed its plain words: it needs the operator and offers no button, since only their new login can end it (hq ADR 0253, ADR 0254) --- cmd/mesh-controller/module_health.go | 16 +++++++++++++++- cmd/mesh-controller/person_wait_test.go | 19 +++++++++++++++++++ cmd/mesh-controller/plain_words.go | 15 +++++++++++++++ 3 files changed, 49 insertions(+), 1 deletion(-) diff --git a/cmd/mesh-controller/module_health.go b/cmd/mesh-controller/module_health.go index 0d6ddc15..f408af19 100644 --- a/cmd/mesh-controller/module_health.go +++ b/cmd/mesh-controller/module_health.go @@ -256,13 +256,27 @@ func sayWaiters(ctx context.Context, k *conditions.Keeper, hold *holding, p cata } // reloginObservation is a module waiting for a person's new login on a machine (novox/hq ADR 0254): the -// operator's, a warning, its summary the one sentence that says what to do; the resources are evidence. +// operator's, a warning, its summary the one sentence that says what to do; the resources are evidence. Its +// plain words (ADR 0253) are the kind's: it needs the operator, and offers no answer — no verb can log a +// person in again, and a restart of the module's service would start it in the same session. func reloginObservation(module, node, said string, rs []inventory.ResourceHealth) conditions.Observation { o := moduleUnhealthyObservation(module, node, rs) o.Token, o.Kind, o.Resolver, o.Summary = kindReloginNeeded, kindReloginNeeded, conditions.ResolverOperator, said + w := reloginWords(module, node) + o.Headline, o.Explanation, o.Resolved, o.Needs, o.Actions = w.Headline, w.Explanation, w.Resolved, w.Needs, nil return o } +// reloginWords is what the operator reads of a module waiting for their new login on a machine (ADR 0253, +// ADR 0254): never quiet, since only the operator can do it, and no button, since nothing else can. +func reloginWords(module, node string) words { + return words{Headline: fmt.Sprintf("%s waits for a new login on %s", module, node), + Needs: fmt.Sprintf("log out of %s completely and log in again, or restart it.", node), + Explanation: fmt.Sprintf("%s put your account in a group it needs. You logged in before that, so %s "+ + "cannot run until you log in again. Its update is in place and nothing was undone.", conditions.Capital(module), module), + Resolved: fmt.Sprintf("%s runs on %s after your new login", module, node)} +} + // moduleUnhealthyObservation is a module unhealthy on a machine, in words: the summary names the module, // the machine and what is wrong with each resource; the detail — targets, streaks, since — is evidence. func moduleUnhealthyObservation(module, node string, rs []inventory.ResourceHealth) conditions.Observation { diff --git a/cmd/mesh-controller/person_wait_test.go b/cmd/mesh-controller/person_wait_test.go index 3030f80b..4670fa0c 100644 --- a/cmd/mesh-controller/person_wait_test.go +++ b/cmd/mesh-controller/person_wait_test.go @@ -193,3 +193,22 @@ func TestAModuleHealthyOnItsOwnKeepsItsPassWhenItsSendFails(t *testing.T) { t.Errorf("the walk does not say what kept its pass: %s", p.Note) } } + +// What the operator reads of the wait (ADR 0253, ADR 0254): it needs them, so it is never quiet, and it offers +// no button, since nothing but their own new login can do it. +func TestTheReloginConditionNeedsTheOperatorAndOffersNoButton(t *testing.T) { + o := reloginObservation("openrazer", "g14", "relogin needed on g14: …", []inventory.ResourceHealth{ + relogin("openrazer", "operator"), userUnit("openrazer", "operator")}) + plainExample(t, o, "openrazer waits for a new login on g14", + "Needs you: log out of g14 completely and log in again, or restart it. Openrazer put your account in a "+ + "group it needs. You logged in before that, so openrazer cannot run until you log in again. Its "+ + "update is in place and nothing was undone.") + if len(o.Actions) != 0 || o.Needs == "" { + t.Errorf("relogin: needs %q, actions %+v; want needs and no button", o.Needs, o.Actions) + } + // The kind's own wording, for a condition raised without words, says the same. + if w := plainWordings[kindReloginNeeded](conditions.Observation{Scope: conditions.ScopeModule, ID: "openrazer.g14", Machine: "g14"}); w.Needs != o.Needs || + w.Headline != o.Headline || len(w.Actions) != 0 { + t.Errorf("the kind's wording: %+v", w) + } +} diff --git a/cmd/mesh-controller/plain_words.go b/cmd/mesh-controller/plain_words.go index edd6a849..6ece69aa 100644 --- a/cmd/mesh-controller/plain_words.go +++ b/cmd/mesh-controller/plain_words.go @@ -183,6 +183,13 @@ var plainWordings = map[string]func(conditions.Observation) words{ Explanation: fmt.Sprintf("%s is not healthy. It clears as soon as it runs again.", conditions.Capital(thing)), Resolved: conditions.Capital(thing) + " works again"} }), + kindReloginNeeded: worded(func(o conditions.Observation) words { + module := "" + if o.Scope == conditions.ScopeModule { + module = idPart(o, 0) + } + return reloginWords(orModule(module), machineOr(o, "a machine")) + }), kindProviderFailing: worded(func(o conditions.Observation) words { thing, consumer := conditions.ThingWords(o), idPart(o, 2) if consumer == "" { @@ -759,3 +766,11 @@ func unitReasonWords(reason string) string { } return "failed" } + +// orModule is a module's name, or "a module" when none is known. +func orModule(m string) string { + if m == "" { + return "a module" + } + return m +}