Seats are data the controller owns, loaded from its store (ADR 0122, phase 1)
The seat set was a Go slice compiled into the controller and referenced by name everywhere, so changing it meant a rebuild and a freeze-prone deploy. It is now a table: catalogue keeps the shipped set as defaultSeats (the seed and the fallback) and a loadable working set; inventory adds the seat table (migration 0034), Seats to read it, and SeedSeats to fill it idempotently without overwriting an operator's edit; migrate seeds it; openInventory loads it, and an empty or unreadable table leaves the compiled defaults in force so it can never brick the control plane's boot. Behaviour-neutral: the seeded table equals the defaults. Phase 2 (reference by a stable id so a rename touches no manifest or code, and the builder reads the set from the mesh) follows.
This commit is contained in:
@@ -0,0 +1,78 @@
|
||||
package inventory
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/novox/mesh-controller/internal/catalogue"
|
||||
)
|
||||
|
||||
// The seat set is data the control plane owns (novox/hq ADR 0122): seeded from the binary's
|
||||
// defaults, read back as the working set, and thereafter an operator's to change without a rebuild.
|
||||
|
||||
func TestSeatsAreSeededFromTheDefaultsAndReadBack(t *testing.T) {
|
||||
inv := ForTest(t)
|
||||
defaults := catalogue.DefaultSeats()
|
||||
|
||||
added, err := inv.SeedSeats(t.Context(), defaults)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if added != len(defaults) {
|
||||
t.Fatalf("seeded %d of %d seats", added, len(defaults))
|
||||
}
|
||||
got, err := inv.Seats(t.Context())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(got) != len(defaults) {
|
||||
t.Fatalf("read back %d seats, seeded %d", len(got), len(defaults))
|
||||
}
|
||||
// The set round-trips: name, scope and what it delivers survive the store.
|
||||
by := map[string]catalogue.Seat{}
|
||||
for _, s := range got {
|
||||
by[s.Name] = s
|
||||
}
|
||||
for _, d := range defaults {
|
||||
if by[d.Name].Scope != d.Scope || by[d.Name].Delivers != d.Delivers {
|
||||
t.Errorf("%s came back as %+v, seeded %+v", d.Name, by[d.Name], d)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Re-seeding an already-seeded set adds nothing and changes nothing — every deploy runs SeedSeats,
|
||||
// and a mesh already holding the set must be left exactly as it is (an operator's edit to a row
|
||||
// included). A row's fields are not overwritten: on conflict the insert does nothing.
|
||||
//
|
||||
// (Phase 1 keys the table by name, so a seat *renamed* in the table would have its old name
|
||||
// re-seeded — the move to a stable id a rename does not touch is the next step, ADR 0122. This test
|
||||
// asserts only the property that holds now: an unchanged set re-seeds to a no-op.)
|
||||
func TestReSeedingAnUnchangedSetIsANoOp(t *testing.T) {
|
||||
inv := ForTest(t)
|
||||
defaults := catalogue.DefaultSeats()
|
||||
if _, err := inv.SeedSeats(t.Context(), defaults); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// An operator changes a row's scope in the table — the point of it being data.
|
||||
if _, err := inv.store.Pool().Exec(t.Context(),
|
||||
`update seat set scope = 'mesh' where name = 'node-uplink'`); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
added, err := inv.SeedSeats(t.Context(), defaults)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if added != 0 {
|
||||
t.Fatalf("re-seeding an already-present set added %d rows", added)
|
||||
}
|
||||
got, err := inv.Seats(t.Context())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, s := range got {
|
||||
if s.Name == "node-uplink" && s.Scope != "mesh" {
|
||||
t.Fatalf("re-seeding overwrote the operator's change: node-uplink scope is %q", s.Scope)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user