mesh-config resolves a module's config values per node from which module runs where and what each registered module declares, and both lived only in the controller's store (hq issue 652, design 57 F3). The controller now keeps two record mirrors, mesh-controller_assignments (<node>.<module>) and mesh-controller_declared-config (<module>), built from the store by the lease holder every ten seconds and written only where a value differs, by compare-and-set, with one assignment-changed or declared-config-changed per write and a purge said as removed (ADR 0289, design 54 §2-3). A module reads them through `reads` as mesh-controller.<mirror>; the writers table keeps every other writer out. Neither holds a secret: an assignment names its node and module, a declaration is what the public manifest declares. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
128 lines
4.6 KiB
Go
128 lines
4.6 KiB
Go
package broker
|
|
|
|
import (
|
|
"context"
|
|
"slices"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/nats-io/nats.go/jetstream"
|
|
|
|
"github.com/novox/mesh-controller/internal/testbus"
|
|
)
|
|
|
|
// **The controller may write every bucket it writes** (novox/hq to-be 45 §1, issue 269). Writing a
|
|
// key is a publish to the bucket's own subject, which the management interface's grant does not
|
|
// cover: the cancelled sets' writes timed out for want of this, and the controller's own buckets
|
|
// would have.
|
|
func TestTheControllerMayWriteEveryBucketItWrites(t *testing.T) {
|
|
p, err := PermissionsFor(Principal{Kind: KindController, PasswordHash: "x"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
want := []string{"$KV." + CallsBucket + ".>", "$KV." + HandActsBucket + ".>",
|
|
"$KV." + AssignmentsBucket + ".>", "$KV." + DeclaredConfigBucket + ".>"}
|
|
for _, seat := range seatsTheControllerAsks {
|
|
if hasCancelledSet(seat) {
|
|
want = append(want, "$KV."+CancelledSetName(seat)+".>")
|
|
}
|
|
}
|
|
for _, subject := range want {
|
|
if !slices.Contains(p.Publish, subject) {
|
|
t.Errorf("the controller may not publish %s, so it cannot write that bucket", subject)
|
|
}
|
|
}
|
|
if slices.Contains(p.Publish, "$KV.>") {
|
|
t.Error("the controller may write any bucket, a module's state included")
|
|
}
|
|
}
|
|
|
|
// **A module reads a record mirror through `reads`, and never writes it** (ADR 0201 §1, design 54 §5,
|
|
// design 57 F3): granted the direct reads and watches of the bucket, and refused at composition a grant to
|
|
// write it, whose writer is the controller.
|
|
func TestAModuleReadsTheControllersMirrorsAndWritesNone(t *testing.T) {
|
|
reader := Principal{Kind: KindModule, Node: "one", Module: "mesh-config", PasswordHash: "x",
|
|
Reads: []string{"mesh-controller." + MirrorAssignments, "mesh-controller." + MirrorDeclaredConfig}}
|
|
p, err := PermissionsFor(reader)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, bucket := range []string{AssignmentsBucket, DeclaredConfigBucket} {
|
|
for _, subject := range []string{"$JS.API.STREAM.INFO.KV_" + bucket, "$JS.API.DIRECT.GET.KV_" + bucket + ".>",
|
|
"$JS.API.CONSUMER.CREATE.KV_" + bucket + ".>"} {
|
|
if !slices.Contains(p.Publish, subject) {
|
|
t.Errorf("a module reading %s may not publish %s", bucket, subject)
|
|
}
|
|
}
|
|
for _, subject := range p.Publish {
|
|
if SubjectsOverlap(subject, "$KV."+bucket+".>") {
|
|
t.Errorf("a module reading %s may write it through %s", bucket, subject)
|
|
}
|
|
}
|
|
if err := CheckWriters(reader, []string{"$KV." + bucket + ".>"}); err == nil {
|
|
t.Errorf("a module granted a write of %s would compose", bucket)
|
|
}
|
|
}
|
|
}
|
|
|
|
// **A machine's node tools may say they are there, as that machine and no other** (novox/hq to-be 45
|
|
// S11), and the controller may hear the bus's advisories and ask who answers — read-only, named.
|
|
func TestTheWatchedSignalsMayBeSaidAndHeard(t *testing.T) {
|
|
tools, err := PermissionsFor(Principal{Kind: KindNodeTools, Node: "anchor", Module: RuntimeModule, PasswordHash: "x"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !slices.Contains(tools.Publish, "mesh.control.anchor.tools-alive") {
|
|
t.Error("the node tools may not say they are there")
|
|
}
|
|
for _, s := range tools.Publish {
|
|
if strings.Contains(s, "tools-alive") && s != "mesh.control.anchor.tools-alive" {
|
|
t.Errorf("the node tools may say %s", s)
|
|
}
|
|
}
|
|
controller, err := PermissionsFor(Principal{Kind: KindController, PasswordHash: "x"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, s := range BusAdvisories {
|
|
if !slices.Contains(controller.Subscribe, s) {
|
|
t.Errorf("the controller may not hear %s", s)
|
|
}
|
|
}
|
|
if !slices.Contains(controller.Publish, "$SRV.INFO") || slices.Contains(controller.Subscribe, "$JS.EVENT.>") {
|
|
t.Error("the controller may not ask who answers, or hears every API call")
|
|
}
|
|
}
|
|
|
|
// The controller's record of what it asked the operator is bounded (correctness review of 2026-10-08): one
|
|
// value a key, a month's age, and a size it cannot outgrow.
|
|
func TestWhatTheControllerAskedIsBounded(t *testing.T) {
|
|
js, err := Dial(testbus.URL(t))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer js.Close()
|
|
if err := js.EnsureControllerBuckets(); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
|
defer cancel()
|
|
kv, err := jetstream.New(js.Conn())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
bucket, err := kv.KeyValue(ctx, AskedBucket)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
status, err := bucket.Status(ctx)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
info := status.(*jetstream.KeyValueBucketStatus).StreamInfo()
|
|
if status.History() != 1 || status.TTL() != AskedKeptFor || info.Config.MaxBytes <= 0 || info.Config.MaxBytes > 64<<20 {
|
|
t.Errorf("history %d, age %s, bytes %d", status.History(), status.TTL(), info.Config.MaxBytes)
|
|
}
|
|
}
|