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
112 lines
5.9 KiB
Go
112 lines
5.9 KiB
Go
package resolution
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
|
|
mcat "git.novox.be/novox/mesh-sdk/go/catalogue"
|
|
)
|
|
|
|
const misspelledShell = "${shel:zsh:first}"
|
|
|
|
const undeclaredSetting = "${setting:Undeclared}"
|
|
|
|
const misspelledMachine = "${machnie:address}"
|
|
|
|
const shellsOwn = "${XDG_CACHE_HOME:-x}"
|
|
|
|
// The shell's operators after a word the mesh also uses, in any case: the shell's, and passed.
|
|
const shellsOperators = "${PORT:-8080} ${SHELL:-/bin/sh} ${SECRET:?unset} ${dir:-/tmp}"
|
|
|
|
// Composition refuses the same placeholders in the same words — a manifest the store already holds
|
|
// was never parsed by this binary — and a namespace the mesh knows, written in a field its pass does
|
|
// not read, is refused there too, because it would reach the machine as the same literal text.
|
|
func TestAMisspelledPlaceholderIsRefusedAtComposition(t *testing.T) {
|
|
for _, c := range []struct {
|
|
field string
|
|
r map[string]any
|
|
token string
|
|
}{
|
|
{"content", map[string]any{"id": "rc", "type": "file", "path": "/etc/speller.rc", "content": "a=" + misspelledShell + "\n"}, misspelledShell},
|
|
{"content", map[string]any{"id": "rc", "type": "file", "path": "/etc/speller.rc", "content": "b=" + undeclaredSetting + "\n"}, undeclaredSetting},
|
|
{"content", map[string]any{"id": "rc", "type": "file", "path": "/etc/speller.rc", "content": "c=" + misspelledMachine + "\n"}, misspelledMachine},
|
|
{"env.NAME", map[string]any{"id": "rc", "type": "process", "name": "speller", "env": map[string]any{"NAME": "${machine:name}"}}, "${machine:name}"},
|
|
{"content", map[string]any{"id": "rc", "type": "file", "path": "/etc/speller.rc", "content": "${Machine:address}"}, "${Machine:address}"},
|
|
{"content", map[string]any{"id": "rc", "type": "file", "path": "/etc/speller.rc", "content": "${machine:.address}"}, "${machine:.address}"},
|
|
{"content", map[string]any{"id": "rc", "type": "file", "path": "/etc/speller.rc", "content": "${machine: address}"}, "${machine: address}"},
|
|
{"content", map[string]any{"id": "rc", "type": "file", "path": "/etc/speller.rc", "content": "${Dir:x}"}, "${Dir:x}"},
|
|
{"content", map[string]any{"id": "rc", "type": "file", "path": "/etc/speller.rc", "content": "a=${machine:address\nb=1"}, "${machine:address"},
|
|
} {
|
|
m := mcat.Manifest{Module: "speller", Version: "1", Resources: []map[string]any{c.r}}
|
|
r := mcat.Resolution{Node: "workstation", Account: "op", Modules: []mcat.Manifest{m}}
|
|
_, err := Declaration(r, mcat.Rendering{})
|
|
if err == nil || !strings.Contains(err.Error(), "speller's resource rc holds "+c.token+" in its "+c.field) {
|
|
t.Errorf("%s in its %s was composed rather than refused by name: %v", c.token, c.field, err)
|
|
}
|
|
}
|
|
|
|
m := mcat.Manifest{Module: "speller", Version: "1", Resources: []map[string]any{
|
|
{"id": "rc", "type": "file", "path": "/etc/speller.rc", "content": "d=" + shellsOwn + " " + shellsOperators + "\n"},
|
|
{"id": "server", "type": "process", "name": "server", "env": map[string]any{"PORT": "${PORT:-80}"}},
|
|
}}
|
|
out, err := Declaration(mcat.Resolution{Node: "workstation", Account: "op", Modules: []mcat.Manifest{m}}, mcat.Rendering{})
|
|
if err != nil {
|
|
t.Fatalf("the shell's own syntax was refused: %v", err)
|
|
}
|
|
if got := contentOf(t, out, "speller.rc"); got != "d="+shellsOwn+" "+shellsOperators+"\n" {
|
|
t.Fatalf("the shell's own syntax did not pass through as written: %q", got)
|
|
}
|
|
}
|
|
|
|
// contentOf is the content of the composed resource with this id, failing when it was not composed.
|
|
func contentOf(t *testing.T, out []map[string]any, id string) string {
|
|
t.Helper()
|
|
for _, res := range out {
|
|
if res["id"] == id {
|
|
return plainly(res["content"])
|
|
}
|
|
}
|
|
t.Fatalf("%s was not composed: %v", id, out)
|
|
return ""
|
|
}
|
|
|
|
// What a value puts into a file is its software's text, not the definition's, and is never swept
|
|
// (review of mesh-controller #211): an operator's setting holding `${labels:instance}` filled through
|
|
// ${setting:…}, and a JSON setting `${level:upper}` merged into a mergeable file, reach the machine as
|
|
// set — software that templates its own configuration (Log4j's `${env:…}`, Spring's `${timeout:30}`)
|
|
// is configured exactly this way.
|
|
func TestAValueHoldingAPlaceholderShapeComposesUnchanged(t *testing.T) {
|
|
m := mcat.Manifest{Module: "templater", Version: "1", Resources: []map[string]any{
|
|
{"id": "conf", "type": "file", "path": "/etc/templater.conf", "content": "template=${setting:template}\n"},
|
|
{"id": "json", "type": "file", "path": "/etc/templater.json", "merge": mcat.MergeJSON, "content": `{"fmt":"plain"}`},
|
|
}}
|
|
settings := mcat.SettingsBy{"templater": {{From: "the operator", Values: map[string]any{
|
|
"template": "${labels:instance}", "fmt": "${level:upper}",
|
|
}}}}
|
|
out, err := Declaration(mcat.Resolution{Node: "workstation", Account: "op", Modules: []mcat.Manifest{m}}, mcat.Rendering{Settings: settings})
|
|
if err != nil {
|
|
t.Fatalf("a value holding a placeholder's shape was refused as the definition's: %v", err)
|
|
}
|
|
if got := contentOf(t, out, "templater.conf"); got != "template=${labels:instance}\n" {
|
|
t.Errorf("the setting did not reach the file as set: %q", got)
|
|
}
|
|
if got := contentOf(t, out, "templater.json"); !strings.Contains(got, `${level:upper}`) {
|
|
t.Errorf("the merged setting did not reach the file as set: %q", got)
|
|
}
|
|
}
|
|
|
|
// Contributed shell code is the shell's, and no pass reads it (novox/hq ADR 0204): zsh's own
|
|
// `${path:t}` is namespace-shaped, and reaches the holder's file untouched.
|
|
func TestContributedShellCodeIsNotSwept(t *testing.T) {
|
|
modules := []mcat.Manifest{zshHolder(), {Module: "modifier", Shell: []mcat.ShellCode{
|
|
{For: "zsh", Slot: "normal", Code: "echo ${path:t} ${shel:zsh:first}"},
|
|
}}}
|
|
out, err := Declaration(mcat.Resolution{Node: "workstation", Account: "op", Modules: modules}, mcat.Rendering{})
|
|
if err != nil {
|
|
t.Fatalf("contributed shell code was swept: %v", err)
|
|
}
|
|
if got := contentOf(t, out, "zsh.zshrc"); !strings.Contains(got, "echo ${path:t} ${shel:zsh:first}") {
|
|
t.Fatalf("the contributed code did not reach the holder's file as written: %q", got)
|
|
}
|
|
}
|