Load the records the live build kept, remind every quiet warning, silence only its own condition
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.
This commit is contained in:
jochen
2026-10-08 14:53:11 +02:00
parent d83711f11e
commit 700dd7fa1a
3 changed files with 65 additions and 6 deletions
+7 -2
View File
@@ -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()
@@ -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)
}
}
+6 -4
View File
@@ -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