Files
mesh-controller/cmd/mesh-controller/placement_words_moved_test.go
T
mesh-tools 97a2d841c7 Land feature 57's controller work on its release branch
The controller plans config, templates and moved providers' provisions,
hands bus accounts to the vault and judges the new manifest words, on the
feature's SDK and node-engine validator (feature 57, config and provisioning).
2026-10-15 21:27:33 +00:00

244 lines
9.4 KiB
Go

package main
import (
"encoding/json"
"os"
"path/filepath"
"reflect"
"sort"
"strings"
"testing"
"github.com/novox/mesh-controller/internal/catalogue"
"github.com/novox/mesh-controller/internal/link"
)
// wordedMesh is every live layer that says `endpoints`, `expose`, `reach` or `ports` (issue 736) — 36, read with
// the settings verb on 2026-10-14, ace's as anchor's and novox's as laptop's, one stand-in for an address — split as
// the store holds them once migration 0097 has run: the four words still among the settings, `places` and
// `accesses` already beside them. With each module shaped as its definition in the catalogue (mesh-catalog's, and
// the media catalogue's) is where the four words reach: its listens, what its containers publish and on which
// network, its route, its directories and accesses, and its mergeable files — every key of the layer, or the keys
// they name. The shapes are the modules'; the files' text and the images are the test's.
type wordedMesh struct {
Modules []json.RawMessage `json:"modules"`
Layers []struct {
Node string `json:"node"`
Module string `json:"module"`
Settings map[string]any `json:"settings"`
Placement map[string]any `json:"placement"`
} `json:"layers"`
}
func theWordedMesh(t *testing.T) wordedMesh {
t.Helper()
raw, err := os.ReadFile(filepath.Join("testdata", "placement-words-moved", "mesh.json"))
if err != nil {
t.Fatal(err)
}
var w wordedMesh
if err := json.Unmarshal(raw, &w); err != nil {
t.Fatal(err)
}
return w
}
// aMeshWithTheWordsInItsLayers is aMesh with every module of the worded mesh registered, a route provider on both
// machines, and its layers held as the store holds them before the four words move.
func aMeshWithTheWordsInItsLayers(t *testing.T) (*stores, wordedMesh) {
t.Helper()
open := aMesh(t)
ctx := t.Context()
inv := open.inventory
w := theWordedMesh(t)
image := "registry.invalid:5000/library@sha256:" + strings.Repeat("a", 64)
register(t, open, catalogue.Manifest{Module: "proxy", Version: "1", Provides: catalogue.FromAnywhere("route"),
Receives: map[string]string{"route": "/var/lib/proxy/routes.json"}})
for _, text := range w.Modules {
m, err := catalogue.ParseManifest([]byte(strings.ReplaceAll(string(text), "IMAGE", image)))
if err != nil {
t.Fatal(err)
}
register(t, open, m)
}
on := map[string][]string{"anchor": {"proxy"}, "laptop": {"proxy"}}
for _, l := range w.Layers {
settings, err := json.Marshal(l.Settings)
if err != nil {
t.Fatal(err)
}
inv.ExecForTest(t, `insert into settings (node, module, values) select id, $2, $3 from node where name = $1`,
l.Node, l.Module, string(settings))
if l.Placement != nil {
placement, err := json.Marshal(l.Placement)
if err != nil {
t.Fatal(err)
}
inv.ExecForTest(t, `insert into placement (node, module, values) select id, $2, $3 from node where name = $1`,
l.Node, l.Module, string(placement))
}
on[l.Node] = append(on[l.Node], l.Module)
}
for _, node := range []string{"anchor", "laptop"} {
if err := inv.SetPublicDomain(ctx, node, node+".example.test"); err != nil {
t.Fatal(err)
}
if _, err := assign(ctx, open, node, on[node]...); err != nil {
t.Fatalf("assign %s %v: %v", node, on[node], err)
}
}
return open, w
}
// toldOfTheWords is what a machine is told of the modules whose layers say the four words, and of the route
// provider that serves their names: those modules' resources and the filter's rules, as JSON a person can diff.
func toldOfTheWords(t *testing.T, open *stores, node string) string {
t.Helper()
ctx := t.Context()
gens, err := generators(ctx, open)
if err != nil {
t.Fatal(err)
}
plan, settings, err := planFor(ctx, open, node)
if err != nil {
t.Fatal(err)
}
with, _, err := renderingFor(ctx, open, node, plan, settings, gens, Allocating)
if err != nil {
t.Fatal(err)
}
composed, err := plan.Compose(with)
if err != nil {
t.Fatal(err)
}
if len(composed.LeftOut) > 0 {
t.Fatalf("%s leaves out %v", node, composed.LeftOut)
}
rules, err := plan.Rules(with)
if err != nil {
t.Fatal(err)
}
ours := map[string]bool{"proxy": true}
for _, l := range theWordedMesh(t).Layers {
ours[l.Module] = true
}
var told []map[string]any
for _, r := range composed.Resources {
if ours[composed.Owner[r["id"].(string)]] {
told = append(told, r)
}
}
sort.Slice(told, func(i, j int) bool { return told[i]["id"].(string) < told[j]["id"].(string) })
got, err := json.MarshalIndent(map[string]any{"resources": told, "rules": rules}, "", " ")
if err != nil {
t.Fatal(err)
}
return string(got) + "\n"
}
// sameAsTheGolden holds what a machine is told to the golden file the controller before issue 736 wrote from the
// same seed (MESH_WRITE_GOLDEN=1 writes it instead).
func sameAsTheGolden(t *testing.T, open *stores, when string) {
t.Helper()
for _, node := range []string{"anchor", "laptop"} {
got := toldOfTheWords(t, open, node)
golden := filepath.Join("testdata", "placement-words-moved", node+".json")
if os.Getenv("MESH_WRITE_GOLDEN") != "" {
if err := os.MkdirAll(filepath.Dir(golden), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(golden, []byte(got), 0o644); err != nil {
t.Fatal(err)
}
continue
}
want, err := os.ReadFile(golden)
if err != nil {
t.Fatal(err)
}
if string(want) != got {
t.Errorf("%s: %s is told something else:\n%s\nwas:\n%s", when, node, got, want)
}
}
}
// **The four words leave the settings layers and no machine is told anything new** (novox/hq ADR 0348 clause 11,
// issue 736). Every live layer that says `endpoints`, `expose`, `reach` or `ports` is seeded as the store holds it
// after delivery, on a module shaped as its definition is, and every machine's declaration is composed. What each
// is told is byte for byte what the controller before this change composed from the same seed.
func TestADeclarationIsTheSameWhileTheWordsAreStillInTheLayers(t *testing.T) {
open, _ := aMeshWithTheWordsInItsLayers(t)
sameAsTheGolden(t, open, "before the move")
}
// **And after the move, still nothing new** (issue 736). The move of placement, asked without confirming, says
// every layer that holds one of the four words and moves nothing; confirmed, it moves them all, no settings layer
// holds a placement key after, and each machine is told byte for byte what it was told before the move — and what
// the controller before this change told it. Asked again, it moves nothing.
func TestADeclarationIsTheSameAfterTheMoveOfPlacement(t *testing.T) {
open, w := aMeshWithTheWordsInItsLayers(t)
ctx := t.Context()
inv := open.inventory
asked, err := runMovePlacement(ctx, inv, false)
if err != nil || !asked.DryRun || len(asked.Moved) != len(w.Layers) {
t.Fatalf("the move, not confirmed, says %+v (%v); %d layers hold the words", asked, err, len(w.Layers))
}
if held := inv.QueryForTest(t, `select module from settings where values ?| $1`, catalogue.PlacementKeys()); len(held) != len(w.Layers) {
t.Fatalf("the move, not confirmed, moved something: %d layers still hold the words", len(held))
}
sameAsTheGolden(t, open, "after the move was asked without confirming")
moved, err := runMovePlacement(ctx, inv, true)
if err != nil || moved.DryRun || !reflect.DeepEqual(moved.Moved, asked.Moved) {
t.Fatalf("the move says %+v (%v), it said it would move %+v", moved, err, asked.Moved)
}
if held := inv.QueryForTest(t, `select module from settings where values ?| $1`, catalogue.PlacementKeys()); len(held) != 0 {
t.Fatalf("a settings layer still holds a placement key: %v", held)
}
sameAsTheGolden(t, open, "after the move")
if again, err := runMovePlacement(ctx, inv, true); err != nil || len(again.Moved) != 0 {
t.Fatalf("asked again, the move moved %+v (%v)", again, err)
}
if said := describeMovePlacement(movePlacementAnswer{}); !strings.Contains(said, "nothing to move") {
t.Fatalf("an empty move says %q", said)
}
}
// The verb's command line: a dry run unless confirmed, and a confirmed move says why, recorded as a person's step
// of the release, never a repair (issue 736).
func TestTheMoveOfPlacementIsADryRunUnlessConfirmed(t *testing.T) {
for _, c := range []struct {
args map[string]any
want string
}{
{map[string]any{}, "move-placement --json"},
{map[string]any{"confirm": "false"}, "move-placement --json"},
{map[string]any{"confirm": "true", "why": "release 57 is delivered"}, "move-placement --json --confirm --why release 57 is delivered"},
} {
argv, err := argvFor("move-placement", c.args)
if err != nil || strings.Join(argv, " ") != c.want {
t.Errorf("%v: %q %v, want %q", c.args, argv, err, c.want)
}
}
for _, args := range []map[string]any{
{"confirm": "true"}, // a move without a why
{"why": "tidy"}, // a why for a dry run, recorded nowhere
{"confirm": "yes", "why": "x"}, // a switch is true or false
{"node": "anchor"},
} {
if argv, err := argvFor("move-placement", args); err == nil {
t.Errorf("%v was composed as %q", args, argv)
}
}
if repairingCommand([]string{"move-placement", "--json", "--confirm", "--why", "x"}) != "move-placement" {
t.Error("a confirmed move through the generic verb would go unrecorded")
}
if repairingCommand([]string{"move-placement", "--json"}) != "" {
t.Error("a dry run was taken for an act by hand")
}
if !personsDecision(link.HandAct{Verb: "move-placement"}) {
t.Error("a move of the release would count toward a healer the mesh lacks")
}
}