diff --git a/cmd/mesh-controller/replay349_test.go b/cmd/mesh-controller/replay349_test.go new file mode 100644 index 00000000..69e9e4db --- /dev/null +++ b/cmd/mesh-controller/replay349_test.go @@ -0,0 +1,67 @@ +package main + +import ( + "testing" + "time" + + "github.com/novox/mesh-controller/internal/catalogue" + "github.com/novox/mesh-controller/internal/inventory" + "github.com/novox/mesh-controller/internal/link" +) + +// Replay of novox/hq issue 349 (2026-10-09): two merges into the catalogue's main were acted on in reverse order. +// The later one (a082615b, a security fix to the forge's module) was planned first; the earlier one (8ff8197a, +// another module), handed over late, was planned after it, and its plan superseded the later merge's at tier 0 +// with "gitea planned there again": the forge's module was to be built for the commit without the fix. Written +// only with what the controller had before its fix (mesh-controller #179): a mesh, two registered modules of the +// catalogue, the merges as the forge announces them, the plans the inventory keeps — so it is laid over the +// commit before. +func TestReplay349(t *testing.T) { + open := aMesh(t) + ctx := t.Context() + inv := open.inventory + asksWithPaths(t) + for _, module := range []string{"app", "notes"} { + if err := inv.RegisterModule(ctx, catalogue.Manifest{Module: module, Version: "1"}, + inventory.Source{Repository: "novox/mesh-catalog", Seat: "git", Path: "modules/" + module, Ref: "main", + BuiltFrom: "c0", Head: "c0"}); err != nil { + t.Fatal(err) + } + } + // Both made after the modules were last looked at, so neither is history. + made := time.Now().UTC().Add(time.Minute) + merge := func(commit, module string, at time.Time) link.SourceMoved { + return link.SourceMoved{Owner: "novox", Repo: "mesh-catalog", Base: "main", Commit: commit, + MergedAt: at.Format(time.RFC3339Nano), Paths: []string{"modules/" + module + "/module.json"}, + ModuleDirs: []string{"modules/" + module}, ModuleDirsSaid: true} + } + const fix, earlier = "a082615bfix", "8ff8197aearlier" + // The later merge, the fix to notes, is acted on first; the earlier one, moving app, after it. + if err := (following{open: open}).SourceMoved(ctx, merge(fix, "notes", made.Add(time.Minute))); err != nil { + t.Fatal(err) + } + if err := (following{open: open}).SourceMoved(ctx, merge(earlier, "app", made)); err != nil { + t.Fatal(err) + } + + plans, err := inv.RecentPlans(ctx, 20) + if err != nil { + t.Fatal(err) + } + // A plan superseded hands its modules to the plan that took it over; the plans left standing are what the mesh + // builds. One of them builds the fixed module, and every one that does builds it at the commit holding the fix. + built := 0 + for _, p := range plans { + t.Logf("%s %s at %.8s: %v %s", p.ID, p.State, p.Commit, p.Tiers, p.Note) + if _, holds := p.Modules["notes"]; !holds || p.State == inventory.PlanSuperseded { + continue + } + built++ + if p.Commit != fix { + t.Errorf("%s plans the fixed module notes at %s, a commit without the fix %s: %s", p.ID, p.Commit, fix, p.Note) + } + } + if built == 0 { + t.Error("no plan left standing builds notes, the module the later merge fixed") + } +} diff --git a/testdata/store-tests b/testdata/store-tests index 1a2b6c6c..2c024822 100644 --- a/testdata/store-tests +++ b/testdata/store-tests @@ -41,3 +41,4 @@ ./cmd/mesh-controller TestPlanAndNodeListAnOptionalRequirementLeftUnbound ./cmd/mesh-controller TestReplay340 ./cmd/mesh-controller TestOnlyATrustedFilesNamedKeyIsRefusedThroughAVerb +./cmd/mesh-controller TestReplay349