From 700dd7fa1abde9f7ed3976c72147d8b931dd8c11 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 8 Oct 2026 14:53:11 +0200 Subject: [PATCH] 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. --- modules/messenger/cmd/messenger/answers.go | 9 +++- .../messenger/cmd/messenger/answers_test.go | 52 +++++++++++++++++++ modules/messenger/cmd/messenger/holder.go | 10 ++-- 3 files changed, 65 insertions(+), 6 deletions(-) diff --git a/modules/messenger/cmd/messenger/answers.go b/modules/messenger/cmd/messenger/answers.go index f926ff22..70639300 100644 --- a/modules/messenger/cmd/messenger/answers.go +++ b/modules/messenger/cmd/messenger/answers.go @@ -77,6 +77,11 @@ func answerable(a Action) bool { return true } +// answerableFor is answerable for this record: a silence of its own condition, and of no other. +func answerableFor(r *Record, a Action) bool { + return answerable(a) && a.Arguments["silence"] == r.Key +} + func sameActions(a, b []Action) bool { x, _ := json.Marshal(a) y, _ := json.Marshal(b) @@ -101,7 +106,7 @@ func (h *Holder) choices(r *Record) []Choice { r.Tokens = map[string]Grant{} var out []Choice for i, a := range r.Actions { - if !answerable(a) || len(out) == MostActions-1 { + if !answerableFor(r, a) || len(out) == MostActions-1 { continue } t := token() @@ -174,7 +179,7 @@ func (h *Holder) Chosen(channel, machine, tok, event string) (string, error) { return refuse("it is more than a day old") case channel == "desktop" && !contains(shownOn(r), machine): return refuse("it was not shown on " + machine) - case g.Index >= len(r.Actions) || (g.Index >= 0 && !answerable(r.Actions[g.Index])): + case g.Index >= len(r.Actions) || (g.Index >= 0 && !answerableFor(r, r.Actions[g.Index])): return refuse("it is not an answer a notification may give") } h.mu.Lock() diff --git a/modules/messenger/cmd/messenger/answers_test.go b/modules/messenger/cmd/messenger/answers_test.go index 277fc3ff..8a5f28a6 100644 --- a/modules/messenger/cmd/messenger/answers_test.go +++ b/modules/messenger/cmd/messenger/answers_test.go @@ -245,3 +245,55 @@ func TestANotifierThatRestartedIsOfferedTheAnswersAgain(t *testing.T) { 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) + } +} diff --git a/modules/messenger/cmd/messenger/holder.go b/modules/messenger/cmd/messenger/holder.go index c75c244b..7361d1aa 100644 --- a/modules/messenger/cmd/messenger/holder.go +++ b/modules/messenger/cmd/messenger/holder.go @@ -136,9 +136,11 @@ type Record struct { Resolved string `json:"resolved,omitempty"` // Needs is what the operator does, empty for nothing; Actions the answers offered, each by a token // (Tokens: token -> the action's index, or -1 for the details) a channel hands back when chosen. - Needs string `json:"needs,omitempty"` - Actions []Action `json:"actions,omitempty"` - Tokens map[string]Grant `json:"tokens,omitempty"` + Needs string `json:"needs,omitempty"` + Actions []Action `json:"actions,omitempty"` + // Kept as "grants": the build before ADR 0258 kept "tokens" as token -> index, which this form cannot + // read; under its own name it is ignored and the record loads (the old tokens die, as they must). + Tokens map[string]Grant `json:"grants,omitempty"` // QuietReminded: a warning kept quiet as needing nothing was said once after QuietRemindAfter, so // nothing stays silent for ever. QuietReminded bool `json:"quiet_reminded,omitempty"` @@ -548,7 +550,7 @@ func (h *Holder) Tick() { // No channel could take it when it was said: tried again. h.deliver(r, r.Pending) h.persist(r) - case needsNothing(r) && !r.told() && !r.QuietReminded && !r.History && now.Sub(r.Raised) >= QuietRemindAfter: + case needsNothing(r) && !r.told() && !r.QuietReminded && now.Sub(r.Raised) >= QuietRemindAfter: // Quiet for a day: said once, so nothing stays silent for ever (ADR 0258). h.mu.Lock() r.QuietReminded = true