mesh/merge-gate pass: builds messenger → novox; no bus step; every machine composes with the change as it did without (4 of 4 compose)
mesh/repo-check pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-check-typescript.sh (typescript)
mesh/delivery superseded: a newer head of the same pull request
The review of #192 found three faults, all verified by running. The tests did not prove the claim the change leads with: a probe that sent, queued, recorded and wrote from inside Preview still passed, because the condition they used was a warning needing nothing, which is held rather than sent. They now hold an urgent condition, snapshot what a send would move — the channels, the queue, the history, the refusals, the stored record and the record held — assert none of it moved, and tick once more. Every one of those probes now fails the tests. `cleared` and `silenced` said wording no send would carry: an open record has no clearing time, so a condition open four hours cleared "after a moment", and no silence, so a silence read "until Mon 1 Jan 00:17". A clearing is now composed as if it cleared now, which the answer says; a silence is refused unless the condition is under one. The preview composed only its own words, while a send reduces a message whose words the content rule refuses. The escalation walk is now one function, allowedWords, which `words` calls for a send and the preview calls alone — so the words read are the words that would leave, and reading them records no refusal and states nothing on the bus. withTracking hands back the line it put in, so the preview reports that line instead of deciding the rule a second time; and a record that is not a condition, or one carrying an open ask, is refused or says that a channel's message carries more.
226 lines
12 KiB
Go
226 lines
12 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// A condition's message says which issue tracks it and where that issue stands (novox/hq issue 400, ADR 0285):
|
|
// one case per wording, each through the message as composed, and the content rule passes every line.
|
|
|
|
var trackNow = time.Date(2026, 10, 10, 19, 30, 0, 0, time.UTC)
|
|
|
|
func at(h, m int) *time.Time {
|
|
t := time.Date(2026, 10, 10, h, m, 0, 0, time.UTC)
|
|
return &t
|
|
}
|
|
|
|
// trackedMessage is the message of a warning that needs nothing, as the controller words a hand repair, with the
|
|
// issues given tracking it.
|
|
func trackedMessage(issues []TrackedIssue, known bool) Message {
|
|
r := &Record{Key: "mesh.hand-acts.rolled-back.healer-wanted", Origin: "condition", Severity: Warning,
|
|
Headline: "Rolled-back keeps being fixed by hand",
|
|
Explanation: "Nothing for you to do. A person repaired rolled-back by hand 4 times in 14 days, so an automatic repair is wanted for it. Nothing is broken now.",
|
|
Raised: trackNow.Add(-26 * time.Hour)}
|
|
m := compose(r, "quiet-reminder", trackNow, time.UTC, 0)
|
|
out, _ := withTracking(m, r, "quiet-reminder", issues, known, trackNow, time.UTC)
|
|
return out
|
|
}
|
|
|
|
func TestAConditionsMessageSaysWhereTheIssueThatTracksItStands(t *testing.T) {
|
|
cases := []struct {
|
|
name string
|
|
issues []TrackedIssue
|
|
known bool
|
|
line string
|
|
verdict string // what the body opens with
|
|
}{
|
|
{"no issue", nil, true,
|
|
"No issue tracks this yet.", "Nothing for you to do."},
|
|
{"the tracker gave no answer", nil, false,
|
|
"Whether an issue tracks this could not be checked.", "Nothing for you to do."},
|
|
{"nobody works on it", []TrackedIssue{{Number: 352, Status: "located"}}, true,
|
|
"Tracked as issue 352: nobody is working on it.", "A person repaired"},
|
|
{"an agent is fixing it", []TrackedIssue{{Number: 386, Status: "fixing", Worker: "fix-386@g14", Since: at(21, 10)}}, true,
|
|
"Tracked as issue 386: fix-386 on g14 is fixing it since 21:10.", "Nothing for you to do."},
|
|
{"an agent looks for its cause", []TrackedIssue{{Number: 386, Status: "diagnosing", Worker: "jochen@shanks", Since: at(18, 5)}}, true,
|
|
"Tracked as issue 386: jochen on shanks is looking for its cause since 18:05.", "Nothing for you to do."},
|
|
{"its worker went quiet", []TrackedIssue{{Number: 386, Status: "fixing", Worker: "fix-386@g14", Since: at(9, 0), Active: at(9, 30), Stale: true}}, true,
|
|
"Tracked as issue 386: nobody has worked on it since 09:30.", "A person repaired"},
|
|
{"a fix waits to be merged", []TrackedIssue{{Number: 386, Status: "located", Fix: "not-merged"}}, true,
|
|
"Tracked as issue 386: a fix is written and waits to be merged.", "Nothing for you to do."},
|
|
{"its fix is on its way", []TrackedIssue{{Number: 386, Status: "fixing", Worker: "fix-386@g14", Since: at(9, 0), Fix: "merged"}}, true,
|
|
"Tracked as issue 386: its fix is merged and on its way.", "Nothing for you to do."},
|
|
{"the delivery was not heard yet", []TrackedIssue{{Number: 386, Status: "fixing", Fix: "unheard"}}, true,
|
|
"Tracked as issue 386: a fix is named, and nothing is heard yet of its delivery.", "Nothing for you to do."},
|
|
{"its fix is held", []TrackedIssue{{Number: 386, Status: "fixing", Fix: "held"}}, true,
|
|
"Tracked as issue 386: its fix is merged and held before it is delivered.", "Nothing for you to do."},
|
|
{"its fix's delivery failed", []TrackedIssue{{Number: 386, Status: "fixing", Fix: "failed"}}, true,
|
|
"Tracked as issue 386: its fix is merged, but its delivery failed.", "Nothing for you to do."},
|
|
{"its fix is delivered", []TrackedIssue{{Number: 386, Status: "fixing", Fix: "delivered"}}, true,
|
|
"Tracked as issue 386: its fix is delivered and waits to be checked.", "Nothing for you to do."},
|
|
{"it was resolved", []TrackedIssue{{Number: 386, Status: "resolved", Fix: "delivered"}}, true,
|
|
"Tracked as issue 386: it was resolved, yet this still happens.", "A person repaired"},
|
|
{"it will not be fixed", []TrackedIssue{{Number: 386, Status: "wontfix"}}, true,
|
|
"Tracked as issue 386: it will not be fixed.", "A person repaired"},
|
|
{"one not fixed after one open", []TrackedIssue{{Number: 100, Status: "wontfix"}, {Number: 200, Status: "open"}}, true,
|
|
"Tracked as issue 200: nobody is working on it. Also as issue 100: it will not be fixed.", "A person repaired"},
|
|
{"its worker went quiet on one that waits for a decision", []TrackedIssue{{Number: 400, Status: "fixing", Needs: "decision",
|
|
Worker: "fix-400@g14", Since: at(9, 0), Active: at(9, 30), Stale: true}}, true,
|
|
"Tracked as issue 400: nobody has worked on it since 09:30.", "A person repaired"},
|
|
{"it waits for the operator", []TrackedIssue{{Number: 336, Status: "open", Needs: "operator"}}, true,
|
|
"Tracked as issue 336: it waits for your answer.", "Needs you: answer issue 336. A person repaired"},
|
|
{"it waits for a decision", []TrackedIssue{{Number: 400, Status: "open", Needs: "decision"}}, true,
|
|
"Tracked as issue 400: nobody is working on it; it waits for a decision first.", "A person repaired"},
|
|
{"two issues, one worked on", []TrackedIssue{
|
|
{Number: 386, Status: "fixing", Worker: "fix-386@g14", Since: at(21, 10)},
|
|
{Number: 379, Status: "located"}}, true,
|
|
"Tracked as issue 379: nobody is working on it. Also as issue 386: fix-386 on g14 is fixing it since 21:10.",
|
|
"Nothing for you to do."},
|
|
{"two issues, nobody on either", []TrackedIssue{{Number: 386, Status: "open"}, {Number: 379, Status: "located"}}, true,
|
|
"Tracked as issue 379: nobody is working on it. Also as issue 386: nobody is working on it.", "A person repaired"},
|
|
{"one of two waits for the operator", []TrackedIssue{
|
|
{Number: 386, Status: "fixing", Worker: "fix-386@g14", Since: at(21, 10)},
|
|
{Number: 379, Status: "open", Needs: "operator"}}, true,
|
|
"Tracked as issue 379: it waits for your answer. Also as issue 386: fix-386 on g14 is fixing it since 21:10.",
|
|
"Needs you: answer issue 379."},
|
|
{"more than three", []TrackedIssue{{Number: 1, Status: "open"}, {Number: 2, Status: "open"}, {Number: 3, Status: "open"},
|
|
{Number: 4, Status: "open"}, {Number: 5, Status: "resolved"}}, true,
|
|
"Tracked as issue 1: nobody is working on it. Also as issue 2: nobody is working on it. Also as issue 3: nobody is " +
|
|
"working on it. And 2 more issues.", "A person repaired"},
|
|
}
|
|
for _, c := range cases {
|
|
t.Run(c.name, func(t *testing.T) {
|
|
m := trackedMessage(c.issues, c.known)
|
|
lines := strings.Split(m.Body, "\n")
|
|
if len(lines) != 3 || lines[1] != c.line {
|
|
t.Fatalf("the issue line:\n got %q\nwant %q", m.Body, c.line)
|
|
}
|
|
if !strings.HasPrefix(lines[0], c.verdict) {
|
|
t.Errorf("the body opens %q, want %q", lines[0], c.verdict)
|
|
}
|
|
if !strings.HasPrefix(lines[2], "Open since ") {
|
|
t.Errorf("since is not last: %q", m.Body)
|
|
}
|
|
if r, ok := Check(m.Text()); !ok {
|
|
t.Errorf("the content rule refuses it: %v", r)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// Never "Nothing for you to do." while an issue tracking it waits for the operator, nor while nobody works on any.
|
|
func TestNothingForYouToDoIsNeverSaidWhileTheIssueWaitsOnTheOperatorOrNobodyWorksOnIt(t *testing.T) {
|
|
for _, issues := range [][]TrackedIssue{
|
|
{{Number: 336, Status: "open", Needs: "operator"}},
|
|
{{Number: 336, Status: "fixing", Worker: "fix-336@g14", Since: at(9, 0), Needs: "operator"}},
|
|
{{Number: 352, Status: "located"}},
|
|
{{Number: 352, Status: "fixing", Worker: "fix-352@g14", Since: at(9, 0), Stale: true}},
|
|
} {
|
|
if m := trackedMessage(issues, true); strings.Contains(m.Body, "Nothing for you to do") {
|
|
t.Errorf("%+v: %q", issues, m.Body)
|
|
}
|
|
}
|
|
}
|
|
|
|
// A verdict that already needs the operator is left as it is; the line is added below it.
|
|
func TestAVerdictThatNeedsTheOperatorStands(t *testing.T) {
|
|
r := &Record{Key: "module.mounts.shanks.unhealthy", Origin: "condition", Severity: Urgent, Headline: "mounts not working on shanks",
|
|
Explanation: "Needs you: give the share's login. mounts on shanks is not healthy.", Raised: trackNow.Add(-time.Hour)}
|
|
m, _ := withTracking(compose(r, "raised", trackNow, time.UTC, 0), r, "raised",
|
|
[]TrackedIssue{{Number: 386, Status: "open", Needs: "operator"}}, true, trackNow, time.UTC)
|
|
want := "Needs you: give the share's login. mounts on shanks is not healthy.\nTracked as issue 386: it waits for your answer.\nSince 18:30."
|
|
if m.Body != want {
|
|
t.Fatalf("got %q\nwant %q", m.Body, want)
|
|
}
|
|
}
|
|
|
|
// A clearing, a silence and a notice from a module say nothing of issues.
|
|
func TestAClearingASilenceAndANoticeSayNoIssue(t *testing.T) {
|
|
issues := []TrackedIssue{{Number: 352, Status: "located"}}
|
|
r := &Record{Key: "k", Origin: "condition", Headline: "h", Explanation: "Nothing for you to do. x.", Raised: trackNow.Add(-time.Hour),
|
|
Cleared: trackNow, SilencedTill: trackNow.Add(time.Hour)}
|
|
for _, what := range []string{"cleared", "silenced"} {
|
|
m := compose(r, what, trackNow, time.UTC, 0)
|
|
got, line := withTracking(m, r, what, issues, true, trackNow, time.UTC)
|
|
if got.Body != m.Body || line != "" {
|
|
t.Errorf("%s: %q, line %q", what, got.Body, line)
|
|
}
|
|
}
|
|
n := &Record{Key: "n", Origin: "notify", Headline: "a notice", Raised: trackNow}
|
|
m := compose(n, "raised", trackNow, time.UTC, 0)
|
|
got, line := withTracking(m, n, "raised", issues, true, trackNow, time.UTC)
|
|
if got.Body != m.Body || line != "" {
|
|
t.Errorf("notice: %q, line %q", got.Body, line)
|
|
}
|
|
}
|
|
|
|
// Through the router: the tracker is asked outside the work, its word is kept, a word older than TrackingKept is
|
|
// not used, and a tracker that fails is said as not checked.
|
|
func TestTheRouterAsksTheTrackerAndSaysItsWordInTheMessage(t *testing.T) {
|
|
h, tg, dt, c, _ := newHolder(t)
|
|
h.Zone = func() *time.Location { return time.UTC }
|
|
asked := 0
|
|
fail := false
|
|
h.Ask = func(address string, body any) (json.RawMessage, error) {
|
|
if address != TrackingVerb {
|
|
return json.RawMessage(`{"ok":true}`), nil
|
|
}
|
|
asked++
|
|
if fail {
|
|
return nil, errors.New("no responders")
|
|
}
|
|
return json.RawMessage(`{"conditions":{"module.mounts.shanks.unhealthy":[` +
|
|
`{"number":379,"status":"located"},` +
|
|
`{"number":386,"status":"fixing","worker":"fix-386@g14","since":"2026-10-06T11:10:00Z"}]}}`), nil
|
|
}
|
|
k := cond("module.mounts.shanks.unhealthy", Urgent, "mounts on shanks is not healthy")
|
|
k.Explanation, k.Needs = "Nothing for you to do. mounts on shanks is not healthy.", ""
|
|
k.Raised = c.now()
|
|
h.Condition(EventRaised, k)
|
|
settle(h, c)
|
|
if len(tg.sends) != 1 || len(dt.sends) != 1 {
|
|
t.Fatalf("urgent: telegram %d, desktop %d", len(tg.sends), len(dt.sends))
|
|
}
|
|
body := tg.sends[0].Body
|
|
if !strings.Contains(body, "\nTracked as issue 379: nobody is working on it. Also as issue 386: fix-386 on g14 is fixing it since 11:10.\n") {
|
|
t.Fatalf("body: %q", body)
|
|
}
|
|
if asked != 1 {
|
|
t.Errorf("asked the tracker %d times within a minute, want once", asked)
|
|
}
|
|
keyless(t, tg.sends...)
|
|
|
|
// The tracker stops answering: its word is used until it is TrackingKept old, then said as not checked.
|
|
fail = true
|
|
c.pass(TrackingKept + time.Minute)
|
|
h.Tick()
|
|
h.mu.Lock()
|
|
r := h.open["module.mounts.shanks.unhealthy"]
|
|
h.mu.Unlock()
|
|
if m := h.say(r, "reminder"); !strings.Contains(m.Body, "\nWhether an issue tracks this could not be checked.\n") {
|
|
t.Fatalf("an old word was used: %q", m.Body)
|
|
}
|
|
if asked < 2 {
|
|
t.Errorf("the tracker was not asked again")
|
|
}
|
|
}
|
|
|
|
// A router that cannot ask the tracker (a test's) adds nothing, so a missing word is never confused with a router
|
|
// built without it.
|
|
func TestARouterThatCannotAskAddsNothing(t *testing.T) {
|
|
h, _, _, c, _ := newHolder(t)
|
|
k := cond("machine.ace.silent", Urgent, "ace is silent")
|
|
k.Raised = c.now()
|
|
h.Condition(EventRaised, k)
|
|
h.mu.Lock()
|
|
r := h.open["machine.ace.silent"]
|
|
h.mu.Unlock()
|
|
if m := h.say(r, "raised"); strings.Contains(m.Body, "issue") {
|
|
t.Fatalf("%q", m.Body)
|
|
}
|
|
}
|