Files
mesh-host/internal/compose/resolution/backup_placement_test.go
T
jschoubben d68e241724 Take the catalogue's composition rules into the node-engine
ADR 0593 §7: the composer with its refusals leaves mesh-sdk, so mesh-sdk
carries no composition rule. The catalogue functions only the
node-engine's composition reaches (Resolve, Resolution's Compose,
Declaration, DataItems, LeftOut, Rules and the rest, the settings,
placeholder and template refusals, the rendering helpers) are copied
with their tests into internal/compose/resolution; a method of a
mesh-sdk shape becomes a function taking it first. The composition
calls its own copies, and the captured catalogue its tests read lives
here now, shared by mesh-compose's tests.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 19:15:55 +01:00

115 lines
4.6 KiB
Go

package resolution
import (
"strings"
"testing"
mcat "git.novox.be/novox/mesh-sdk/go/catalogue"
)
const oldBackupText = "# lines\n${contribution:node-backup:backup}"
const oldDataText = "# data\n${contribution:node-backup:data}"
const placedBackupTxt = "# lines\n${backup:lines}"
const placedDataTxt = "# data\n${backup:data}"
const backupHolder = `{"module":"restic","version":"1","moved":true,"claims":[{"name":"node-backup","scope":"node"}],
"build":{"artifacts":[{"name":"files","kind":"archive","from":"files"}]},
"resources":[{"id":"declared","type":"file","path":"/etc/backups.conf","template":"files/backups"},
{"id":"data-declared","type":"file","path":"/etc/data.conf","template":"files/data"}]}`
func dataModule(name string) string {
return `{"module":"` + name + `","version":"1","resources":[{"id":"d","type":"directory","mode":"0700"}],
"data":{"own":[{"id":"d","path":"${dir:d}","class":"irreplaceable","why":"the ` + name + ` data"}]}}`
}
// backupsOn is a machine of a holder whose files say these texts and the modules that declare data.
func backupsOn(t *testing.T, lines, data string, modules ...string) mcat.Resolution {
t.Helper()
all := []mcat.Manifest{held(t, backupHolder, builtWith("restic", map[string]string{"backups": lines, "data": data}))}
names := []string{"restic"}
for _, name := range modules {
all = append(all, mustParse(t, dataModule(name)))
names = append(names, name)
}
r, err := Resolve(shelf(all...), names, workstation(), mcat.World{})
if err != nil {
t.Fatal(err)
}
r.Account = "op"
return r
}
var backupModules = []string{"alpha", "beta", "gamma"}
// gammaLeftOut is a machine whose gamma does not place: its lines are kept, best effort.
func gammaLeftOut(with mcat.Rendering) mcat.Rendering {
with.NotPlaced = map[string]string{"gamma": "its placement setting does not read"}
return with
}
func TestTheBackupPlacementIsTheTextTheOldPlaceholdersRender(t *testing.T) {
// The old form is an unmoved holder: its placeholders are the controller's alone.
oldHolder := strings.Replace(backupHolder, `"moved":true,`, "", 1)
all := []mcat.Manifest{held(t, oldHolder, builtWith("restic", map[string]string{"backups": oldBackupText, "data": oldDataText}))}
for _, name := range backupModules {
all = append(all, mustParse(t, dataModule(name)))
}
old, err := Resolve(shelf(all...), append([]string{"restic"}, backupModules...), workstation(), mcat.World{})
if err != nil {
t.Fatal(err)
}
old.Account = "op"
wantLines, wantData := fileOf(t, old, gammaLeftOut(mcat.Rendering{}), "restic.declared"), fileOf(t, old, gammaLeftOut(mcat.Rendering{}), "restic.data-declared")
if !strings.Contains(wantLines, "# alpha\npath ") || !strings.Contains(wantLines, "# gamma\n") || !strings.Contains(wantData, "item d") {
t.Fatalf("the old form renders\n%s\n%s", wantLines, wantData)
}
moved := backupsOn(t, placedBackupTxt, placedDataTxt, backupModules...)
// Not handed: the controller fills the new placeholders with the old text.
if got := fileOf(t, moved, gammaLeftOut(mcat.Rendering{}), "restic.declared"); got != wantLines {
t.Errorf("not handed, the lines are\n%s\nwant\n%s", got, wantLines)
}
if got := fileOf(t, moved, gammaLeftOut(mcat.Rendering{}), "restic.data-declared"); got != wantData {
t.Errorf("not handed, the data is\n%s\nwant\n%s", got, wantData)
}
// Handed: the plan carries the same text as placement, and the files are the client's.
c, _ := composeWith(t, moved, gammaLeftOut(handingTo("store:5000")))
p := c.Plans["restic"].Placement
if got := "# lines\n" + p["backup:lines"]; got != wantLines {
t.Errorf("handed, the plan's lines are\n%s\nwant\n%s", got, wantLines)
}
if got := "# data\n" + p["backup:data"]; got != wantData {
t.Errorf("handed, the plan's data is\n%s\nwant\n%s", got, wantData)
}
if len(p) != 2 {
t.Errorf("the plan's placement is %v", p)
}
}
func TestAnUnknownBackupKeyIsRefused(t *testing.T) {
r := backupsOn(t, "${backup:rows}", placedDataTxt, backupModules...)
if _, err := Compose(r, handingTo("store:5000")); err == nil || !strings.Contains(err.Error(), "backup:rows") {
t.Errorf("handed, an unknown key is %v", err)
}
if _, err := Declaration(r, mcat.Rendering{}); err == nil || !strings.Contains(err.Error(), "backup:rows") {
t.Errorf("not handed, an unknown key is %v", err)
}
}
func fileOf(t *testing.T, r mcat.Resolution, with mcat.Rendering, id string) string {
t.Helper()
out, err := Declaration(r, with)
if err != nil {
t.Fatal(err)
}
for _, res := range out {
if res["id"] == id {
return res["content"].(string)
}
}
t.Fatalf("%s was not composed", id)
return ""
}