The node-engine alone composes (ADR 0593 §6, §8), so the provisioner stops composing with frombus and fromruntime and reads each module's plan from composed-<node> (read as mesh-host.composed): at its want, or the build its have runs, and at the indexer's newest build, so a key ahead of its want still carries its plan entry. A refused or not yet published composition holds the module with its last plan kept. The golden test proves the plans and provisions are today's, captured from the composing holder before the change. It no longer reads the builds or the resolved values. Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
205 lines
8.2 KiB
Go
205 lines
8.2 KiB
Go
package main
|
|
|
|
// The provisioner makes every plan from the provisions the node-engine's published compositions carry (novox/hq ADR
|
|
// 0593 §6, §8), with the plans and provisions it wrote when it composed them itself.
|
|
//
|
|
// The fixtures were captured on 2026-11-02 from the holder as it stood before (frombus.PlansHeldTwice over mesh-sdk's
|
|
// frombustest.Provisioned mesh: restic on n1 requiring postgres-database from postgres there, the vault on n2):
|
|
// testdata/<case>today-composed.json is what that composition answered at the node's wants and at its newest builds,
|
|
// testdata/<case>today-states.golden.json what the holder then wrote. "ahead-" has restic wanted at an older build
|
|
// that requires nothing, so its key is ahead of its want and carries its plan entry (issue 1126 review 1).
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
|
"git.novox.be/novox/mesh-sdk/go/delivery"
|
|
)
|
|
|
|
// today is a captured composition: the node, the builds its wants named, and the plans at them and at the newest.
|
|
type today struct {
|
|
Node string `json:"node"`
|
|
KeptAt map[string]string `json:"kept-at"`
|
|
AtWants struct {
|
|
Plans map[string]catalogue.Plan `json:"Plans"`
|
|
Held map[string]string `json:"Held"`
|
|
} `json:"at-wants"`
|
|
Newest struct {
|
|
Plans map[string]catalogue.Plan `json:"Plans"`
|
|
} `json:"newest"`
|
|
}
|
|
|
|
// fpsOf is frombustest's modules and the fingerprints they are built at (frombustest.FPs).
|
|
var fpsOf = map[string]string{"restic": "src2:" + strings.Repeat("3", 64), "mesh-provisioner-client": "src2:" + strings.Repeat("4", 64),
|
|
"store": "src2:" + strings.Repeat("5", 64), "postgres": "src2:" + strings.Repeat("8", 64)}
|
|
|
|
// published puts on the store what n1's engine publishes for the captured case: each placed module at its newest done
|
|
// build, and each wanted one at its want, each key carrying its plan.
|
|
func published(t *testing.T, s *memStore, c today) {
|
|
t.Helper()
|
|
put := func(module, fp string, plan catalogue.Plan, has bool) {
|
|
v := map[string]any{"v": 1, "node": c.Node, "module": module, "fp": fp, "resources": []any{}}
|
|
if has {
|
|
v["plan"] = plan
|
|
}
|
|
raw, _ := json.Marshal(v)
|
|
s.write(t, ComposedRead, mustKey(delivery.ComposedKey(c.Node, module, fp)), string(raw))
|
|
}
|
|
for module, fp := range fpsOf {
|
|
plan, has := c.Newest.Plans[module]
|
|
put(module, fp, plan, has)
|
|
}
|
|
for module, fp := range c.KeptAt {
|
|
plan, has := c.AtWants.Plans[module]
|
|
put(module, fp, plan, has)
|
|
}
|
|
s.write(t, ComposedRead, mustKey(delivery.ComposedNodeKey(c.Node)), `{"v":1,"node":"`+c.Node+`","resources":[]}`)
|
|
}
|
|
|
|
// goldenMesh is the store as the provisioner reads it for frombustest's mesh: the indexer's modules, n1's placement, the
|
|
// vault's on n2, restic's want.
|
|
func goldenMesh(t *testing.T, c today) *memStore {
|
|
t.Helper()
|
|
s := newStore()
|
|
for module, fp := range fpsOf {
|
|
// A provider's manifest says whether it keeps entries: postgres's, as frombustest has it, keeps none.
|
|
raw := `{"module":"` + module + `","version":"1"}`
|
|
if module == "postgres" {
|
|
raw = `{"module":"postgres","version":"1","provides":[{"name":"postgres-database","scope":"mesh"}],` +
|
|
`"serves":{"postgres-database":{"port":5432}}}`
|
|
}
|
|
digest := delivery.ManifestDigest([]byte(raw))
|
|
value, _ := delivery.EncodeManifest(delivery.Manifest{Module: module, JSON: raw})
|
|
s.write(t, ManifestsRead, mustKey(delivery.ManifestsKey(digest)), string(value))
|
|
s.write(t, ModulesRead, module, `{"v":1,"fp2":"`+fp+`","manifest":"`+digest+`"}`)
|
|
}
|
|
s.write(t, PlacementRead, "placement."+c.Node, `{"v":1,"node":"`+c.Node+`","decided":{},"modules":[{"module":"store"},`+
|
|
`{"module":"postgres"},{"module":"mesh-provisioner-client"},{"module":"restic"}]}`)
|
|
s.write(t, PlacementRead, "placement.n2", `{"v":1,"node":"n2","decided":{},"modules":[{"module":"mesh-vault"}]}`)
|
|
s.write(t, WantsRead, "want."+c.Node+".restic", `{"v":1,"fp":"`+c.KeptAt["restic"]+`","self":true}`)
|
|
return s
|
|
}
|
|
|
|
// The plans and provisions made from the published compositions are today's, value for value, with a key ahead of its
|
|
// want carrying its plan entry; a pass over the same compositions writes nothing.
|
|
func TestThePlansFromCompositionsAreTodays(t *testing.T) {
|
|
for _, name := range []string{"", "ahead-"} {
|
|
t.Run("case "+name, func(t *testing.T) {
|
|
var c today
|
|
readJSON(t, "testdata/"+name+"today-composed.json", &c)
|
|
var golden map[string]map[string]json.RawMessage
|
|
readJSON(t, "testdata/"+name+"today-states.golden.json", &golden)
|
|
s := goldenMesh(t, c)
|
|
published(t, s, c)
|
|
now := time.Date(2026, 11, 2, 18, 0, 0, 0, time.UTC)
|
|
h := &Holder{Store: s, Asks: &router{}, Logf: t.Logf, Now: func() time.Time { return now }}
|
|
h.Compose = FromCompositions(s)
|
|
out, err := h.ComposeNode(context.Background(), c.Node)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(out.Problems)+len(out.Held) > 0 {
|
|
t.Errorf("problems %v, held %v", out.Problems, out.Held)
|
|
}
|
|
for state, want := range golden {
|
|
keys, _ := s.Keys(state)
|
|
if len(keys) != len(want) {
|
|
t.Errorf("%s holds %v, today %d keys", state, keys, len(want))
|
|
}
|
|
for key, raw := range want {
|
|
e, _ := s.Get(state, key)
|
|
if e == nil || !sameBytes(t, e.Value, raw) {
|
|
t.Errorf("%s %s:\n got %s\nwant %s", state, key, valueOf(e), raw)
|
|
}
|
|
}
|
|
}
|
|
again, err := h.ComposeNode(context.Background(), c.Node)
|
|
if err != nil || len(again.Plans)+len(again.Gone)+len(again.Changed) > 0 {
|
|
t.Errorf("a second pass over the same compositions wrote %+v (%v)", again, err)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
// A composition the engine refused at the wanted build holds its module, its last plan kept and its words in `refused`;
|
|
// one the engine has not published yet holds it too, saying so; a module whose newest build is refused keeps its
|
|
// plan at its want.
|
|
func TestARefusedOrMissingCompositionHoldsItsModule(t *testing.T) {
|
|
var c today
|
|
readJSON(t, "testdata/today-composed.json", &c)
|
|
s := goldenMesh(t, c)
|
|
published(t, s, c)
|
|
h := &Holder{Store: s, Asks: &router{}, Logf: t.Logf, Now: time.Now}
|
|
h.Compose = FromCompositions(s)
|
|
if _, err := h.ComposeNode(context.Background(), c.Node); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
plan, _ := s.Get(PlansState, "n1.restic")
|
|
key := mustKey(delivery.ComposedKey("n1", "restic", fpsOf["restic"]))
|
|
s.write(t, ComposedRead, key, `{"v":1,"node":"n1","module":"restic","fp":"`+fpsOf["restic"]+`","resources":[],`+
|
|
`"refused":"restic requires postgres-database and no module offers it"}`)
|
|
out, err := h.ComposeNode(context.Background(), c.Node)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(out.Held["restic"], "no module offers it") {
|
|
t.Errorf("the refused module is held as %v", out.Held)
|
|
}
|
|
if e, _ := s.Get(PlansState, "n1.restic"); e == nil || string(e.Value) != string(plan.Value) {
|
|
t.Errorf("the held module's plan was not kept: %s", valueOf(e))
|
|
}
|
|
if e, _ := s.Get(RefusedState, "n1.restic"); e == nil || !strings.Contains(string(e.Value), "no module offers it") {
|
|
t.Errorf("refused says %s", valueOf(e))
|
|
}
|
|
if p, _ := s.Get(ProvisionsState, "n1.restic.postgres-database"); p == nil || strings.Contains(string(p.Value), "retiring") {
|
|
t.Errorf("the held module's key: %s", valueOf(p))
|
|
}
|
|
// Not published yet: the want moved to a build the engine has not composed.
|
|
other := "src2:" + strings.Repeat("9", 64)
|
|
s.write(t, WantsRead, "want.n1.restic", `{"v":1,"fp":"`+other+`","self":true}`)
|
|
out, err = h.ComposeNode(context.Background(), c.Node)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(out.Held["restic"], "not published") {
|
|
t.Errorf("the unpublished composition holds restic as %v", out.Held)
|
|
}
|
|
if e, _ := s.Get(PlansState, "n1.restic"); e == nil || string(e.Value) != string(plan.Value) {
|
|
t.Errorf("the plan was not kept while unpublished: %s", valueOf(e))
|
|
}
|
|
}
|
|
|
|
func readJSON(t *testing.T, path string, v any) {
|
|
t.Helper()
|
|
raw, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := json.Unmarshal(raw, v); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func sameBytes(t *testing.T, a, b []byte) bool {
|
|
t.Helper()
|
|
var x, y any
|
|
if json.Unmarshal(a, &x) != nil || json.Unmarshal(b, &y) != nil {
|
|
return false
|
|
}
|
|
ra, _ := json.Marshal(x)
|
|
rb, _ := json.Marshal(y)
|
|
return string(ra) == string(rb)
|
|
}
|
|
|
|
func valueOf(e *Entry) string {
|
|
if e == nil {
|
|
return "(none)"
|
|
}
|
|
return string(e.Value)
|
|
}
|