The builder reads a module's own directory (the repository for one built from its root) and a repository its recipe packages, nothing else. A file in no module's directory was read as shared code and rebuilt everything built from the repository: 103 modules for a merge-check.sh added at the catalogue's root. It now touches nothing, in the merge handler, the release planner and the pull request's check alike, and the gate says so.
313 lines
14 KiB
Go
313 lines
14 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"reflect"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/novox/mesh-controller/internal/catalogue"
|
|
"github.com/novox/mesh-controller/internal/inventory"
|
|
"github.com/novox/mesh-controller/internal/link"
|
|
)
|
|
|
|
// entry is a module as the catalogue holds it: built, so its manifest carries no `build` any more.
|
|
func entry(module string, _ ...string) inventory.Entry {
|
|
return inventory.Entry{Manifest: catalogue.Manifest{Module: module}}
|
|
}
|
|
|
|
// stoodOn is what each module's newest build recorded it was handed.
|
|
func stoodOn(edges map[string][]string) map[string][]string {
|
|
out := map[string][]string{}
|
|
for module, bases := range edges {
|
|
for _, b := range bases {
|
|
out[module] = append(out[module], catalogue.ArtifactStoreScheme+b+"/runtime@sha256:"+strings.Repeat("0", 64))
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// A module built before the module it stands on is built against the old one and reports success
|
|
// (novox/hq 04-ISSUES/131). So bases come first, however the set arrived.
|
|
func TestBasesAreBuiltBeforeWhatStandsOnThem(t *testing.T) {
|
|
in := []inventory.Entry{entry("app"), entry("runtime"), entry("other"), entry("base")}
|
|
edges := stoodOn(map[string][]string{"app": {"runtime"}, "runtime": {"base"}})
|
|
got := orderByBases(in, edges)
|
|
pos := map[string]int{}
|
|
for i, e := range got {
|
|
pos[e.Manifest.Module] = i
|
|
}
|
|
if !(pos["base"] < pos["runtime"] && pos["runtime"] < pos["app"]) {
|
|
t.Fatalf("bases not first: %v", pos)
|
|
}
|
|
if len(got) != 4 {
|
|
t.Fatalf("an entry was lost or doubled: %d", len(got))
|
|
}
|
|
// A base outside the set is not waited for: it is not being rebuilt.
|
|
got = orderByBases([]inventory.Entry{entry("app")}, stoodOn(map[string][]string{"app": {"elsewhere"}}))
|
|
if len(got) != 1 {
|
|
t.Fatalf("a dependency outside the set changed the set: %v", got)
|
|
}
|
|
}
|
|
|
|
// A module registered from its manifest and never built still names its bases there; once built,
|
|
// the recorded edge is what says so. Both are read, and a module never stands on itself.
|
|
func TestWhatStandsOnAModuleIsReadFromItsBuildOrItsManifest(t *testing.T) {
|
|
built := entry("gitea")
|
|
edges := stoodOn(map[string][]string{"gitea": {"mesh-tools"}})
|
|
if !standsOnModule(built, "mesh-tools", edges) {
|
|
t.Fatal("a recorded edge was not read")
|
|
}
|
|
if standsOnModule(built, "gitea", edges) || standsOnModule(built, "postgres", edges) {
|
|
t.Fatal("an edge was invented")
|
|
}
|
|
fresh := inventory.Entry{Manifest: catalogue.Manifest{Module: "plex", Build: &catalogue.Build{
|
|
On: []catalogue.BuildsOn{{Arg: "RUNTIME_BASE", Module: "mesh-tools", Artifact: "runtime"}},
|
|
}}}
|
|
if !standsOnModule(fresh, "mesh-tools", nil) {
|
|
t.Fatal("a manifest's own base was not read")
|
|
}
|
|
}
|
|
|
|
// A merge names a repository the way the forge does; a source is recorded the way a build was
|
|
// asked for. The two meet on owner/repo and branch, whichever form the record took.
|
|
func TestAMergeMatchesTheSourcesBuiltFromIt(t *testing.T) {
|
|
m := link.SourceMoved{Owner: "novox", Repo: "mesh-controller", Base: "main",
|
|
CloneURL: "http://forge.internal:20000/novox/mesh-controller.git"}
|
|
for _, s := range []inventory.Source{
|
|
{Repository: "http://forge.internal:20000/novox/mesh-controller.git", Ref: "main"},
|
|
{Repository: "novox/mesh-controller", Seat: "git", Ref: ""},
|
|
{Repository: "https://elsewhere.example/novox/mesh-controller", Ref: "main"},
|
|
} {
|
|
if !sourceIs(s, m) {
|
|
t.Errorf("%+v was not matched by the merge", s)
|
|
}
|
|
}
|
|
for _, s := range []inventory.Source{
|
|
{Repository: "novox/mesh-host", Seat: "git"},
|
|
{Repository: "http://forge.internal:20000/novox/mesh-controller.git", Ref: "release"},
|
|
} {
|
|
if sourceIs(s, m) {
|
|
t.Errorf("%+v was matched by a merge that is not its", s)
|
|
}
|
|
}
|
|
}
|
|
|
|
// A merge made before the source was last seen is history: it does not move the source, and a
|
|
// merge that says nothing about when it was made is taken as news.
|
|
func TestAMergeOlderThanTheLastLookIsHistory(t *testing.T) {
|
|
seen := time.Date(2026, 9, 28, 3, 0, 0, 0, time.UTC)
|
|
if !isHistory("2026-09-28T02:00:00Z", seen) {
|
|
t.Fatal("an older merge was taken as news")
|
|
}
|
|
if isHistory("2026-09-28T04:00:00Z", seen) {
|
|
t.Fatal("a newer merge was taken as history")
|
|
}
|
|
if isHistory("", seen) || isHistory("2026-09-28T02:00:00Z", time.Time{}) {
|
|
t.Fatal("a merge or a source with no time on it was refused")
|
|
}
|
|
}
|
|
|
|
// A module as the catalogue holds it: built from a repository, at a directory inside it.
|
|
func fromRepo(module, repository, path string) inventory.Entry {
|
|
return inventory.Entry{
|
|
Manifest: catalogue.Manifest{Module: module},
|
|
Source: inventory.Source{Repository: repository, Path: path, Ref: "main"},
|
|
}
|
|
}
|
|
|
|
// A merge rebuilds the modules whose own directories it changed, and everything when what it changed
|
|
// is shared. One repository holding many modules is the ordinary case here, and rebuilding all of
|
|
// them for a change to one is what exhausted a registry's pull limit the first night this ran.
|
|
func TestAMergeRebuildsTheModulesItChanged(t *testing.T) {
|
|
const repo = "http://forge.internal:20000/novox/mesh-catalog.git"
|
|
gitea := fromRepo("gitea", repo, "modules/gitea")
|
|
keycloak := fromRepo("keycloak", repo, "modules/keycloak")
|
|
known := []inventory.Entry{gitea, keycloak, fromRepo("plex", repo, "modules/plex")}
|
|
candidates := []inventory.Entry{gitea, keycloak}
|
|
merge := func(paths []string, truncated bool) link.SourceMoved {
|
|
return link.SourceMoved{Owner: "novox", Repo: "mesh-catalog", Base: "main",
|
|
Paths: paths, PathsTruncated: truncated}
|
|
}
|
|
named := func(entries []inventory.Entry) string {
|
|
var names []string
|
|
for _, e := range entries {
|
|
names = append(names, e.Manifest.Module)
|
|
}
|
|
return strings.Join(names, ",")
|
|
}
|
|
for _, c := range []struct {
|
|
what string
|
|
m link.SourceMoved
|
|
want string
|
|
}{
|
|
{"one module's own files", merge([]string{"modules/gitea/index.ts", "modules/gitea/client.ts"}, false), "gitea"},
|
|
{"two modules' files", merge([]string{"modules/gitea/index.ts", "modules/keycloak/module.json"}, false), "gitea,keycloak"},
|
|
{"a file at the root no build reads (issue 280)", merge([]string{"tsconfig.json"}, false), ""},
|
|
{"a module the mesh does not hold", merge([]string{"modules/plex/index.ts"}, false), ""},
|
|
{"nothing said about the files", merge(nil, false), "gitea,keycloak"},
|
|
{"more files than were listed", merge([]string{"modules/gitea/index.ts"}, true), "gitea,keycloak"},
|
|
// novox/hq issue 252: a module the mesh has never registered is still a module, when the merge
|
|
// shows it is one — and a directory that may be shared code is still shared.
|
|
{"a new module beside a held one", merge([]string{"modules/gitea/x", "modules/newmod/module.json"}, false), "gitea"},
|
|
{"a new module's other files", merge([]string{"modules/newmod/index.ts", "modules/newmod/module.json"}, false), ""},
|
|
{"a module removed", merge([]string{"modules/gone/module.json"}, false), ""},
|
|
{"a directory with no manifest", merge([]string{"modules/lib/x.go"}, false), ""},
|
|
{"a file directly among the modules", merge([]string{"modules/README.md"}, false), ""},
|
|
{"a root file beside a module's", merge([]string{"merge-check.sh", "modules/keycloak/x.ts"}, false), "keycloak"},
|
|
} {
|
|
if got := named(whatTheMergeTouched(candidates, known, c.m)); got != c.want {
|
|
t.Errorf("%s: rebuilt %q, wanted %q", c.what, got, c.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// A module whose recipe packages source from another repository is affected when that repository
|
|
// moves — the manifest the mesh keeps says nothing about it, so the record of what the build read is
|
|
// the only thing that can say so.
|
|
func TestAModuleIsAffectedByTheRepositoryItPackages(t *testing.T) {
|
|
m := link.SourceMoved{Owner: "novox", Repo: "mesh-controller", Base: "main",
|
|
CloneURL: "http://forge.internal:20000/novox/mesh-controller.git"}
|
|
for _, read := range [][]inventory.ReadRepository{
|
|
{{Repository: "http://forge.internal:20000/novox/mesh-controller.git", Ref: "main"}},
|
|
{{Repository: "novox/mesh-controller"}},
|
|
{{Repository: "https://elsewhere.example/novox/other"}, {Repository: "novox/mesh-controller.git", Ref: "main"}},
|
|
} {
|
|
if !readsFrom(read, m) {
|
|
t.Errorf("%+v was not matched by the merge", read)
|
|
}
|
|
}
|
|
for _, read := range [][]inventory.ReadRepository{
|
|
nil,
|
|
{{Repository: "novox/mesh-host", Ref: "main"}},
|
|
{{Repository: "novox/mesh-controller", Ref: "release"}},
|
|
} {
|
|
if readsFrom(read, m) {
|
|
t.Errorf("%+v was matched by a merge that is not its", read)
|
|
}
|
|
}
|
|
}
|
|
|
|
// What a module handed over by hand records about where it came from, and what is refused.
|
|
func TestWhatAHandedOverModuleRecordsAboutItsSource(t *testing.T) {
|
|
// The whole location: a repository on the mesh's own forge, the directory inside it, the branch
|
|
// and the commit the manifest was read at.
|
|
from, err := whereItComesFrom("novox/mesh-catalog", "main", "c0ffee", "modules/gitea", true)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if from.Path != "modules/gitea" || from.Seat != "git" || from.Repository != "novox/mesh-catalog" {
|
|
t.Fatalf("the source records as %+v", from)
|
|
}
|
|
// A manifest with no provenance at all is legitimate: fixing something in a hurry.
|
|
if from, err := whereItComesFrom("", "", "", "", false); err != nil || !reflect.DeepEqual(from, inventory.Source{}) {
|
|
t.Fatalf("a manifest handed over with no provenance was refused: %+v, %v", from, err)
|
|
}
|
|
for _, c := range []struct {
|
|
what string
|
|
repository, ref, commit, path string
|
|
self bool
|
|
}{
|
|
{what: "a source with no commit", repository: "novox/mesh-catalog", commit: ""},
|
|
{what: "a commit with no source", commit: "c0ffee"},
|
|
{what: "a directory inside nothing", path: "modules/gitea"},
|
|
{what: "a forge holding nothing", self: true},
|
|
{what: "an address given as a path on the forge", repository: "http://forge.internal:20000/novox/x.git", commit: "c0ffee", self: true},
|
|
} {
|
|
if _, err := whereItComesFrom(c.repository, c.ref, c.commit, c.path, c.self); err == nil {
|
|
t.Errorf("%s was recorded as a source", c.what)
|
|
}
|
|
}
|
|
}
|
|
|
|
// novox/hq 04-ISSUES/215: a module once built at a commit still follows its branch — a merge into it
|
|
// matches the module, and a plan re-asks the branch, not the old commit.
|
|
func TestAModuleBuiltAtACommitStillFollowsItsBranch(t *testing.T) {
|
|
m := link.SourceMoved{Owner: "novox", Repo: "mesh-catalog", Base: "main"}
|
|
pinned := inventory.Source{Repository: "novox/mesh-catalog", Seat: "git", Ref: "9c97a8a"}
|
|
if !sourceIs(pinned, m) {
|
|
t.Error("a module whose record names a commit is left out of a merge into its branch")
|
|
}
|
|
full := inventory.Source{Repository: "novox/mesh-catalog", Seat: "git", Ref: "9c97a8a1d2c3b4a5f60718293a4b5c6d7e8f9012"}
|
|
if !sourceIs(full, m) {
|
|
t.Error("a full commit hash is read as a branch")
|
|
}
|
|
if got := followedBranch("9c97a8a"); got != "" {
|
|
t.Errorf("a plan would re-ask the old commit %q", got)
|
|
}
|
|
if got := followedBranch("release"); got != "release" {
|
|
t.Errorf("a branch is not followed as named: %q", got)
|
|
}
|
|
// A module that follows another branch is still not this merge's.
|
|
if sourceIs(inventory.Source{Repository: "novox/mesh-catalog", Seat: "git", Ref: "release"}, m) {
|
|
t.Error("a module following another branch was matched")
|
|
}
|
|
}
|
|
|
|
// novox/hq issues 278 and 280: a merge touching the code of a module the mesh does not hold — the
|
|
// catalogue's reference module, whose manifest it left alone — read as shared and rebuilt every module
|
|
// built from the repository (103 of them on 2026-10-06, 88 byte-identical); so did a merge-check.sh added at
|
|
// the root. No build reads a file outside its module's directory, so neither rebuilds anything, said by the
|
|
// announcer or not.
|
|
func TestAChangeInsideAModuleIsThatModulesHeldOrNot(t *testing.T) {
|
|
const repo = "http://forge.internal:20000/novox/mesh-catalog.git"
|
|
gitea := fromRepo("gitea", repo, "modules/gitea")
|
|
keycloak := fromRepo("keycloak", repo, "modules/keycloak")
|
|
known := []inventory.Entry{gitea, keycloak}
|
|
candidates := []inventory.Entry{gitea, keycloak}
|
|
merge := func(paths, modules []string, said bool) link.SourceMoved {
|
|
return link.SourceMoved{Owner: "novox", Repo: "mesh-catalog", Base: "main", Paths: paths,
|
|
ModuleDirs: modules, ModuleDirsSaid: said}
|
|
}
|
|
named := func(entries []inventory.Entry) string {
|
|
var names []string
|
|
for _, e := range entries {
|
|
names = append(names, e.Manifest.Module)
|
|
}
|
|
return strings.Join(names, ",")
|
|
}
|
|
showcase := []string{"modules/showcase/index.ts"}
|
|
for _, c := range []struct {
|
|
what string
|
|
m link.SourceMoved
|
|
want string
|
|
}{
|
|
{"a module held by none, said", merge(showcase, []string{"modules/showcase"}, true), ""},
|
|
{"beside a held one's change", merge(append([]string{"modules/gitea/index.ts"}, showcase...),
|
|
[]string{"modules/gitea", "modules/showcase"}, true), "gitea"},
|
|
{"deeper inside it", merge([]string{"modules/showcase/daemon/index.ts"}, []string{"modules/showcase"}, true), ""},
|
|
{"a directory holding no manifest, read by no build", merge([]string{"modules/lib/x.go"}, nil, true), ""},
|
|
{"one of two files in no module", merge([]string{"modules/showcase/index.ts", "modules/lib/x.go"},
|
|
[]string{"modules/showcase"}, true), ""},
|
|
{"the root is never a module directory", merge([]string{"tsconfig.json"}, []string{"", "/", "."}, true), ""},
|
|
{"not said: still no build reads it", merge(showcase, []string{"modules/showcase"}, false), ""},
|
|
{"an old announcer saying nothing", merge(showcase, nil, false), ""},
|
|
{"a manifest the merge removed, said or not", merge([]string{"modules/gone/module.json", "modules/gone/x.ts"}, nil, true), ""},
|
|
} {
|
|
if got := named(whatTheMergeTouched(candidates, known, c.m)); got != c.want {
|
|
t.Errorf("%s: rebuilt %q, wanted %q", c.what, got, c.want)
|
|
}
|
|
}
|
|
}
|
|
|
|
// What the announcer says reaches the controller as it is sent: the two fields, by their names on the
|
|
// wire, and an older announcement without them reads as not said.
|
|
func TestTheAnnouncerSaysWhichDirectoriesAreModules(t *testing.T) {
|
|
var m link.SourceMoved
|
|
if err := json.Unmarshal([]byte(`{"owner":"novox","repo":"mesh-catalog","merge_commit_sha":"abc",`+
|
|
`"paths":["modules/showcase/index.ts"],"module_dirs":["modules/showcase"],"module_dirs_said":true}`), &m); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !m.ModuleDirsSaid || !reflect.DeepEqual(m.ModuleDirs, []string{"modules/showcase"}) {
|
|
t.Fatalf("the announcer's word was lost: %+v", m)
|
|
}
|
|
var old link.SourceMoved
|
|
if err := json.Unmarshal([]byte(`{"owner":"novox","repo":"mesh-catalog","merge_commit_sha":"abc","paths":["x"]}`), &old); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if old.ModuleDirsSaid || old.ModuleDirs != nil {
|
|
t.Fatalf("an older announcement says nothing about module directories: %+v", old)
|
|
}
|
|
}
|