Files
mesh-catalog/modules/messenger/cmd/messenger/answers_test.go
T
jochen 700dd7fa1a
mesh/merge-gate pass: builds dunst, messenger → g14, novox, shanks; 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
mesh/delivery delivered
mesh/delivery-group group fix/notifications-after-review delivered: every member is delivered
Load the records the live build kept, remind every quiet warning, silence only its own condition
The build live since 14:38 keeps tokens as token -> index; reading them as grants
failed and skipped the record. A quiet warning known from history was never
reminded, and an answer could silence another condition than its own.
2026-10-08 14:53:11 +02:00

300 lines
12 KiB
Go

package main
import (
"encoding/json"
"fmt"
"strings"
"testing"
"time"
)
// The review of 2026-10-08 (novox/hq ADR 0258): a desk click performs only an acknowledgement, and an
// answer's token is a one-time ticket bound to its condition and its machine.
// answering is a holder with the failed-units condition shown on the desktop of g14, and the calls the
// holder makes recorded.
func answering(t *testing.T, actions ...Action) (*Holder, *fakeChannel, *clock, *[]string, Condition) {
t.Helper()
h, _, dt, c, _ := newHolder(t)
h.Telegram = nil
asked := &[]string{}
h.Ask = func(address string, body any) (json.RawMessage, error) {
raw, _ := json.Marshal(body)
*asked = append(*asked, address+" "+string(raw))
return json.RawMessage(`{"ok":true}`), nil
}
e := examples[3]
if len(actions) > 0 {
e.actions = actions
}
k, err := DecodeCondition(EventRaised, e.event(c.now(), false))
if err != nil {
t.Fatal(err)
}
h.Condition(EventRaised, k)
settle(h, c)
if len(dt.sends) != 1 {
t.Fatalf("desktop sends %d", len(dt.sends))
}
return h, dt, c, asked, k
}
func tokenOf(t *testing.T, m Message, label string) string {
t.Helper()
for _, a := range m.Actions {
if a.Label == label {
return a.Token
}
}
t.Fatalf("%q is not offered: %+v", label, m.Actions)
return ""
}
// **Only an acknowledgement is offered or performed.** Release, Stop, Start and Restart are refused by the
// allow-list: never offered, and a token for one (forged into the state) is refused without a call.
func TestOnlyAnAcknowledgementIsAnAnswer(t *testing.T) {
release := Action{Label: "Release", Verb: "mesh-delivery.release", Arguments: map[string]string{"id": "x", "why": ""}}
restart := Action{Label: "Restart", Verb: "node-service-manager.restart", Machine: "g14",
Arguments: map[string]string{"unit": "x.service"}}
start := Action{Label: "Start", Verb: "mesh-controller.plans", Arguments: map[string]string{"go": "p", "why": ""}}
sneaky := Action{Label: "Silence", Verb: "mesh-controller.conditions", Arguments: map[string]string{"silence": "k", "stop": "p"}}
silence := Action{Label: "Silence for a week", Verb: "mesh-controller.conditions",
Arguments: map[string]string{"silence": "machine.shanks.units", "for": "7d", "why": "", "cause": "operator-answer"}}
for _, a := range []Action{release, restart, start, sneaky} {
if answerable(a) {
t.Errorf("%s is answerable", a.Label)
}
}
if !answerable(silence) {
t.Error("silence is not answerable")
}
h, dt, _, asked, _ := answering(t, release, restart, start, silence)
m := dt.sends[0]
var labels []string
for _, a := range m.Actions {
labels = append(labels, a.Label)
}
if strings.Join(labels, ",") != "Silence for a week,Details" {
t.Fatalf("offered %v", labels)
}
// A token for Release, put in the state by hand: refused, nothing called, and said on the desk.
h.mu.Lock()
r := h.open["machine.shanks.units"]
r.Tokens["forged"] = Grant{Index: 0, Issued: h.Now()}
h.mu.Unlock()
before := len(dt.sends)
if out, err := h.Chosen("desktop", "g14", "forged", "e1"); err == nil || out != "refused" || len(*asked) != 0 {
t.Fatalf("a release was performed from a click: %q %v %v", out, err, *asked)
}
if len(dt.sends) != before+1 || dt.sends[len(dt.sends)-1].Title != "That answer was not taken" {
t.Errorf("the refusal was not said where the click was: %+v", dt.sends[len(dt.sends)-1])
}
}
// **At most four answers**, Details included.
func TestANotificationOffersAtMostFourAnswers(t *testing.T) {
var many []Action
for i := 0; i < 6; i++ {
many = append(many, Action{Label: fmt.Sprintf("Silence %d", i), Verb: "mesh-controller.conditions",
Arguments: map[string]string{"silence": "machine.shanks.units", "for": "7d", "why": ""}})
}
_, dt, _, _, _ := answering(t, many...)
if n := len(dt.sends[0].Actions); n != MostActions || dt.sends[0].Actions[n-1].Label != DetailsLabel {
t.Fatalf("offered %+v", dt.sends[0].Actions)
}
}
// **A token is used once, an event acted on once**: the same token twice is refused the second time; the
// same event delivered twice is acted on once and answers what came of it.
func TestATokenIsUsedOnceAndAnEventActedOnOnce(t *testing.T) {
h, dt, _, asked, _ := answering(t)
tok := tokenOf(t, dt.sends[0], "Silence for a week")
if out, err := h.Chosen("desktop", "g14", tok, "e1"); err != nil || out != "done" || len(*asked) != 1 {
t.Fatalf("%q %v %v", out, err, *asked)
}
if out, err := h.Chosen("desktop", "g14", tok, "e1"); err != nil || out != "done" || len(*asked) != 1 {
t.Fatalf("a redelivered event was acted on again: %q %v %v", out, err, *asked)
}
if out, err := h.Chosen("desktop", "g14", tok, "e2"); err == nil || out != "refused" || len(*asked) != 1 {
t.Fatalf("the same token was taken twice: %q %v %v", out, err, *asked)
}
}
// **A token is bound to its machine and its condition's life**: from another machine, for a condition
// cleared or silenced, or past its day, it is refused.
func TestATokenIsRefusedElsewhereAfterItsConditionAndPastItsDay(t *testing.T) {
h, dt, c, asked, k := answering(t)
tok := tokenOf(t, dt.sends[0], "Silence for a week")
if out, _ := h.Chosen("desktop", "shanks", tok, "e1"); out != "refused" {
t.Errorf("an answer from a machine it was not shown on was taken: %q", out)
}
if out, _ := h.Chosen("desktop", "g14", "0123456789abcdef01234567", "e2"); out != "refused" {
t.Errorf("an unknown token was taken: %q", out)
}
c.pass(TokenFor + time.Minute)
if out, _ := h.Chosen("desktop", "g14", tok, "e3"); out != "refused" {
t.Errorf("a token past its day was taken: %q", out)
}
h, dt, c, asked, k = answering(t)
tok = tokenOf(t, dt.sends[0], "Silence for a week")
k.SilencedTill = c.now().Add(time.Hour)
h.Condition(EventChanged, k)
if out, _ := h.Chosen("desktop", "g14", tok, "e4"); out != "refused" {
t.Errorf("an answer for a silenced condition was taken: %q", out)
}
h, dt, c, asked, k = answering(t)
tok = tokenOf(t, dt.sends[0], "Silence for a week")
h.Condition(EventCleared, Condition{Key: k.Key})
if out, _ := h.Chosen("desktop", "g14", tok, "e5"); out != "refused" || len(*asked) != 0 {
t.Errorf("an answer for a cleared condition was taken: %q %v", out, *asked)
}
}
// **A refused verb is said where it was chosen**: the mesh's refusal edits the notification.
func TestAVerbTheMeshRefusesIsSaid(t *testing.T) {
h, dt, _, _, _ := answering(t)
h.Ask = func(string, any) (json.RawMessage, error) {
return json.RawMessage(`{"isError":true,"content":[{"text":"silence is refused: no such condition"}]}`), nil
}
tok := tokenOf(t, dt.sends[0], "Silence for a week")
out, err := h.Chosen("desktop", "g14", tok, "e1")
if err == nil || !strings.HasPrefix(out, "failed") {
t.Fatalf("%q %v", out, err)
}
if m := dt.edits["g14=1"]; !strings.HasPrefix(m.Title, "Could not silence for a week") ||
!strings.Contains(m.Body, "no such condition") {
t.Errorf("the refusal was not said: %+v", m)
}
}
// **A words-only change to "Needs you" after a quiet time is said**, through the event and through a
// reading of the state; and a quiet warning is said once after a day.
func TestAQuietWarningIsSaidWhenItsWordsNeedTheOperator(t *testing.T) {
quiet := examples[0]
needing := quiet
needing.needs = "check that mesh-delivery runs."
needing.explanation = "Needs you: check that mesh-delivery runs. It has waited."
// Through the event.
h, tg, dt, c, _ := newHolder(t)
k, _ := DecodeCondition(EventRaised, quiet.event(c.now(), false))
h.Condition(EventRaised, k)
runFor(h, c, 2*time.Hour)
if len(dt.sends)+len(tg.sends) != 0 {
t.Fatal("said while it needed nothing")
}
k, _ = DecodeCondition(EventChanged, needing.event(c.now(), false))
h.Condition(EventChanged, k)
settle(h, c)
if len(dt.sends) != 1 || !strings.HasPrefix(dt.sends[0].Body, "Needs you: check that mesh-delivery runs.") {
t.Fatalf("event: desktop %+v", dt.sends)
}
// Through a reading of the state.
h, tg, dt, c, _ = newHolder(t)
k, _ = DecodeCondition(EventRaised, quiet.event(c.now(), false))
h.Condition(EventRaised, k)
runFor(h, c, 2*time.Hour)
k, _ = DecodeCondition("conditions", needing.event(c.now(), false))
k.At = time.Time{}
h.Sync([]Condition{k}, c.now())
settle(h, c)
if len(dt.sends) != 1 || !strings.HasPrefix(dt.sends[0].Body, "Needs you: check that mesh-delivery runs.") {
t.Fatalf("sync: desktop %+v", dt.sends)
}
// After a day, once, even if it still needs nothing.
h, tg, dt, c, _ = newHolder(t)
c.t = time.Date(2026, 10, 8, 10, 40, 0, 0, time.UTC)
k, _ = DecodeCondition(EventRaised, quiet.event(c.now(), false))
h.Condition(EventRaised, k)
for i := 0; i < 50; i++ {
c.pass(time.Hour)
h.Tick()
settle(h, c)
}
if len(dt.sends) != 1 || !strings.HasPrefix(dt.sends[0].Title, "Still open after a day: ") ||
!strings.HasPrefix(dt.sends[0].Body, "Nothing for you to do.") {
t.Fatalf("after a day: desktop %+v", dt.sends)
}
_ = tg
}
// **A notifier that started again is offered the open answers again**, with fresh tokens: the old ones
// are refused, the new ones taken.
func TestANotifierThatRestartedIsOfferedTheAnswersAgain(t *testing.T) {
h, dt, _, asked, _ := answering(t)
old := tokenOf(t, dt.sends[0], "Silence for a week")
if n := h.Reoffer("g14"); n != 1 {
t.Fatalf("offered again: %d", n)
}
again := dt.edits["g14=1"]
fresh := tokenOf(t, again, "Silence for a week")
if fresh == old || again.Title != dt.sends[0].Title {
t.Fatalf("not offered again: %+v", again)
}
if out, _ := h.Chosen("desktop", "g14", old, "e1"); out != "refused" {
t.Errorf("the lost answer's token still works: %q", out)
}
if out, _ := h.Chosen("desktop", "g14", fresh, "e2"); out != "done" || len(*asked) != 1 {
t.Errorf("the fresh token: %q %v", out, *asked)
}
if n := h.Reoffer("shanks"); n != 0 {
t.Errorf("offered on a machine it was never shown on: %d", n)
}
}
// **A silence answers only its own condition** (ADR 0258): an action silencing another key is not offered,
// and a token for one is refused.
func TestASilenceAnswersOnlyItsOwnCondition(t *testing.T) {
other := Action{Label: "Silence another", Verb: "mesh-controller.conditions",
Arguments: map[string]string{"silence": "machine.ace.silent", "for": "7d", "why": ""}}
own := Action{Label: "Silence for a week", Verb: "mesh-controller.conditions",
Arguments: map[string]string{"silence": "machine.shanks.units", "for": "7d", "why": ""}}
h, dt, _, asked, _ := answering(t, other, own)
var labels []string
for _, a := range dt.sends[0].Actions {
labels = append(labels, a.Label)
}
if strings.Join(labels, ",") != "Silence for a week,Details" {
t.Fatalf("offered %v", labels)
}
h.mu.Lock()
h.open["machine.shanks.units"].Tokens["forged"] = Grant{Index: 0, Issued: h.Now()}
h.mu.Unlock()
if out, _ := h.Chosen("desktop", "g14", "forged", "e1"); out != "refused" || len(*asked) != 0 {
t.Fatalf("another condition was silenced: %q %v", out, *asked)
}
}
// **A record kept by the build before ADR 0258 loads**: its tokens were token -> index; it is read with
// them dropped, never skipped (the old tokens die, as they must).
func TestARecordKeptByTheBuildBeforeLoads(t *testing.T) {
old := `{"key":"machine.shanks.units","kind":"units","subject":"machine shanks","severity":"warning",` +
`"summary":"s","headline":"3 failed services on shanks","origin":"condition","more":"m",` +
`"raised":"2026-10-08T12:38:00Z","sent":{"desktop":"g14=7"},"count":1,` +
`"actions":[{"label":"Release","verb":"mesh-delivery.release","arguments":{"id":"x","why":""}}],` +
`"tokens":{"0123456789abcdef01234567":0,"89abcdef0123456789abcdef":-1}}`
var r Record
if err := json.Unmarshal([]byte(old), &r); err != nil {
t.Fatalf("the old record does not load: %v", err)
}
if r.Key != "machine.shanks.units" || len(r.Tokens) != 0 || r.Sent["desktop"] != "g14=7" {
t.Fatalf("%+v", r)
}
// Loaded into a holder, its old answer is refused and nothing is called.
h, _, _, _, _ := newHolder(t)
asked := 0
h.Ask = func(string, any) (json.RawMessage, error) { asked++; return nil, nil }
st := &memStore{recs: map[string]Record{r.Key: r}}
h.Store = st
if err := h.Load(); err != nil {
t.Fatal(err)
}
if out, _ := h.Chosen("desktop", "g14", "0123456789abcdef01234567", "e1"); out != "refused" || asked != 0 {
t.Fatalf("an old token was taken: %q %d", out, asked)
}
}