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

438 lines
18 KiB
Go

package main
import (
"encoding/json"
"errors"
"fmt"
"strings"
"sync"
"testing"
"time"
meshconditions "git.novox.be/novox/mesh-contracts/health-and-repair/go/seats/meshconditions"
occ "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/clients/operatorchannelclient"
oc "git.novox.be/novox/mesh-contracts/operator-and-conversation/go/seats/operatorchannel"
)
// fakeDelivery is the delivery parts as mesh-config asks them: each node's have, whether a node-engine states what it
// reads, and who reads a module's keys (nil rows and known false: the indexer does not serve the tool yet).
type fakeDelivery struct {
mu sync.Mutex
haves map[string]string // "<node>.<module>" → fingerprint
notSays map[string]bool // node → its node-engine states nothing
rows []ReaderRow
known bool
}
func (d *fakeDelivery) Have(node, module string) (string, bool, error) {
d.mu.Lock()
defer d.mu.Unlock()
fp, ok := d.haves[node+"."+module]
return fp, ok, nil
}
func (d *fakeDelivery) Reads(node string) error {
d.mu.Lock()
defer d.mu.Unlock()
if d.notSays[node] {
return errors.New("the verb manifest-reads is not served")
}
return nil
}
func (d *fakeDelivery) Readers(string) ([]ReaderRow, bool, error) {
d.mu.Lock()
defer d.mu.Unlock()
return d.rows, d.known, nil
}
// fakeVault records the retirements asked of the seat mesh-vault.
type fakeVault struct{ asked []RetireRequest }
func (v *fakeVault) Retire(r RetireRequest) error { v.asked = append(v.asked, r); return nil }
// The fingerprints of the first, second and third build of player.
var (
fp1 = "src2:" + strings.Repeat("1", 64)
fp2 = "src2:" + strings.Repeat("2", 64)
fp3 = "src2:" + strings.Repeat("3", 64)
)
const (
oldPlayer = `{"module":"player","config":{"volume":{"type":"number","default":5}}}`
newPlayer = `{"module":"player","config":{"level":{"type":"number","default":5,"renamed-from":"volume"}}}`
// donePlayer is the manifest once `renamed-from` left it.
donePlayer = `{"module":"player","config":{"level":{"type":"number","default":5}}}`
)
// aRenamingMesh is a mesh with player on g14 and novox, both running its first build (reading `volume`), a clock the
// test moves, and the delivery parts faked.
func aRenamingMesh(t *testing.T) (*Config, *memStore, *fakeDelivery, *time.Time) {
t.Helper()
m := newStore()
m.declare(t, "player", oldPlayer)
indexerList(t, m, "aaaaaaaaaaaa", theList)
for _, node := range []string{"g14", "novox"} {
m.assign(t, node, "player")
m.want(t, node, "player", fp1, oldPlayer)
}
clock := time.Date(2026, 11, 4, 10, 0, 0, 0, time.UTC)
d := &fakeDelivery{haves: map[string]string{"g14.player": fp1, "novox.player": fp1}, notSays: map[string]bool{}}
c := &Config{Store: m, Now: func() time.Time { return clock }, Logf: t.Logf, Delivery: d}
if _, err := c.Reconcile(); err != nil {
t.Fatal(err)
}
return c, m, d, &clock
}
func findingsOf(t *testing.T, m *memStore, kind string) []meshconditions.FindingsByKeyValue {
t.Helper()
var out []meshconditions.FindingsByKeyValue
for _, k := range m.keysOf(FindingsState) {
e, _ := m.Get(FindingsState, k)
f, err := meshconditions.DecodeFindingsByKeyValue(e.Value)
if err != nil {
t.Fatalf("finding %s: %v", k, err)
}
if f.Kind == kind {
out = append(out, f)
}
}
return out
}
// **A renamed value moves with its trust, both names answer until the node-engines run the new build, and the window
// closes per node** (design 32, C20): the value set under the old name is under the new one with no operator input,
// `renamed` is said once, the old name answers the new value on a node until that node's have runs a build that reads the
// new name (on `delivered`, or for a returning node on `manifest-reads-changed` once its node-engine states what it
// reads), and the rename goes once every window is closed and `renamed-from` left the manifest.
func TestARenamedValueMovesAndTheOldNameAnswersUntilEachNodeRunsTheNewBuild(t *testing.T) {
c, m, d, _ := aRenamingMesh(t)
set(t, c, Call{Key: "volume", Value: 8.0, Module: "player", Node: "g14"})
m.declare(t, "player", newPlayer)
r, err := c.Reconcile()
if err != nil || len(r.Renamed) != 1 {
t.Fatalf("reconcile: %+v, %v", r, err)
}
if v, ok := storedValue(t, m, "module.g14.player.level"); !ok || string(v.Value) != "8" || v.By == "" {
t.Errorf("the value is %+v under the new name (set: %v)", v, ok)
}
if _, ok := storedValue(t, m, "module.g14.player.volume"); ok {
t.Error("the old name still holds a value")
}
for _, c := range []struct{ node, key, want string }{{"g14", "level", "8"}, {"g14", "volume", "8"}, {"novox", "level", "5"}, {"novox", "volume", "5"}} {
if r := m.resolved(t, c.node, "player", c.key); string(r.Value) != c.want {
t.Errorf("%s answers %s on %s as %s, want %s", c.key, "player", c.node, r.Value, c.want)
}
}
if got := m.saidOf(EventRenamed); len(got) != 1 || got[0].(RenamedEvent).From != "volume" || got[0].(RenamedEvent).To != "level" {
t.Errorf("renamed was said as %+v", got)
}
// The history keeps the move; a second pass says and moves nothing more.
if h, _ := c.History(Call{Module: "player", Node: "g14"}); len(h) == 0 || h[len(h)-1].Action != "resolve" && h[len(h)-1].Action != "rename" {
t.Errorf("history %+v", h)
}
if _, err := c.Reconcile(); err != nil || len(m.saidOf(EventRenamed)) != 1 {
t.Errorf("a second pass said renamed %d times: %v", len(m.saidOf(EventRenamed)), err)
}
rows, _ := c.Renames("", true)
if len(rows) != 1 || strings.Join(rows[0].Open, ",") != "g14,novox" || strings.Join(rows[0].Moved, ",") != "g14" {
t.Fatalf("renames %+v", rows)
}
// A delivery while both nodes still run the first build closes nothing.
if said := c.Windows(""); len(said) != 2 || !strings.Contains(said[0], "still runs a build that reads volume") {
t.Errorf("windows said %v", said)
}
// g14 applies the new build.
m.want(t, "g14", "player", fp2, newPlayer)
d.haves["g14.player"] = fp2
if said := c.Windows(""); len(said) == 0 {
t.Error("g14 applied the new build and its window did not close")
}
if _, err := m.Get(ResolvedState, ResolvedKey("g14", "player", "volume")); err != nil {
t.Fatal(err)
} else if e, _ := m.Get(ResolvedState, ResolvedKey("g14", "player", "volume")); e != nil {
t.Error("g14 still answers the old name after its window closed")
}
m.resolved(t, "novox", "player", "volume") // novox still answers it
// novox applies it, but its node-engine, returning, states nothing yet: the window stays open.
m.want(t, "novox", "player", fp2, newPlayer)
d.haves["novox.player"] = fp2
d.notSays["novox"] = true
c.Windows("novox")
if rows, _ := c.Renames("", true); len(rows) != 1 || strings.Join(rows[0].Open, ",") != "novox" {
t.Errorf("a node-engine that states nothing closed its window: %+v", rows)
}
d.notSays["novox"] = false
c.Windows("novox")
if rows, _ := c.Renames("", true); len(rows) != 0 {
t.Errorf("windows still open: %+v", rows)
}
// The rename stays while `renamed-from` is in the manifest, and goes when it left.
if all, _ := c.Renames("", false); len(all) != 1 || all[0].Closed["g14"] != "applied" {
t.Errorf("renames %+v", all)
}
m.declare(t, "player", donePlayer)
if _, err := c.Reconcile(); err != nil {
t.Fatal(err)
}
if all, _ := c.Renames("", false); len(all) != 0 {
t.Errorf("the rename stayed after renamed-from left the manifest: %+v", all)
}
}
// A refused move is a finding, cleared once it moved (C4): both names declared, both names holding different values, a
// trusted new name over a value set without the operator's Approve. Nothing is moved, nothing is said.
func TestARefusedRenameIsAFindingAndIsClearedOnceItMoves(t *testing.T) {
both := `{"module":"player","config":{"volume":{"type":"number","default":5},"level":{"type":"number","default":5,"renamed-from":"volume"}}}`
trusted := `{"module":"player","config":{"level":{"type":"number","default":5,"trusted":true,"renamed-from":"volume"}}}`
for _, tc := range []struct {
name, manifest string
both bool
why string
fixed string
}{
{"both names declared", both, false, "declares both names", newPlayer},
{"both names hold a value", newPlayer, true, "both names hold a value", newPlayer},
{"a trusted name over an unapproved value", trusted, false, "without the operator's Approve", newPlayer},
} {
t.Run(tc.name, func(t *testing.T) {
c, m, _, _ := aRenamingMesh(t)
set(t, c, Call{Key: "volume", Value: 8.0, Module: "player", Node: "g14"})
if tc.both {
// The new name is set meanwhile, to another value, in the state a faulty hand wrote it.
if err := m.Put(ValuesState, "module.g14.player.level", Value{Value: json.RawMessage("9"), By: "x", At: time.Now()}); err != nil {
t.Fatal(err)
}
}
m.declare(t, "player", tc.manifest)
if _, err := c.Reconcile(); err != nil {
t.Fatal(err)
}
fs := findingsOf(t, m, FindingRenameRefused)
if len(fs) != 1 || !strings.Contains(fs[0].Summary, tc.why) || fs[0].Observer != Module {
t.Fatalf("findings %+v, want one saying %q", fs, tc.why)
}
if _, ok := storedValue(t, m, "module.g14.player.volume"); !ok || len(m.saidOf(EventRenamed)) != 0 {
t.Errorf("a refused rename moved the value or said renamed")
}
// Put right, it moves by hand and the finding goes.
m.declare(t, "player", tc.fixed)
if tc.both {
_ = m.Delete(ValuesState, "module.g14.player.level")
}
if _, err := c.RenameAgain("player", "level"); err != nil {
t.Fatal(err)
}
if fs := findingsOf(t, m, FindingRenameRefused); len(fs) != 0 {
t.Errorf("the finding stayed after the move: %+v", fs)
}
if v, ok := storedValue(t, m, "module.g14.player.level"); !ok || string(v.Value) != "8" {
t.Errorf("the value is %+v after the move", v)
}
})
}
}
// A window held open seven days is a finding naming the node, cleared when it closes; dropping a node by hand is a hand
// act with its why (C4, C5), and the seat's verb `renames` names the nodes holding a window open.
func TestAWindowHeldSevenDaysIsAFindingAndAHandCloseIsAHandAct(t *testing.T) {
c, m, d, clock := aRenamingMesh(t)
m.declare(t, "player", newPlayer)
if _, err := c.Reconcile(); err != nil {
t.Fatal(err)
}
m.want(t, "g14", "player", fp2, newPlayer)
d.haves["g14.player"] = fp2
c.Windows("")
*clock = clock.Add(8 * 24 * time.Hour)
if _, err := c.Reconcile(); err != nil {
t.Fatal(err)
}
fs := findingsOf(t, m, FindingWindowHeld)
if len(fs) != 1 || fs[0].Subject.Machine != "novox" || !strings.Contains(fs[0].Summary, "novox") {
t.Fatalf("findings %+v, want one naming novox", fs)
}
// The seat's verb names the node holding the window open.
var verb func(map[string]any) (any, error)
for _, tool := range Tools(c) {
if tool.Name == Seat+"."+RenamesVerb {
verb = tool.Run
}
}
got, err := verb(map[string]any{"module": "player"})
if raw, _ := json.Marshal(got); err != nil || !strings.Contains(string(raw), `"open":["novox"]`) {
t.Errorf("renames answered %s, %v", raw, err)
}
if _, err := c.RenameClose("player", "level", "novox", ""); err == nil {
t.Error("a window was dropped with no why")
}
if _, err := c.RenameClose("player", "level", "g14", "g14 is closed already"); err == nil {
t.Error("a node holding no open window was dropped")
}
m.handActs = nil
if _, err := c.RenameClose("player", "level", "novox", "novox is retired for good"); err != nil {
t.Fatal(err)
}
if len(m.handActs) != 1 || !strings.Contains(m.handActs[0], "novox is retired for good") {
t.Errorf("hand acts %v", m.handActs)
}
if fs := findingsOf(t, m, FindingWindowHeld); len(fs) != 0 {
t.Errorf("the finding stayed after the window closed: %+v", fs)
}
if rows, _ := c.Renames("", true); len(rows) != 0 {
t.Errorf("open renames %+v", rows)
}
}
// retiringMesh is a mesh where player no longer declares `volume`, a value of it is set on g14, and g14 runs the first
// build (which reads it).
func retiringMesh(t *testing.T) (*Config, *memStore, *fakeDelivery, *standIn, *fakeVault, *time.Time) {
t.Helper()
c, m, d, clock := aRenamingMesh(t)
set(t, c, Call{Key: "volume", Value: 8.0, Module: "player", Node: "g14"})
m.declare(t, "player", donePlayer)
r := newStandIn()
v := &fakeVault{}
c.Asks, c.Vault = r, v
d.known = true
d.rows = []ReaderRow{{Module: "player", Key: "volume", Kind: KindValue, Readers: []string{}}}
return c, m, d, r, v, clock
}
func retiringOf(t *testing.T, c *Config) []RetiringRow {
t.Helper()
got, err := c.ConfigRetiring("", "", "")
if err != nil {
t.Fatal(err)
}
return got.(map[string]any)["retiring"].([]RetiringRow)
}
// **A value no manifest reads is retired only on the operator's Approve** (C24): while a node runs a build that reads it
// it is not marked and nobody is asked; once none does, `retiring` is said and the operator is asked once; the value stays
// until the Approve, then it is removed, `retired` is said for the node it was set on, and the warrant is recorded.
func TestAValueNoManifestReadsIsRetiredOnlyOnTheOperatorsApprove(t *testing.T) {
c, m, d, r, _, _ := retiringMesh(t)
// The indexer does not serve the tool yet: nothing retires on that.
d.known = false
if said := c.Retirements("player"); len(said) != 1 || !strings.Contains(said[0], "does not say") || len(retiringOf(t, c)) != 0 {
t.Errorf("an indexer without the tool: %v", said)
}
d.known = true
// g14 still runs a build that reads volume: draining, no ask, no event.
c.Retirements("player")
if rows := retiringOf(t, c); len(rows) != 1 || rows[0].State != RetireDraining || strings.Join(rows[0].Nodes, ",") != "g14,novox" {
t.Fatalf("retiring %+v, want draining on g14 and novox", rows)
}
if len(r.asks) != 0 || len(m.saidOf(EventRetiring)) != 0 {
t.Fatalf("asked %d times and said retiring %d times while two nodes read the value", len(r.asks), len(m.saidOf(EventRetiring)))
}
// Both nodes apply the build that reads it no longer.
for _, node := range []string{"g14", "novox"} {
m.want(t, node, "player", fp3, donePlayer)
d.haves[node+".player"] = fp3
}
c.Windows("")
rows := retiringOf(t, c)
if len(rows) != 1 || rows[0].State != RetireAsked || len(r.asks) != 1 || len(m.saidOf(EventRetiring)) != 1 {
t.Fatalf("retiring %+v, %d asks, %d events", rows, len(r.asks), len(m.saidOf(EventRetiring)))
}
if _, ok := storedValue(t, m, "module.g14.player.volume"); !ok {
t.Fatal("the value was removed before the operator answered")
}
// A second manifest-accepted asks nobody again.
c.Retirements("player")
if len(r.asks) != 1 {
t.Errorf("asked %d times", len(r.asks))
}
r.choose(t, rows[0].Ask, OptionApprove, nil)
m.handActs = nil
if said, err := c.Decide(); err != nil || len(said) == 0 {
t.Fatalf("decide: %v, %v", said, err)
}
if _, ok := storedValue(t, m, "module.g14.player.volume"); ok {
t.Error("the approved value is still set")
}
if got := m.saidOf(EventRetired); len(got) != 1 || got[0].(RetiredEvent).Node != "g14" || got[0].(RetiredEvent).Name != "volume" {
t.Errorf("retired was said as %+v", got)
}
if len(retiringOf(t, c)) != 0 || len(m.handActs) == 0 {
t.Errorf("retiring %+v, hand acts %v", retiringOf(t, c), m.handActs)
}
}
// A declined retirement is not asked again until the value is read again; a value read again is dropped; an ask unanswered
// is asked once more after seven days and then written as a finding, cleared by an answer or a cancel; a secret is
// retired through the seat mesh-vault with the ask that warranted it (C24, C7).
func TestARetirementDeclinedUnansweredCancelledOrASecret(t *testing.T) {
c, m, d, r, v, clock := retiringMesh(t)
for _, node := range []string{"g14", "novox"} {
m.want(t, node, "player", fp3, donePlayer)
d.haves[node+".player"] = fp3
}
c.Retirements("player")
ask := retiringOf(t, c)[0].Ask
// Unanswered: the ask expires, a week passes, it is asked once more, expires again: a finding.
r.choose(t, ask, OptionApprove, func(w *oc.Warrant) { w.Outcome = occ.OutcomeExpired })
c.Decide()
if rows := retiringOf(t, c); rows[0].State != RetireUnanswered || len(findingsOf(t, m, FindingRetireUnanswered)) != 0 {
t.Fatalf("after the first expiry: %+v", rows)
}
*clock = clock.Add(8 * 24 * time.Hour)
c.Decide()
rows := retiringOf(t, c)
if rows[0].State != RetireAsked || len(r.asks) != 2 || rows[0].Ask == ask {
t.Fatalf("after a week: %+v, %d asks", rows, len(r.asks))
}
r.choose(t, rows[0].Ask, OptionApprove, func(w *oc.Warrant) { w.Outcome = occ.OutcomeExpired })
c.Decide()
if fs := findingsOf(t, m, FindingRetireUnanswered); len(fs) != 1 {
t.Fatalf("findings %+v after two asks unanswered", fs)
}
// Asked again by hand, then declined: the finding goes and it is not asked again.
if _, err := c.ConfigRetiring("ask-again", "player", "volume"); err != nil {
t.Fatal(err)
}
third := retiringOf(t, c)[0].Ask
r.choose(t, third, OptionDecline, nil)
c.Decide()
if rows := retiringOf(t, c); rows[0].State != RetireDeclined || len(findingsOf(t, m, FindingRetireUnanswered)) != 0 {
t.Fatalf("after the decline: %+v", rows)
}
asked := len(r.asks)
c.Retirements("player")
if len(r.asks) != asked {
t.Error("a declined retirement was asked again")
}
// Read again: dropped.
d.rows = []ReaderRow{{Module: "player", Key: "volume", Kind: KindValue, Readers: []string{"player"}}}
if said := c.Retirements("player"); len(said) != 1 || !strings.Contains(said[0], "read again") || len(retiringOf(t, c)) != 0 {
t.Errorf("read again: %v", said)
}
// A secret: its nodes come from the indexer's answer; on the Approve the vault is asked for each, once.
d.rows = []ReaderRow{{Module: "player", Key: "token", Kind: KindSecret, Readers: []string{}, Nodes: []string{}}}
c.Retirements("player")
secret := retiringOf(t, c)[0]
if secret.Kind != KindSecret || secret.State != RetireAsked {
t.Fatalf("the secret: %+v", secret)
}
r.choose(t, secret.Ask, OptionApprove, nil)
c.Decide()
if len(v.asked) != 2 || v.asked[0].Node != "g14" || v.asked[0].Name != "token" || v.asked[0].Ask != secret.Ask {
t.Errorf("the vault was asked %+v", v.asked)
}
if len(m.saidOf(EventRetired)) != 2 || len(retiringOf(t, c)) != 0 {
t.Errorf("retired %d times, retiring %+v", len(m.saidOf(EventRetired)), retiringOf(t, c))
}
// A secret whose readers' nodes the indexer cannot name is not marked.
d.rows = []ReaderRow{{Module: "player", Key: "other", Kind: KindSecret, Readers: []string{}}}
if c.Retirements("player"); len(retiringOf(t, c)) != 0 {
t.Errorf("a secret read on unknown nodes was marked: %+v", retiringOf(t, c))
}
_ = fmt.Sprint
}