Files
mesh-delivery/modules/mesh-delivery-checker/internal/check/pins_test.go
T
jschoubben ab502a7174 DLV-C3r1: a group's Go package version is keyed by the module path go-package-registry's published names
The seat's contract now carries the Go module path, so the checker no longer
works it out from the artifact's paths, which could disagree with it.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 13:39:08 +01:00

346 lines
17 KiB
Go

package check
import (
"context"
"encoding/json"
"maps"
"reflect"
"slices"
"strings"
"sync"
"testing"
"time"
"git.novox.be/novox/mesh-delivery/modules/mesh-delivery-builder/buildrun"
"git.novox.be/novox/mesh-sdk/go/buildstep"
"git.novox.be/novox/mesh-sdk/go/catalogue"
"git.novox.be/novox/mesh-sdk/go/delivery"
)
// published is the latest published version of each mesh package, as the indexer's modules say (Examiner.Published).
type published delivery.PackageVersions
func (p published) LatestPackages() delivery.PackageVersions {
return maps.Clone(delivery.PackageVersions(p))
}
// packagesOfFetch is the package versions the fetch of repository was handed (delivery.StepAsk.Packages, issue 1585).
func packagesOfFetch(t *testing.T, b *bench, repository string) delivery.PackageVersions {
t.Helper()
for _, f := range b.steps.tool("fetch") {
if f.args["repository"] == repository {
p, err := buildstep.PackagesArg(f.args["packages"])
if err != nil {
t.Fatal(err)
}
return p
}
}
t.Fatalf("%s was not fetched", repository)
return nil
}
// ADR 0573 §4 (issue 1585 S14): a group's Go member is built against the head of the group's mesh-sdk member, whatever its
// go.mod names (an older head of it included, which issue 1624's pin check refused), and never refused for its pin; a
// pull request alone is built against the latest published version of each package, and its own repository's packages
// come with its tree.
func TestAGroupMemberIsBuiltAgainstTheSDKMembersHeadWhateverItsGoModNames(t *testing.T) { // 1624/AC2, AC4 as ADR 0573 §4 changes them
const contract = "git.novox.be/novox/mesh-delivery/contract"
b := newBench(t)
b.fetch.Modules = []string{"modules/m"}
b.ex.Published = published{SDKModulePath: trunkS, contract: trunkA}
v := b.judge(t, caseOf(t, pullOf(repoA, 7, head1), pullOf(repoSDK, 12, head2))).Verdict
if v.State != delivery.VerdictPass {
t.Fatalf("verdict = %+v", v)
}
if got := packagesOfFetch(t, b, repoA); len(got) != 1 || got[SDKModulePath] != head2 {
t.Errorf("the member's packages = %v, want mesh-sdk at the group's head %s alone (its own contract comes with its tree)", got, head2)
}
if s := besideOf(t, b, repoA)[repoSDK]; s.Commit != head2 {
t.Error("mesh-delivery's tests were not given the SDK member at its head")
}
b = newBench(t)
b.ex.Published = published{SDKModulePath: trunkS, contract: trunkA}
if v := b.judge(t, caseOf(t, pullOf("novox/mesh-catalog", 7, head1))).Verdict; v.State != delivery.VerdictPass {
t.Fatalf("a pull request alone: verdict = %+v", v)
}
if got := packagesOfFetch(t, b, "novox/mesh-catalog"); len(got) != 2 || got[SDKModulePath] != trunkS || got[contract] != trunkA {
t.Errorf("a pull request alone is built against %v, want every package at its latest published version", got)
}
// Design 51 *Tagged versions* (DLV-C3): a group's check says tag-wanted for each package a member declares and builds no
// member before its published; a lone pull request asks nothing; no published within the bound fails naming the package.
sdk := catalogue.Manifest{Module: "mesh-sdk", Build: &catalogue.Build{Artifacts: []catalogue.Artifact{
{Name: "go", Kind: catalogue.ArtifactPackage, Language: "go", Paths: []string{"go"}}, {Name: "lib", Kind: catalogue.ArtifactPackage, Language: "typescript"}}}}
contracts := catalogue.Manifest{Module: "mesh-contracts", Build: &catalogue.Build{Artifacts: []catalogue.Artifact{
{Name: "go", Kind: catalogue.ArtifactPackage, Language: "go", Paths: []string{"go"}},
{Name: "go-gopackageregistry", Kind: catalogue.ArtifactPackage, Language: "go", Paths: []string{"go/seats/gopackageregistry"}}}}}
digest, contractsDigest := strings.Repeat("c", 64), strings.Repeat("d", 64)
sdkPull := func() delivery.CandidatePull {
p := pullOf(repoSDK, 12, head2)
p.Modules = []string{"mesh-sdk"}
p.Fps = map[string]delivery.CandidateModule{"mesh-sdk": {FP2: "src2:" + strings.Repeat("f", 64), Manifest: digest}}
return p
}
unpreviewed := func() delivery.CandidatePull { // a package new in the group: no preview carries its manifest yet
p := sdkPull()
p.Fps = map[string]delivery.CandidateModule{"mesh-sdk": {FP2: "src2:" + strings.Repeat("f", 64)}}
return p
}
contractsPull := func() delivery.CandidatePull {
p := pullOf("novox/mesh-catalog", 4, strings.Repeat("3", 40))
p.Modules = []string{"mesh-contracts"}
p.Fps = map[string]delivery.CandidateModule{"mesh-contracts": {FP2: "src2:" + strings.Repeat("e", 64), Manifest: contractsDigest}}
return p
}
const sdkGo, contractsGo, seatGo = SDKModulePath, "git.novox.be/novox/mesh-catalog/go", "git.novox.be/novox/mesh-catalog/go/seats/gopackageregistry"
for _, row := range []struct {
name string
pulls []delivery.CandidatePull
published []string // the seats that publish the members' heads
wants int
fails string
// built is the Go packages mesh-delivery's member is built against (ADR 0607 §3, §5), node the package its Node
// fetch takes from the registry; nil checks none.
built map[string]string
node map[string]any
// noModule: the Go seat's published names no Go module path (ruling 1 on A2d's finding 1: never worked out).
noModule bool
}{
{name: "a group", pulls: []delivery.CandidatePull{pullOf(repoA, 7, head1), sdkPull()},
published: []string{"go-package-registry", "npm-package-registry"}, wants: 2,
built: map[string]string{sdkGo: "v0.1.15-feat-x.1"}, node: map[string]any{"name": "lib", "version": "0.1.15-feat-x.1"}},
{name: "a package new in the group, read at its head", pulls: []delivery.CandidatePull{pullOf(repoA, 7, head1), unpreviewed()},
published: []string{"go-package-registry", "npm-package-registry"}, wants: 2, built: map[string]string{sdkGo: "v0.1.15-feat-x.1"}},
{name: "one head, two Go packages (as mesh-contracts' head)", pulls: []delivery.CandidatePull{pullOf(repoA, 7, head1), contractsPull()},
published: []string{"go-package-registry"}, wants: 2,
built: map[string]string{contractsGo: "v0.1.15-feat-x.1", seatGo: "v0.1.3-feat-x.1"}},
{name: "a lone pull request", pulls: []delivery.CandidatePull{sdkPull()}},
{name: "a Go published naming no module", pulls: []delivery.CandidatePull{pullOf(repoA, 7, head1), sdkPull()},
published: []string{"go-package-registry", "npm-package-registry"}, wants: 2, noModule: true,
fails: "no published of the tagged version of go of mesh-sdk (novox/mesh-sdk#12) on go-package-registry within 50ms"},
{name: "a package never published", pulls: []delivery.CandidatePull{pullOf(repoA, 7, head1), sdkPull()},
published: []string{"go-package-registry"}, wants: 2,
fails: "no published of the tagged version of lib of mesh-sdk (novox/mesh-sdk#12) on npm-package-registry within 50ms"},
} {
b := newBench(t)
b.fetch.Modules = []string{"modules/m"}
b.ex.Read = fixedManifests{digest: sdk, contractsDigest: contracts}
b.ex.Modules = langs{repoA: {buildrun.GoBuildModule, buildrun.NodeBuildModule}}
raw, _ := json.Marshal(sdk)
b.ex.Forge = headFiles{repoSDK + " module.json " + head2: string(raw)}
tags := &recordedTags{heard: &PublishedHeard{}, publish: row.published, noModule: row.noModule}
b.ex.Tags, b.ex.PublishBound = tags, 50*time.Millisecond
b.asks.Answer = func(a delivery.StepAsk) delivery.Step {
if len(tags.wanted) != row.wants {
t.Errorf("%s: a member was asked before every tag-wanted was said: %d of %d", row.name, len(tags.wanted), row.wants)
}
return b.answer(a)
}
v := b.judge(t, caseOf(t, row.pulls...)).Verdict
if len(tags.wanted) != row.wants {
t.Errorf("%s: tag-wanted said %d times, want %d: %+v", row.name, len(tags.wanted), row.wants, tags.wanted)
}
for _, w := range tags.wanted {
if !slices.Contains([]string{repoSDK, "novox/mesh-catalog"}, w.Repository) || w.Branch != "feat/x" || w.Head == head1 || w.Module == "" || w.Pull == 7 || w.Tag != "" {
t.Errorf("%s: tag-wanted = %+v", row.name, w)
}
}
switch {
case row.fails == "" && v.State != delivery.VerdictPass:
t.Errorf("%s: verdict = %+v", row.name, v)
case row.fails != "" && (v.State != delivery.VerdictFail || !strings.Contains(v.Failed.Words, row.fails)):
t.Errorf("%s: verdict = %+v, want it to fail saying %q", row.name, v, row.fails)
case row.fails != "" && len(b.steps.tool("fetch")) != 0:
t.Errorf("%s: a member was built though a package was not published", row.name)
}
if row.built != nil {
got := packagesOfFetch(t, b, repoA)
for pkg, version := range row.built {
if got[pkg] != version {
t.Errorf("%s: mesh-delivery is built against %s %q, want the group's tagged version %s (all: %v)", row.name, pkg, got[pkg], version, got)
}
}
}
if row.node != nil {
if got := nodeFetchOf(b, repoA).args["package"]; !reflect.DeepEqual(got, row.node) {
t.Errorf("%s: mesh-delivery's Node fetch takes %v, want the group's tagged version %v from the registry", row.name, got, row.node)
}
}
}
}
// headFiles are the forge's files by "<repository> <path> <ref>"; any other is not found.
type headFiles map[string]string
func (h headFiles) File(_ context.Context, repository, path, ref string) (string, error) {
if text, ok := h[repository+" "+path+" "+ref]; ok {
return text, nil
}
return "", ErrNotFound
}
// recordedTags are the checker's tag asks as a test hears them: each tag-wanted kept, and the seats in publish answering it
// at once with a tagged version, as the builder and a registry seat would.
type recordedTags struct {
heard *PublishedHeard
publish []string
// noModule publishes a Go package naming no Go module path, against go-package-registry's contract.
noModule bool
mu sync.Mutex
wanted []TagWanted
}
func (r *recordedTags) Want(_ context.Context, w TagWanted) error {
r.mu.Lock()
r.wanted = append(r.wanted, w)
r.mu.Unlock()
for _, seat := range r.publish {
version := "0.1.15-feat-x.1"
if w.Package == "go-gopackageregistry" {
version = "0.1.3-feat-x.1"
}
module := "" // go-package-registry's published names the Go module path (its contract); npm's names none
if seat == "go-package-registry" && !r.noModule {
module = map[string]string{repoSDK + " go": SDKModulePath, "novox/mesh-catalog go": "git.novox.be/novox/mesh-catalog/go",
"novox/mesh-catalog go-gopackageregistry": "git.novox.be/novox/mesh-catalog/go/seats/gopackageregistry"}[w.Repository+" "+w.Package]
}
r.heard.Heard(seat, TaggedPublished{Name: w.Package, Kind: "package", Commit: w.Head, Tag: "feat-x", Version: version, Module: module})
}
return nil
}
func (r *recordedTags) Await(ctx context.Context, seat, head string, names []string) (TaggedPublished, bool) {
return r.heard.Await(ctx, seat, head, names)
}
type runsAt map[string]string
func (r runsAt) CommitOf(module string) string { return r[module] }
func besideOf(t *testing.T, b *bench, repository string) map[string]BesideRef {
t.Helper()
out := map[string]BesideRef{}
for _, f := range b.steps.tool("fetch") {
if f.args["repository"] == repository {
refs, _ := refsOf(f.args["beside"]), true
for _, r := range refs {
out[r.Repository] = r
}
}
}
return out
}
func TestTheSiblingsAreWhereTheTestsStateThem(t *testing.T) { // review D2, ADR 0484 clause 4
const catalogue, host, controller, networking = "novox/mesh-catalog", "novox/mesh-host", "novox/mesh-controller", "novox/mesh-networking"
hostRuns := strings.Repeat("7", 40)
b := newBench(t)
b.ex.Trunks = trunksOf(t, map[string]string{SDKRepository: trunkS, catalogue: trunkA, host: trunkA2})
b.ex.Runs = runsAt{"mesh-host": hostRuns}
// A group of mesh-sdk and the catalogue: mesh-sdk's tests read the group's catalogue head, the heads the group merges
// (ADR 0482 §2, ADR 0573 §4; issue 1126: a judge in the group was judged against the catalogue's trunk).
out := b.judge(t, caseOf(t, pullOf(SDKRepository, 7, head1), pullOf(catalogue, 12, head2)))
// ADR 0548 §6, issue 568's own example: the verdict names the node-engine at the commit the mesh runs; the catalogue,
// a member here, is not named.
if out.Beside[host] != hostRuns || out.Beside[catalogue] != "" {
t.Errorf("the siblings named for the verdict = %v, want the node-engine at %s", out.Beside, hostRuns[:12])
}
got := besideOf(t, b, SDKRepository)
if c := got[catalogue]; c.Commit != head2 || c.Ref != "refs/pull/12/head" {
t.Errorf("the catalogue beside mesh-sdk in a group = %+v, want the member's head %s", c, head2)
}
if h := got[host]; h.Commit != hostRuns {
t.Errorf("the node-engine beside mesh-sdk = %+v, want the commit the mesh runs %s", h, hostRuns)
}
// ADR 0593: route-proxy's proofs of a composition moved into the node-engine's tests, which read its manifest in
// mesh-networking: beside a node-engine change, at the group's head when the group holds one, else at its trunk.
b = newBench(t)
b.ex.Trunks = trunksOf(t, map[string]string{SDKRepository: trunkS, catalogue: trunkA, host: trunkA2, networking: trunkA2})
b.judge(t, caseOf(t, pullOf(host, 8, head1), pullOf(networking, 3, head2)))
if n := besideOf(t, b, host)[networking]; n.Commit != head2 || n.Ref != "refs/pull/3/head" {
t.Errorf("mesh-networking beside the node-engine in a group = %+v, want the member's head %s", n, head2)
}
// Alone, mesh-sdk's tests read the catalogue's trunk, and the verdict names it.
b = newBench(t)
b.ex.Trunks = trunksOf(t, map[string]string{SDKRepository: trunkS, catalogue: trunkA, host: trunkA2})
if out := b.judge(t, caseOf(t, pullOf(SDKRepository, 7, head1))); out.Beside[catalogue] != trunkA {
t.Errorf("the siblings named for the verdict alone = %v, want the catalogue at its trunk %s", out.Beside, trunkA)
}
if c := besideOf(t, b, SDKRepository)[catalogue]; c.Commit != trunkA || c.Ref != "" {
t.Errorf("the catalogue beside mesh-sdk alone = %+v, want its trunk %s", c, trunkA)
}
// 1126/B8 (novox/hq ADR 0590, ADR 0593 §11): the controller's repository is emptied and archived, so no repository's
// tests read it and it has no siblings of its own: a pull request of it has only its own trunk beside it.
b = newBench(t)
b.ex.Trunks = trunksOf(t, map[string]string{controller: trunkA, catalogue: trunkS, host: trunkA2, SDKRepository: trunkS})
b.judge(t, caseOf(t, pullOf(controller, 9, head2)))
if got := besideOf(t, b, controller); len(got) != 1 || got[controller].Commit != trunkA {
t.Errorf("beside the controller's own change = %+v, want its own trunk alone", got)
}
for repo, siblings := range besideRules {
if repo == controller || slices.Contains(siblings, controller) {
t.Errorf("the tests of %s read the controller, or it has siblings", repo)
}
}
}
// 1887/A12: the change's own repository is placed beside it at the trunk it is merged onto, for every repository, so a
// shrink-only list test reads the trunk's copy at $MESH_CHECK_BESIDE/<name>.
func TestTheChangesOwnRepositoryIsBesideItAtItsTrunk(t *testing.T) {
b := newBench(t)
b.judge(t, caseOf(t, pullOf(repoA, 7, head1)))
got := besideOf(t, b, repoA)
if own := got[repoA]; own.Commit != trunkA || own.Ref != "" {
t.Fatalf("the repository beside its own change = %+v, want its trunk %s", own, trunkA)
}
}
// 1887/A12: a catalogue change's siblings hold the catalogue itself at the trunk it is merged onto, beside mesh-sdk, so
// the catalogue's shrink-only list tests read $MESH_CHECK_BESIDE/mesh-catalog. And the node-engine at the commit the mesh
// runs (ADR 0593 §7): mesh-build-go's composer test builds the node-engine's composer command from it.
func TestACatalogueChangeHasTheCatalogueBesideItAtItsTrunk(t *testing.T) {
hostRuns := strings.Repeat("7", 40)
b := newBench(t)
b.ex.Trunks = trunksOf(t, map[string]string{catalogueRepository: trunkA, SDKRepository: trunkS, "novox/mesh-controller": trunkA2, nodeEngineRepo: trunkA2})
b.ex.Runs = runsAt{nodeEngineModule: hostRuns}
out := b.judge(t, caseOf(t, pullOf(catalogueRepository, 12, head1)))
// ADR 0548 §6: the outcome names the siblings read at their trunk's head or the commit the mesh runs, never the
// repository it merges.
if out.Beside[SDKRepository] != trunkS || out.Beside[nodeEngineRepo] != hostRuns || out.Beside["novox/mesh-controller"] != "" || out.Beside[catalogueRepository] != "" {
t.Errorf("the siblings named for the verdict = %v, want mesh-sdk at its trunk, the node-engine at the commit the mesh runs, and neither the controller nor the catalogue", out.Beside)
}
got := besideOf(t, b, catalogueRepository)
if c := got[catalogueRepository]; c.Commit != trunkA || c.Ref != "" {
t.Fatalf("the catalogue beside its own change = %+v, want its trunk %s", c, trunkA)
}
if _, ok := got[SDKRepository]; !ok {
t.Errorf("mesh-sdk is no longer beside a catalogue change: %v", got)
}
if h := got[nodeEngineRepo]; h.Commit != hostRuns {
t.Errorf("the node-engine beside a catalogue change = %+v, want the commit the mesh runs %s", h, hostRuns)
}
}
// 1887/A12: a mesh-lab head gets a check: with its trunk known it is judged like any repository, its Go fetch finding
// the nested modules itself.
func TestAMeshLabHeadGetsACheck(t *testing.T) {
const lab = "novox/mesh-lab"
raw := candidateOf(t, "fix/x", pullOf(lab, 3, head1))
c, err := delivery.DecodeCandidate(raw)
if err != nil {
t.Fatal(err)
}
cs := CaseOf(c, map[string]string{lab: trunkA})
if cs == nil || len(cs.Pulls) != 1 || cs.Pulls[0].Repository != lab || len(cs.Left) != 0 {
t.Fatalf("the case = %+v, want mesh-lab#3 judged", cs)
}
b := newBench(t)
if v := b.judge(t, cs).Verdict; v.State != delivery.VerdictPass {
t.Fatalf("verdict = %+v", v)
}
if fetched := b.steps.tool("fetch"); len(fetched) == 0 || fetched[0].args["repository"] != lab {
t.Fatalf("mesh-lab was not fetched: %+v", fetched)
}
}