Files
mesh-controller/internal/conditions/plain_test.go
T
jochen 76cfbac7a1
mesh/merge-gate pass: builds build-agent, mesh-controller, route-proxy → ace, g14, novox, shanks; no bus step; every machine composes with the change as it…
mesh/repo-check pass: its merge-check.sh passed
mesh/delivery delivered
mesh/delivery-group group fix/notifications-after-review delivered: every member is delivered
Offer only acknowledgements as answers, keep a refused verdict, and say a change of words
Review found that a desk click proves nothing about who chose, that refused words
could turn "Needs you" into "Nothing for you to do", that sound words were refused,
and that a quiet warning whose words came to need the operator was never said
(hq ADR 0258).
2026-10-08 14:53:36 +02:00

248 lines
11 KiB
Go

package conditions
import (
"encoding/json"
"strings"
"testing"
)
// **The plain rule** (novox/hq ADR 0253): what the operator reads names no identifier, no code and no time
// of its own; the words conditions are made of pass.
func TestThePlainRuleRefusesIdentifiersCodeAndTimes(t *testing.T) {
refused := map[string]string{
"the walk of novox/mesh-catalog a6385479 waits": "a hash",
"plan plan-1791454185265004861 is waiting": "a numbered id",
"see `mesh-delivery.show` for why": "markup",
"call mesh-delivery.show for why": "a dotted name",
"key plan.x.waiting": "a dotted name",
"waited 56m0s": "a duration in code",
"since 10:40": "a clock time",
"since 2026-10-08": "a clock time",
"raised at noon UTC": "a clock time",
"start it with plans go --why": "a command's flag",
"one line\nand another": "a line break",
"the resolver at 10.77.0.1 is silent": "",
"": "nothing",
"**bold**": "markup",
"storage-media.mount failed": "a dotted name",
"the store at /var/lib/mesh is full": "",
"a commit 0123abc landed": "a hash",
}
for text, want := range refused {
why, ok := Plain(text, "g14")
if ok {
t.Errorf("%q passed", text)
continue
}
if want != "" && !strings.HasPrefix(why, want) {
t.Errorf("%q: refused for %q, want %q", text, why, want)
}
}
for _, text := range []string{
"openrazer delivery waiting to start",
"The change to openrazer is merged and built, and has waited 56 minutes for mesh-delivery to let it start.",
"3 failed services on shanks",
"On shanks, mnt-recalbox (a mount), storage-media (a mount) and greenclip failed.",
"Name lookups fail on g14",
"Retired data of postgres on anchor waits for app_db",
"Push keeps being fixed by hand",
} {
if why, ok := Plain(text, "g14", "shanks", "anchor"); !ok {
t.Errorf("%q refused: %s", text, why)
}
}
}
// **A condition carries its plain words**: a producer's own; else its kind's; else its scope's — and the
// event says them beside the summary, for every channel.
func TestAConditionCarriesItsPlainWords(t *testing.T) {
k, _, told, _ := keeper(t)
before := Unworded
var unworded []string
Unworded = func(o Observation, why string) { unworded = append(unworded, why) }
t.Cleanup(func() { Unworded = before })
Wording("test-silent", func(o Observation) Words {
return Words{Headline: o.Machine + " is not answering", Explanation: "The mesh has not heard from it.",
Resolved: o.Machine + " answers again"}
})
c, err := k.Observe(t.Context(), Observation{Scope: ScopeMachine, ID: "ace", Kind: "test-silent", Machine: "ace",
Severity: Warning, Source: "S1", Summary: "ace has not been heard from since 12:00 UTC (bound 3m0s)"})
if err != nil {
t.Fatal(err)
}
if c.Headline != "ace is not answering" || c.Resolved != "ace answers again" || c.Explanation == "" {
t.Fatalf("the kind's words were not kept: %+v", c)
}
if c.Summary != "ace has not been heard from since 12:00 UTC (bound 3m0s)" {
t.Errorf("the summary was changed: %q", c.Summary)
}
said := settled(t, told, 1)
body, _ := json.Marshal(said[0])
for _, field := range []string{`"headline":"ace is not answering"`, `"explanation":"Nothing for you to do. The mesh has not heard from it."`,
`"resolved":"ace answers again"`} {
if !strings.Contains(string(body), field) {
t.Errorf("the event does not carry %s: %s", field, body)
}
}
// A producer's own words win; a resolved line it leaves out is made from its headline.
c, _ = k.Observe(t.Context(), Observation{Scope: ScopeModule, ID: "openrazer.g14", Kind: "test-silent",
Machine: "g14", Severity: Warning, Source: "health", Summary: "openrazer on g14 is not healthy",
Headline: "openrazer not working on g14", Explanation: "Its service stopped with an error."})
if c.Headline != "openrazer not working on g14" || c.Resolved != "Resolved: openrazer not working on g14" {
t.Errorf("the producer's words: %+v", c)
}
if len(unworded) != 0 {
t.Errorf("words that were plain were reported: %v", unworded)
}
// Words that are not plain, and a kind with none, are said from the scope — and reported.
c, _ = k.Observe(t.Context(), Observation{Scope: ScopeModule, ID: "openrazer.g14", Token: "x", Kind: "test-unworded",
Machine: "g14", Severity: Warning, Source: "health", Summary: "openrazer on g14 is not healthy"})
if c.Headline != "Openrazer on g14 needs a look" || !strings.HasPrefix(c.Explanation, NothingToDo) {
t.Errorf("the scope's words: %+v", c)
}
c, _ = k.Observe(t.Context(), Observation{Scope: ScopePlan, ID: "plan-1791454185265004861", Kind: "test-silent",
Severity: Warning, Source: "S16", Summary: "the walk waits", Headline: "plan-1791454185265004861 waits"})
if strings.Contains(c.Headline, "plan-") || c.Headline != "A walk needs a look" {
t.Errorf("an id reached the headline: %+v", c)
}
if len(unworded) != 2 || !strings.Contains(unworded[0], "no plain words") || !strings.Contains(unworded[1], "not plain") {
t.Errorf("what was said in borrowed words was not reported: %v", unworded)
}
}
// **An explanation opens with its verdict, and never sends the operator to an agent.**
func TestEveryExplanationOpensWithItsVerdict(t *testing.T) {
if got := Verdict("", "It may be asleep."); got != "Nothing for you to do. It may be asleep." {
t.Errorf("%q", got)
}
if got := Verdict("release it, or stop it.", "It is held."); got != "Needs you: release it, or stop it. It is held." {
t.Errorf("%q", got)
}
for _, w := range []Words{
{Headline: "x waits", Explanation: "Have an agent start it.", Resolved: "x started"},
{Headline: "x waits", Explanation: "It waits.", Needs: "start it by hand.", Resolved: "x started"},
{Headline: "x waits", Explanation: "It waits.", Needs: "start it", Resolved: "x started"},
{Headline: "x waits", Explanation: "It waits.", Resolved: "x started", Actions: []Action{{Label: "Start"}}},
} {
if _, ok := PlainWords(w); ok {
t.Errorf("passed: %+v", w)
}
}
if why, ok := PlainWords(Words{Headline: "push keeps being fixed by hand", Explanation: "A person repaired " +
"push by hand 35 times.", Resolved: "Resolved", Needs: "release it, or stop it.",
Actions: []Action{{Label: "Release", Verb: "mesh-delivery.release"}}}); !ok {
t.Errorf("refused: %s", why)
}
}
// **A healer that gave up says so in the explanation**: the words of the kind said "nothing to do"; once the
// budget is spent, the operator is needed.
func TestAnEscalatedConditionSaysItNeedsTheOperator(t *testing.T) {
k, _, _, _ := keeper(t)
ctx := t.Context()
o := Observation{Scope: ScopeMachine, ID: "ace", Kind: "silent", Machine: "ace", Severity: Warning, Source: "S1",
Summary: "ace is silent", Headline: "ace is not answering", Explanation: "It may be asleep."}
if _, err := k.Observe(ctx, o); err != nil {
t.Fatal(err)
}
c, _, err := k.Escalate(ctx, o.Key(), Attempt{What: "budget spent", Outcome: "escalated", By: "healer H1"})
if err != nil {
t.Fatal(err)
}
if want := "Needs you: " + Escalated + " It may be asleep."; c.Explanation != want || c.Needs != Escalated {
t.Fatalf("escalated: %q, want %q", c.Explanation, want)
}
c, _ = k.Observe(ctx, o)
if strings.Count(c.Explanation, Escalated) != 1 || strings.Contains(c.Explanation, NothingToDo) {
t.Errorf("seen again after escalation: %q", c.Explanation)
}
}
// **What the plain rule refused wrongly** (novox/hq ADR 0258): a login session is the operator's own word,
// "e.g." is not a dotted name, a long count is not a hash, and a long explanation is cut, not refused.
func TestThePlainRuleRefusesNoPlainWords(t *testing.T) {
for _, text := range []string{
"log out of every session on g14 and log in again.",
"A module, e.g. the lighting daemon, waits.",
"It holds 12345678 files.",
"A person repaired push by hand 35 times in 14 days.",
} {
if why, ok := Plain(text, "g14"); !ok {
t.Errorf("%q refused: %s", text, why)
}
}
if _, ok := Plain("commit 0123abc landed"); ok {
t.Error("a hash passed")
}
k, _, _, _ := keeper(t)
long := strings.Repeat("The bus refused a message. ", 30)
Wording("test-long", func(o Observation) Words {
return Words{Headline: "A long one", Explanation: long, Resolved: "Done"}
})
before := Unworded
var unworded []string
Unworded = func(o Observation, why string) { unworded = append(unworded, why) }
t.Cleanup(func() { Unworded = before })
c, err := k.Observe(t.Context(), Observation{Scope: ScopeBus, ID: "x", Kind: "test-long", Severity: Warning,
Source: "test", Summary: "long"})
if err != nil {
t.Fatal(err)
}
if c.Headline != "A long one" || len(c.Explanation) > ExplanationMax || !strings.HasSuffix(c.Explanation, "…") ||
len(unworded) != 0 {
t.Errorf("a long explanation was not cut: %d %q %v", len(c.Explanation), c.Headline, unworded)
}
}
// **Words refused keep their verdict and their answers** (ADR 0258): a producer that needed the operator
// still does, said as "read the details", and its sound answers stay.
func TestRefusedWordsKeepTheirVerdictAndAnswers(t *testing.T) {
k, _, _, _ := keeper(t)
before := Unworded
Unworded = func(Observation, string) {}
t.Cleanup(func() { Unworded = before })
silence := SilenceAction("module.openrazer.g14.x")
c, err := k.Observe(t.Context(), Observation{Scope: ScopeModule, ID: "openrazer.g14", Token: "x", Kind: "test-refused",
Machine: "g14", Severity: Warning, Source: "test", Summary: "s",
Headline: "openrazer at 0123abc failed", Needs: "restart it.", Explanation: "It failed.",
Actions: []Action{silence}})
if err != nil {
t.Fatal(err)
}
if c.Needs != FallbackNeeds || !strings.HasPrefix(c.Explanation, NeedsYou+" "+FallbackNeeds) ||
len(c.Actions) != 1 || c.Actions[0].Label != silence.Label || strings.Contains(c.Headline, "0123abc") {
t.Errorf("the verdict or the answers were lost: %+v", c)
}
// Words that needed nothing stay "Nothing for you to do.".
c, _ = k.Observe(t.Context(), Observation{Scope: ScopeModule, ID: "openrazer.g14", Token: "y", Kind: "test-refused",
Machine: "g14", Severity: Warning, Source: "test", Summary: "s", Headline: "at 0123abc", Explanation: "x."})
if c.Needs != "" || !strings.HasPrefix(c.Explanation, NothingToDo) {
t.Errorf("%+v", c)
}
}
// **Words that now need the operator are said as a change** (ADR 0258): a condition whose needs or answers
// change emits condition-changed, so a warning kept quiet is said.
func TestAChangeOfWordsIsSaid(t *testing.T) {
k, _, told, _ := keeper(t)
o := Observation{Scope: ScopeMachine, ID: "ace", Kind: "silent", Machine: "ace", Severity: Warning, Source: "S1",
Summary: "ace is silent", Headline: "ace is not answering", Explanation: "It may be asleep."}
if _, err := k.Observe(t.Context(), o); err != nil {
t.Fatal(err)
}
if _, err := k.Observe(t.Context(), o); err != nil { // the same words: no event
t.Fatal(err)
}
o.Needs = "check that ace is on."
if _, err := k.Observe(t.Context(), o); err != nil {
t.Fatal(err)
}
said := settled(t, told, 2)
if len(said) != 2 || said[1].Change != ChangeWords || said[1].Event != EventChanged || said[1].Needs != o.Needs {
t.Fatalf("%+v", said)
}
}