//go:build integration package main import ( "encoding/json" "strings" "testing" "time" "github.com/nats-io/nats.go" "github.com/novox/mesh-controller/internal/broker" "github.com/novox/mesh-controller/internal/catalogue" "github.com/novox/mesh-controller/internal/inventory" "github.com/novox/mesh-controller/internal/link" "github.com/novox/mesh-controller/internal/testbus" ) func TestAMergeWaitsForItsDeliverysWordAndThePersonsWordWorksWithoutIt(t *testing.T) { open := aMesh(t) ctx := t.Context() inv := open.inventory asked := asksRecorded(t) withConditionsInMemory(t) for _, name := range []string{"app", "mesh-delivery"} { m := catalogue.Manifest{Module: name, Version: "1"} if name == "mesh-delivery" { m.Claims = []catalogue.Claim{{Name: catalogue.DeliverySeat, Scope: catalogue.ScopeMesh}} } if err := inv.RegisterModule(ctx, m, inventory.Source{Repository: "novox/mesh-catalog", Seat: "git", Path: "modules/" + name, Ref: "main", BuiltFrom: "c0", Head: "c0"}); err != nil { t.Fatal(err) } } merge := func(commit string, paths ...string) inventory.Plan { t.Helper() m := link.SourceMoved{Owner: "novox", Repo: "mesh-catalog", Base: "main", Commit: commit, Paths: paths, ModuleDirs: []string{"modules/app", "modules/mesh-delivery"}, ModuleDirsSaid: true} if err := (following{open: open}).SourceMoved(ctx, m); err != nil { t.Fatal(err) } recent, err := inv.RecentPlans(ctx, 1) if err != nil || len(recent) != 1 || recent[0].Commit != commit { t.Fatalf("no plan for %s: %v %v", commit, recent, err) } return recent[0] } // finish ends a walk as done, so the next merge has nothing of it to take over. finish := func(id string) { t.Helper() w, err := inv.PlanByID(ctx, id) if err != nil { t.Fatal(err) } w.State = inventory.PlanDone if err := inv.SavePlan(ctx, &w); err != nil { t.Fatal(err) } } // No holder on record: the merge starts its walk, as before. p := merge("c1aaaaaaaa", "modules/app/index.ts") if p.Waiting() || len(*asked) != 1 { t.Fatalf("with no holder on record the walk waited (%v) or asked %v", p.Waiting(), *asked) } // The holder on record: the next merge waits, asking nothing, and an advance asks nothing either. One walk // is open at a time (novox/hq ADR 0276): the first ends before the next merge's batch is cut. finish(p.ID) if _, err := inv.Assign(ctx, "anchor", "mesh-delivery"); err != nil { t.Fatal(err) } p = merge("c2bbbbbbbb", "modules/app/index.ts") if !p.Waiting() || len(*asked) != 1 || !strings.Contains(p.Note, "plans go "+p.ID) { t.Fatalf("the walk did not wait for its word: waiting %v, asked %v, note %q", p.Waiting(), *asked, p.Note) } advanceHeld(ctx, open) if len(*asked) != 1 { t.Fatalf("a waiting walk was advanced into asking: %v", *asked) } if line := planLine(p, p.Created); strings.Contains(line, "LATE") || !strings.Contains(line, "waits for") { t.Errorf("a waiting walk reads %q", line) } // The delivery's word starts it. if err := deliveryCommand(ctx, []string{"go", p.ID, "--by", catalogue.DeliverySeat, "--why", "its turn"}); err != nil { t.Fatal(err) } if err := deliveryCommand(ctx, []string{"go", p.ID}); err == nil { t.Fatal("a walk was let go twice") } advanceHeld(ctx, open) if len(*asked) != 2 { t.Fatalf("the word did not start the walk: %v", *asked) } got, err := inv.PlanByID(ctx, p.ID) if err != nil || got.Delivery == nil || got.Delivery.By != catalogue.DeliverySeat || got.Delivery.Why != "its turn" { t.Fatalf("the word was not kept: %+v %v", got.Delivery, err) } // The delivery's owner's own merge never waits for it. A walk that started is never taken over (novox/hq // ADR 0276): the merge's batch is cut once it ended. finish(p.ID) p = merge("c3cccccccc", "modules/mesh-delivery/main.go") if p.Waiting() || len(*asked) != 3 { t.Fatalf("mesh-delivery's own walk waited for mesh-delivery: %v %v", p.Waiting(), *asked) } // The owner down: a person starts a waiting walk, with why. finish(p.ID) p = merge("c4dddddddd", "modules/app/index.ts") if !p.Waiting() { t.Fatal("the walk did not wait") } if err := plansCommand(ctx, []string{"go", p.ID}); err == nil || !strings.Contains(err.Error(), "--why") { t.Fatalf("a walk was started by hand without why: %v", err) } if err := plansCommand(ctx, []string{"go", p.ID, "--why", "mesh-delivery is down"}); err != nil { t.Fatal(err) } advanceHeld(ctx, open) got, _ = inv.PlanByID(ctx, p.ID) if got.Waiting() || !strings.HasPrefix(got.Delivery.By, "a person") || len(*asked) < 4 { t.Fatalf("a person's word did not start the walk: %+v, asked %v", got.Delivery, *asked) } // The owner's stop: failed, said as stopped by it. finish(p.ID) p = merge("c5eeeeeeee", "modules/app/index.ts") if err := deliveryCommand(ctx, []string{"stop", p.ID, "--why", "the operator stopped it", "--by", "mesh-delivery for jochen"}); err != nil { t.Fatal(err) } got, _ = inv.PlanByID(ctx, p.ID) if got.State != inventory.PlanFailed || got.Delivery.Stopped != "mesh-delivery for jochen" || !strings.Contains(got.Note, "the operator stopped it") { t.Fatalf("the stop was kept as %s %+v %q", got.State, got.Delivery, got.Note) } // Unassigned: the mesh is back on the controller's own path. if err := inv.Unassign(ctx, "anchor", "mesh-delivery"); err != nil { t.Fatal(err) } asks := len(*asked) if p = merge("c6ffffffff", "modules/app/index.ts"); p.Waiting() || len(*asked) != asks+1 { t.Fatalf("with the holder gone the walk waited: %v", p.Waiting()) } } func TestAWalkLetGoByAVerbIsSaidAsPlanMoved(t *testing.T) { open := aMesh(t) ctx := t.Context() url := testbus.URL(t) js, err := broker.Dial(url) if err != nil { t.Fatal(err) } t.Cleanup(js.Close) if err := broker.AssertMeshStreams(js); err != nil { t.Fatal(err) } before := handActConn handActConn = js.Conn() t.Cleanup(func() { handActConn = before }) heard := make(chan *nats.Msg, 4) sub, err := js.Conn().ChanSubscribe(link.SeatEventSubject(link.MeshControllerSeat, link.KeyPlanMoved), heard) if err != nil { t.Fatal(err) } t.Cleanup(func() { _ = sub.Unsubscribe() }) waiting := inventory.Plan{ID: "plan-waits", Repository: "novox/mesh-catalog", Branch: "main", Commit: "c7c7c7c7", Created: time.Now().UTC(), State: inventory.PlanBuilding, Tiers: [][]string{{"app"}}, Modules: map[string]*inventory.PlanModule{"app": {}}, Delivery: &inventory.PlanDelivery{Awaits: catalogue.DeliverySeat}} if err := open.inventory.SavePlan(ctx, &waiting); err != nil { t.Fatal(err) } if err := deliveryCommand(ctx, []string{"go", waiting.ID, "--why", "its turn"}); err != nil { t.Fatal(err) } select { case m := <-heard: var said inventory.Plan if err := json.Unmarshal(m.Data, &said); err != nil || said.ID != waiting.ID || said.Delivery == nil || said.Delivery.Go == nil { t.Fatalf("plan-moved said %s (%v)", m.Data, err) } case <-time.After(5 * time.Second): t.Fatal("a walk let go by a verb was not said") } if checkEvents != nil || inventory.PlanSaved != nil { t.Fatal("the command's own bus was left in place") } }