diff --git a/modules/mesh-grants/grants_test.go b/modules/mesh-grants/grants_test.go index 00e7835d..a1c1e3ca 100644 --- a/modules/mesh-grants/grants_test.go +++ b/modules/mesh-grants/grants_test.go @@ -5207,3 +5207,67 @@ func (h heldHalf) Revision() (meshwants.AssignmentRevision, bool) { } return *h.rev, true } + +// TestPassBeforeTheCatalogHalfComposesFromTheTrunkToo is design 47 G A-363: while the catalog half is unread, each account is composed +// from the manifest the indexer's `modules` first view names for its module (the trunk's) in union with what its want's entry gives, never +// from the list in force alone, so the first parts of a new build get their subjects before any of them serves. +func TestPassBeforeTheCatalogHalfComposesFromTheTrunkToo(t *testing.T) { + older := strings.Replace(xManifest, `{"name":"kept","history":3}`, `{"name":"older"}`, 1) + rows := []struct { + name string + entry bool // the want's entry is answered (the older build); else it is not answered + wantsOld bool + }{ + {"the want's entry is not answered", false, false}, + {"the want names an entry that is asked", false, false}, + {"the want's entry is an older build", true, true}, + } + for _, row := range rows { + t.Run(row.name, func(t *testing.T) { + b := fixture() + if strings.Contains(row.name, "asked") { + b.put(grants.ReadWants, "want.a.mod-x", meshwantsclient.Want{V: keys.Version, Fp: "src2:" + strings.Repeat("cd", 32), Composed: "sha256:" + strings.Repeat("ef", 32)}) + b.put(grants.ReadHave, "a.mod-x", nodeengine.HaveByNodeModuleValue{V: keys.Version, Phase: nodeengineclient.PhaseRunning, Health: nodeengineclient.HealthOK, + Fp: "src2:" + strings.Repeat("cd", 32)}) + b.entriesAsked = true + } + if row.entry { + digest := meshcatalogclient.ManifestDigest([]byte(older)) + b.put(grants.ReadManifests, digest, meshcatalog.ManifestsByDigestValue{V: keys.Version, Module: "mod-x", Manifest: older}) + fp := "src2:" + strings.Repeat("cd", 32) + b.put(grants.ReadWants, "want.a.mod-x", meshwantsclient.Want{V: keys.Version, Fp: fp, Composed: "sha256:" + strings.Repeat("ef", 32)}) + b.entry(fp, digest) + } + b.mu.Lock() + b.catalogWaited = []string{"the catalog half of the branch main"} + b.mu.Unlock() + in := grants.ReadInputs(b, &grants.Manifests{}) + if in.CatalogUnread == "" { + t.Fatal("the catalog half is not unread") + } + // An entry asked and not answered is no failed read while the catalog half is unread: the trunk's manifest composes the + // account and the list is not held for it. + if f := in.ReadFailed(); len(f) > 0 { + t.Errorf("the unread catalog half's asked entry is a failed read: %v", f) + } + recs, _ := grants.ListRecords(in) + users, err := grants.ComposeUsers(recs) + if err != nil { + t.Fatal(err) + } + var got []string + for _, u := range users { + if u.User == "a.mod-x" { + got = append(got, u.Publish...) + got = append(got, u.Subscribe...) + } + } + if !slices.ContainsFunc(got, func(s string) bool { return strings.Contains(s, "kept") }) { + t.Errorf("a.mod-x is not granted the trunk manifest's state kept: %v", got) + } + if row.wantsOld && !slices.ContainsFunc(got, func(s string) bool { return strings.Contains(s, "older") }) { + t.Errorf("a.mod-x is not granted its want's entry's state older: %v", got) + } + }) + } +} diff --git a/modules/mesh-grants/internal/grants/catalog.go b/modules/mesh-grants/internal/grants/catalog.go index a3527e09..4a067cac 100644 --- a/modules/mesh-grants/internal/grants/catalog.go +++ b/modules/mesh-grants/internal/grants/catalog.go @@ -204,7 +204,7 @@ func catalogWaiting(r Reader) string { return "" } slices.Sort(w) - return fmt.Sprintf("the first views of the catalog half are not in (%v), so each account is composed from its want's entry by fingerprint and the list in force is kept", w) + return fmt.Sprintf("the first views of the catalog half are not in (%v), so each account is composed from the trunk's manifest of its module in union with its want's entry by fingerprint, and the list in force is kept in union", w) } // **The grants part reads the assignment half** (design 46 approval mark §6, design 47 G §195, lead answer A-203): the view of diff --git a/modules/mesh-grants/internal/grants/inputs.go b/modules/mesh-grants/internal/grants/inputs.go index 7e96a6a4..af15a250 100644 --- a/modules/mesh-grants/internal/grants/inputs.go +++ b/modules/mesh-grants/internal/grants/inputs.go @@ -836,7 +836,10 @@ func runningByHaves(r Reader, in *Inputs, cache *Manifests, into map[string]cata if err != nil { if state == meshcatalogclient.EntryAsked { in.HadFailed[node+"."+module] = "the catalog half's entry of " + h.Fp + " is asked" - in.Gaps = append(in.Gaps, Gap{Input: "catalog entry " + h.Fp, Why: "asked and not answered yet, so the manifest of the build " + node + "." + module + " runs is unknown: " + err.Error(), Failed: true}) + // While the catalog half is unread an entry cannot be answered yet: that is no failed read, and the account is composed + // from the trunk's manifest of its module (the indexer's `modules` view) in union with what its entry gives, so the list is + // not held to the list in force alone (design 47 G, A-363). + in.Gaps = append(in.Gaps, Gap{Input: "catalog entry " + h.Fp, Why: "asked and not answered yet, so the manifest of the build " + node + "." + module + " runs is unknown: " + err.Error(), Failed: in.CatalogUnread == ""}) } fromTrunk = append(fromTrunk, node+"."+module) continue