diff --git a/modules/mesh-grants/grants_test.go b/modules/mesh-grants/grants_test.go index 117f1ee0..7d57b724 100644 --- a/modules/mesh-grants/grants_test.go +++ b/modules/mesh-grants/grants_test.go @@ -801,6 +801,7 @@ func TestGrants(t *testing.T) { } return nil } + p.Say = channelTakes(t, p.Say) for _, s := range b.streams { if s["name"] == "OBJ_mesh-knowledge_sections" { s["bytes"] = int64(3500 << 20) @@ -1664,6 +1665,7 @@ func TestGrants(t *testing.T) { var said []oc.Condition 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 := func(edit func(modX *grants.User)) func() { return func() { @@ -1768,6 +1770,7 @@ func TestGrants(t *testing.T) { } return nil } + fp.Say = channelTakes(t, fp.Say) fp.Compose() 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) @@ -2442,6 +2445,7 @@ func TestGrants(t *testing.T) { } return nil } + p.Say = channelTakes(t, p.Say) // With no lease key (1126/A1). // The private network the mesh provides is placed with no fingerprint by construction: never a missing grant. b.assign("b", "mesh-wireguard") @@ -3074,6 +3078,7 @@ func TestGrants(t *testing.T) { sayMu.Unlock() return nil } + say = channelTakes(t, say) fv := grants.NewViews(owner, say, func() {}, nil) fv.Rest = b var minutes atomic.Int64 @@ -3467,6 +3472,7 @@ func TestGrants(t *testing.T) { } return nil } + p.Say = channelTakes(t, p.Say) made["max_deliver"] = 9 res = p.Compose() 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{} var said []oc.Condition p.Say = func(c oc.Condition) error { said = append(said, c); return nil } + p.Say = channelTakes(t, p.Say) p.Compose() inForce, _, _ := p.Sealed() if inForce.Digest == "" { @@ -3708,6 +3715,7 @@ func TestGrants(t *testing.T) { } return nil } + p.Say = channelTakes(t, p.Say) p.Compose() inForce, _, _ := p.Sealed() if inForce.Digest == "" { @@ -3793,6 +3801,7 @@ func TestGrants(t *testing.T) { } return nil } + p.Say = channelTakes(t, p.Say) p.Compose() inForce, _, _ := p.Sealed() 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) + } +} diff --git a/modules/mesh-grants/internal/grants/consumers.go b/modules/mesh-grants/internal/grants/consumers.go index 38d08ba6..c3ee1029 100644 --- a/modules/mesh-grants/internal/grants/consumers.go +++ b/modules/mesh-grants/internal/grants/consumers.go @@ -224,9 +224,9 @@ func (p *Part) sayDiffer(st *ConsumersState) { } else { c.Severity, c.State = occ.SeverityWarning, occ.StateRaised 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.Explanation = occ.NeedsYou + "a look. The bus holds these consumers otherwise than the bus composer composes them, " + - "and the grants part never changes a held one by itself (its delivery position would be lost): " + why + c.Headline = "A module's consumer is not as composed" + needsYou(&c, "a look, and grants_consumer_match for each", "The bus holds these consumers otherwise than the bus composer "+ + "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" } if p.Say(c) == nil { @@ -321,9 +321,9 @@ func (p *Part) sayBehind(st *ConsumersState) { } else { 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.Headline, c.Needs = "Messages pile up for a listener", "a look at the module that reads them" - c.Explanation = occ.NeedsYou + "a look. 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 + c.Headline = "Messages pile up for a listener" + 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) c.Resolved = "every account consumer is back within the bound" } if p.Say(c) == nil { diff --git a/modules/mesh-grants/internal/grants/firstviews.go b/modules/mesh-grants/internal/grants/firstviews.go index 2871c7f5..b4e5389b 100644 --- a/modules/mesh-grants/internal/grants/firstviews.go +++ b/modules/mesh-grants/internal/grants/firstviews.go @@ -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, fv.Tool, s.Asked.Format(time.RFC3339), strings.TrimSpace(s.Error)) 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; " + - "it never reads the state itself, and keeps what it composed meanwhile (novox/hq ADR 0615 §6). The answer clears this." - c.Needs = "a look at " + fv.Owner + "'s runtime of its own: is it running and connected?" + needsYou(&c, "a look at "+fv.Owner+"'s runtime of its own: is it running and connected?", "The grants part reads "+fv.Owner+ + "'s "+fv.State+" only through "+fv.Tool+", and keeps asking it; it never reads the state itself, and keeps what it "+ + "composed meanwhile (novox/hq ADR 0615 §6). The answer clears this.") return c } @@ -376,10 +376,10 @@ func emptyCondition(fv FirstView, raised bool) oc.Condition { 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.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 " + - "not keep the data, or a fresh mesh), so the grants part drops no login and keeps what it holds; a login revoked meanwhile " + - "stays until the owner's first change (novox/hq design 25 § 12.2)." - c.Needs = "let the owner write its state again from its sources (" + step + "), then grants_first_view_again" + needsYou(&c, "let the owner write its state again from its sources ("+step+"), then grants_first_view_again", "The owner holds "+ + "nothing of "+fv.State+" (its state rebuilt by a recreate that could not keep the data, or a fresh mesh), so the grants "+ + "part drops no login and keeps what it holds; a login revoked meanwhile stays until the owner's first change "+ + "(novox/hq design 25 § 12.2).") return c } diff --git a/modules/mesh-grants/internal/grants/grants.go b/modules/mesh-grants/internal/grants/grants.go index 3390df0a..25fc80d8 100644 --- a/modules/mesh-grants/internal/grants/grants.go +++ b/modules/mesh-grants/internal/grants/grants.go @@ -339,9 +339,9 @@ func (p *Part) sayMissing(missing []string) { } else { 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.Headline, c.Needs = "A placed module has no bus grant", "a look at why each has none" - c.Explanation = occ.NeedsYou + "a look. The grants part wrote no grant for these modules, so the assigner's " + - "credential rule gives them no want: " + why + c.Headline = "A placed module has no bus grant" + 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) c.Resolved = "every placed module has a grant at the fingerprint the indexer names" } if p.Say(c) == nil { diff --git a/modules/mesh-grants/internal/grants/held.go b/modules/mesh-grants/internal/grants/held.go index 23dc5c33..6bf1cad2 100644 --- a/modules/mesh-grants/internal/grants/held.go +++ b/modules/mesh-grants/internal/grants/held.go @@ -143,9 +143,10 @@ func (p *Part) sayHeldModules(held map[string]string, kept map[string]User) { } else { 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.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.Explanation = occ.NeedsYou + "a manifest the indexer accepts (a refused one mended, or a waiting one 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.Headline = "A module's manifest is held, its bus users are kept" + needsYou(&c, "a manifest the indexer accepts, or a judge that knows its fields", "A refused one mended, or a waiting one "+ + "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" } if p.Say(c) == nil { diff --git a/modules/mesh-grants/internal/grants/needsyou.go b/modules/mesh-grants/internal/grants/needsyou.go new file mode 100644 index 00000000..1d9845a5 --- /dev/null +++ b/modules/mesh-grants/internal/grants/needsyou.go @@ -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 +} diff --git a/modules/mesh-grants/internal/grants/refused.go b/modules/mesh-grants/internal/grants/refused.go index bff94ec3..54b32caa 100644 --- a/modules/mesh-grants/internal/grants/refused.go +++ b/modules/mesh-grants/internal/grants/refused.go @@ -127,9 +127,10 @@ func (p *Part) sayPrincipalsRefused(refused map[string]string, kept map[string]U } else { c.Severity, c.State = occ.SeverityWarning, occ.StateRaised 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.Explanation = occ.NeedsYou + "a fix of what makes it a second writer. 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.Headline = "A principal's bus grants are refused" + needsYou(&c, "a fix of the manifest or the writers table, of what makes it a second writer", + "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" } if p.Say(c) == nil { diff --git a/modules/mesh-grants/internal/grants/sealed.go b/modules/mesh-grants/internal/grants/sealed.go index 591c5c94..cd5b27ff 100644 --- a/modules/mesh-grants/internal/grants/sealed.go +++ b/modules/mesh-grants/internal/grants/sealed.go @@ -180,8 +180,8 @@ func (p *Part) sayHeld(why string) { } else { reason := strings.TrimPrefix(why, "held: ") 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.Explanation = occ.NeedsYou + "a look at why. The bus node keeps following the list written before, not the one composed now: " + reason + c.Summary, c.Headline = "the user list is held: "+reason, "The user list in force is held back" + 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" } if p.Say(c) == nil {