package main import ( "strings" "testing" "time" "github.com/novox/mesh-controller/internal/catalogue" "github.com/novox/mesh-controller/internal/inventory" "github.com/novox/mesh-controller/internal/link" ) // novox/hq issue 349: on 2026-10-09 the plan of mesh-catalog at a082615b (the merge of a security fix to the // forge's module) was "superseded at tier 0 by" the plan at 8ff8197a, the merge before it, which the // catch-up acted on late; what the later plan had not built was folded into a plan at the commit before the // fix. Plans of one branch are ordered by when the forge made their merges, and a merge older than an open // plan of its branch is planned at that plan's commit. // TestTwoMergesActedOnInReverseOrderBuildTheNewerCommit replays it: the later merge acted on first, the // earlier one second (the catch-up). One plan is left open, at the later commit, and it builds both. func TestTwoMergesActedOnInReverseOrderBuildTheNewerCommit(t *testing.T) { open := aCatalogueMesh(t) ctx := t.Context() asksWithPaths(t) for _, m := range []string{"gitea", "notes"} { if err := open.inventory.RegisterModule(ctx, catalogue.Manifest{Module: m, Version: "1"}, inventory.Source{Repository: "novox/mesh-catalog", Seat: "git", Path: "modules/" + m, Ref: "main", BuiltFrom: "c0", Head: "c0"}); err != nil { t.Fatal(err) } } at := time.Now().UTC().Add(-20 * time.Minute).Truncate(time.Second) fix := link.SourceMoved{Owner: "novox", Repo: "mesh-catalog", Base: "main", Commit: "a082615bfix", MergedAt: at.Add(2 * time.Minute).Format(time.RFC3339Nano), Paths: []string{"modules/gitea/module.json"}, ModuleDirs: []string{"modules/gitea"}, ModuleDirsSaid: true} before := link.SourceMoved{Owner: "novox", Repo: "mesh-catalog", Base: "main", Commit: "8ff8197abefore", MergedAt: at.Format(time.RFC3339Nano), Paths: []string{"modules/notes/module.json"}, ModuleDirs: []string{"modules/notes"}, ModuleDirsSaid: true} for _, m := range []link.SourceMoved{fix, before} { if err := (following{open: open}).SourceMoved(ctx, m); err != nil { t.Fatal(err) } } plans, err := open.inventory.OpenPlans(ctx) if err != nil { t.Fatal(err) } if len(plans) != 1 { var said []string for _, p := range plans { said = append(said, p.ID+" "+p.Commit+" "+p.Note) } t.Fatalf("open plans: %s", strings.Join(said, "; ")) } p := plans[0] if p.Commit != fix.Commit { t.Fatalf("the open plan builds %s, not the newer commit %s", p.Commit, fix.Commit) } for _, m := range []string{"gitea", "notes"} { if _, has := p.Modules[m]; !has { t.Fatalf("the open plan at the newer commit does not build %s: %v", m, p.Modules) } } } // A late older merge after the newer plan is done (review of PR 179, A1): what it moved is built from the // newer commit, and what the newer merge already looked at is not built again at the older one. func TestALateMergeAfterTheNewerPlanEndedBuildsTheNewerCommit(t *testing.T) { open := aCatalogueMesh(t) ctx := t.Context() asksWithPaths(t) for _, m := range []string{"gitea", "notes"} { if err := open.inventory.RegisterModule(ctx, catalogue.Manifest{Module: m, Version: "1"}, inventory.Source{Repository: "novox/mesh-catalog", Seat: "git", Path: "modules/" + m, Ref: "main", BuiltFrom: "c0", Head: "c0"}); err != nil { t.Fatal(err) } } at := time.Now().UTC().Add(-20 * time.Minute).Truncate(time.Second) fix := link.SourceMoved{Owner: "novox", Repo: "mesh-catalog", Base: "main", Commit: "a082615bfix", MergedAt: at.Add(2*time.Minute + 500*time.Millisecond).Format(time.RFC3339Nano), Paths: []string{"modules/gitea/module.json"}, ModuleDirs: []string{"modules/gitea"}, ModuleDirsSaid: true} if err := (following{open: open}).SourceMoved(ctx, fix); err != nil { t.Fatal(err) } plans, err := open.inventory.OpenPlans(ctx) if err != nil || len(plans) != 1 { t.Fatalf("%v %v", plans, err) } done := plans[0] done.State = inventory.PlanDone if err := open.inventory.SavePlan(ctx, &done); err != nil { t.Fatal(err) } if got, err := open.inventory.PlanByID(ctx, done.ID); err != nil || !got.Merged.Equal(at.Add(2*time.Minute+500*time.Millisecond)) { t.Fatalf("the merge time kept is %s, not to the nanosecond (%v)", got.Merged, err) } before := link.SourceMoved{Owner: "novox", Repo: "mesh-catalog", Base: "main", Commit: "8ff8197abefore", MergedAt: at.Format(time.RFC3339Nano), Paths: []string{"modules/notes/module.json", "modules/gitea/module.json"}, ModuleDirs: []string{"modules/notes", "modules/gitea"}, ModuleDirsSaid: true} if err := (following{open: open}).SourceMoved(ctx, before); err != nil { t.Fatal(err) } plans, err = open.inventory.OpenPlans(ctx) if err != nil || len(plans) != 1 { t.Fatalf("open plans after the late merge: %+v %v", plans, err) } p := plans[0] if p.Commit != fix.Commit { t.Fatalf("the late merge was planned at %s, not the newer commit %s", p.Commit, fix.Commit) } if _, has := p.Modules["notes"]; !has { t.Fatalf("what only the late merge moved is not built: %v", p.Modules) } if _, has := p.Modules["gitea"]; has { t.Fatalf("what the newer merge already built is built again: %v", p.Modules) } } // Pure: the branch's order is the merges', where both plans know it; and a later merge of the branch is // found in any state, never a release's, another repository's or another branch's. func TestTheBranchOrderIsTheMerges(t *testing.T) { t0 := time.Date(2026, 10, 9, 10, 0, 0, 0, time.UTC) newerMerge := inventory.Plan{ID: "plan-1", Repository: "novox/mesh-catalog", Branch: "main", Commit: "a082615b", Merged: t0.Add(time.Minute), Created: t0.Add(2 * time.Minute), State: inventory.PlanRolling} olderMerge := inventory.Plan{ID: "plan-2", Repository: "novox/mesh-catalog", Branch: "main", Commit: "8ff8197a", Merged: t0, Created: t0.Add(10 * time.Minute), State: inventory.PlanBuilding} if earlierOnTheBranch(newerMerge, olderMerge) || !earlierOnTheBranch(olderMerge, newerMerge) { t.Fatal("ordered by when the plans were made, not by when the merges were") } if _, closed := supersededBy(olderMerge, []inventory.Plan{newerMerge}, func(string) bool { return true }); len(closed) != 0 { t.Fatalf("the plan of an older merge superseded a newer one: %s", closed[0].Note) } unknown := newerMerge unknown.Merged = time.Time{} if !earlierOnTheBranch(unknown, olderMerge) { t.Fatal("without a merge time the plans' own order does not stand") } same := olderMerge same.Merged = newerMerge.Merged if !earlierOnTheBranch(newerMerge, same) || earlierOnTheBranch(same, newerMerge) { t.Fatal("one merge time: the plans' own order does not stand") } m := link.SourceMoved{Owner: "novox", Repo: "mesh-catalog", Base: "main", Commit: "8ff8197a", MergedAt: t0.Add(500 * time.Millisecond).Format(time.RFC3339Nano)} newerMerge.State = inventory.PlanDone if !laterOnTheBranch(m, newerMerge) { t.Fatal("a later merge of the branch, its plan done, was not found") } for name, change := range map[string]func(p *inventory.Plan, m *link.SourceMoved){ "another branch": func(_ *inventory.Plan, m *link.SourceMoved) { m.Base = "release" }, "another repository": func(p *inventory.Plan, _ *link.SourceMoved) { p.Repository = "novox/mesh-controller" }, "a release": func(p *inventory.Plan, _ *link.SourceMoved) { p.Release = &inventory.PlanRelease{} }, "no merge time": func(p *inventory.Plan, _ *link.SourceMoved) { p.Merged = time.Time{} }, "the same commit": func(p *inventory.Plan, m *link.SourceMoved) { m.Commit = p.Commit }, "an older merge": func(p *inventory.Plan, _ *link.SourceMoved) { p.Merged = t0 }, "the same moment": func(p *inventory.Plan, _ *link.SourceMoved) { p.Merged = t0.Add(500 * time.Millisecond) }, "an unreadable time": func(_ *inventory.Plan, m *link.SourceMoved) { m.MergedAt = "yesterday" }, } { p, mm := newerMerge, m change(&p, &mm) if laterOnTheBranch(mm, p) { t.Errorf("%s was taken as a later merge of the branch", name) } } // To the nanosecond: two merges within a second keep their order. m.MergedAt = t0.Add(time.Minute + 200*time.Millisecond).Format(time.RFC3339Nano) if !laterOnTheBranch(m, inventory.Plan{Repository: "novox/mesh-catalog", Branch: "main", Commit: "x", Merged: t0.Add(time.Minute + 700*time.Millisecond)}) { t.Fatal("merges within one second lost their order") } } // A merge time with a fraction of a second is kept whole in its plan, and two merges within one second keep // their order through the plans and the lookup (review of PR 179). func TestAMergeTimeKeepsItsFractionOfASecond(t *testing.T) { at := "2026-10-09T10:57:52.123456789Z" p := planOfMerge(link.SourceMoved{Owner: "novox", Repo: "mesh-catalog", Base: "main", Commit: "c1", MergedAt: at}, nil, nil) want := time.Date(2026, 10, 9, 10, 57, 52, 123456789, time.UTC) if !p.Merged.Equal(want) { t.Fatalf("the plan's merge time is %s, not %s", p.Merged, want) } earlier := planOfMerge(link.SourceMoved{Owner: "novox", Repo: "mesh-catalog", Base: "main", Commit: "c0", MergedAt: "2026-10-09T10:57:52.123456788Z"}, nil, nil) earlier.Created = p.Created.Add(time.Second) // made after, merged before if !earlierOnTheBranch(earlier, p) || earlierOnTheBranch(p, earlier) { t.Fatal("two merges a nanosecond apart lost their order") } m := link.SourceMoved{Owner: "novox", Repo: "mesh-catalog", Base: "main", Commit: "c0", MergedAt: "2026-10-09T10:57:52.123456788Z"} if !laterOnTheBranch(m, p) { t.Fatal("a merge a nanosecond later was not found as the later one") } }