Files
mesh-controller/internal/inventory/holdings_test.go
jschoubben 585a6abbdd A seat is handed over as one act, and the holder is on record
`seat <name> --to <node>/<module>` makes one assignment the holder of a seat in
the same write that removes the previous one. The row is new (migration 0039);
without one, the resolver derives the holder as it always did — the sole eligible
assignment, two refused — so nothing changes for a mesh that never hands a seat
over. With one, the recorded assignment holds and any other whose module could
hold the seat is eligible and silent: not refused, not holding. That is what lets
the next holder run beside the current one until the switch (hq design 26, design
28 task 5.3, ADR 0131).

Why: the controller finds its own bus through a seat, and the day that seat was
left with nobody in it — because two eligible holders could not coexist and the
old one's claim was taken away — the control plane looped for two hours while
every service stayed up. A handover that is never empty in between is the fix,
not a workaround for it.

`CanHold` is the one judgement of whether a module may hold a seat — claims it at
its scope, provides what it delivers, against the store's row — shared by
registration and the handover so they cannot drift apart. The holding belongs to
the assignment and goes when it does, so a seat never points at nothing running.

Tests: the resolver with and without a record, on the same and another machine,
under a former name; the store's row replaced not added, refused for an
unassigned target, removed with its assignment; CanHold's four answers and that
they follow the store. Full suite green against a real NATS and store.
2026-09-27 23:22:20 +02:00

83 lines
2.8 KiB
Go

package inventory
import (
"context"
"testing"
"github.com/novox/mesh-controller/internal/catalogue"
)
// A seat's holder is a row, changed as one act (novox/hq ADR 0131). What these pin is the shape of
// that row's life: it needs an assignment to point at, it is replaced rather than added to, and it
// goes when the assignment does — so a seat never points at something that is not running anywhere.
func twoBrokersOnTwoNodes(t *testing.T) (*Inventory, context.Context) {
t.Helper()
old := catalogue.Manifest{Module: "old-broker", Version: "1",
Provides: []catalogue.Offer{{Name: "mesh-bus", Scope: catalogue.ScopeMesh}},
Claims: []catalogue.Claim{{Name: "mesh-broker", Scope: catalogue.ScopeMesh}}}
new := catalogue.Manifest{Module: "new-broker", Version: "1",
Provides: []catalogue.Offer{{Name: "mesh-bus", Scope: catalogue.ScopeMesh}},
Claims: []catalogue.Claim{{Name: "mesh-broker", Scope: catalogue.ScopeMesh}}}
inv, ctx := aMeshWith(t, old, new)
// The holding references the seat's row, which `migrate` seeds on a real mesh.
if _, err := inv.SeedSeats(ctx, catalogue.DefaultSeats()); err != nil {
t.Fatal(err)
}
for _, n := range []string{"anchor", "laptop"} {
if _, err := inv.AddNode(ctx, n); err != nil {
t.Fatal(err)
}
}
if _, err := inv.Assign(ctx, "anchor", "old-broker"); err != nil {
t.Fatal(err)
}
if _, err := inv.Assign(ctx, "laptop", "new-broker"); err != nil {
t.Fatal(err)
}
return inv, ctx
}
func TestAHandoverIsOneRowReplacedNotOneAdded(t *testing.T) {
inv, ctx := twoBrokersOnTwoNodes(t)
if err := inv.HoldSeat(ctx, "mesh-broker", catalogue.ScopeMesh, "anchor", "old-broker"); err != nil {
t.Fatal(err)
}
if err := inv.HoldSeat(ctx, "mesh-broker", catalogue.ScopeMesh, "laptop", "new-broker"); err != nil {
t.Fatal(err)
}
held, err := inv.Holdings(ctx)
if err != nil {
t.Fatal(err)
}
if len(held) != 1 || held[0].Node != "laptop" || held[0].Module != "new-broker" || held[0].Scope != catalogue.ScopeMesh {
t.Fatalf("after a handover the seat is not held by exactly the new holder: %+v", held)
}
}
func TestASeatCannotBeHandedToSomethingNotAssigned(t *testing.T) {
inv, ctx := twoBrokersOnTwoNodes(t)
// new-broker is assigned to laptop, not anchor.
if err := inv.HoldSeat(ctx, "mesh-broker", catalogue.ScopeMesh, "anchor", "new-broker"); err == nil {
t.Fatal("a seat was handed to a module not assigned where it was named")
}
}
func TestUnassigningTheHolderTakesTheHoldingWithIt(t *testing.T) {
inv, ctx := twoBrokersOnTwoNodes(t)
if err := inv.HoldSeat(ctx, "mesh-broker", catalogue.ScopeMesh, "laptop", "new-broker"); err != nil {
t.Fatal(err)
}
if err := inv.Unassign(ctx, "laptop", "new-broker"); err != nil {
t.Fatal(err)
}
held, err := inv.Holdings(ctx)
if err != nil {
t.Fatal(err)
}
if len(held) != 0 {
t.Fatalf("the holding outlived the assignment it pointed at: %+v", held)
}
}