Files
mesh-catalog/modules/mesh-config/cmd/mesh-config/config_test.go
T
jschoubben 96a37f58ca
mesh/delivery closed: the pull request closed unmerged
mesh/check pass 96a37f58ca76 on 030f9ee4e780 | unit 1173 run, 3272 kept, 13 skipped | integration 1212 run, 3274 kept, 22 skipped | over bound: unit, …
mesh-config: read declarations from each placed module's own manifests as built, and placement alone, so a module from its own repository resolves (issue 1953)
The controller's declared-config mirror never held de-spiegel's keys, and Resolve skipped
an assigned module it held no declarations of, silently: de-spiegel stayed held on novox
with its values set. Declarations now come from the indexer's trunk manifest with the
wanted build's laid over it per node; which module runs where from the seat
mesh-placement alone. A placed module with no readable manifest is listed as unresolved
with why, logged, and keeps its last resolved values.
2026-10-29 15:19:24 +01:00

736 lines
29 KiB
Go

package main
import (
"encoding/json"
"errors"
"fmt"
"sort"
"strings"
"sync"
"testing"
"time"
"git.novox.be/novox/mesh-sdk/go/delivery"
)
// 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)
}
// assign places a module on a node, in the seat mesh-placement's key for the node.
func (m *memStore) assign(t *testing.T, node, module string) {
t.Helper()
m.place(t, node, module, true)
}
// unassign takes a module off a node's placement.
func (m *memStore) unassign(t *testing.T, node, module string) {
t.Helper()
m.place(t, node, module, false)
}
func (m *memStore) place(t *testing.T, node, module string, on bool) {
t.Helper()
type placed struct {
Module string `json:"module"`
}
p := struct {
V int `json:"v"`
Node string `json:"node"`
Modules []placed `json:"modules"`
}{V: 1, Node: node}
if e, _ := m.Get(PlacementRead, "placement."+node); e != nil {
if err := json.Unmarshal(e.Value, &p); err != nil {
t.Fatal(err)
}
}
var kept []placed
for _, x := range p.Modules {
if x.Module != module {
kept = append(kept, x)
}
}
if on {
kept = append(kept, placed{Module: module})
}
p.Modules = kept
if err := m.Put(PlacementRead, "placement."+node, p); err != nil {
t.Fatal(err)
}
}
// manifest keeps a manifest in the indexer's manifests by its digest, as the indexer writes it, and answers the digest.
func (m *memStore) manifest(t *testing.T, module, raw string) string {
t.Helper()
if !json.Valid([]byte(raw)) {
t.Fatalf("not JSON: %s", raw)
}
digest := delivery.ManifestDigest([]byte(raw))
value, err := delivery.EncodeManifest(delivery.Manifest{Module: module, JSON: raw})
if err != nil {
t.Fatal(err)
}
m.mirror(t, ManifestsRead, digest, string(value))
return digest
}
// declare makes raw the module's manifest at the trunk: kept by digest, and named by the indexer's modules.<module>.
func (m *memStore) declare(t *testing.T, module, raw string) {
t.Helper()
digest := m.manifest(t, module, raw)
m.mirror(t, ModulesRead, module, fmt.Sprintf(`{"v":1,"manifest":%q,"repository":"novox/%s","commit":"c1"}`,
digest, module))
}
// want makes a node's want name a build of the module whose manifest is raw, as the assigner and the builder keep them.
func (m *memStore) want(t *testing.T, node, module, fp, raw string) {
t.Helper()
digest := m.manifest(t, module, raw)
key, err := delivery.BuilderBuildKey(module, fp)
if err != nil {
t.Fatal(err)
}
m.mirror(t, BuildsRead, key, fmt.Sprintf(`{"v":1,"state":"done","manifest":%q}`, digest))
m.mirror(t, WantsRead, "want."+node+"."+module, fmt.Sprintf(`{"v":1,"fp":%q,"self":true}`, fp))
}
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.declare(t, "dunst", dunstDeclared)
m.declare(t, "claude-code", `{"module":"claude-code","settings":{
"managed":{"kind":"","default":{},"why":"","type":"object","description":"managed settings","trusted":true,"predefined":"agent-managed-settings"}}}`)
m.declare(t, "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 placed on", Call{Key: "width", Value: 400.0, Node: "ace", Module: "dunst"},
"it is placed on: 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))
}
}
// **A placement, 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.unassign(t, "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)
}
}
// despiegel is de-spiegel's manifest as its own repository declares it (novox/de-spiegel at e25e1beb, its config
// alone): required trusted values, no default.
const despiegel = `{"module":"de-spiegel","version":"1","requires":["route","smtp"],"config":{
"praktijk-name":{"type":"string","description":"the practice's public name","trusted":true,"required":true},
"praktijk-www-name":{"type":"string","description":"its www form","trusted":true,"required":true}}}`
// Issue 1953: **each placed module's declarations come from its own manifests as built**, the trunk's at the indexer
// and the wanted build's at the builder, never from the controller's mirror; a module placed with no manifest to read
// is said unresolved with why, in the listing and the log, and keeps what was resolved for it.
func TestDeclarationsComeFromTheManifestsAsBuilt(t *testing.T) {
fp := "src2:" + strings.Repeat("ab", 32)
cases := []struct {
name string
given func(t *testing.T, m *memStore)
// resolved is each key's resolved value on novox, `<module>.<key>` to source and value ("" for none).
resolved map[string]string
absent []string
unresolved string
}{
{name: "a module of its own repository resolves from the trunk's manifest, its value set",
given: func(t *testing.T, m *memStore) {
m.declare(t, "de-spiegel", despiegel)
m.assign(t, "novox", "de-spiegel")
must(t, m.Put(ValuesState, Level{Kind: LevelModule, Node: "novox", Module: "de-spiegel"}.ValueKey(
"praktijk-name"), Value{Value: json.RawMessage(`"praktijkdespiegel.be"`), By: "the operator"}))
// The controller's mirror says otherwise, and is not read.
m.mirror(t, "mesh-controller.declared-config", "de-spiegel", `{"module":"de-spiegel","settings":{}}`)
},
resolved: map[string]string{"de-spiegel.praktijk-name": `module "praktijkdespiegel.be"`,
"de-spiegel.praktijk-www-name": "unset "}},
{name: "the wanted build's manifest lays its keys over the trunk's on its node",
given: func(t *testing.T, m *memStore) {
m.declare(t, "de-spiegel", despiegel)
m.assign(t, "novox", "de-spiegel")
m.want(t, "novox", "de-spiegel", fp, `{"module":"de-spiegel","config":{
"praktijk-name":{"type":"string","description":"d","default":"old.example"},
"old-key":{"type":"number","description":"d","default":3}}}`)
},
resolved: map[string]string{"de-spiegel.praktijk-name": `default "old.example"`,
"de-spiegel.old-key": "default 3", "de-spiegel.praktijk-www-name": "unset "}},
{name: "a removed want resolves from the trunk alone",
given: func(t *testing.T, m *memStore) {
m.declare(t, "de-spiegel", despiegel)
m.assign(t, "novox", "de-spiegel")
m.want(t, "novox", "de-spiegel", fp, `{"module":"de-spiegel","config":{
"old-key":{"type":"number","description":"d","default":3}}}`)
m.mirror(t, WantsRead, "want.novox.de-spiegel", `{"v":1,"removed":true}`)
},
resolved: map[string]string{"de-spiegel.praktijk-name": "unset "}, absent: []string{"de-spiegel.old-key"}},
{name: "a placed module the indexer holds no manifest of is unresolved, said, its values kept",
given: func(t *testing.T, m *memStore) {
must(t, m.Put(ResolvedState, ResolvedKey("novox", "de-spiegel", "praktijk-name"),
Resolved{Node: "novox", Module: "de-spiegel", Key: "praktijk-name", Source: SourceUnset}))
m.assign(t, "novox", "de-spiegel")
},
resolved: map[string]string{"de-spiegel.praktijk-name": "unset "},
unresolved: "the indexer holds no modules.de-spiegel"},
}
for _, tc := range cases {
t.Run(tc.name, func(t *testing.T) {
m := newStore()
tc.given(t, m)
var said []string
c := &Config{Store: m, Now: time.Now, Logf: func(f string, a ...any) { said = append(said, fmt.Sprintf(f, a...)) }}
if _, err := c.Reconcile(); err != nil {
t.Fatal(err)
}
for k, want := range tc.resolved {
module, key, _ := strings.Cut(k, ".")
r := m.resolved(t, "novox", module, key)
if got := r.Source + " " + string(r.Value); got != want {
t.Errorf("%s resolved as %s, want %s", k, got, want)
}
}
for _, k := range tc.absent {
module, key, _ := strings.Cut(k, ".")
if e, _ := m.Get(ResolvedState, ResolvedKey("novox", module, key)); e != nil {
t.Errorf("%s is resolved: %s", k, e.Value)
}
}
l, err := c.List(Call{Module: "de-spiegel"})
if err != nil {
t.Fatal(err)
}
if tc.unresolved == "" {
if len(l.Unresolved) != 0 {
t.Errorf("unresolved: %+v", l.Unresolved)
}
return
}
if len(l.Unresolved) != 1 || l.Unresolved[0].Node != "novox" ||
!strings.Contains(l.Unresolved[0].Why, tc.unresolved) {
t.Errorf("the listing says %+v, want %q", l.Unresolved, tc.unresolved)
}
if !strings.Contains(strings.Join(said, "\n"), tc.unresolved) {
t.Errorf("the log does not say it: %q", said)
}
})
}
}
func must(t *testing.T, err error) {
t.Helper()
if err != nil {
t.Fatal(err)
}
}