From f737418bb3d79cc62c654ada914f1ae9a688fdf1 Mon Sep 17 00:00:00 2001 From: jochen Date: Mon, 19 Oct 2026 12:04:58 +0200 Subject: [PATCH] Keep a module's condition open when the controller's conditions are read (issue 1217, review round 1 finding 1) --- modules/messenger/cmd/messenger/condition.go | 31 ++++++++++--------- .../cmd/messenger/condition_intake_test.go | 22 +++++++++++++ modules/messenger/cmd/messenger/holder.go | 10 +++--- modules/messenger/cmd/messenger/sync.go | 3 +- 4 files changed, 47 insertions(+), 19 deletions(-) diff --git a/modules/messenger/cmd/messenger/condition.go b/modules/messenger/cmd/messenger/condition.go index c646089a..1378d596 100644 --- a/modules/messenger/cmd/messenger/condition.go +++ b/modules/messenger/cmd/messenger/condition.go @@ -59,19 +59,22 @@ const ( // Condition is what this holder needs of one. type Condition struct { - Key string - Kind string - Scope string - ID string - Machine string - Severity string - Summary string - Headline string - Explanation string - Resolved string - Needs string - Actions []Action - Source string + Key string + Kind string + Scope string + ID string + Machine string + Severity string + Summary string + Headline string + Explanation string + Resolved string + Needs string + Actions []Action + Source string + // Publisher is the module that said the condition on the operator channel's one subject; empty for the + // controller's own, which a reading of the controller's conditions may end. + Publisher string Resolver string Raised time.Time LastObserved time.Time @@ -360,7 +363,7 @@ func (h *Holder) takeCondition(publisher string, body []byte) { } c := Condition{Key: sc.Key, Scope: sc.Subject.Scope, ID: sc.Subject.ID, Machine: sc.Subject.Machine, Severity: sc.Severity, Summary: sc.Summary, Headline: sc.Headline, Explanation: sc.Explanation, - Resolved: sc.Resolved, Needs: sc.Needs, Source: publisher} + Resolved: sc.Resolved, Needs: sc.Needs, Source: publisher, Publisher: publisher} if i := strings.LastIndex(sc.Key, "."); i >= 0 { c.Kind = sc.Key[i+1:] } diff --git a/modules/messenger/cmd/messenger/condition_intake_test.go b/modules/messenger/cmd/messenger/condition_intake_test.go index 3312b1fa..e3a1899d 100644 --- a/modules/messenger/cmd/messenger/condition_intake_test.go +++ b/modules/messenger/cmd/messenger/condition_intake_test.go @@ -6,6 +6,7 @@ import ( "fmt" "strings" "testing" + "time" "git.novox.be/novox/mesh-sdk/go/asks" ) @@ -132,3 +133,24 @@ func TestABodyThatIsNotAConditionIsDroppedAndTheLogNamesThePublisher(t *testing. } } } + +func TestAModulesConditionStaysOpenWhenTheControllersConditionsAreRead(t *testing.T) { // 1217/AC7 + h, tg, dt, c, _ := newHolder(t) + body := conditionBody(t, sdkCondition("look at it.")) + if err := h.TakeAsk("condition", "mesh-delivery-teller", body); err != nil { + t.Fatal(err) + } + settle(h, c) + c.pass(time.Second) + res := h.Sync(nil, c.now()) // the controller lists nothing: the teller's condition is not its to end + if len(res.Gone) != 0 || len(h.Open()) != 1 { + t.Fatalf("the reading ended a module's condition: gone %v, open %+v", res.Gone, h.Open()) + } + if err := h.TakeAsk("condition", "mesh-delivery-teller", body); err != nil { // raised again: the same message + t.Fatal(err) + } + settle(h, c) + if len(dt.sends)+len(tg.sends) != 1 { + t.Fatalf("said %d times, want once", len(dt.sends)+len(tg.sends)) + } +} diff --git a/modules/messenger/cmd/messenger/holder.go b/modules/messenger/cmd/messenger/holder.go index f24ae6d7..8fe66c20 100644 --- a/modules/messenger/cmd/messenger/holder.go +++ b/modules/messenger/cmd/messenger/holder.go @@ -184,8 +184,10 @@ type Record struct { Answered string `json:"answered,omitempty"` // Ask is the open ask about this record, `.`: its options are this record's answers // (novox/hq ADR 0259). - Ask string `json:"ask,omitempty"` - Origin string `json:"origin"` + Ask string `json:"ask,omitempty"` + Origin string `json:"origin"` + // Publisher is the module that said it on the operator channel's one subject; empty for the controller's. + Publisher string `json:"publisher,omitempty"` More string `json:"more"` Raised time.Time `json:"raised"` SilencedTill time.Time `json:"silenced_till,omitempty"` @@ -228,8 +230,8 @@ func recordOf(c Condition) Record { return Record{Key: c.Key, Kind: c.Kind, Subject: c.SubjectWords(), Scope: c.Scope, SubjectID: c.ID, Machine: c.Machine, Severity: c.Severity, Summary: c.Summary, Headline: c.Headline, Explanation: c.Explanation, Resolved: c.Resolved, Needs: c.Needs, Actions: c.Actions, - Origin: "condition", - More: "conditions show " + c.Key, Raised: c.Raised, SilencedTill: c.SilencedTill, SilencedWhy: c.SilencedWhy} + Origin: "condition", Publisher: c.Publisher, + More: "conditions show " + c.Key, Raised: c.Raised, SilencedTill: c.SilencedTill, SilencedWhy: c.SilencedWhy} } // takeWords keeps the plain words current, as the summary is: a condition's words follow what is seen. diff --git a/modules/messenger/cmd/messenger/sync.go b/modules/messenger/cmd/messenger/sync.go index 35587be1..efeaa6ee 100644 --- a/modules/messenger/cmd/messenger/sync.go +++ b/modules/messenger/cmd/messenger/sync.go @@ -33,13 +33,14 @@ func (h *Holder) Sync(open []Condition, asked time.Time) SyncResult { for _, c := range open { in[c.Key] = true } + // A module's condition (Publisher set) is not the controller's to end: only what it said is ended here. // What is open here and no longer there ended while this holder was not told: forgotten without // a message — said only where an edit notifies nobody. What this holder heard of after the // reading was asked for is newer than it, and stands. h.mu.Lock() var gone []*Record for _, r := range h.open { - if r.Origin == "condition" && r.Cleared.IsZero() && !in[r.Key] && !r.Seen.After(asked) { + if r.Origin == "condition" && r.Publisher == "" && r.Cleared.IsZero() && !in[r.Key] && !r.Seen.After(asked) { gone = append(gone, r) } }