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
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).
248 lines
11 KiB
Go
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)
|
|
}
|
|
}
|