From 25d58a4595ece44a7ba47eedea2443e38f303fbd Mon Sep 17 00:00:00 2001 From: jochen Date: Fri, 6 Nov 2026 02:41:58 +0100 Subject: [PATCH] A group's composer is built against the group's tagged versions once heard, as the check's other Go builds are (issue 1921, issue 1966) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ADR 0607 §3, §5 and design 51 *Tagged versions*: the composition is handed the check's heard tags (CompositionReader.Compose's tagged), so the node-engine member's composer no longer takes the other members' heads where a tag stands. The trunk's manifest validator stays without tags (ADR 0633 clause 6). Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i --- .../internal/check/composition_test.go | 3 ++- .../internal/check/engines.go | 8 ++++---- .../internal/check/engines_test.go | 19 ++++++++++++++----- .../internal/check/judge.go | 8 +++++--- 4 files changed, 25 insertions(+), 13 deletions(-) diff --git a/modules/mesh-delivery-checker/internal/check/composition_test.go b/modules/mesh-delivery-checker/internal/check/composition_test.go index 6dd2f466..20a810a0 100644 --- a/modules/mesh-delivery-checker/internal/check/composition_test.go +++ b/modules/mesh-delivery-checker/internal/check/composition_test.go @@ -13,6 +13,7 @@ import ( "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshdeliverychecker" "git.novox.be/novox/mesh-delivery/contracts/go/seats/meshwants" buildsrec "git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/builds" + "git.novox.be/novox/mesh-delivery/sdk/buildstep" "git.novox.be/novox/mesh-sdk/go/catalogue" ) @@ -25,7 +26,7 @@ type fakeCompositions struct { waits bool } -func (f *fakeCompositions) Compose(ctx context.Context, c *Case) (meshwants.CompositionsByGroupHeadsValue, error) { +func (f *fakeCompositions) Compose(ctx context.Context, c *Case, _ buildstep.PackageVersions) (meshwants.CompositionsByGroupHeadsValue, error) { f.asked = append(f.asked, c.Key) if f.waits { <-ctx.Done() diff --git a/modules/mesh-delivery-checker/internal/check/engines.go b/modules/mesh-delivery-checker/internal/check/engines.go index ae35e1d2..a4b0bb1c 100644 --- a/modules/mesh-delivery-checker/internal/check/engines.go +++ b/modules/mesh-delivery-checker/internal/check/engines.go @@ -76,7 +76,7 @@ func composerHead(c *Case) string { } // Compose asks every placed node and judges their answers (JudgeEngines). An error is only a case it cannot ask at all. -func (e EngineCompositions) Compose(ctx context.Context, c *Case) (meshwants.CompositionsByGroupHeadsValue, error) { +func (e EngineCompositions) Compose(ctx context.Context, c *Case, tagged buildstep.PackageVersions) (meshwants.CompositionsByGroupHeadsValue, error) { began := time.Now() key, err := nodeengineclient.CompositionKey(c.Group, c.Heads) if err != nil { @@ -109,7 +109,7 @@ func (e EngineCompositions) Compose(ctx context.Context, c *Case) (meshwants.Com // The composer's build and run are the check's to bound, not the engine's answer bound. A composer that // cannot run is no silent node (ADR 0593 §9 lets only that pass): the node's composition fails, naming // the head. - if composed, serr := e.inSandbox(ctx, c, head, node, a); serr != nil { + if composed, serr := e.inSandbox(ctx, c, tagged, head, node, a); serr != nil { a = ne.ComposeCandidateAnswer{V: a.V, Node: node, Candidate: a.Candidate, Verdict: nec.Refused, Refused: "the composer at the node-engine's head " + head + " could not compose it: " + serr.Error()} } else { @@ -198,7 +198,7 @@ func JudgeEngines(group, heads, composerName string, nodes []string, answers map // inSandbox composes a node's answer again with the composer built from the node-engine member's head, over the input the // node's engine composed from: the node's answer becomes the sandbox's, refused with the composer's words where the // composer refuses the node. -func (e EngineCompositions) inSandbox(ctx context.Context, c *Case, head, node string, a ne.ComposeCandidateAnswer) (ne.ComposeCandidateAnswer, error) { +func (e EngineCompositions) inSandbox(ctx context.Context, c *Case, tagged buildstep.PackageVersions, head, node string, a ne.ComposeCandidateAnswer) (ne.ComposeCandidateAnswer, error) { if len(a.Input) == 0 { return a, fmt.Errorf("the engine of %s answered no composition input, so the node-engine's head cannot compose it", node) } @@ -242,7 +242,7 @@ func (e EngineCompositions) inSandbox(ctx context.Context, c *Case, head, node s if err != nil { return a, err } - job := buildstep.ComposeJob{Fetch: buildstep.Fetch{Repository: nodeEngineRepo, Commit: head, Packages: packagesFor(e.Published, c, nil, nodeEngineRepo)}, Dir: meshcatalogclient.RootPath, + job := buildstep.ComposeJob{Fetch: buildstep.Fetch{Repository: nodeEngineRepo, Commit: head, Packages: packagesFor(e.Published, c, tagged, nodeEngineRepo)}, Dir: meshcatalogclient.RootPath, Package: composer.Package, Request: raw, Reader: "the node-engine head of " + c.Group} verdict, err := e.Sandbox.Compose(ctx, job) if err != nil { diff --git a/modules/mesh-delivery-checker/internal/check/engines_test.go b/modules/mesh-delivery-checker/internal/check/engines_test.go index 27368925..1587dab0 100644 --- a/modules/mesh-delivery-checker/internal/check/engines_test.go +++ b/modules/mesh-delivery-checker/internal/check/engines_test.go @@ -91,7 +91,7 @@ func TestTheCompositionIsMadeFromTheEnginesAnswers(t *testing.T) { t.Helper() ec := EngineCompositions{Asker: e, Placed: func() ([]string, error) { return nodes, nil }, Bound: 50 * time.Millisecond, Composer: func() string { return strings.Repeat("7", 40) }} - v, err := ec.Compose(context.Background(), cs) + v, err := ec.Compose(context.Background(), cs, nil) if err != nil { t.Fatal(err) } @@ -159,7 +159,7 @@ func TestTheCompositionIsMadeFromTheEnginesAnswers(t *testing.T) { }} ec := EngineCompositions{Asker: e, Placed: func() ([]string, error) { return []string{"anchor"}, nil }, Bound: time.Second, Sandbox: sb, Manifests: digests{"web": "sha256:" + strings.Repeat("c", 64)}, Published: published{}} - v, err := ec.Compose(context.Background(), host) + v, err := ec.Compose(context.Background(), host, nil) if err != nil { t.Fatal(err) } @@ -177,6 +177,15 @@ func TestTheCompositionIsMadeFromTheEnginesAnswers(t *testing.T) { if pkgs := job.Fetch.Packages; len(pkgs) != 1 || pkgs[trunk.ForgeModulePrefix+"mesh-delivery"] != head1 { t.Errorf("the composer is fetched with the packages %v, want the group's mesh-delivery member at its head %s alone", pkgs, head1) } + // ADR 0607 §3, §5, design 51 *Tagged versions*: once the check heard the group's tagged version of a member's Go + // package, the composer is built against that, not the member's head, as the check's other Go builds are. + const tagged = "v0.1.15-feat-x.1" + if _, err := ec.Compose(context.Background(), host, buildstep.PackageVersions{trunk.ForgeModulePrefix + "mesh-delivery": tagged}); err != nil { + t.Fatal(err) + } + if pkgs := sb.jobs[len(sb.jobs)-1].Fetch.Packages; len(pkgs) != 1 || pkgs[trunk.ForgeModulePrefix+"mesh-delivery"] != tagged { + t.Errorf("the composer is fetched with the packages %v, want the group's tagged version %s of mesh-delivery", pkgs, tagged) + } if v.State != meshdeliverycheckerclient.CompositionFail || v.Sdk != head2 || !strings.Contains(v.Words, "the new composer refuses it") { t.Errorf("verdict = %+v", v) } @@ -193,7 +202,7 @@ func TestTheCompositionIsMadeFromTheEnginesAnswers(t *testing.T) { sb.v = func(node string) composer.Verdict { return composer.Verdict{V: composer.Version, Cases: []composer.CaseVerdict{{Name: "change", Machines: map[string]composer.Machine{node: row.m}}}} } - v, err = ec.Compose(context.Background(), host) + v, err = ec.Compose(context.Background(), host, nil) named := len(v.Warnings) == 1 && strings.Contains(v.Warnings[0], "anchor: "+nec.NoAnswer+" (its node-engine sends no running state for web)") if err != nil || (v.State == meshdeliverycheckerclient.CompositionPass) != row.pass || named != row.pass { t.Errorf("%+v: verdict %+v, %v", row.m, v, err) @@ -202,7 +211,7 @@ func TestTheCompositionIsMadeFromTheEnginesAnswers(t *testing.T) { // Review finding 3 (ADR 0593 §9 lets only a node that does not answer pass): a composer at the head that cannot // run fails the composition, naming the head, never passing as a silent node. sb.err = errors.New("the build of mesh-compose failed") - v, err = ec.Compose(context.Background(), host) + v, err = ec.Compose(context.Background(), host, nil) if err != nil || v.State != meshdeliverycheckerclient.CompositionFail || !strings.Contains(v.Words, head2) || !strings.Contains(v.Words, "the build of mesh-compose failed") || len(v.Warnings) != 0 { t.Errorf("a composer that cannot run: %+v, %v", v, err) @@ -212,7 +221,7 @@ func TestTheCompositionIsMadeFromTheEnginesAnswers(t *testing.T) { // Review finding 4: assignments that cannot be read is no mesh with no node: the composition is not made, said. t.Run("an unreadable assignments makes no composition", func(t *testing.T) { ec := EngineCompositions{Asker: &fakeEngines{}, Placed: func() ([]string, error) { return nil, errors.New("no such bucket") }} - if _, err := ec.Compose(context.Background(), cs); err == nil || !strings.Contains(err.Error(), "no such bucket") { + if _, err := ec.Compose(context.Background(), cs, nil); err == nil || !strings.Contains(err.Error(), "no such bucket") { t.Errorf("an unreadable assignments: %v", err) } b := newBench(t) diff --git a/modules/mesh-delivery-checker/internal/check/judge.go b/modules/mesh-delivery-checker/internal/check/judge.go index 2eb290a2..1c0ffd1c 100644 --- a/modules/mesh-delivery-checker/internal/check/judge.go +++ b/modules/mesh-delivery-checker/internal/check/judge.go @@ -180,9 +180,11 @@ type TryCandidates interface { TryCandidate(ctx context.Context, group, heads string, bound time.Duration) (meshcatalog.CandidatesByGroupValue, error) } -// CompositionReader makes a check's composition verdict; an error is only a case that cannot be composed at all. +// CompositionReader makes a check's composition verdict; an error is only a case that cannot be composed at all. tagged +// are the group's tagged versions of its Go packages the check heard (design 51 *Tagged versions*), nil for none: a +// composer built at the node-engine member's head is built against them, as the check's other Go builds are. type CompositionReader interface { - Compose(ctx context.Context, c *Case) (meshwants.CompositionsByGroupHeadsValue, error) + Compose(ctx context.Context, c *Case, tagged buildstep.PackageVersions) (meshwants.CompositionsByGroupHeadsValue, error) } func (j *Examiner) now() time.Time { @@ -395,7 +397,7 @@ func (st *state) composed(ctx context.Context, v *meshdeliverychecker.VerdictsBy return } words := st.awaitBuilds(ctx, v) // ADR 0603 §10: the check composes with the candidate's builds - comp, err := st.j.Compositions.Compose(ctx, st.c) + comp, err := st.j.Compositions.Compose(ctx, st.c, st.tags.goPackages) if err != nil { // Said in the verdict, never left as a composition not yet run (review finding 4). st.j.logf("%s: the composition could not be made, so it is said as not run: %v", st.c.Key, err)