Open every grants condition's explanation as the operator-channel requires (issue 1966, recovery)

The channel refuses a raised condition whose explanation does not open with
"Needs you: " and its needs, so seven of grants' conditions, among them
first-view-unanswered, never reached the operator. One helper now writes both,
and every condition the unit tests say passes the channel's own check.

Claude-Session: https://claude.ai/code/session_01NcbgfFfQ4Z9UE5jK32PmE5
This commit is contained in:
2026-11-08 23:27:39 +01:00
parent 3f96301177
commit e7779d2afd
8 changed files with 67 additions and 24 deletions
+21
View File
@@ -801,6 +801,7 @@ func TestGrants(t *testing.T) {
} }
return nil return nil
} }
p.Say = channelTakes(t, p.Say)
for _, s := range b.streams { for _, s := range b.streams {
if s["name"] == "OBJ_mesh-knowledge_sections" { if s["name"] == "OBJ_mesh-knowledge_sections" {
s["bytes"] = int64(3500 << 20) s["bytes"] = int64(3500 << 20)
@@ -1664,6 +1665,7 @@ func TestGrants(t *testing.T) {
var said []oc.Condition var said []oc.Condition
p.Say = func(c oc.Condition) error { said = append(said, c); return nil } p.Say = func(c oc.Condition) error { said = append(said, c); return nil }
p.Say = channelTakes(t, p.Say)
// settled is the assignments of the lists agreeing, every hash held, and the loaded list as edit leaves it. // settled is the assignments of the lists agreeing, every hash held, and the loaded list as edit leaves it.
settled := func(edit func(modX *grants.User)) func() { settled := func(edit func(modX *grants.User)) func() {
return func() { return func() {
@@ -1768,6 +1770,7 @@ func TestGrants(t *testing.T) {
} }
return nil return nil
} }
fp.Say = channelTakes(t, fp.Say)
fp.Compose() fp.Compose()
if len(fresh) == 0 || fresh[0].Key != grants.HeldCondition || fresh[0].State != occ.StateCleared { if len(fresh) == 0 || fresh[0].Key != grants.HeldCondition || fresh[0].State != occ.StateCleared {
t.Errorf("a fresh part's first pass does not say the held warning is cleared: %+v", fresh) t.Errorf("a fresh part's first pass does not say the held warning is cleared: %+v", fresh)
@@ -2442,6 +2445,7 @@ func TestGrants(t *testing.T) {
} }
return nil return nil
} }
p.Say = channelTakes(t, p.Say)
// With no lease key (1126/A1). // With no lease key (1126/A1).
// The private network the mesh provides is placed with no fingerprint by construction: never a missing grant. // The private network the mesh provides is placed with no fingerprint by construction: never a missing grant.
b.assign("b", "mesh-wireguard") b.assign("b", "mesh-wireguard")
@@ -3074,6 +3078,7 @@ func TestGrants(t *testing.T) {
sayMu.Unlock() sayMu.Unlock()
return nil return nil
} }
say = channelTakes(t, say)
fv := grants.NewViews(owner, say, func() {}, nil) fv := grants.NewViews(owner, say, func() {}, nil)
fv.Rest = b fv.Rest = b
var minutes atomic.Int64 var minutes atomic.Int64
@@ -3467,6 +3472,7 @@ func TestGrants(t *testing.T) {
} }
return nil return nil
} }
p.Say = channelTakes(t, p.Say)
made["max_deliver"] = 9 made["max_deliver"] = 9
res = p.Compose() res = p.Compose()
if len(res.Consumers.Differ) != 1 || !strings.Contains(res.Consumers.Differ[0], "EVENTS.b_mod-c: max deliver 9") || if len(res.Consumers.Differ) != 1 || !strings.Contains(res.Consumers.Differ[0], "EVENTS.b_mod-c: max deliver 9") ||
@@ -3538,6 +3544,7 @@ func TestGrants(t *testing.T) {
p.Objects = &fakeObjects{} p.Objects = &fakeObjects{}
var said []oc.Condition var said []oc.Condition
p.Say = func(c oc.Condition) error { said = append(said, c); return nil } p.Say = func(c oc.Condition) error { said = append(said, c); return nil }
p.Say = channelTakes(t, p.Say)
p.Compose() p.Compose()
inForce, _, _ := p.Sealed() inForce, _, _ := p.Sealed()
if inForce.Digest == "" { if inForce.Digest == "" {
@@ -3708,6 +3715,7 @@ func TestGrants(t *testing.T) {
} }
return nil return nil
} }
p.Say = channelTakes(t, p.Say)
p.Compose() p.Compose()
inForce, _, _ := p.Sealed() inForce, _, _ := p.Sealed()
if inForce.Digest == "" { if inForce.Digest == "" {
@@ -3793,6 +3801,7 @@ func TestGrants(t *testing.T) {
} }
return nil return nil
} }
p.Say = channelTakes(t, p.Say)
p.Compose() p.Compose()
inForce, _, _ := p.Sealed() inForce, _, _ := p.Sealed()
before, _, ok, _ := b.ownGet(grants.StateUserList, grants.SealedUserKey(inForce.Digest, "b.mod-r.mod-r-tools")) before, _, ok, _ := b.ownGet(grants.StateUserList, grants.SealedUserKey(inForce.Digest, "b.mod-r.mod-r-tools"))
@@ -4084,3 +4093,15 @@ func TestPassComposesMembershipFromTheHaveBuild(t *testing.T) {
} }
}) })
} }
// channelTakes is say, after the operator-channel's own check of each condition: one the channel would refuse (its
// explanation not opening with "Needs you: " and what it needs) fails the test, since the operator would never see it.
func channelTakes(t *testing.T, say func(oc.Condition) error) func(oc.Condition) error {
t.Helper()
return func(c oc.Condition) error {
if _, err := occ.EncodeCondition(c); err != nil {
t.Errorf("the channel refuses the condition %s: %v", c.Key, err)
}
return say(c)
}
}
@@ -224,9 +224,9 @@ func (p *Part) sayDiffer(st *ConsumersState) {
} else { } else {
c.Severity, c.State = occ.SeverityWarning, occ.StateRaised c.Severity, c.State = occ.SeverityWarning, occ.StateRaised
c.Summary = fmt.Sprintf("%d durable consumers differ from their composed configuration", len(st.Differ)) c.Summary = fmt.Sprintf("%d durable consumers differ from their composed configuration", len(st.Differ))
c.Headline, c.Needs = "A module's consumer is not as composed", "a look, and grants_consumer_match for each" c.Headline = "A module's consumer is not as composed"
c.Explanation = occ.NeedsYou + "a look. The bus holds these consumers otherwise than the bus composer composes them, " + needsYou(&c, "a look, and grants_consumer_match for each", "The bus holds these consumers otherwise than the bus composer "+
"and the grants part never changes a held one by itself (its delivery position would be lost): " + why "composes them, and the grants part never changes a held one by itself (its delivery position would be lost): "+why)
c.Resolved = "every account consumer matches, by grants_consumer_match or otherwise" c.Resolved = "every account consumer matches, by grants_consumer_match or otherwise"
} }
if p.Say(c) == nil { if p.Say(c) == nil {
@@ -321,9 +321,9 @@ func (p *Part) sayBehind(st *ConsumersState) {
} else { } else {
c.Severity, c.State = occ.SeverityWarning, occ.StateRaised c.Severity, c.State = occ.SeverityWarning, occ.StateRaised
c.Summary = fmt.Sprintf("%d durable consumers are more than %d messages behind their stream's head", len(st.Behind), BehindBound) c.Summary = fmt.Sprintf("%d durable consumers are more than %d messages behind their stream's head", len(st.Behind), BehindBound)
c.Headline, c.Needs = "Messages pile up for a listener", "a look at the module that reads them" c.Headline = "Messages pile up for a listener"
c.Explanation = occ.NeedsYou + "a look. These consumers stayed far behind their stream's head at two passes " + needsYou(&c, "a look at the module that reads them", "These consumers stayed far behind their stream's head at two passes "+
"running, so what is sent to their modules waits unread (how far each is, in the grants part's last pass): " + why "running, so what is sent to their modules waits unread (how far each is, in the grants part's last pass): "+why)
c.Resolved = "every account consumer is back within the bound" c.Resolved = "every account consumer is back within the bound"
} }
if p.Say(c) == nil { if p.Say(c) == nil {
@@ -354,9 +354,9 @@ func unansweredCondition(fv FirstView, s views.Status, raised bool) oc.Condition
c.Summary = fmt.Sprintf("the grants part has no first view of %s's %s: %s has not answered since %s (%s)", fv.Owner, fv.State, c.Summary = fmt.Sprintf("the grants part has no first view of %s's %s: %s has not answered since %s (%s)", fv.Owner, fv.State,
fv.Tool, s.Asked.Format(time.RFC3339), strings.TrimSpace(s.Error)) fv.Tool, s.Asked.Format(time.RFC3339), strings.TrimSpace(s.Error))
c.Headline = "The grants part waits for " + fv.Owner c.Headline = "The grants part waits for " + fv.Owner
c.Explanation = "The grants part reads " + fv.Owner + "'s " + fv.State + " only through " + fv.Tool + ", and keeps asking it; " + needsYou(&c, "a look at "+fv.Owner+"'s runtime of its own: is it running and connected?", "The grants part reads "+fv.Owner+
"it never reads the state itself, and keeps what it composed meanwhile (novox/hq ADR 0615 §6). The answer clears this." "'s "+fv.State+" only through "+fv.Tool+", and keeps asking it; it never reads the state itself, and keeps what it "+
c.Needs = "a look at " + fv.Owner + "'s runtime of its own: is it running and connected?" "composed meanwhile (novox/hq ADR 0615 §6). The answer clears this.")
return c return c
} }
@@ -376,10 +376,10 @@ func emptyCondition(fv FirstView, raised bool) oc.Condition {
c.State, c.Severity = occ.StateRaised, occ.SeverityWarning c.State, c.Severity = occ.StateRaised, occ.SeverityWarning
c.Summary = fmt.Sprintf("%s answered %s's %s empty at revision 0, so the grants part keeps the user list in force", fv.Tool, fv.Owner, fv.State) c.Summary = fmt.Sprintf("%s answered %s's %s empty at revision 0, so the grants part keeps the user list in force", fv.Tool, fv.Owner, fv.State)
c.Headline = fv.Owner + "'s " + fv.State + " is empty" c.Headline = fv.Owner + "'s " + fv.State + " is empty"
c.Explanation = occ.NeedsYou + "a step. The owner holds nothing of " + fv.State + " (its state rebuilt by a recreate that could " + needsYou(&c, "let the owner write its state again from its sources ("+step+"), then grants_first_view_again", "The owner holds "+
"not keep the data, or a fresh mesh), so the grants part drops no login and keeps what it holds; a login revoked meanwhile " + "nothing of "+fv.State+" (its state rebuilt by a recreate that could not keep the data, or a fresh mesh), so the grants "+
"stays until the owner's first change (novox/hq design 25 § 12.2)." "part drops no login and keeps what it holds; a login revoked meanwhile stays until the owner's first change "+
c.Needs = "let the owner write its state again from its sources (" + step + "), then grants_first_view_again" "(novox/hq design 25 § 12.2).")
return c return c
} }
@@ -339,9 +339,9 @@ func (p *Part) sayMissing(missing []string) {
} else { } else {
c.Severity, c.State = occ.SeverityWarning, occ.StateRaised c.Severity, c.State = occ.SeverityWarning, occ.StateRaised
c.Summary = fmt.Sprintf("%d placed module(s) have no grant, so no want can name them", len(missing)) c.Summary = fmt.Sprintf("%d placed module(s) have no grant, so no want can name them", len(missing))
c.Headline, c.Needs = "A placed module has no bus grant", "a look at why each has none" c.Headline = "A placed module has no bus grant"
c.Explanation = occ.NeedsYou + "a look. The grants part wrote no grant for these modules, so the assigner's " + needsYou(&c, "a look at why each has none", "The grants part wrote no grant for these modules, so the assigner's "+
"credential rule gives them no want: " + why "credential rule gives them no want: "+why)
c.Resolved = "every placed module has a grant at the fingerprint the indexer names" c.Resolved = "every placed module has a grant at the fingerprint the indexer names"
} }
if p.Say(c) == nil { if p.Say(c) == nil {
+4 -3
View File
@@ -143,9 +143,10 @@ func (p *Part) sayHeldModules(held map[string]string, kept map[string]User) {
} else { } else {
c.Severity, c.State = occ.SeverityWarning, occ.StateRaised c.Severity, c.State = occ.SeverityWarning, occ.StateRaised
c.Summary = fmt.Sprintf("%d bus user(s) of a module whose manifest is held are kept in the list: %s", len(users), why) c.Summary = fmt.Sprintf("%d bus user(s) of a module whose manifest is held are kept in the list: %s", len(users), why)
c.Headline, c.Needs = "A module's manifest is held, its bus users are kept", "a manifest the indexer accepts, or a judge that knows its fields" c.Headline = "A module's manifest is held, its bus users are kept"
c.Explanation = occ.NeedsYou + "a manifest the indexer accepts (a refused one mended, or a waiting one judged by an indexer that knows its fields). " + needsYou(&c, "a manifest the indexer accepts, or a judge that knows its fields", "A refused one mended, or a waiting one "+
"The rest of the user list is written as ever; a held module's users stay as the list in force holds them: " + why "judged by an indexer that knows its fields. The rest of the user list is written as ever; a held module's users stay "+
"as the list in force holds them: "+why)
c.Resolved = "every module's manifest is accepted" c.Resolved = "every module's manifest is accepted"
} }
if p.Say(c) == nil { if p.Say(c) == nil {
@@ -0,0 +1,20 @@
package grants
import (
"strings"
occ "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient"
oc "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel"
)
// needsYou sets what a raised condition needs of the operator and its explanation: the channel takes a condition whose
// explanation opens with "Needs you: " and those very words (operatorchannelclient.EncodeCondition, novox/hq ADR 0253 §1),
// and refuses any other, so the operator would never see it. rest is the explanation after them.
func needsYou(c *oc.Condition, needs, rest string) {
c.Needs = needs
sep := ". "
if strings.HasSuffix(needs, "?") || strings.HasSuffix(needs, ".") {
sep = " "
}
c.Explanation = occ.NeedsYou + needs + sep + rest
}
@@ -127,9 +127,10 @@ func (p *Part) sayPrincipalsRefused(refused map[string]string, kept map[string]U
} else { } else {
c.Severity, c.State = occ.SeverityWarning, occ.StateRaised c.Severity, c.State = occ.SeverityWarning, occ.StateRaised
c.Summary = fmt.Sprintf("%d principal(s) refused their composed grants: %s", len(refused), why) c.Summary = fmt.Sprintf("%d principal(s) refused their composed grants: %s", len(refused), why)
c.Headline, c.Needs = "A principal's bus grants are refused", "a fix of the manifest or the writers table" c.Headline = "A principal's bus grants are refused"
c.Explanation = occ.NeedsYou + "a fix of what makes it a second writer. The rest of the user list is written as ever; " + needsYou(&c, "a fix of the manifest or the writers table, of what makes it a second writer",
"each refused principal keeps the grants the list in force gives it, or stays out where it holds none: " + why "The rest of the user list is written as ever; each refused principal keeps the grants the list in force gives it, "+
"or stays out where it holds none: "+why)
c.Resolved = "every principal's grants compose again" c.Resolved = "every principal's grants compose again"
} }
if p.Say(c) == nil { if p.Say(c) == nil {
@@ -180,8 +180,8 @@ func (p *Part) sayHeld(why string) {
} else { } else {
reason := strings.TrimPrefix(why, "held: ") reason := strings.TrimPrefix(why, "held: ")
c.Severity, c.State = occ.SeverityWarning, occ.StateRaised c.Severity, c.State = occ.SeverityWarning, occ.StateRaised
c.Summary, c.Headline, c.Needs = "the user list is held: "+reason, "The user list in force is held back", "a look at why" c.Summary, c.Headline = "the user list is held: "+reason, "The user list in force is held back"
c.Explanation = occ.NeedsYou + "a look at why. The bus node keeps following the list written before, not the one composed now: " + reason needsYou(&c, "a look at why", "The bus node keeps following the list written before, not the one composed now: "+reason)
c.Resolved = "a pass finds nothing to hold back and the list advances" c.Resolved = "a pass finds nothing to hold back and the list advances"
} }
if p.Say(c) == nil { if p.Say(c) == nil {