package main import ( "encoding/json" "strings" "testing" "time" "github.com/novox/mesh-controller/internal/catalogue" "github.com/novox/mesh-controller/internal/inventory" ) // A tier whose module is no move — made from the source every machine running it runs, so nothing is // sent — is not waited on for a report after its marking (novox/hq issue 648). Its machines answered the // send that last carried that build before the walk got there, and nothing will make them report again: // the walk asks the next tier in the same step, rather than "waiting for app on anchor, laptop" for ever. func TestANoMoveTierWaitsForNoNewReport(t *testing.T) { judgeLeavesAsTheCore(t) g := aGateMesh(t) ctx := t.Context() inv := g.open.inventory // app holds the build seat, so a module of the next tier is built by it; since novox/hq ADR 0360 §3 that gates // nothing. for _, m := range []catalogue.Manifest{ {Module: "app", Version: "c2", Claims: []catalogue.Claim{{Name: "node-build-agent", Scope: catalogue.ScopeNode}}}, {Module: "web", Version: "w1"}, } { if err := inv.RegisterModule(ctx, m, inventory.Source{Repository: "novox/mesh-catalog", Seat: "git", Path: "modules/" + m.Module, BuiltFrom: m.Version, Head: m.Version, Asked: time.Now().Add(-time.Minute)}); err != nil { t.Fatal(err) } } // What anchor and laptop were last sent (c1) was made from the source the plan's build of c2 is. for _, b := range []inventory.Build{ {ID: "build-1s", Module: "app", Commit: "c1", SourceFingerprint: "src1:app", Asked: time.Now().Add(-30 * time.Second), At: time.Now().Add(-30 * time.Second)}, {ID: "build-2s", Module: "app", Commit: "c2", SourceFingerprint: "src1:app", Asked: time.Now(), At: time.Now()}, } { b.Manifest, _ = json.Marshal(catalogue.Manifest{Module: "app", Version: "1"}) if err := inv.RecordBuild(ctx, b); err != nil { t.Fatal(err) } } // Each machine answered that send, applied, before the walk's tier was built. for _, n := range []string{"anchor", "laptop"} { if _, err := inv.RecordDoing(ctx, nodeID(t, g.open, n), inventory.Doing{Node: n, Outcome: inventory.OutcomeApplied, Declared: "d-" + n + "-c1", Applied: 1, At: time.Now().Add(-10 * time.Second)}); err != nil { t.Fatal(err) } } built := time.Now().UTC() p := g.plan(t) p.Tiers = [][]string{{"app"}, {"web"}} p.Modules["app"].Build, p.Modules["app"].BuiltAt = "build-2s", &built if err := inv.SavePlan(ctx, &p); err != nil { t.Fatal(err) } advancePlans(ctx, g.open) if len(g.sent) != 0 { t.Fatalf("a no-move build was sent: %v", g.sent) } p = g.plan(t) if s := p.Modules["app"]; s == nil || !strings.HasPrefix(s.Why, "no move") { t.Fatalf("the build was not marked no move: %+v", s) } if p.Tier < 1 || strings.Contains(p.Note, "waiting for app") { t.Fatalf("the walk waits on a no-move tier for a report nothing will cause: tier %d, %s %q", p.Tier, p.State, p.Note) } // A machine that has not answered its last send is not waited on either: a built-by edge gates no tier // (novox/hq ADR 0360 §3). Before, the walk waited for app on laptop. p.Tier, p.State, p.Note = 0, inventory.PlanRolling, "" if err := inv.RecordSent(ctx, nodeID(t, g.open, "laptop"), "d-laptop-later", map[string]string{"app": "c1"}); err != nil { t.Fatal(err) } if err := inv.SavePlan(ctx, &p); err != nil { t.Fatal(err) } advancePlans(ctx, g.open) p = g.plan(t) if p.Tier < 1 || strings.Contains(p.Note, "waiting for app") { t.Fatalf("the walk waits on a built-by edge: tier %d, %q", p.Tier, p.Note) } }