Files
mesh-catalog/modules/mesh-config/cmd/mesh-config/takeover_test.go
T
jschoubben 35ff67bda0
mesh/delivery-group group feat/774-l3-reland rejected: composed together: the checker's verdict at the members' heads (b48c90fea31f): integration: mesh-self-ch…
mesh/delivery closed: a newer head of the same pull request
mesh/check failed: integration: mesh-self-check/cmd/mesh-self-check.TestTheSelfCheckWholeRunsOnTheBus: the run answered {Run:doctor-1793351737-1 At:20…
Design 47's last milestones: the keeper, self-check, healers, hand-acts, provisioner, vault, artifact sweep, disk readings, credential forms (issues 774, 1952, 1953, 1954, 1955)
One commit with exactly the tree of the reviewed head 35b4509c0a, on main 12c2574c9d, so two compiled programs added and removed on the branch leave no trace in history (issue 1577).
2026-10-30 02:21:51 +01:00

239 lines
10 KiB
Go

package main
import (
"encoding/json"
"slices"
"strings"
"testing"
"time"
"git.novox.be/novox/mesh-sdk/go/delivery"
)
// The trunk's manifests aMesh declares, as the controller's mirror declared-config holds the same declarations.
const (
claudeDeclared = `{"module":"claude-code","settings":{
"managed":{"kind":"","default":{},"why":"","type":"object","description":"managed settings","trusted":true,"predefined":"agent-managed-settings"}}}`
oldDeclared = `{"module":"old","settings":{"workers":{"kind":"preference","default":4,"why":"two per core"}}}`
)
// anAgreeingMesh is aProposingMesh whose controller mirrors say what mesh-config's own sources say: the mirror's
// declarations of every placed module, and the indexer's predefined list equal to the mirror's.
func anAgreeingMesh(t *testing.T) (*Config, *memStore, *standIn) {
t.Helper()
c, m, r := aProposingMesh(t)
m.mirror(t, MirrorDeclaredRead, "dunst", dunstDeclared)
m.mirror(t, MirrorDeclaredRead, "claude-code", claudeDeclared)
m.mirror(t, MirrorDeclaredRead, "old", oldDeclared)
indexerList(t, m, "aaaaaaaaaaaa", `{"agent-managed-settings":{"type":"object","description":"managed settings","trusted":true},
"font-size":{"type":"number","description":"text size, in points","minimum":6,"maximum":72}}`)
if _, err := c.Reconcile(); err != nil {
t.Fatal(err)
}
return c, m, r
}
// indexerList is the indexer's `predefined.list` at a commit.
func indexerList(t *testing.T, m *memStore, commit, list string) {
t.Helper()
raw, _ := json.Marshal(delivery.PredefinedKept{V: 1, Commit: commit, List: json.RawMessage(list)})
m.mirror(t, IndexerPredefinedRead, delivery.PredefinedListKey, string(raw))
}
// candidate is the indexer's config candidate of a pull request.
func candidate(t *testing.T, m *memStore, pull, head, declares string) {
t.Helper()
key, err := delivery.ConfigCandidateKey(pull)
if err != nil {
t.Fatal(err)
}
m.mirror(t, CandidatesRead, key, `{"v":1,"pull":"`+pull+`","head":"`+head+`","declares":`+declares+`}`)
}
func kinds(cmp *Comparison) []string {
var out []string
for _, d := range cmp.Differences {
k := d.What + ":" + d.Module + d.Pull + d.Key
if d.Node != "" {
k += "@" + d.Node
}
if !slices.Contains(out, k) {
out = append(out, k)
}
}
return out
}
// compare lists every difference with its reason — a module's declarations, the predefined list, a pull request's
// config, and each value a node resolves both ways — and the take-over is refused while one stands (ADR 0565 §4 and
// its insight of 2026-10-29; issue 1953 L5-C4).
func TestCompareListsEveryDifferenceAndTheTakeOverWaitsForNone(t *testing.T) {
cases := []struct {
name string
change func(t *testing.T, m *memStore)
want []string
}{
{"all agree", func(*testing.T, *memStore) {}, nil},
{"the mirror declares a default otherwise", func(t *testing.T, m *memStore) {
m.mirror(t, MirrorDeclaredRead, "dunst", strings.Replace(dunstDeclared, `"default":300`, `"default":400`, 1))
}, []string{"declared:dunst", "value:dunstwidth@g14", "value:dunstwidth@novox"}},
{"the mirror lacks a placed module", func(t *testing.T, m *memStore) { _ = m.Delete(MirrorDeclaredRead, "old") },
[]string{"declared:old", "value:old@g14", "value:oldworkers@g14"}},
{"a node's build adds a key the mirror lacks", func(t *testing.T, m *memStore) {
m.want(t, "g14", "dunst", "src2:"+strings.Repeat("ab", 32), strings.Replace(dunstDeclared, `"width"`, `"height":{"type":"number","default":5},"width"`, 1))
}, []string{"value:dunstheight@g14", "value:dunstheight@novox"}}, // a key only a build declares resolves on every node (#558)
{"the lists differ", func(t *testing.T, m *memStore) {
indexerList(t, m, "bbbbbbbbbbbb", `{"font-size":{"type":"number","description":"text size, in points","minimum":6,"maximum":72}}`)
}, []string{"predefined:agent-managed-settings"}},
{"a pull request only the indexer holds", func(t *testing.T, m *memStore) {
candidate(t, m, "novox/mesh-catalog#9", "h1", `{"dunst":{"gap":{"kind":"","type":"number","default":2}}}`)
}, []string{"pull:novox/mesh-catalog#9"}},
}
for _, tc := range cases {
c, m, r := anAgreeingMesh(t)
tc.change(t, m)
cmp, err := c.Compare()
if err != nil {
t.Fatalf("%s: %v", tc.name, err)
}
if got := kinds(cmp); !slices.Equal(got, tc.want) {
t.Errorf("%s: compare lists %v, want %v (%+v)", tc.name, got, tc.want, cmp.Differences)
}
for _, d := range cmp.Differences {
if d.Why == "" {
t.Errorf("%s: a difference without its reason: %+v", tc.name, d)
}
}
a, err := c.TakeOver(Call{Why: "compare is empty", Caller: "person.jochen"})
if len(tc.want) > 0 {
if err == nil || !strings.Contains(err.Error(), "refused") || len(r.subjects) != 0 {
t.Errorf("%s: the take-over was asked while compare differs: %v", tc.name, err)
}
continue
}
if err != nil || a.Ask == "" || len(r.subjects) != 1 {
t.Errorf("%s: the take-over was not asked: %v", tc.name, err)
}
}
}
// The operator's Approve switches config to the indexer: pull-declares is refused, the list and the pull requests'
// config come from the indexer; hand-back returns to the mirrors until the lease write and is gone after it; from the
// lease write a trusted value is set only on an Approve; declared is written once per manifest (ADR 0565 §3, §4, §13).
func TestTheTakeOverAndTheHandBack(t *testing.T) {
c, m, r := anAgreeingMesh(t)
steps := []struct {
name string
do func() (string, error)
want string // what the answer or the error says
}{
{"declared is written when it differs, and once", func() (string, error) {
_ = m.Delete(DeclaredState, "dunst")
first, err := c.Reconcile()
again, _ := c.Reconcile()
return strings.Join(first.Declared, ",") + "|" + strings.Join(again.Declared, ","), err
}, "dunst|"},
{"pull-declares taken before the take-over", func() (string, error) {
a, err := c.PullDeclares(Call{Caller: ControllerCaller, Pull: "novox/mesh-catalog#7", Head: "h0",
Declares: map[string]any{}})
return answered(a), err
}, "keeps what"},
{"the take-over is asked", func() (string, error) {
_ = m.Delete(PullsState, "novox.mesh-catalog.7") // compare holds the told pull request against no candidate
a, err := c.TakeOver(Call{Why: "the release is delivered", Caller: "person.jochen"})
return answered(a), err
}, "is asked"},
{"the operator approves: switched to the indexer", func() (string, error) {
r.choose(t, r.lastAsk(t), OptionApprove, nil)
out, err := c.Decide()
return strings.Join(out, "; "), err
}, "reads its own sources from now"},
{"pull-declares refused after it, naming the indexer", func() (string, error) {
_, err := c.PullDeclares(Call{Caller: ControllerCaller, Pull: "novox/mesh-catalog#8", Head: "h1",
Declares: map[string]any{}})
return "", err
}, "config-candidates"},
{"the list comes from the indexer", func() (string, error) {
indexerList(t, m, "cccccccccccc", `{"font-size":{"type":"number","description":"text size","minimum":6,"maximum":72},
"agent-managed-settings":{"type":"object","description":"managed settings","trusted":true},
"theme":{"type":"string","description":"the desktop's theme"}}`)
res, err := c.Reconcile()
return res.Published, err
}, "theme"},
{"a pull request's config comes from the indexer", func() (string, error) {
candidate(t, m, "novox/mesh-catalog#9", "h9", `{"dunst":{"gap":{"kind":"","type":"number","default":2}}}`)
res, err := c.Reconcile()
return strings.Join(res.Candidates, "; "), err
}, "from the indexer"},
{"hand-back returns to the mirrors", func() (string, error) {
a, err := c.HandBack(Call{Why: "a difference was found after the switch", Caller: "person.jochen"})
return answered(a), err
}, "controller's records again"},
{"after it, declarations resolve from the mirror", func() (string, error) {
m.mirror(t, MirrorDeclaredRead, "dunst", strings.Replace(dunstDeclared, `"default":300`, `"default":400`, 1))
_, err := c.Reconcile()
return string(m.resolved(t, "g14", "dunst", "width").Value), err
}, "400"},
{"hand-back is gone after the lease write", func() (string, error) {
m.mirror(t, LeaseRead, "holder", `{"handed-to":"the operator's hand-over"}`)
_, err := c.HandBack(Call{Why: "again", Caller: "person.jochen"})
return "", err
}, "gone"},
{"the terminal sets no trusted value after the lease write", func() (string, error) {
_, err := c.Change(Call{Action: "set", Key: "managed", Module: "claude-code", Node: "g14", Value: map[string]any{},
Why: "x", Caller: ControllerCaller, By: "person.jochen"})
return "", err
}, "propose"},
}
for _, s := range steps {
got, err := s.do()
text := got
if err != nil {
text = err.Error()
}
if !strings.Contains(text, s.want) {
t.Errorf("%s: said %q, want %q", s.name, text, s.want)
}
}
if !slices.Contains(m.handActs, "handed config back to the controller's records | a difference was found after the switch | config-hand-back") {
t.Errorf("the hand-back is not recorded: %v", m.handActs)
}
if e, _ := m.Get(DeclaredState, "dunst"); e == nil || !strings.Contains(string(e.Value), `"manifest":"`) || !strings.Contains(string(e.Value), `"commit":"c1"`) {
t.Errorf("declared holds no dunst with its manifest: %v", e)
}
// An Approve that finds compare differing at the answer switches nothing.
c2, m2, r2 := anAgreeingMesh(t)
if _, err := c2.TakeOver(Call{Why: "now", Caller: "person.jochen"}); err != nil {
t.Fatal(err)
}
m2.mirror(t, MirrorDeclaredRead, "dunst", strings.Replace(dunstDeclared, `"default":300`, `"default":400`, 1))
r2.choose(t, r2.lastAsk(t), OptionApprove, nil)
out, _ := c2.Decide()
if sw, _ := c2.source(); sw.Source != SourceInterim || !strings.Contains(strings.Join(out, ""), "difference") {
t.Errorf("an Approve while compare differs switched: %s, %v", sw.Source, out)
}
}
// lastAsk is the newest ask the router took.
func (r *standIn) lastAsk(t *testing.T) string {
t.Helper()
r.mu.Lock()
defer r.mu.Unlock()
var newest string
var at time.Time
for id, a := range r.asks {
if newest == "" || a.Expires.After(at) {
newest, at = id, a.Expires
}
}
if newest == "" {
t.Fatal("the router took no ask")
}
return newest
}
func answered(a any) string {
raw, _ := json.Marshal(a)
return string(raw)
}