package main import ( "encoding/json" "fmt" "math/rand/v2" "sort" "strings" "testing" "time" snapshot "github.com/novox/mesh-controller/internal/facts" "github.com/novox/mesh-controller/internal/inventory" "github.com/novox/mesh-controller/internal/link" ) // The delivery planner's rules, as recorded (novox/hq ADR 0162 §1, ADR 0238 §3), held by one table and // one property over reachOfMerge — the planner's one answer to "what does this merge move, and in which // order". A row that fails here on main is a planner that breaks a recorded rule: the row stays, the // expectation is not bent to the code. // // The kinds of edge, as the code reads them (release_plan.go): // // kind widens the plan orders the tiers // stands-on yes yes, after its base is built // declared yes yes, after its base is built // packages no no — retired by novox/hq ADR 0267; one recorded before is read and ignored // built-by no yes, after the build machine — except for what the build machine stands // on, and for the controller whose worker it binds // worker-of no yes, the build seat's holder after the controller (hq issue 206) const ( repoOne = "http://forge.internal:20000/novox/one.git" repoTwo = "http://forge.internal:20000/novox/two.git" ) // dep is one edge of the catalogue's relation: from depends on to, in the way kind says. func dep(from, kind, to string) inventory.Edge { return inventory.Edge{From: from, To: to, Kind: kind} } // tiered is a plan's tiers as one line: a tier's modules by comma, tiers by " | ". Empty for no plan. func tiered(tiers [][]string) string { var out []string for _, t := range tiers { out = append(out, strings.Join(t, ",")) } return strings.Join(out, " | ") } // planMerge is what reachOfMerge plans for a merge of these files into a repository's main: its tiers as // one line, and the files no build reads. It also holds the plan to its own shape: every module it builds // is in exactly one tier. func planMerge(t *testing.T, repo string, paths []string, entries []inventory.Entry, read map[string][]inventory.ReadRepository, edges []inventory.Edge) (mergeReach, string) { t.Helper() m := link.SourceMoved{Owner: "novox", Repo: repo, Base: "main", Commit: "head", Paths: paths} r := reachOfMerge(m, entries, read, edges) seen := map[string]int{} for _, tier := range r.Plan.Tiers { for _, name := range tier { seen[name]++ } } for name := range r.Plan.Modules { if seen[name] != 1 { t.Errorf("%s/%v: %s is in %d tiers of %v", repo, paths, name, seen[name], r.Plan.Tiers) } } if len(seen) != len(r.Plan.Modules) { t.Errorf("%s/%v: the tiers %v hold modules the plan does not (%d)", repo, paths, r.Plan.Tiers, len(r.Plan.Modules)) } return r, tiered(r.Plan.Tiers) } // **A merge moves the modules whose directory it changed, and everything built on them; nothing else.** // Each row is a catalogue (modules in directories of one or two repositories), its dependencies with // their kinds, the files one commit changed, and the exact plan: the moved set and its tier order. func TestAPlanIsWhatTheChangeTouchedAndWhatIsBuiltOnIt(t *testing.T) { in := func(repo, dir string, names ...string) []inventory.Entry { var out []inventory.Entry for _, n := range names { d := dir + "/" + n if dir == "" { d = n } out = append(out, fromRepo(n, repo, d)) } return out } // Modules a to f, x and z in novox/one under modules/; g in novox/two at g/. entries := append(in(repoOne, "modules", "a", "b", "c", "d", "e", "f", "x", "z"), in(repoTwo, "", "g")...) const ( standsOn = inventory.EdgeStandsOn declared = inventory.EdgeDeclared packages = inventory.EdgePackages builtBy = inventory.EdgeBuiltBy workerOf = inventory.EdgeWorkerOf ) // The two real cycles the kinds resolve themselves (ADR 0162 §1, hq issue 206). runtime := append(in(repoOne, "modules", "runtime", "builder"), fromRepo("controller", repoTwo, "")) for _, c := range []struct { what string entries []inventory.Entry edges []inventory.Edge repo string paths []string want string // the tiers, " | " between them unread string cycle bool }{ // The operator's case: two modules changed, a third beside them in the same repository untouched, // each changed one with a dependent. {what: "A and B changed, C untouched beside them, D on A and E on B", edges: []inventory.Edge{dep("d", standsOn, "a"), dep("e", declared, "b")}, repo: "one", paths: []string{"modules/a/main.go", "modules/b/module.json"}, want: "a,b | d,e"}, {what: "only A's directory: A and what stands on it", edges: []inventory.Edge{dep("d", standsOn, "a"), dep("e", declared, "b")}, repo: "one", paths: []string{"modules/a/main.go"}, want: "a | d"}, {what: "only C's directory: C alone, nothing is built on it", edges: []inventory.Edge{dep("d", standsOn, "a"), dep("e", declared, "b")}, repo: "one", paths: []string{"modules/c/Dockerfile"}, want: "c"}, {what: "only the dependent changed: its base does not move", edges: []inventory.Edge{dep("d", standsOn, "a")}, repo: "one", paths: []string{"modules/d/main.go"}, want: "d"}, {what: "transitive: F on D on A, A changed", edges: []inventory.Edge{dep("f", standsOn, "d"), dep("d", standsOn, "a")}, repo: "one", paths: []string{"modules/a/x"}, want: "a | d | f"}, {what: "transitive across kinds stops at a packages edge: F declared on D, D packages A (ADR 0267)", edges: []inventory.Edge{dep("f", declared, "d"), dep("d", packages, "a")}, repo: "one", paths: []string{"modules/a/x"}, want: "a"}, // Each kind alone: X depends on A, A changed (widening), then both changed (ordering). {what: "stands-on (built against A's artifact) widens", edges: []inventory.Edge{dep("x", standsOn, "a")}, repo: "one", paths: []string{"modules/a/x"}, want: "a | x"}, {what: "stands-on orders", edges: []inventory.Edge{dep("x", standsOn, "a")}, repo: "one", paths: []string{"modules/a/x", "modules/x/y"}, want: "a | x"}, {what: "declared (build.on) widens", edges: []inventory.Edge{dep("x", declared, "a")}, repo: "one", paths: []string{"modules/a/x"}, want: "a | x"}, {what: "declared orders", edges: []inventory.Edge{dep("x", declared, "a")}, repo: "one", paths: []string{"modules/a/x", "modules/x/y"}, want: "a | x"}, {what: "packages, recorded before ADR 0267, widens nothing", edges: []inventory.Edge{dep("x", packages, "a")}, repo: "one", paths: []string{"modules/a/x"}, want: "a"}, {what: "packages does not order", edges: []inventory.Edge{dep("x", packages, "a")}, repo: "one", paths: []string{"modules/a/x", "modules/x/y"}, want: "a,x"}, {what: "built-by never widens", edges: []inventory.Edge{dep("x", builtBy, "a")}, repo: "one", paths: []string{"modules/a/x"}, want: "a"}, // Whichever builder runs builds it (novox/hq ADR 0360 §3): built-by orders nothing, as before it ordered. {what: "built-by does not order", edges: []inventory.Edge{dep("x", builtBy, "a")}, repo: "one", paths: []string{"modules/a/x", "modules/x/y"}, want: "a,x"}, {what: "worker-of never widens", edges: []inventory.Edge{dep("x", workerOf, "a")}, repo: "one", paths: []string{"modules/a/x"}, want: "a"}, {what: "worker-of orders", edges: []inventory.Edge{dep("x", workerOf, "a")}, repo: "one", paths: []string{"modules/a/x", "modules/x/y"}, want: "a | x"}, {what: "a change to the base alone does not move what it builds", edges: []inventory.Edge{dep("x", builtBy, "a"), dep("d", builtBy, "a"), dep("e", standsOn, "a")}, repo: "one", paths: []string{"modules/a/module.json"}, want: "a | e"}, // The cycles the kinds resolve: the build machine stands on the runtime image the runtime image is // built by; the build seat's holder follows the controller that is built by it. {what: "the build machine's base comes first, built by the build machine that runs", entries: runtime, edges: []inventory.Edge{dep("runtime", builtBy, "builder"), dep("builder", standsOn, "runtime")}, repo: "one", paths: []string{"modules/runtime/Dockerfile", "modules/builder/main.go"}, want: "runtime | builder"}, {what: "the runtime image alone takes the build machine on it along", entries: runtime, edges: []inventory.Edge{dep("runtime", builtBy, "builder"), dep("builder", standsOn, "runtime")}, repo: "one", paths: []string{"modules/runtime/Dockerfile"}, want: "runtime | builder"}, {what: "the build machine alone moves alone", entries: runtime, edges: []inventory.Edge{dep("runtime", builtBy, "builder"), dep("builder", standsOn, "runtime")}, repo: "one", paths: []string{"modules/builder/main.go"}, want: "builder"}, {what: "the build seat's holder follows the controller it binds the worker of", entries: runtime, edges: []inventory.Edge{dep("controller", builtBy, "builder"), dep("builder", workerOf, "controller")}, repo: "two", paths: []string{"cmd/main.go"}, want: "controller"}, // Two repositories. {what: "a dependent in another repository follows its base", edges: []inventory.Edge{dep("g", standsOn, "a")}, repo: "one", paths: []string{"modules/a/x"}, want: "a | g"}, {what: "a directory of the same name in another repository is not this one's", edges: []inventory.Edge{dep("g", standsOn, "a")}, repo: "two", paths: []string{"modules/a/x"}, want: "", unread: "modules/a/x"}, {what: "the dependent's own repository moves the dependent alone", edges: []inventory.Edge{dep("g", standsOn, "a")}, repo: "two", paths: []string{"g/main.go"}, want: "g"}, // A diamond. {what: "a diamond, one side changed", edges: []inventory.Edge{dep("d", standsOn, "a"), dep("d", standsOn, "b")}, repo: "one", paths: []string{"modules/a/x"}, want: "a | d"}, {what: "a diamond, both sides changed", edges: []inventory.Edge{dep("d", standsOn, "a"), dep("d", standsOn, "b")}, repo: "one", paths: []string{"modules/a/x", "modules/b/x"}, want: "a,b | d"}, {what: "a diamond on one base", edges: []inventory.Edge{dep("d", standsOn, "a"), dep("d", declared, "b"), dep("a", standsOn, "z"), dep("b", standsOn, "z")}, repo: "one", paths: []string{"modules/z/x"}, want: "z | a,b | d"}, {what: "a diamond of mixed kinds orders on the ordering side only", edges: []inventory.Edge{dep("d", standsOn, "a"), dep("d", packages, "b")}, repo: "one", paths: []string{"modules/b/x"}, want: "b"}, // A cycle the catalogue should never produce: what remains is one last tier, and said. {what: "a cycle is one last tier, not lost", edges: []inventory.Edge{dep("a", standsOn, "b"), dep("b", standsOn, "a"), dep("f", standsOn, "c")}, repo: "one", paths: []string{"modules/a/x", "modules/c/x"}, want: "c | f | a,b", cycle: true}, // Files no build reads. {what: "a README at the root of a repository whose modules all live below it", edges: []inventory.Edge{dep("d", standsOn, "a")}, repo: "one", paths: []string{"README.md"}, want: "", unread: "README.md"}, {what: "a directory no module lives in", edges: []inventory.Edge{dep("d", standsOn, "a")}, repo: "one", paths: []string{"modules/lib/x.go", "modules/README.md"}, want: "", unread: "modules/lib/x.go,modules/README.md"}, {what: "a module's directory beside a root file", edges: []inventory.Edge{dep("d", standsOn, "a")}, repo: "one", paths: []string{"merge-check.sh", "modules/a/x"}, want: "a | d", unread: "merge-check.sh"}, {what: "a directory whose name begins with a module's", edges: []inventory.Edge{dep("d", standsOn, "a")}, repo: "one", paths: []string{"modules/ab/x"}, want: "", unread: "modules/ab/x"}, } { e := entries if c.entries != nil { e = c.entries } r, got := planMerge(t, c.repo, c.paths, e, nil, c.edges) if got != c.want { t.Errorf("%s: planned %q, wanted %q", c.what, got, c.want) } if u := strings.Join(r.Unread, ","); u != c.unread { t.Errorf("%s: unread %q, wanted %q", c.what, u, c.unread) } // A packages edge in the last tier is no cycle; hasCycle said one on main at 8170fc5. if hasCycle(r.Plan.Tiers, c.edges) != c.cycle { t.Errorf("%s: a cycle said %v, wanted %v (%v)", c.what, !c.cycle, c.cycle, r.Plan.Tiers) } } } // **A shared repository moves only what a change's files are in the build source of** (novox/hq ADR 0267, // issue 338, issue 363). The controller is built from its repository's root as a Go bundle; the route proxy // and the build seat's holder build images whose context is that repository and which name the package they // compile. Each newest trunk build said its build source — the import closure of its program — and a merge // is mapped onto those. The rows tagged 338 held the opposite until ADR 0267 was built: every merge to the // controller's repository planned all three, in three tiers. // // The build sources are this repository's own programs as GoBuildSource reads them (held to that by // TestThisRepositorysProgramsHaveBuildSourcesOfTheirOwn in internal/builder), cut to what the rows need. func TestASharedRepositoryMovesOnlyWhatItsBuildSourceHolds(t *testing.T) { const catalogueRepo = "http://forge.internal:20000/novox/mesh-catalog.git" const controllerRepo = "http://forge.internal:20000/novox/mesh-controller.git" entries := []inventory.Entry{ fromRepo("mesh-controller", controllerRepo, ""), fromRepo("build-agent", catalogueRepo, "modules/build-agent"), fromRepo("route-proxy", catalogueRepo, "modules/route-proxy"), fromRepo("gitea", catalogueRepo, "modules/gitea"), } gomod := []string{"go.mod", "go.sum", "vendor/modules.txt"} with := func(paths ...string) []string { return append(append([]string{}, gomod...), paths...) } read := map[string][]inventory.ReadRepository{ "mesh-controller": {{Own: true, Paths: with("module.json", "cmd/mesh-controller/", "internal/conditions/", "internal/broker/", "internal/builder/", "internal/inventory/", "internal/inventory/migrations/**", "vendor/github.com/nats-io/nats.go/")}}, "build-agent": { {Repository: "novox/mesh-controller", Ref: "main", Paths: with("cmd/mesh-builder/", "internal/broker/", "internal/builder/", "internal/inventory/", "internal/inventory/migrations/**", "vendor/github.com/nats-io/nats.go/")}, {Own: true, Paths: []string{"modules/build-agent/Dockerfile", "modules/build-agent/module.json"}}, }, "route-proxy": { {Repository: "novox/mesh-controller", Ref: "main", Paths: with("examples/route-proxy/", "internal/broker/", "vendor/github.com/nats-io/nats.go/")}, {Own: true, Paths: []string{"modules/route-proxy/Dockerfile", "modules/route-proxy/module.json"}}, }, } // With no build source said — before each module's first trunk build under ADR 0267, or while an // earlier merge's build of it is pending — a module is read as before. unsaid := map[string][]inventory.ReadRepository{ "build-agent": {{Repository: "novox/mesh-controller", Ref: "main"}}, "route-proxy": {{Repository: "novox/mesh-controller", Ref: "main"}}, } edges := sharedRepositoryEdges for _, c := range []struct { what, repo string paths []string read map[string][]inventory.ReadRepository want string unread string }{ // The operator's acceptance: a merge of the controller's own code plans the controller alone. {"the controller's own command: the controller alone (338)", "mesh-controller", []string{"cmd/mesh-controller/main.go"}, read, "mesh-controller", ""}, {"a package only the controller builds from: the controller alone (338)", "mesh-controller", []string{"internal/conditions/condition.go", "internal/conditions/bus.go"}, read, "mesh-controller", ""}, {"a test beside the controller's command: nothing is built from it", "mesh-controller", []string{"cmd/mesh-controller/main_test.go"}, read, "", "cmd/mesh-controller/main_test.go"}, {"a README of the controller's repository: no module (338)", "mesh-controller", []string{"README.md"}, read, "", "README.md"}, {"the route proxy's program alone: the route proxy alone (338)", "mesh-controller", []string{"examples/route-proxy/main.go"}, read, "route-proxy", ""}, {"the build seat's program alone: its holder alone", "mesh-controller", []string{"cmd/mesh-builder/main.go"}, read, "build-agent", ""}, {"a package the build seat's program and the controller build from: both", "mesh-controller", []string{"internal/builder/builder.go"}, read, "mesh-controller | build-agent", ""}, {"a migration the controller and the build seat's program embed: both", "mesh-controller", []string{"internal/inventory/migrations/0088-a-build-says-its-build-source.sql"}, read, "mesh-controller | build-agent", ""}, // A package all three build from: all three; the build seat's holder after the controller whose worker // it binds (worker-of, issue 206), the proxy beside the controller: built-by orders nothing (ADR 0360 §3). {"a package all three build from: all three", "mesh-controller", []string{"internal/broker/broker.go"}, read, "mesh-controller,route-proxy | build-agent", ""}, {"a vendored package all three build from: all three", "mesh-controller", []string{"vendor/github.com/nats-io/nats.go/nats.go"}, read, "mesh-controller,route-proxy | build-agent", ""}, {"go.sum: all three", "mesh-controller", []string{"go.sum"}, read, "mesh-controller,route-proxy | build-agent", ""}, {"a file added to the proxy's package: the proxy", "mesh-controller", []string{"examples/route-proxy/new.go"}, read, "route-proxy", ""}, // Before any build source is said: as before. {"no build source said: a README moves all three, as before", "mesh-controller", []string{"README.md"}, unsaid, "mesh-controller,route-proxy | build-agent", ""}, // In the catalogue, where they live, each by its own build source. {"the route proxy's recipe: it alone", "mesh-catalog", []string{"modules/route-proxy/Dockerfile"}, read, "route-proxy", ""}, {"the route proxy's manifest: it alone", "mesh-catalog", []string{"modules/route-proxy/module.json"}, read, "route-proxy", ""}, {"the route proxy's README: nothing", "mesh-catalog", []string{"modules/route-proxy/README.md"}, read, "", "modules/route-proxy/README.md"}, {"the build agent's manifest: it alone, nothing it builds", "mesh-catalog", []string{"modules/build-agent/module.json"}, read, "build-agent", ""}, {"another module of the catalogue: neither", "mesh-catalog", []string{"modules/gitea/index.ts"}, read, "gitea", ""}, } { r, got := planMerge(t, c.repo, c.paths, entries, c.read, edges) if got != c.want { t.Errorf("%s: planned %q, wanted %q", c.what, got, c.want) } if u := strings.Join(r.Unread, ","); u != c.unread { t.Errorf("%s: unread %q, wanted %q", c.what, u, c.unread) } } // Files not all said: everything the repository builds, as before. m := link.SourceMoved{Owner: "novox", Repo: "mesh-controller", Base: "main", Commit: "head", Paths: []string{"README.md"}, PathsTruncated: true} if got := tiered(reachOfMerge(m, entries, read, edges).Plan.Tiers); got != "mesh-controller,route-proxy | build-agent" { t.Errorf("a merge whose files were not all said: planned %q, wanted all three", got) } } // **A module whose recorded build source a plan has overtaken is read whole** (novox/hq ADR 0267): a merge // that added an import to the route proxy is planned; before a build of it works — still building, failed, // or its plan closed before reaching it — a merge changing only the newly imported package must still move // the proxy, since the build source its last build said does not hold that package. func TestAModuleAPlanOvertookIsReadWhole(t *testing.T) { built := time.Date(2026, 10, 10, 1, 0, 0, 0, time.UTC) read := map[string][]inventory.ReadRepository{ "route-proxy": { {Repository: "novox/mesh-controller", Ref: "main", Paths: []string{"examples/route-proxy/", "go.mod"}, Built: built}, {Own: true, Paths: []string{"modules/route-proxy/module.json"}, Built: built}, }, "mesh-controller": {{Own: true, Paths: []string{"module.json", "cmd/mesh-controller/"}, Built: built}}, } m := link.SourceMoved{Owner: "novox", Repo: "mesh-controller", Base: "main", Paths: []string{"internal/newly/imported.go"}} if readsFrom(read["route-proxy"], m) { t.Fatal("the said build source holds the new package: the fixture is wrong") } proxy := func(state string) map[string]*inventory.PlanModule { return map[string]*inventory.PlanModule{"route-proxy": {State: state}, "gitea": {State: "built"}} } for _, c := range []struct { what string plans []inventory.Plan whole bool }{ {"no plan", nil, false}, {"a plan still building it", []inventory.Plan{{State: inventory.PlanBuilding, Created: built.Add(-time.Hour), Modules: proxy("building")}}, true}, {"a plan made after its build that failed before building it", []inventory.Plan{{State: "failed", Created: built.Add(time.Minute), Modules: proxy("waiting")}}, true}, {"a plan made after its build that built it", []inventory.Plan{{State: inventory.PlanDone, Created: built.Add(time.Minute), Modules: proxy("built")}}, false}, {"a plan closed before its build", []inventory.Plan{{State: "failed", Created: built.Add(-time.Hour), Modules: proxy("waiting")}}, false}, } { view := planningView(read, c.plans) if readsFrom(view["route-proxy"], m) != c.whole { t.Errorf("%s: read whole %v, wanted %v", c.what, !c.whole, c.whole) } if (ownSource(view["route-proxy"]) == nil) != c.whole { t.Errorf("%s: its own build source kept %v", c.what, ownSource(view["route-proxy"]) != nil) } if ownSource(view["mesh-controller"]) == nil { t.Errorf("%s: a module no plan holds lost its build source", c.what) } } } // **A missed merge that moves only a module packaging the repository is acted on** (novox/hq ADR 0267, // issue 266): the catch-up asks wouldMove, which counts it; once a plan or build of it is made after the // merge, the merge is history for it, and the catch-up leaves it. func TestAMissedMergeMovingOnlyAPackagingModuleIsActedOnOnce(t *testing.T) { merged := time.Date(2026, 10, 10, 1, 0, 0, 0, time.UTC) entries := []inventory.Entry{ fromRepo("mesh-controller", "http://forge.internal:20000/novox/mesh-controller.git", ""), fromRepo("route-proxy", "http://forge.internal:20000/novox/mesh-catalog.git", "modules/route-proxy"), } read := map[string][]inventory.ReadRepository{ "mesh-controller": {{Own: true, Paths: []string{"module.json", "cmd/mesh-controller/"}, Built: merged.Add(-time.Hour)}}, "route-proxy": {{Repository: "novox/mesh-controller", Ref: "main", Paths: []string{"examples/route-proxy/"}, Built: merged.Add(-time.Hour), Looked: merged.Add(-time.Hour)}}, } m := link.SourceMoved{Owner: "novox", Repo: "mesh-controller", Base: "main", Commit: "c1", MergedAt: merged.Format(time.RFC3339), Paths: []string{"examples/route-proxy/main.go"}} if got := wouldMove(m, entries, planningView(read, nil)); len(got) != 1 || got[0].Manifest.Module != "route-proxy" { t.Fatalf("a missed merge of the proxy's program would move %v", got) } // Acted on at once: the plan answering this very merge is a look, however close the clocks. atOnce := []inventory.Plan{{State: inventory.PlanBuilding, Commit: "c1", Created: merged.Add(2 * time.Second), Modules: map[string]*inventory.PlanModule{"route-proxy": {State: "building"}}}} if got := wouldMove(m, entries, planningView(read, atOnce)); len(got) != 0 { t.Fatalf("a merge whose own plan holds the proxy would move %v again", got) } acted := []inventory.Plan{{State: inventory.PlanBuilding, Created: merged.Add(2 * time.Minute), Modules: map[string]*inventory.PlanModule{"route-proxy": {State: "building"}}}} if got := wouldMove(m, entries, planningView(read, acted)); len(got) != 0 { t.Fatalf("a merge acted on for the proxy would move %v again", got) } // A plan that closed without building it looked at nothing: the merge is still news for it. closed := []inventory.Plan{{State: "failed", Created: merged.Add(2 * time.Minute), Modules: map[string]*inventory.PlanModule{"route-proxy": {State: "waiting"}}}} if got := wouldMove(m, entries, planningView(read, closed)); len(got) != 1 { t.Fatalf("a plan that never built the proxy hid the merge from it: %v", got) } // A look just before the merge, on clocks a little apart, is no look after it. skewed := []inventory.Plan{{State: inventory.PlanBuilding, Created: merged.Add(30 * time.Second), Modules: map[string]*inventory.PlanModule{"route-proxy": {State: "building"}}}} if got := wouldMove(m, entries, planningView(read, skewed)); len(got) != 1 { t.Fatalf("a look within the clocks' margin made the merge history: %v", got) } } // sharedRepositoryEdges is what dependenciesOf derives for the catalogue of the test above, sorted as it // sorts them: no packages edge (novox/hq ADR 0267 rule 4). var sharedRepositoryEdges = []inventory.Edge{ dep("build-agent", inventory.EdgeWorkerOf, "mesh-controller"), dep("gitea", inventory.EdgeBuiltBy, "build-agent"), dep("mesh-controller", inventory.EdgeBuiltBy, "build-agent"), dep("route-proxy", inventory.EdgeBuiltBy, "build-agent"), } // **The planner's invariant, over random catalogues.** For any catalogue whose dependencies form no cycle // and any set of changed files in one repository: // // - the plan is exactly the modules of that repository whose directory holds a changed file (every file, // for a module built from the root), and everything reachable from them along stands-on and declared — // never along packages (novox/hq ADR 0267), built-by or worker-of; // - every stands-on, declared, built-by and worker-of edge with both ends in the plan has the module // depended on in an earlier tier; // - no cycle is said. // // Seeded, so a failure is replayed by its seed and case. func TestAPlanIsTheTouchedModulesAndWhatIsReachableAlongTheWideningEdges(t *testing.T) { kinds := []string{inventory.EdgeStandsOn, inventory.EdgeDeclared, inventory.EdgePackages, inventory.EdgeBuiltBy, inventory.EdgeWorkerOf} widens := map[string]bool{inventory.EdgeStandsOn: true, inventory.EdgeDeclared: true} // Built-by orders nothing since novox/hq ADR 0360 §3. orders := map[string]bool{inventory.EdgeStandsOn: true, inventory.EdgeDeclared: true, inventory.EdgeWorkerOf: true} // Directory names drawn from one pool, so two repositories hold directories of the same name, and one // is a prefix of another. dirs := []string{"a", "ab", "b", "c", "lib/x", "lib/y", "modules/a", "modules/a/sub"} files := []string{"README.md", "merge-check.sh", "lib/z.go", "docs/x.md", "modules/README.md"} falseCycles, firstFalseCycle := 0, "" for _, seed := range []uint64{1, 2, 3, 0x338, 0x162} { rng := rand.New(rand.NewPCG(seed, seed^0x9e3779b97f4a7c15)) for n := 0; n < 100; n++ { repos := 1 + rng.IntN(3) repoName := func(i int) string { return fmt.Sprintf("r%d", i) } count := 1 + rng.IntN(12) var entries []inventory.Entry repoOf, dirOf := map[string]int{}, map[string]string{} for i := 0; i < count; i++ { name := fmt.Sprintf("m%02d", i) repo := rng.IntN(repos) dir := dirs[rng.IntN(len(dirs))] if rng.IntN(12) == 0 { dir = "" // built from the repository's root } repoOf[name], dirOf[name] = repo, dir entries = append(entries, fromRepo(name, "http://forge.internal:20000/novox/"+repoName(repo)+".git", dir)) } // A graph with no cycle: a module depends only on modules made before it. var edges []inventory.Edge for i := 1; i < count; i++ { for j := 0; j < i; j++ { if rng.IntN(4) == 0 { edges = append(edges, dep(fmt.Sprintf("m%02d", i), kinds[rng.IntN(len(kinds))], fmt.Sprintf("m%02d", j))) } } } merged := rng.IntN(repos) var paths []string for k := 1 + rng.IntN(4); k > 0; k-- { if rng.IntN(3) == 0 { paths = append(paths, files[rng.IntN(len(files))]) } else { paths = append(paths, dirs[rng.IntN(len(dirs))]+"/f.go") } } // What the rules say. want := map[string]bool{} for _, e := range entries { name := e.Manifest.Module if repoOf[name] != merged { continue } for _, p := range paths { if d := dirOf[name]; d == "" || p == d || strings.HasPrefix(p, d+"/") { want[name] = true } } } for grew := true; grew; { grew = false for _, e := range edges { if widens[e.Kind] && want[e.To] && !want[e.From] { want[e.From], grew = true, true } } } r, _ := planMerge(t, repoName(merged), paths, entries, nil, edges) got := map[string]bool{} tierOf := map[string]int{} for i, tier := range r.Plan.Tiers { for _, name := range tier { got[name], tierOf[name] = true, i } } replay := func() string { return fmt.Sprintf("seed %#x case %d: repository %s, files %v\n modules %v\n edges %v\n tiers %v", seed, n, repoName(merged), paths, describe(entries), edges, r.Plan.Tiers) } if !sameSet(got, want) { t.Fatalf("planned %v, wanted %v\n%s", keys(got), keys(want), replay()) } for _, e := range edges { if orders[e.Kind] && got[e.From] && got[e.To] && tierOf[e.To] >= tierOf[e.From] { t.Fatalf("%s %s %s, and %s is in tier %d, not before %s's %d\n%s", e.From, e.Kind, e.To, e.To, tierOf[e.To], e.From, tierOf[e.From], replay()) } } if hasCycle(r.Plan.Tiers, edges) { falseCycles++ if firstFalseCycle == "" { firstFalseCycle = replay() } } } } // Said once, after the other invariants have run over every case, so it hides none of them (hasCycle // counted a packages edge on main at 8170fc5: 19 of these 500 cases). if falseCycles > 0 { t.Errorf("a cycle said of a graph with none in %d of 500 cases; "+ "the first:\n%s", falseCycles, firstFalseCycle) } } func sameSet(a, b map[string]bool) bool { if len(a) != len(b) { return false } for k := range a { if !b[k] { return false } } return true } func keys(m map[string]bool) []string { out := make([]string, 0, len(m)) for k := range m { out = append(out, k) } sort.Strings(out) return out } func describe(entries []inventory.Entry) []string { var out []string for _, e := range entries { out = append(out, fmt.Sprintf("%s@%s:%q", e.Manifest.Module, strings.TrimSuffix(strings.TrimPrefix(e.Source.Repository, "http://forge.internal:20000/novox/"), ".git"), e.Source.Path)) } return out } // **The merge gate reads the build sources the snapshot carries** (novox/hq ADR 0267): the gate's plan of a // change is the merge handler's, so a snapshot taken by a controller that records build sources narrows the // gate's plan as it narrows the merge's; one without them reads every module as before. func TestTheGatePlansFromTheBuildSourcesTheSnapshotCarries(t *testing.T) { manifest := func(name string) json.RawMessage { return json.RawMessage(`{"module":"` + name + `","version":"1"}`) } facts := snapshot.Facts{Modules: []snapshot.Module{ {Name: "mesh-controller", Repository: "novox/mesh-controller", Manifest: manifest("mesh-controller"), Sources: []snapshot.BuildSource{{Own: true, Paths: []string{"module.json", "cmd/mesh-controller/", "internal/broker/"}}}}, {Name: "route-proxy", Repository: "novox/mesh-catalog", Path: "modules/route-proxy", Manifest: manifest("route-proxy"), Reads: []string{"novox/mesh-controller"}, Sources: []snapshot.BuildSource{{Repository: "novox/mesh-controller", Paths: []string{"examples/route-proxy/", "internal/broker/"}}, {Own: true, Paths: []string{"modules/route-proxy/module.json"}}}}, }} for paths, want := range map[string]string{ "cmd/mesh-controller/main.go": "mesh-controller", "examples/route-proxy/main.go": "route-proxy", "internal/broker/broker.go": "mesh-controller,route-proxy", "README.md": "", } { r, err := reachOfChange(facts, "novox/mesh-controller", []string{paths}, "") if err != nil { t.Fatal(err) } if got := tiered(r.Plan.Tiers); got != want { t.Errorf("%s: the gate planned %q, wanted %q", paths, got, want) } } // A snapshot without build sources: as before. for i := range facts.Modules { facts.Modules[i].Sources = nil } r, err := reachOfChange(facts, "novox/mesh-controller", []string{"README.md"}, "") if err != nil { t.Fatal(err) } if got := tiered(r.Plan.Tiers); got != "mesh-controller,route-proxy" { t.Errorf("a snapshot without build sources: the gate planned %q for a README", got) } } // **A plan says why each module is in it** (novox/hq ADR 0267, issue 363): the files of its build source the // merge changed, or why it is read whole — never that it packages a repository as though that moved it. func TestAPlanSaysWhyEachModuleIsInIt(t *testing.T) { const controllerRepo = "http://forge.internal:20000/novox/mesh-controller.git" controller := fromRepo("mesh-controller", controllerRepo, "") agent := fromRepo("build-agent", "http://forge.internal:20000/novox/mesh-catalog.git", "modules/build-agent") gitea := fromRepo("gitea", "http://forge.internal:20000/novox/mesh-catalog.git", "modules/gitea") built := time.Date(2026, 10, 10, 12, 26, 0, 0, time.UTC) read := map[string][]inventory.ReadRepository{ "mesh-controller": {{Own: true, Paths: []string{"module.json", "cmd/mesh-controller/", "internal/link/"}, Built: built}}, "build-agent": { {Repository: "novox/mesh-controller", Ref: "main", Paths: []string{"cmd/mesh-builder/", "internal/link/"}, Built: built}, {Own: true, Paths: []string{"modules/build-agent/module.json"}, Built: built}, }, } merge := func(repo string, paths ...string) link.SourceMoved { return link.SourceMoved{Owner: "novox", Repo: repo, Base: "main", Paths: paths} } open := []inventory.Plan{{ID: "plan-1", State: inventory.PlanBuilding, Created: built.Add(time.Minute), Modules: map[string]*inventory.PlanModule{"build-agent": {State: "asked"}}}} for _, c := range []struct { what string e inventory.Entry read map[string][]inventory.ReadRepository m link.SourceMoved says string }{ {"the controller's own closure", controller, read, merge("mesh-controller", "README.md", "internal/link/handacts.go"), "its build source changed: 1 changed file(s) in it, e.g. internal/link/handacts.go"}, {"the build seat's closure, through its context", agent, read, merge("mesh-controller", "internal/link/handacts.go"), "its build source in novox/mesh-controller changed: 1 changed file(s) in it, e.g. internal/link/handacts.go"}, {"an open plan has yet to build it", agent, planningView(read, open), merge("mesh-controller", "cmd/mesh-controller/main.go"), "read whole: plan plan-1 has not built it yet, so every file of novox/mesh-controller, which its build context is, is its build source"}, {"nothing recorded, built from its root", controller, nil, merge("mesh-controller", "README.md"), "read whole: no build source recorded, so every file of its repository is its build source"}, {"nothing recorded, in its directory", gitea, nil, merge("mesh-catalog", "modules/gitea/index.ts"), "read whole: no build source recorded, so its directory is its build source; e.g. modules/gitea/index.ts"}, {"a root manifest is not in a module's directory", gitea, nil, merge("mesh-catalog", "module.json", "modules/gitea/x.ts"), "read whole: no build source recorded, so its directory is its build source; e.g. modules/gitea/x.ts"}, {"a context on another branch says nothing of this one", agent, map[string][]inventory.ReadRepository{"build-agent": { {Repository: "novox/mesh-controller", Ref: "release", Paths: []string{"internal/link/"}}, {Repository: "novox/mesh-controller", Ref: "main"}}}, merge("mesh-controller", "internal/link/handacts.go"), "read whole: no build source recorded, so every file of novox/mesh-controller, which its build context is, is its build source"}, {"files not all said", controller, read, link.SourceMoved{Owner: "novox", Repo: "mesh-controller", PathsTruncated: true, Paths: []string{"x"}}, "read whole: the merge's changed files were not all said"}, } { if got := whyMoved(c.e, c.read[c.e.Manifest.Module], c.m); got != c.says { t.Errorf("%s:\n said %q\n wanted %q", c.what, got, c.says) } } } // **A context naming the module's own repository is the module's own build source** (novox/hq ADR 0420 §4, // issue 1025). A module registered from the controller's repository whose image compiles cmd/x through a // context naming that same repository is moved by a change to the program, and by none outside its closure. func TestAContextNamingTheModulesOwnRepositoryIsItsOwnBuildSource(t *testing.T) { const controllerRepo = "http://forge.internal:20000/novox/mesh-controller.git" entries := []inventory.Entry{fromRepo("mover", controllerRepo, "modules/mover")} said := []inventory.ReadRepository{ {Repository: "novox/mesh-controller", Ref: "main", Paths: []string{"go.mod", "cmd/x/", "internal/y/"}}, {Own: true, Paths: []string{"modules/mover/module.json"}}, } read := map[string][]inventory.ReadRepository{"mover": inventory.OwnRepositoryAsOwn(said, controllerRepo)} for _, c := range []struct { what string paths []string want string }{ {"the program its image compiles", []string{"cmd/x/main.go"}, "mover"}, {"a package the program imports", []string{"internal/y/y.go"}, "mover"}, {"its own manifest", []string{"modules/mover/module.json"}, "mover"}, {"a file outside its closure", []string{"cmd/other/main.go"}, ""}, } { if _, got := planMerge(t, "mesh-controller", c.paths, entries, read, nil); got != c.want { t.Errorf("%s: planned %q, wanted %q", c.what, got, c.want) } } // A context of another repository stays a context read, and a context of this one that said no paths too. other := []inventory.ReadRepository{{Repository: "novox/mesh-catalog", Paths: []string{"a/"}}, {Repository: "novox/mesh-controller"}} if got := inventory.OwnRepositoryAsOwn(other, controllerRepo); len(got) != 2 || got[0].Own || got[1].Own { t.Errorf("a context that is not this repository's with paths became its own: %+v", got) } }