Grants keep the last published membership when the running build cannot be read; holders stay as merged (issue 1966, review round 2)
mesh/delivery merged: source.main moved to novox/mesh-delivery@f9cadbfdbb93
mesh/check failed (2): integration: mesh-delivery-assigner/cmd/mesh-delivery-assigner.TestAProposalIsAnswer…; and 1 more | 5452fa92a731 on f9cadbfdbb93

Claude-Session: https://claude.ai/code/session_01NcbgfFfQ4Z9UE5jK32PmE5
This commit is contained in:
2026-11-08 17:02:17 +01:00
parent 563a624a6d
commit 5452fa92a7
5 changed files with 25 additions and 28 deletions
+11 -18
View File
@@ -4064,6 +4064,17 @@ func TestPassComposesMembershipFromTheHaveBuild(t *testing.T) {
t.Errorf("b.mod-y's membership lacks the running build's read: %s", got)
}
})
t.Run("a pass that cannot read the build a have names keeps the membership last published", func(t *testing.T) {
b := fixture()
runs(b)
part := partOn(b)
part.Pass()
b.refused[grants.ReadBuilds] = true
part.Pass()
if got := string(b.own[grants.StateMemberships]["b.mod-y"]); !strings.Contains(got, "mod-x.old") {
t.Errorf("b.mod-y's membership lost the running build's read when its build could not be read: %s", got)
}
})
t.Run("a build that cannot be read is a failed read", func(t *testing.T) {
b := fixture()
runs(b)
@@ -4072,22 +4083,4 @@ func TestPassComposesMembershipFromTheHaveBuild(t *testing.T) {
t.Errorf("a build read refused is not a failed read: %v", f)
}
})
t.Run("the seats held are the have build's claims, then the want's, then the trunk's", func(t *testing.T) {
claims := catalogue.Manifest{Module: "m", Version: "1", Claims: []catalogue.Claim{{Name: "mesh-catalog", Scope: catalogue.ScopeMesh}}}
bare := catalogue.Manifest{Module: "m", Version: "1"}
for name, row := range map[string]struct {
had, running map[string]catalogue.Manifest
held bool
}{
"trunk alone": {nil, nil, false},
"have claims": {map[string]catalogue.Manifest{"a.m": claims}, map[string]catalogue.Manifest{"a.m": bare}, true},
"want claims": {nil, map[string]catalogue.Manifest{"a.m": claims}, true},
"have is bare": {map[string]catalogue.Manifest{"a.m": bare}, map[string]catalogue.Manifest{"a.m": claims}, false},
} {
in := grants.Inputs{Nodes: []string{"a"}, Assigned: map[string][]string{"a": {"m"}}, Manifests: map[string]catalogue.Manifest{"m": bare}, Had: row.had, Running: row.running}
if got := len(grants.HoldingsOf(in).Claimed) > 0; got != row.held {
t.Errorf("%s: claimed %v, want %v", name, got, row.held)
}
}
})
}
@@ -154,6 +154,9 @@ type Inputs struct {
// Had is the manifest of the build each machine runs now (its have's fingerprint, the builder's build of it), by
// `<node>.<module>`; the want's Running may already be a newer build. A membership is composed from both and the trunk's.
Had map[string]catalogue.Manifest
// HadFailed is why the build a machine runs could not be read this pass (a have or a build read that failed), by
// `<node>.<module>`: its membership is then composed without that build's reads, and the part keeps the last published.
HadFailed map[string]string
// Values is each resolved config value, by `<node>/<module>/<key>`.
Values map[string]any
// BusNode is the machine running the bus's module, and Loaded what its node agent says it loaded.
@@ -325,7 +328,7 @@ func ReadInputs(r Reader, cache *Manifests) Inputs {
gate := cache.gate
cache.mu.Unlock()
in := Inputs{Assigned: map[string][]string{}, Manifests: map[string]catalogue.Manifest{}, Digests: map[string]string{}, Fingerprints: map[string]string{},
NoFingerprint: map[string]string{}, Running: map[string]catalogue.Manifest{}, Had: map[string]catalogue.Manifest{},
NoFingerprint: map[string]string{}, Running: map[string]catalogue.Manifest{}, Had: map[string]catalogue.Manifest{}, HadFailed: map[string]string{},
Values: map[string]any{}, Gaps: append([]Gap(nil), standingGaps...)}
in.SeatListUnread = gate != nil && !gate.MayAct()
nodes := map[string]bool{}
@@ -756,6 +759,8 @@ func runningByHaves(r Reader, in *Inputs, cache *Manifests, into map[string]cata
raw, found, err := r.Get(ReadHave, node+"."+module)
if err != nil {
in.Gaps = append(in.Gaps, Gap{Input: ReadHave + " " + node + "." + module, Why: "cannot be read, so the build it runs is unknown: " + err.Error(), Failed: true})
in.HadFailed[node+"."+module] = ReadHave + " cannot be read"
}
if err != nil || !found {
fromTrunk = append(fromTrunk, node+"."+module)
@@ -773,6 +778,7 @@ func runningByHaves(r Reader, in *Inputs, cache *Manifests, into map[string]cata
}
braw, found, err := r.Get(ReadBuilds, bk)
if err != nil {
in.HadFailed[node+"."+module] = ReadBuilds + " cannot be read"
in.Gaps = append(in.Gaps, Gap{Input: ReadBuilds + " " + bk, Why: "cannot be read, so the manifest of the build " + node + "." + module + " runs is unknown: " + err.Error(), Failed: true})
}
if err != nil || !found {
@@ -206,10 +206,7 @@ func HoldingsOf(in Inputs) Holdings {
}
for node, modules := range in.Assigned {
for _, module := range modules {
m, ok := in.Had[node+"."+module]
if !ok {
m, ok = in.Running[node+"."+module]
}
m, ok := in.Running[node+"."+module]
if !ok {
m, ok = in.Manifests[module]
}
+5 -1
View File
@@ -436,7 +436,11 @@ func (p *Part) Compose() Result {
// A pass that composes part of the mesh only grows a published membership: it publishes the union of the one last
// published and the one composed (keeploaded.go, issue 1966), so a tool runner or a seat holder goes on serving what it
// serves and still receives what the pass that ends the hold needs.
if why := in.keepsLoaded(); found && why != "" {
why := in.keepsLoaded()
if why == "" && in.HadFailed[key] != "" {
why = "the build it runs is unread (" + in.HadFailed[key] + ")"
}
if found && why != "" {
if bare, err := membership.BareGrantsMembership(was); err == nil {
if body, err := json.Marshal(ms[key].Membership); err == nil {
if grown, kept, err := UnionMembership(bare, body); err == nil && len(kept) > 0 {
@@ -448,10 +448,7 @@ func holdersAfterPass(in Inputs) ([]catalogue.Held, []Gap) {
var assignments []catalogue.Assignment
for _, node := range slices.Sorted(maps.Keys(in.Assigned)) {
for _, module := range in.Assigned[node] {
m, ok := in.Had[node+"."+module]
if !ok {
m, ok = in.Running[node+"."+module]
}
m, ok := in.Running[node+"."+module]
if !ok {
m, ok = in.Manifests[module]
}