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
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:
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user