A controller restart lost every call's outcome, `status` composed the mesh while its caller waited (18.6s live on 2026-10-06, past the 10s window), a repair by hand left no trace, and the core's bounds had nothing measured to be set from. - calls: kept in the controller's bucket mesh-controller_calls (last 1000 or 14 days, answers bounded to 64 KiB), read by id across a restart; a controller starting marks a stopped one's running calls abandoned; each call names its caller from the inbox its answer goes to. - status: the serving controller composes it at start, after news from a machine, a build or an acting verb, and every minute; the verb answers the last composition at once with when and how long it took. Composing resolves each machine once instead of twice. - hand-act log in mesh-controller_hand-acts: push (required through the seat), plans stop/close, broker consumer-reset and the new hand-act record take --why/--cause/--condition; `hand-acts` lists them and repeated causes; status counts the week's. - durations (migration 0066): apply (send to first report), heartbeat gap, plan tier and build, recorded as heard; `durations` summarises them. - the controller's seat row takes this binary's definition of its own verbs, so the console no longer judges calls against an older build's schema. - the controller is granted its two buckets' subjects.
159 lines
5.4 KiB
Go
159 lines
5.4 KiB
Go
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)
|
|
}
|
|
}
|
|
}
|
|
|
|
// A rename is one operation: the seat gets the new name, the old name becomes an alias that still
|
|
// resolves to it (novox/hq ADR 0122).
|
|
func TestRenameSeatKeepsTheFormerNameAsAnAlias(t *testing.T) {
|
|
inv := ForTest(t)
|
|
if _, err := inv.SeedSeats(t.Context(), catalogue.DefaultSeats()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := inv.RenameSeat(t.Context(), "node-packet-filter", "node-firewall"); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
seats, err := inv.Seats(t.Context())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
names := map[string]bool{}
|
|
for _, s := range seats {
|
|
names[s.Name] = true
|
|
}
|
|
if !names["node-firewall"] || names["node-packet-filter"] {
|
|
t.Fatalf("the seat was not renamed in place: %v", names)
|
|
}
|
|
aliases, err := inv.Aliases(t.Context())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if aliases["node-packet-filter"] != "node-firewall" {
|
|
t.Fatalf("the former name is not an alias of the new one: %v", aliases)
|
|
}
|
|
// Renaming what has no seat is refused; renaming to the same name is refused.
|
|
if err := inv.RenameSeat(t.Context(), "no-such-seat", "x"); err == nil {
|
|
t.Fatal("renaming a seat that does not exist was accepted")
|
|
}
|
|
if err := inv.RenameSeat(t.Context(), "node-firewall", "node-firewall"); err == nil {
|
|
t.Fatal("renaming a seat to its own name was accepted")
|
|
}
|
|
}
|
|
|
|
// **The controller's verbs in the row are the binary's** (novox/hq issue 244, to-be 45 §7): a row
|
|
// seeded by an older build describes `push` without the arguments this one takes, and the console
|
|
// judges a call against the row — so re-seeding brings the controller's verbs to this binary's
|
|
// definition, and keeps a verb only the row has, which a newer build added (ADR 0185).
|
|
func TestTheControllersVerbsInTheRowAreTheBinarys(t *testing.T) {
|
|
inv := ForTest(t)
|
|
ctx := t.Context()
|
|
if _, err := inv.SeedSeats(ctx, catalogue.DefaultSeats()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
old := `[{"name":"push","description":"an older push","input":{"type":"object","properties":{"node":{"type":"string"}}}},
|
|
{"name":"newer","description":"a verb of a newer build"}]`
|
|
if _, err := inv.store.Pool().Exec(ctx, `update seat set serves = $1 where name = $2`,
|
|
[]byte(old), catalogue.ControllerSeatName); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := inv.SeedSeats(ctx, catalogue.DefaultSeats()); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
seats, err := inv.Seats(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
verbs := map[string]catalogue.Verb{}
|
|
for _, s := range seats {
|
|
if s.Name == catalogue.ControllerSeatName {
|
|
for _, v := range s.Serves {
|
|
verbs[v.Name] = v
|
|
}
|
|
}
|
|
}
|
|
props, _ := verbs["push"].Input["properties"].(map[string]any)
|
|
if _, takesWhy := props["why"]; !takesWhy || verbs["push"].Description == "an older push" {
|
|
t.Fatalf("push in the row is still the older build's: %+v", verbs["push"])
|
|
}
|
|
if _, kept := verbs["newer"]; !kept {
|
|
t.Fatal("a verb only the row has was dropped")
|
|
}
|
|
if _, added := verbs["durations"]; !added {
|
|
t.Fatal("a verb this binary adds was not added")
|
|
}
|
|
}
|