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
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.
300 lines
12 KiB
Go
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)
|
|
}
|
|
}
|