//go:build integration package main import ( "context" "strings" "testing" "time" "github.com/novox/mesh-controller/internal/catalogue" "github.com/novox/mesh-controller/internal/inventory" ) // Acceptance 1 of design 47, the controller's part: a feature across the controller and the catalogue with a new seat // verb is delivered as one group, with no `after:` line — its merges are one batch, each module is built once and sent // once, its commits are carried by that one plan, and the plan names the edge that placed each tier. The Approve and // the one-pass merge are mesh-delivery's. // manifestsHeld stubs the forge for a test: the manifest each (repository, path, commit) holds, and none anywhere else. func manifestsHeld(t *testing.T, held map[[3]string]catalogue.Manifest) { t.Helper() was, wasParent := manifestAt, parentOf t.Cleanup(func() { manifestAt, parentOf = was, wasParent }) manifestsReadMu.Lock() clear(manifestsRead) manifestsReadMu.Unlock() parentOf = func(context.Context, string, string, string) (string, error) { return "", nil } manifestAt = func(_ context.Context, _, repo, file, ref string) (catalogue.Manifest, bool, error) { m, ok := held[[3]string{repo, file, ref}] return m, ok, nil } } func TestAFeatureWithANewSeatVerbIsOneBatchAndOnePlanInOrderWithNoAfterLine(t *testing.T) { for name, controllerFirst := range map[string]bool{"controller_first": true, "catalogue_first": false} { t.Run(name, func(t *testing.T) { open := windowed(t) asked := asksWithPaths(t) ctx := t.Context() const head = "feat/one-group-one-plan" // mesh-delivery holds its seat, so a catalogue plan would wait for its word; this plan carries the controller. if err := open.inventory.RegisterModule(ctx, catalogue.Manifest{Module: "mesh-delivery", Version: "1", Claims: []catalogue.Claim{{Name: catalogue.DeliverySeat, Scope: catalogue.ScopeMesh}}}, inventory.Source{Repository: "novox/mesh-catalog", Seat: "git", Path: "modules/mesh-delivery", Ref: "main", BuiltFrom: "c0", Head: "c0"}); err != nil { t.Fatal(err) } if _, err := open.inventory.Assign(ctx, "anchor", "mesh-delivery"); err != nil { t.Fatal(err) } controller := fromHead(repoMerge("mesh-controller", "k1", t0), head) controller.Paths = []string{"module.json", "cmd/mesh-controller/main.go"} catalog := fromHead(catalogueMerge("cat1aaaaaaaa", "app", t0), head) manifestsHeld(t, map[[3]string]catalogue.Manifest{ {"mesh-controller", "module.json", "k1"}: {Module: "mesh-controller", DefinesSeats: []catalogue.SeatDeclaration{{Name: "seat", Serves: []catalogue.Verb{{Name: "verb"}}}}}, {"mesh-catalog", "modules/app/module.json", "cat1aaaaaaaa"}: {Module: "app", Invokes: []string{"seat:seat.verb"}}, }) states := map[string]string{"novox/mesh-controller": "ready", "novox/mesh-catalog": "ready"} groupStates(t, head, states) first, second := controller, catalog if !controllerFirst { first, second = catalog, controller } // The first member merges; the group waits for the second, however long it takes. hear(t, open, first, t0) cutAt(t, open, t0.Add(30*time.Minute)) if ws, bs := walks(t, open); len(ws) != 0 || len(bs) != 1 { t.Fatalf("a member of a group alone: %d walk(s), %d batch(es)", len(ws), len(bs)) } states["novox/"+second.Repo] = "merged" second.MergedAt = t0.Add(30 * time.Minute).Format(time.RFC3339Nano) hear(t, open, second, t0.Add(30*time.Minute)) cutAt(t, open, t0.Add(30*time.Minute+90*time.Second)) ws, bs := walks(t, open) if len(ws) != 1 || len(bs) != 0 { t.Fatalf("after the window: %d walk(s), %d batch(es); want exactly one plan", len(ws), len(bs)) } plan := ws[0] if plan.CommitOf("novox/mesh-controller") != "k1" || plan.CommitOf("novox/mesh-catalog") != "cat1aaaaaaaa" { t.Fatalf("the plan carries %+v", plan.Commits) } if len(plan.Tiers) != 2 || strings.Join(plan.Tiers[0], ",") != "mesh-controller" || strings.Join(plan.Tiers[1], ",") != "app" { t.Fatalf("the plan's tiers are %v; want the controller, then the caller of its verb", plan.Tiers) } if placed := plan.Modules["app"].Placed; !strings.Contains(placed, "verb before caller: seat.verb") || !strings.Contains(placed, "after mesh-controller") { t.Fatalf("the plan does not name the edge that placed the caller: %q", placed) } if plan.Modules["mesh-controller"].Placed != "" { t.Fatalf("the first tier is placed by %q", plan.Modules["mesh-controller"].Placed) } if plan.Delivery != nil && (plan.Delivery.Awaits != "" || plan.Waiting()) { t.Fatalf("a plan carrying the controller waits for %q's word", plan.Delivery.Awaits) } if plan.Modules["mesh-controller"].Build == "" || len(*asked) != 1 { t.Fatalf("the first tier is asked once: build %q, %d ask(s) %v", plan.Modules["mesh-controller"].Build, len(*asked), *asked) } // Every merge is answered by that one plan. for _, repository := range []string{"novox/mesh-controller", "novox/mesh-catalog"} { merges, err := open.inventory.MergesOf(ctx, plan.ID) if err != nil || len(merges) != 2 { t.Fatalf("the plan answers %d merge(s) of %s: %v", len(merges), repository, err) } } }) } } // Issue 1049: a move delivered half before its second merge. The controller adds a module the catalogue removes, in one // group: the merges are one batch and one plan in either order, the module is built once and from the controller's // repository, and the catalogue's removal does not forget it. func TestAMoveOfAModuleAcrossTwoRepositoriesIsOnePlanAndBuildsItOnceFromTheNewOne(t *testing.T) { for name, controllerFirst := range map[string]bool{"controller_first": true, "catalogue_first": false} { t.Run(name, func(t *testing.T) { open := windowed(t) asked := asksWithPaths(t) ctx := t.Context() const head = "feat/whole-modules" // The module runs on a machine from the catalogue's repository, so its removal there cannot forget it. if err := open.inventory.RegisterModule(ctx, catalogue.Manifest{Module: "route-proxy", Version: "1"}, inventory.Source{Repository: "novox/mesh-catalog", Seat: "git", Path: "modules/route-proxy", Ref: "main", BuiltFrom: "c0", Head: "c0"}); err != nil { t.Fatal(err) } if _, err := open.inventory.Assign(ctx, "anchor", "route-proxy"); err != nil { t.Fatal(err) } manifestsHeld(t, nil) controller := fromHead(repoMerge("mesh-controller", "k1", t0), head) controller.Paths = []string{"modules/route-proxy/module.json", "modules/route-proxy/main.go"} controller.ModuleDirs, controller.ModuleDirsSaid = []string{"modules/route-proxy"}, true catalog := fromHead(catalogueMerge("cat1aaaaaaaa", "route-proxy", t0), head) catalog.Removed = []string{"modules/route-proxy/module.json"} catalog.ModuleDirs, catalog.ModuleDirsSaid = nil, true states := map[string]string{"novox/mesh-controller": "ready", "novox/mesh-catalog": "ready"} groupStates(t, head, states) first, second := controller, catalog if !controllerFirst { first, second = catalog, controller } hear(t, open, first, t0) states["novox/"+second.Repo] = "merged" second.MergedAt = t0.Add(20 * time.Second).Format(time.RFC3339Nano) hear(t, open, second, t0.Add(20*time.Second)) cutAt(t, open, t0.Add(20*time.Second+90*time.Second)) ws, bs := walks(t, open) if len(ws) != 1 || len(bs) != 0 { t.Fatalf("a move: %d walk(s), %d batch(es); want exactly one plan", len(ws), len(bs)) } var fromController int for _, a := range *asked { if strings.Contains(a[0], "mesh-controller") && a[1] == "modules/route-proxy" { fromController++ } if strings.Contains(a[0], "mesh-catalog") && a[1] == "modules/route-proxy" { t.Errorf("route-proxy was built from the repository it left: %v", a) } } if fromController != 1 { t.Fatalf("route-proxy was built %d time(s) from the controller's repository: %v", fromController, *asked) } entries, err := open.inventory.Catalogued(ctx) if err != nil { t.Fatal(err) } kept := false for _, e := range entries { kept = kept || e.Manifest.Module == "route-proxy" } if !kept { t.Fatal("the catalogue's removal forgot route-proxy, which a machine runs") } }) } }