package main import ( "context" "strings" "testing" "time" "github.com/novox/mesh-controller/internal/catalogue" "github.com/novox/mesh-controller/internal/conditions" "github.com/novox/mesh-controller/internal/inventory" ) // TestReplay336 replays novox/hq issue 336 (2026-10-08): a catalogue merge built a new bus build for the // control-node; from then on every send to that machine was refused for the bus's planned step, which only a // person starts, and for 28 minutes nothing but the walks' notes said so. The outcome asserted: the first // watchdog tick after the first refusal opens a condition for the operator that names what waits and the verb // that ends it, and it clears once the bus's machine runs the new build. Written with only what the controller // had before its fix — the stores, register, assign, a plan saved and advanced, the watchdogs' gathering and // seeing — so it is laid over the commit before. func TestReplay336(t *testing.T) { open := aMesh(t) ctx := t.Context() inv := open.inventory bus := catalogue.Manifest{Module: "nats", Version: "2", Provides: []catalogue.Offer{{Name: "mesh-bus"}}} if err := inv.RegisterModule(ctx, bus, inventory.Source{Repository: "novox/mesh-catalog", Seat: "git", Path: "modules/nats", BuiltFrom: "32307bd1", Head: "32307bd1"}); err != nil { t.Fatal(err) } register(t, open, catalogue.Manifest{Module: "engine", Version: "1"}) for _, m := range []string{"nats", "engine"} { if _, err := inv.Assign(ctx, "anchor", m); err != nil { t.Fatal(err) } } // The control-node runs the bus build it was last sent; the mesh holds a newer one, which waits for its step. if err := inv.RecordSent(ctx, nodeID(t, open, "anchor"), "d-anchor", map[string]string{"nats": "88135ad0"}); err != nil { t.Fatal(err) } // A walk of the engine, built, whose tier sends to the control-node. now := time.Now().UTC() plan := inventory.Plan{ID: "plan-engine", Repository: "novox/mesh-host", Commit: "e1e1e1e1", Created: now, State: inventory.PlanBuilding, Tiers: [][]string{{"engine"}}, Modules: map[string]*inventory.PlanModule{"engine": {State: "built", BuiltAt: &now, Commit: "e1e1e1e1", Build: "b"}}} if err := inv.SavePlan(ctx, &plan); err != nil { t.Fatal(err) } advancePlans(ctx, open) p, err := inv.PlanByID(ctx, "plan-engine") if err != nil { t.Fatal(err) } if !strings.Contains(p.Note, errBusWaits.Error()) { t.Fatalf("the walk's send to the bus's machine was not refused for the bus: %s %q", p.State, p.Note) } store := conditions.NewInMemory() k := conditions.NewKeeper(ctx, conditions.Options{Store: store, History: store, Teller: &conditions.Told{}}) defer k.Close(context.Background()) w := &watchdogs{open: open, keeper: k, started: now.Add(-time.Hour)} waiting := func() []conditions.Condition { t.Helper() w.see(ctx, w.gather(ctx)) all, err := k.Open(ctx) if err != nil { t.Fatal(err) } var out []conditions.Condition for _, c := range all { if strings.HasPrefix(c.Key, "bus.") && c.Resolver == conditions.ResolverOperator { out = append(out, c) } } return out } got := waiting() if len(got) != 1 { t.Fatalf("the first tick after the refusal told the operator nothing about the bus's step: %d condition(s)", len(got)) } for _, want := range []string{"anchor", "engine", "mesh-controller.bus", "upgrade", "32307bd1"} { if !strings.Contains(got[0].Summary, want) { t.Errorf("the condition does not say %q: %s", want, got[0].Summary) } } // The step taken: the control-node is sent the new bus build, and the wait is over. if err := inv.RecordSent(ctx, nodeID(t, open, "anchor"), "d-anchor-2", map[string]string{"nats": "32307bd1"}); err != nil { t.Fatal(err) } if got := waiting(); len(got) != 0 { t.Fatalf("the bus's machine runs the new build, and the operator is still asked: %+v", got) } }