Files
mesh-catalog/modules/mesh-config/cmd/mesh-config/config_test.go
T

554 lines
22 KiB
Go

package main
import (
"encoding/json"
"errors"
"fmt"
"sort"
"strings"
"sync"
"testing"
"time"
)
// memStore is the bus as mesh-config reaches it, in memory: its states and the mirrors it reads, its history log,
// the events it raised and the hand-acts it recorded.
type memStore struct {
mu sync.Mutex
states map[string]map[string][]byte
logs map[string][]LogEntry
events []Change
handActs []string
// failHandAct makes the hand-act log refuse.
failHandAct bool
}
func newStore() *memStore {
return &memStore{states: map[string]map[string][]byte{}, logs: map[string][]LogEntry{}}
}
func (m *memStore) Get(state, key string) (*Entry, error) {
m.mu.Lock()
defer m.mu.Unlock()
v, ok := m.states[state][key]
if !ok {
return nil, nil
}
return &Entry{Key: key, Value: append([]byte(nil), v...)}, nil
}
func (m *memStore) Put(state, key string, value any) error {
raw, err := json.Marshal(value)
if err != nil {
return err
}
m.mu.Lock()
defer m.mu.Unlock()
if m.states[state] == nil {
m.states[state] = map[string][]byte{}
}
m.states[state][key] = raw
return nil
}
func (m *memStore) Delete(state, key string) error {
m.mu.Lock()
defer m.mu.Unlock()
delete(m.states[state], key)
return nil
}
func (m *memStore) Keys(state string) ([]string, error) {
m.mu.Lock()
defer m.mu.Unlock()
var keys []string
for k := range m.states[state] {
keys = append(keys, k)
}
sort.Strings(keys)
return keys, nil
}
func (m *memStore) Append(log, key string, value any) error {
raw, err := json.Marshal(value)
if err != nil {
return err
}
m.mu.Lock()
defer m.mu.Unlock()
m.logs[log] = append(m.logs[log], LogEntry{Key: key, Value: raw, At: time.Now()})
return nil
}
func (m *memStore) Read(log string) ([]LogEntry, error) {
m.mu.Lock()
defer m.mu.Unlock()
return append([]LogEntry(nil), m.logs[log]...), nil
}
func (m *memStore) Emit(event string, body any) error {
if event != ConfigChanged {
return fmt.Errorf("mesh-config emits no %s", event)
}
m.mu.Lock()
defer m.mu.Unlock()
m.events = append(m.events, body.(Change))
return nil
}
func (m *memStore) HandAct(what, why, cause string) error {
m.mu.Lock()
defer m.mu.Unlock()
if m.failHandAct {
return errors.New("the controller did not answer")
}
m.handActs = append(m.handActs, what+" | "+why+" | "+cause)
return nil
}
// mirror writes what the controller mirrors, in the shapes of its mirrors.
func (m *memStore) mirror(t *testing.T, read, key, raw string) {
t.Helper()
if !json.Valid([]byte(raw)) {
t.Fatalf("not JSON: %s", raw)
}
m.mu.Lock()
defer m.mu.Unlock()
if m.states[read] == nil {
m.states[read] = map[string][]byte{}
}
m.states[read][key] = []byte(raw)
}
func (m *memStore) assign(t *testing.T, node, module string) {
m.mirror(t, AssignmentsRead, node+"."+module,
fmt.Sprintf(`{"node":%q,"module":%q,"assigned":"2026-10-14T00:00:00Z"}`, node, module))
}
func (m *memStore) resolved(t *testing.T, node, module, key string) Resolved {
t.Helper()
e, _ := m.Get(ResolvedState, ResolvedKey(node, module, key))
if e == nil {
t.Fatalf("%s holds no resolved value for %s on %s", key, module, node)
}
var r Resolved
if err := json.Unmarshal(e.Value, &r); err != nil {
t.Fatal(err)
}
return r
}
// dunstDeclared is dunst's declarations as the declared-config mirror on the feature branch writes them: kind, why
// and default always written, so a config-form key without a default comes as kind "", why "" and default null.
const dunstDeclared = `{"module":"dunst","settings":{
"font-size":{"kind":"","default":10,"why":"","type":"number","description":"text size, in points","minimum":6,"maximum":72,"predefined":"font-size"},
"width":{"kind":"","default":300,"why":"","type":"number","description":"the width of a notification, in pixels","minimum":100},
"position":{"kind":"","default":"top-right","why":"","type":"string","description":"where notifications show","enum":["top-right","top-left"]},
"origin":{"kind":"","default":null,"why":"","type":"string","description":"the operator's own origin","required":true,"pattern":"[a-z]+"}}}`
// theList is the controller's predefined mirror, as it writes it.
const theList = `{"keys":{"agent-managed-settings":{"type":"object","description":"managed settings","trusted":true},
"font-size":{"type":"number","description":"text size, in points","minimum":6,"maximum":72}},
"repository":"mesh-catalog","commit":"aaaaaaaaaaaa","generation":5,"build":"b5"}`
func aMesh(t *testing.T) (*Config, *memStore) {
t.Helper()
m := newStore()
m.mirror(t, DeclaredConfigRead, "dunst", dunstDeclared)
m.mirror(t, DeclaredConfigRead, "claude-code", `{"module":"claude-code","settings":{
"managed":{"kind":"","default":{},"why":"","type":"object","description":"managed settings","trusted":true,"predefined":"agent-managed-settings"}}}`)
m.mirror(t, DeclaredConfigRead, "old", `{"module":"old","settings":{"workers":{"kind":"preference","default":4,"why":"two per core"}}}`)
m.mirror(t, PredefinedRead, PredefinedMirrorKey, theList)
for _, node := range []string{"g14", "novox"} {
m.assign(t, node, "dunst")
}
m.assign(t, "g14", "claude-code")
m.assign(t, "g14", "old")
clock := time.Date(2026, 10, 14, 19, 0, 0, 0, time.UTC)
c := &Config{Store: m, Now: func() time.Time { clock = clock.Add(time.Second); return clock }, Logf: t.Logf}
if _, err := c.Reconcile(); err != nil {
t.Fatal(err)
}
m.events = nil // the first resolution's
return c, m
}
func set(t *testing.T, c *Config, call Call) *SetAnswer {
t.Helper()
if call.Action == "" {
call.Action = "set"
}
if call.Caller == "" {
call.Caller = "g14.node-tools"
}
if call.Why == "" {
call.Why = "the operator asked for it"
}
a, err := c.Change(call)
if err != nil {
t.Fatalf("%s %s: %v", call.Action, call.Key, err)
}
return a
}
func expect(t *testing.T, m *memStore, node string, value, source string) {
t.Helper()
r := m.resolved(t, node, "dunst", "font-size")
if string(r.Value) != value || r.Source != source {
t.Errorf("dunst's font-size on %s is %s from %s, want %s from %s", node, r.Value, r.Source, value, source)
}
}
// **The four sources, each in turn** (ADR 0348 §2, How it is checked): the manifest's default; mesh config for the
// predefined key it falls back to; node config for that key on the node; the module's own value on that node. Each
// cleared falls back to the one below.
func TestEachOfTheFourSourcesResolvesInTurnAndEachClearedFallsBack(t *testing.T) {
c, m := aMesh(t)
expect(t, m, "g14", "10", SourceDefault)
expect(t, m, "novox", "10", SourceDefault)
a := set(t, c, Call{Key: "font-size", Value: 12.0})
expect(t, m, "g14", "12", SourceMesh)
expect(t, m, "novox", "12", SourceMesh)
if len(a.Moved) != 2 || a.Level.Kind != LevelMesh {
t.Errorf("a mesh value moved %+v", a.Moved)
}
a = set(t, c, Call{Key: "font-size", Value: 14.0, Node: "g14"})
expect(t, m, "g14", "14", SourceNode)
expect(t, m, "novox", "12", SourceMesh)
if len(a.Moved) != 1 || a.Moved[0].Node != "g14" || string(a.Moved[0].From) != "12" || string(a.Moved[0].To) != "14" {
t.Errorf("g14's node value moved %+v", a.Moved)
}
a = set(t, c, Call{Key: "font-size", Value: 16.0, Node: "g14", Module: "dunst"})
expect(t, m, "g14", "16", SourceModule)
expect(t, m, "novox", "12", SourceMesh)
if len(a.Moved) != 1 || a.Moved[0].Source != SourceModule {
t.Errorf("dunst's own value moved %+v", a.Moved)
}
// A value set below the module's own moves nothing it overrides.
if a = set(t, c, Call{Key: "font-size", Value: 18.0, Node: "g14"}); len(a.Moved) != 0 {
t.Errorf("a node value under a module's own moved %+v", a.Moved)
}
set(t, c, Call{Action: "clear", Key: "font-size", Node: "g14", Module: "dunst"})
expect(t, m, "g14", "18", SourceNode)
set(t, c, Call{Action: "clear", Key: "font-size", Node: "g14"})
expect(t, m, "g14", "12", SourceMesh)
set(t, c, Call{Action: "clear", Key: "font-size"})
expect(t, m, "g14", "10", SourceDefault)
expect(t, m, "novox", "10", SourceDefault)
}
// **A value naming no predefined key ignores mesh and node config** (ADR 0348 §2): it resolves from its default and
// the module's own value only.
func TestAValueNamingNoPredefinedKeyResolvesFromItsDefaultAndItsOwnValueOnly(t *testing.T) {
c, m := aMesh(t)
set(t, c, Call{Key: "font-size", Value: 12.0})
set(t, c, Call{Key: "font-size", Value: 14.0, Node: "g14"})
if r := m.resolved(t, "g14", "dunst", "width"); string(r.Value) != "300" || r.Source != SourceDefault {
t.Errorf("width is %s from %s, want its default", r.Value, r.Source)
}
set(t, c, Call{Key: "width", Value: 400.0, Node: "g14", Module: "dunst"})
if r := m.resolved(t, "g14", "dunst", "width"); string(r.Value) != "400" || r.Source != SourceModule {
t.Errorf("width is %s from %s, want dunst's own value", r.Value, r.Source)
}
// A module not yet moved, in the settings form: its default and its own value.
if r := m.resolved(t, "g14", "old", "workers"); string(r.Value) != "4" || r.Source != SourceDefault {
t.Errorf("old's workers is %s from %s", r.Value, r.Source)
}
set(t, c, Call{Key: "workers", Value: 8.0, Node: "g14", Module: "old"})
if r := m.resolved(t, "g14", "old", "workers"); string(r.Value) != "8" || r.Source != SourceModule {
t.Errorf("old's workers is %s from %s", r.Value, r.Source)
}
}
// **A required value nothing sets is answered unset** (ADR 0348 §4), and once set, it is the value.
func TestARequiredValueNothingSetsIsAnsweredUnset(t *testing.T) {
c, m := aMesh(t)
r := m.resolved(t, "g14", "dunst", "origin")
if r.Source != SourceUnset || !r.Required || len(r.Value) != 0 {
t.Errorf("an unset required value resolves as %+v", r)
}
set(t, c, Call{Key: "origin", Value: "home", Node: "g14", Module: "dunst"})
if r = m.resolved(t, "g14", "dunst", "origin"); r.Source != SourceModule || string(r.Value) != `"home"` {
t.Errorf("a set required value resolves as %+v", r)
}
if r = m.resolved(t, "novox", "dunst", "origin"); r.Source != SourceUnset {
t.Errorf("another node's required value resolves as %+v", r)
}
}
// **The verb refuses** (ADR 0348 §3, §8, §10, How it is checked): a key not declared, naming the declared keys; a
// module value without a node; a value failing validation; a trusted value without the terminal; a change without
// why or a proven caller.
func TestTheVerbRefusesWhatADeclarationDoesNotAllow(t *testing.T) {
c, m := aMesh(t)
for _, tc := range []struct {
name string
call Call
want string
}{
{"a key the module does not declare", Call{Key: "colour", Value: "red", Node: "g14", Module: "dunst"},
"it declares: font-size, origin, position, width"},
{"a key not on the predefined list, at the mesh", Call{Key: "width", Value: 400.0},
"the predefined keys are: agent-managed-settings, font-size"},
{"a key not on the predefined list, at a node", Call{Key: "width", Value: 400.0, Node: "g14"}, "is not one"},
{"a module value without a node", Call{Key: "width", Value: 400.0, Module: "dunst"},
"no module value for the whole mesh"},
{"a module on a node it is not assigned to", Call{Key: "width", Value: 400.0, Node: "ace", Module: "dunst"},
"it is assigned to: g14, novox"},
{"a module not registered", Call{Key: "width", Value: 400.0, Node: "g14", Module: "nobody"}, "not a registered module"},
{"a value of another type", Call{Key: "font-size", Value: "12"}, `"12" is not a number`},
{"a value below its minimum", Call{Key: "font-size", Value: 2.0, Node: "g14"}, "below the minimum 6"},
{"a value above its maximum", Call{Key: "font-size", Value: 99.0, Node: "g14", Module: "dunst"}, "above the maximum 72"},
{"a value not in its enum", Call{Key: "position", Value: "middle", Node: "g14", Module: "dunst"}, "is not one of"},
{"a value not matching its pattern", Call{Key: "origin", Value: "Home 1", Node: "g14", Module: "dunst"},
"does not match the pattern"},
{"a trusted value at the mesh, not from the terminal", Call{Key: "agent-managed-settings",
Value: map[string]any{"a": 1.0}}, "set only at the controller's terminal"},
{"a trusted module value, not from the terminal", Call{Key: "managed", Value: map[string]any{},
Node: "g14", Module: "claude-code"}, "set only at the controller's terminal"},
{"a change without why", Call{Key: "font-size", Value: 12.0, Why: " "}, "say why"},
{"an unproven caller", Call{Key: "font-size", Value: 12.0, Caller: Unproven}, "no proven caller"},
{"by from anyone but the controller", Call{Key: "font-size", Value: 12.0, By: "jochen"}, "only the controller says"},
{"a clear of nothing set", Call{Action: "clear", Key: "font-size", Node: "g14"}, "nothing is set"},
} {
t.Run(tc.name, func(t *testing.T) {
call := tc.call
if call.Action == "" {
call.Action = "set"
}
if call.Caller == "" {
call.Caller = "g14.node-tools"
}
if call.Why == "" {
call.Why = "a test"
}
_, err := c.Change(call)
if err == nil || !strings.Contains(err.Error(), tc.want) {
t.Fatalf("refused with %v, want %q", err, tc.want)
}
})
}
if keys, _ := m.Keys(ValuesState); len(keys) != 0 {
t.Errorf("a refused call wrote %v", keys)
}
if len(m.events) != 0 || len(m.handActs) != 0 {
t.Errorf("a refused call raised %d events and recorded %d hand-acts", len(m.events), len(m.handActs))
}
}
// **A trusted value from the controller's terminal is set**, with who gave it there (ADR 0348 §8).
func TestATrustedValueFromTheTerminalIsSetWithWhoGaveIt(t *testing.T) {
c, m := aMesh(t)
a := set(t, c, Call{Key: "agent-managed-settings", Value: map[string]any{"b": 2.0, "a": 1.0},
Caller: ControllerCaller, By: "jochen"})
if string(a.Value) != `{"a":1,"b":2}` {
t.Errorf("the value is written as %s", a.Value)
}
r := m.resolved(t, "g14", "claude-code", "managed")
if r.Source != SourceMesh || !r.Trusted || string(r.Value) != `{"a":1,"b":2}` {
t.Errorf("claude-code's managed settings resolve as %+v", r)
}
e, _ := m.Get(ValuesState, "mesh.agent-managed-settings")
var v Value
_ = json.Unmarshal(e.Value, &v)
if v.By != "jochen" || v.Via != ControllerCaller {
t.Errorf("the value says it was set by %s via %s", v.By, v.Via)
}
}
// **Every set and clear is in the history and the hand-act log with the proven caller and why, and raises
// config-changed naming what moved** (ADR 0348 §9, §10).
func TestEveryChangeIsKeptWithWhoAndWhyAndRaisesConfigChanged(t *testing.T) {
c, m := aMesh(t)
m.events = nil
set(t, c, Call{Key: "font-size", Value: 12.0, Caller: "novox.node-tools", Why: "bigger on every desk"})
set(t, c, Call{Action: "clear", Key: "font-size", Caller: "g14.node-tools", Why: "back to the default"})
history, err := c.History(Call{})
if err != nil {
t.Fatal(err)
}
var changes []HistoryEntry
for _, h := range history {
if h.Action == "set" || h.Action == "clear" {
changes = append(changes, h)
}
}
if len(changes) != 2 || changes[0].By != "novox.node-tools" || changes[0].Why != "bigger on every desk" ||
changes[1].Action != "clear" || changes[1].By != "g14.node-tools" || string(changes[1].Was) != "12" {
t.Fatalf("the history holds %+v", changes)
}
if len(m.handActs) != 2 || !strings.Contains(m.handActs[0], "config set font-size at mesh config to 12, by novox.node-tools | bigger on every desk") {
t.Errorf("the hand-act log holds %q", m.handActs)
}
if len(m.events) != 2 || len(m.events[0].Moved) != 2 || m.events[0].Key != "font-size" || m.events[1].Action != "clear" {
t.Errorf("config-changed was raised as %+v", m.events)
}
// Narrowed: g14's dunst sees both, through what they moved; a node that holds no dunst sees neither.
if h, _ := c.History(Call{Module: "dunst", Node: "g14"}); len(h) < 2 {
t.Errorf("dunst on g14's history holds %d changes", len(h))
}
if h, _ := c.History(Call{Node: "ace"}); len(h) != 0 {
t.Errorf("ace's history holds %+v", h)
}
}
// **A hand-act the log refuses is said in the answer, never dropped**, and the value is set.
func TestAChangeTheHandActLogRefusesIsSaid(t *testing.T) {
c, m := aMesh(t)
m.failHandAct = true
a := set(t, c, Call{Key: "font-size", Value: 12.0})
if len(a.Unsaid) != 1 || !strings.Contains(a.Unsaid[0], "hand-act log does not hold it") {
t.Errorf("the answer says %q", a.Unsaid)
}
expect(t, m, "g14", "12", SourceMesh)
}
// **The newest list of predefined keys is published, and an older one heard later is not** (ADR 0348 §7).
func TestTheNewestListOfPredefinedKeysIsPublished(t *testing.T) {
c, m := aMesh(t)
published := func() map[string]Published {
out := map[string]Published{}
keys, _ := m.Keys(PredefinedState)
for _, k := range keys {
e, _ := m.Get(PredefinedState, k)
var p Published
_ = json.Unmarshal(e.Value, &p)
out[k] = p
}
return out
}
if p := published(); len(p) != 2 || p["font-size"].Generation != 5 || !p["agent-managed-settings"].Trusted {
t.Fatalf("published %+v", p)
}
m.mirror(t, PredefinedRead, PredefinedMirrorKey, `{"keys":{"font-size":{"type":"number","description":"x"},
"theme":{"type":"string","description":"the theme"}},"commit":"bbbb","generation":7}`)
if _, err := c.Reconcile(); err != nil {
t.Fatal(err)
}
if p := published(); len(p) != 2 || p["theme"].Generation != 7 || p["agent-managed-settings"].Type != "" {
t.Fatalf("a newer list published %+v", p)
}
m.mirror(t, PredefinedRead, PredefinedMirrorKey, theList) // generation 5, heard later
if _, err := c.Reconcile(); err != nil {
t.Fatal(err)
}
if p := published(); p["theme"].Generation != 7 {
t.Fatalf("an older list replaced the newer one: %+v", p)
}
}
// **A list dropping a key that still holds a value is withheld** (ADR 0348 §7): the list published stands, and the
// listing says why until the value is cleared.
func TestAListDroppingAKeyThatHoldsAValueIsWithheldUntilItIsCleared(t *testing.T) {
c, m := aMesh(t)
set(t, c, Call{Key: "font-size", Value: 12.0, Node: "novox"})
m.mirror(t, PredefinedRead, PredefinedMirrorKey, `{"keys":{"agent-managed-settings":{"type":"object",
"description":"managed settings","trusted":true}},"commit":"cccccccccc","generation":9}`)
if _, err := c.Reconcile(); err != nil {
t.Fatal(err)
}
listed, err := c.List(Call{})
if err != nil {
t.Fatal(err)
}
if _, kept := listed.Predefined["font-size"]; !kept ||
!strings.Contains(listed.Withheld, "drops font-size (still set as node config on novox)") {
t.Fatalf("the list was published over a value it drops: withheld %q, published %v", listed.Withheld,
joinKeys(listed.Predefined))
}
set(t, c, Call{Action: "clear", Key: "font-size", Node: "novox"})
if _, err := c.Reconcile(); err != nil {
t.Fatal(err)
}
listed, _ = c.List(Call{})
if _, kept := listed.Predefined["font-size"]; kept || listed.Withheld != "" {
t.Errorf("once cleared, the list is still withheld: %q, %v", listed.Withheld, joinKeys(listed.Predefined))
}
}
// **An assignment, a declaration or its removal moves the resolved values**, written only where they moved, and
// said as config-changed; a pass over unchanged sources writes nothing.
func TestAnAssignmentMovesTheResolvedValuesAndAQuietPassWritesNothing(t *testing.T) {
c, m := aMesh(t)
r, err := c.Reconcile()
if err != nil || len(r.Moved) != 0 {
t.Fatalf("a pass over unchanged sources moved %+v (%v)", r.Moved, err)
}
m.events = nil
m.assign(t, "ace", "dunst")
if r, _ = c.Reconcile(); len(r.Moved) != 4 {
t.Errorf("an assignment moved %+v", r.Moved)
}
expect(t, m, "ace", "10", SourceDefault)
if len(m.events) != 1 || m.events[0].Action != "resolve" {
t.Errorf("an assignment raised %+v", m.events)
}
_ = m.Delete(AssignmentsRead, "ace.dunst")
if r, _ = c.Reconcile(); len(r.Moved) != 4 || !r.Moved[0].Gone {
t.Errorf("an unassignment moved %+v", r.Moved)
}
if e, _ := m.Get(ResolvedState, ResolvedKey("ace", "dunst", "font-size")); e != nil {
t.Error("an unassigned module keeps its resolved values")
}
}
// **The declared-config mirror's two shapes read the same** (the release lead's note on issue 652): kind, why and a
// null default written for a config-form key are not given, and a mirror that leaves them out reads alike.
func TestTheDeclaredConfigMirrorsTwoShapesReadTheSame(t *testing.T) {
with, err := ParseModuleDeclarations([]byte(`{"module":"x","settings":{"k":{"kind":"","default":null,"why":"","type":"string","description":"d","required":true}}}`))
if err != nil {
t.Fatal(err)
}
without, err := ParseModuleDeclarations([]byte(`{"module":"x","settings":{"k":{"type":"string","description":"d","required":true}}}`))
if err != nil {
t.Fatal(err)
}
a, _ := json.Marshal(with)
b, _ := json.Marshal(without)
if string(a) != string(b) || with.Config["k"].HasDefault() {
t.Errorf("the two shapes read as %s and %s", a, b)
}
if r := resolveOne(Sources{}, "n", "x", "k", with.Config["k"]); r.Source != SourceUnset {
t.Errorf("a null default resolved as %+v", r)
}
}
// **A listing narrows by module and node**, and says the mesh and node values its modules fall back to.
func TestAListingNarrowsByModuleAndNode(t *testing.T) {
c, _ := aMesh(t)
set(t, c, Call{Key: "font-size", Value: 12.0})
set(t, c, Call{Key: "font-size", Value: 14.0, Node: "novox"})
l, err := c.List(Call{Module: "dunst", Node: "g14"})
if err != nil {
t.Fatal(err)
}
if len(l.Resolved) != 4 || len(l.Set) != 1 || l.Set[0].Level.Kind != LevelMesh || l.Predefined != nil {
t.Errorf("dunst on g14 lists %d resolved and %+v", len(l.Resolved), l.Set)
}
l, _ = c.List(Call{Node: "novox"})
if len(l.Resolved) != 4 || len(l.Set) != 2 {
t.Errorf("novox lists %d resolved and %+v", len(l.Resolved), l.Set)
}
}
// **The verb's arguments are read strictly**: one it does not know is refused, naming what it takes.
func TestTheVerbsArgumentsAreReadStrictly(t *testing.T) {
if _, err := callOf(map[string]any{"action": "set", "level": "mesh"}); err == nil ||
!strings.Contains(err.Error(), "config takes no level") {
t.Errorf("an unknown argument: %v", err)
}
if _, err := callOf(map[string]any{"key": 3.0}); err == nil {
t.Error("a key that is not a string was read")
}
call, err := callOf(map[string]any{"action": "set", "key": "font-size", "value": 12.0, "why": " w "})
if err != nil || call.Value != 12.0 || call.Why != "w" {
t.Errorf("read %+v (%v)", call, err)
}
}