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
This commit is contained in:
@@ -0,0 +1,238 @@
|
||||
package resolution
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
mcat "git.novox.be/novox/mesh-sdk/go/catalogue"
|
||||
)
|
||||
|
||||
// contributors is a fixed set of contributions, in no particular order: what the renderings are
|
||||
// asserted against byte for byte. go-toolchain and zsh both put ~/.local/bin on PATH, which is the
|
||||
// ordinary case of two modules sharing a directory, and is written once.
|
||||
func contributors() []mcat.Manifest {
|
||||
return []mcat.Manifest{
|
||||
{Module: "zsh", Environment: &mcat.Environment{
|
||||
Variables: map[string]string{"XDG_CONFIG_HOME": "${machine:account-home}/.config", "EDITOR": "vim"},
|
||||
Path: []mcat.PathEntry{
|
||||
{Entry: "${machine:account-home}/.local/bin", At: mcat.PathAtStart},
|
||||
{Entry: "${machine:account-home}/bin", At: mcat.PathAtStart},
|
||||
{Entry: "/opt/scripts", At: mcat.PathAtEnd},
|
||||
},
|
||||
}},
|
||||
{Module: "go-toolchain", Environment: &mcat.Environment{
|
||||
Variables: map[string]string{"GOPATH": "${machine:account-home}/go"},
|
||||
Path: []mcat.PathEntry{
|
||||
{Entry: "${machine:account-home}/go/bin", At: mcat.PathAtStart},
|
||||
{Entry: "/usr/local/go/bin", At: mcat.PathAtStart},
|
||||
{Entry: "${machine:account-home}/.local/bin", At: mcat.PathAtStart},
|
||||
},
|
||||
}},
|
||||
{Module: "agent", Environment: &mcat.Environment{
|
||||
Variables: map[string]string{"DISABLE_AUTOUPDATER": "1"},
|
||||
Path: []mcat.PathEntry{{Entry: "/opt/agent/bin", At: mcat.PathAtEnd}},
|
||||
}},
|
||||
// A module contributing nothing is in the set and writes nothing.
|
||||
{Module: "postgres"},
|
||||
}
|
||||
}
|
||||
|
||||
var operatorFacts = map[string]string{"name": "workstation", "account": "op", "account-home": "/home/op"}
|
||||
|
||||
// The final PATH this set composes, around whatever the account had: the start entries in module
|
||||
// order and then declared order, the account's own, then the end entries.
|
||||
const composedPOSIX = `# agent
|
||||
export DISABLE_AUTOUPDATER='1'
|
||||
# go-toolchain
|
||||
export GOPATH='/home/op/go'
|
||||
# zsh
|
||||
export EDITOR='vim'
|
||||
export XDG_CONFIG_HOME='/home/op/.config'
|
||||
# zsh
|
||||
case ":${PATH}:" in *':/home/op/bin:'*) ;; *) PATH='/home/op/bin'"${PATH:+:${PATH}}" ;; esac
|
||||
# go-toolchain
|
||||
case ":${PATH}:" in *':/home/op/.local/bin:'*) ;; *) PATH='/home/op/.local/bin'"${PATH:+:${PATH}}" ;; esac
|
||||
case ":${PATH}:" in *':/usr/local/go/bin:'*) ;; *) PATH='/usr/local/go/bin'"${PATH:+:${PATH}}" ;; esac
|
||||
case ":${PATH}:" in *':/home/op/go/bin:'*) ;; *) PATH='/home/op/go/bin'"${PATH:+:${PATH}}" ;; esac
|
||||
# agent
|
||||
case ":${PATH}:" in *':/opt/agent/bin:'*) ;; *) PATH="${PATH:+${PATH}:}"'/opt/agent/bin' ;; esac
|
||||
# zsh
|
||||
case ":${PATH}:" in *':/opt/scripts:'*) ;; *) PATH="${PATH:+${PATH}:}"'/opt/scripts' ;; esac
|
||||
export PATH
|
||||
`
|
||||
|
||||
const composedSystemd = `# agent
|
||||
DISABLE_AUTOUPDATER=1
|
||||
# go-toolchain
|
||||
GOPATH=/home/op/go
|
||||
# zsh
|
||||
EDITOR=vim
|
||||
XDG_CONFIG_HOME=/home/op/.config
|
||||
# go-toolchain, zsh
|
||||
PATH=/home/op/go/bin:/usr/local/go/bin:/home/op/.local/bin:/home/op/bin${PATH:+:$PATH}
|
||||
# agent, zsh
|
||||
PATH=${PATH:+$PATH:}/opt/agent/bin:/opt/scripts
|
||||
`
|
||||
|
||||
func TestTheEnvironmentRendersForAPOSIXShellByteForByte(t *testing.T) {
|
||||
env, err := environmentOn(contributors(), operatorFacts)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := env.posix(); got != composedPOSIX {
|
||||
t.Fatalf("the POSIX rendering is\n%s\nnot\n%s", got, composedPOSIX)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTheEnvironmentRendersForTheServiceManagerByteForByte(t *testing.T) {
|
||||
env, err := environmentOn(contributors(), operatorFacts)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := env.systemd(); got != composedSystemd {
|
||||
t.Fatalf("the environment.d rendering is\n%s\nnot\n%s", got, composedSystemd)
|
||||
}
|
||||
}
|
||||
|
||||
// Nothing contributed renders nothing, in both formats — not an empty `export PATH`.
|
||||
func TestNoContributionsRenderNothing(t *testing.T) {
|
||||
env, err := environmentOn([]mcat.Manifest{{Module: "postgres"}}, operatorFacts)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if env.posix() != "" || env.systemd() != "" {
|
||||
t.Fatalf("an empty environment rendered %q and %q", env.posix(), env.systemd())
|
||||
}
|
||||
}
|
||||
|
||||
// A ${machine:…} fact the machine does not have is refused naming the module, as a file's is.
|
||||
func TestAContributedFactTheMachineLacksIsRefused(t *testing.T) {
|
||||
_, err := environmentOn(contributors(), map[string]string{"name": "server"})
|
||||
if err == nil || !strings.Contains(err.Error(), "go-toolchain sets GOPATH") ||
|
||||
!strings.Contains(err.Error(), "${machine:account-home}") {
|
||||
t.Fatalf("a missing account home was not refused by name: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// ADR 0203 §5: two modules setting one variable are refused, both named — neither silently wins.
|
||||
func TestAVariableTwoModulesSetIsRefusedNamingBoth(t *testing.T) {
|
||||
modules := append(contributors(), mcat.Manifest{Module: "neovim", Environment: &mcat.Environment{
|
||||
Variables: map[string]string{"EDITOR": "nvim"}}})
|
||||
_, err := environmentOn(modules, operatorFacts)
|
||||
if err == nil || err.Error() != "neovim and zsh both set EDITOR on this machine; the account has one "+
|
||||
"environment, so one of them must stop setting it (novox/hq ADR 0203)" {
|
||||
t.Fatalf("a variable set twice was not refused naming both: %v", err)
|
||||
}
|
||||
// And at composition, whether or not the node holds the environment.
|
||||
r := mcat.Resolution{Node: "workstation", Account: "op", Modules: modules}
|
||||
if _, err := Declaration(r, mcat.Rendering{}); err == nil || !strings.Contains(err.Error(), "neovim and zsh both set EDITOR") {
|
||||
t.Fatalf("composition accepted a variable set twice: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// shells contributes code for several shells and slots, in no order.
|
||||
func shells() []mcat.Manifest {
|
||||
return []mcat.Manifest{
|
||||
{Module: "zsh-syntax-highlighting", Shell: []mcat.ShellCode{
|
||||
{For: "zsh", Slot: "last", Code: "source /usr/share/zsh/plugins/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh"},
|
||||
}},
|
||||
{Module: "powerlevel10k", Shell: []mcat.ShellCode{
|
||||
{For: "zsh", Slot: "first", Code: "if [[ -r \"${XDG_CACHE_HOME:-$HOME/.cache}/p10k-instant-prompt-${(%):-%n}.zsh\" ]]; then\n" +
|
||||
" source \"${XDG_CACHE_HOME:-$HOME/.cache}/p10k-instant-prompt-${(%):-%n}.zsh\"\nfi\n"},
|
||||
{For: "zsh", Slot: "normal", Code: "source ~/.local/share/powerlevel10k/powerlevel10k.zsh-theme"},
|
||||
{For: "zsh", Slot: "normal", Code: "[[ -f ~/.local/share/powerlevel10k/p10k.zsh ]] && source ~/.local/share/powerlevel10k/p10k.zsh"},
|
||||
}},
|
||||
{Module: "zsh-autosuggestions", Shell: []mcat.ShellCode{
|
||||
{For: "zsh", Slot: "normal", Code: "source /usr/share/zsh/plugins/zsh-autosuggestions/zsh-autosuggestions.zsh"},
|
||||
{For: "bash", Slot: "normal", Code: "echo not for zsh"},
|
||||
}},
|
||||
{Module: "direnv", Shell: []mcat.ShellCode{
|
||||
{For: "fish", Slot: "last", Code: "direnv hook fish | source"},
|
||||
{For: "bash", Slot: "last", Code: "eval \"$(direnv hook bash)\""},
|
||||
}},
|
||||
}
|
||||
}
|
||||
|
||||
// The holder of node-login-shell, as WP3's zsh module writes its block, with its own zsh around the
|
||||
// slots — which holds `${…}` of the shell's own that no mesh pass may touch either.
|
||||
func zshHolder() mcat.Manifest {
|
||||
return mcat.Manifest{Module: "zsh", Claims: []mcat.Claim{{Name: mcat.LoginShellSeat, Scope: mcat.ScopeNode}},
|
||||
Resources: []map[string]any{
|
||||
{"id": "zshrc", "type": "file", "path": "${machine:account-home}/.zshrc", "content": "" +
|
||||
"${shell:zsh:first}" +
|
||||
"PROMPT='%n@%m ${PWD/#$HOME/~} '\n" +
|
||||
"${shell:zsh:normal}" +
|
||||
"alias ll='ls -l'\n" +
|
||||
"${shell:zsh:last}"},
|
||||
}}
|
||||
}
|
||||
|
||||
// The case the ordering exists for: contributed zsh code full of `${…}` reaches the file byte for
|
||||
// byte, because the shell's code is placed after every other placeholder pass and in one pass — a
|
||||
// scanner for the mesh's placeholders that ran after it would read `${XDG_CACHE_HOME:-…}` and
|
||||
// `${(%):-%n}` as the mesh's, or fill a `${machine:…}` some module wrote for its shell to see.
|
||||
func TestShellCodeReachesTheHoldersFileByteForByte(t *testing.T) {
|
||||
modules := append(shells(), zshHolder(), mcat.Manifest{Module: "sly", Shell: []mcat.ShellCode{
|
||||
{For: "zsh", Slot: "last", Code: "echo ${machine:account-home} ${secret:x} ${shell:zsh:first} ${environment:posix}"},
|
||||
}})
|
||||
r := mcat.Resolution{Node: "workstation", Account: "op", Modules: modules}
|
||||
out, err := Declaration(r, mcat.Rendering{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var zshrc map[string]any
|
||||
for _, res := range out {
|
||||
if res["id"] == "zsh.zshrc" {
|
||||
zshrc = res
|
||||
}
|
||||
}
|
||||
if zshrc == nil {
|
||||
t.Fatalf("the holder's file was not composed: %v", out)
|
||||
}
|
||||
if zshrc["path"] != "/home/op/.zshrc" {
|
||||
t.Fatalf("the holder's own placeholders were not filled first: %v", zshrc["path"])
|
||||
}
|
||||
want := "# powerlevel10k\n" +
|
||||
"if [[ -r \"${XDG_CACHE_HOME:-$HOME/.cache}/p10k-instant-prompt-${(%):-%n}.zsh\" ]]; then\n" +
|
||||
" source \"${XDG_CACHE_HOME:-$HOME/.cache}/p10k-instant-prompt-${(%):-%n}.zsh\"\nfi\n" +
|
||||
"PROMPT='%n@%m ${PWD/#$HOME/~} '\n" +
|
||||
"# powerlevel10k\n" +
|
||||
"source ~/.local/share/powerlevel10k/powerlevel10k.zsh-theme\n" +
|
||||
"[[ -f ~/.local/share/powerlevel10k/p10k.zsh ]] && source ~/.local/share/powerlevel10k/p10k.zsh\n" +
|
||||
"# zsh-autosuggestions\n" +
|
||||
"source /usr/share/zsh/plugins/zsh-autosuggestions/zsh-autosuggestions.zsh\n" +
|
||||
"alias ll='ls -l'\n" +
|
||||
"# sly\n" +
|
||||
"echo ${machine:account-home} ${secret:x} ${shell:zsh:first} ${environment:posix}\n" +
|
||||
"# zsh-syntax-highlighting\n" +
|
||||
"source /usr/share/zsh/plugins/zsh-syntax-highlighting/zsh-syntax-highlighting.zsh\n"
|
||||
if got := zshrc["content"]; got != want {
|
||||
t.Fatalf("the holder's .zshrc is\n%s\nnot\n%s", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
// The holder of node-environment places both renderings, and they are the same as rendered alone.
|
||||
func TestTheEnvironmentHolderPlacesBothRenderings(t *testing.T) {
|
||||
holder := mcat.Manifest{Module: "node-env", Claims: []mcat.Claim{{Name: mcat.EnvironmentSeat, Scope: mcat.ScopeNode}},
|
||||
Resources: []map[string]any{
|
||||
{"id": "posix", "type": "file", "path": "${machine:account-home}/.config/mesh/environment.sh",
|
||||
"content": "# The mesh's environment.\n${environment:posix}"},
|
||||
{"id": "systemd", "type": "file", "path": "${machine:account-home}/.config/environment.d/50-mesh.conf",
|
||||
"content": "${environment:systemd}"},
|
||||
}}
|
||||
r := mcat.Resolution{Node: "workstation", Account: "op", Modules: append(contributors(), holder)}
|
||||
out, err := Declaration(r, mcat.Rendering{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
by := map[string]any{}
|
||||
for _, res := range out {
|
||||
by[res["id"].(string)] = res["content"]
|
||||
}
|
||||
if by["node-env.posix"] != "# The mesh's environment.\n"+composedPOSIX {
|
||||
t.Fatalf("the POSIX file is\n%v", by["node-env.posix"])
|
||||
}
|
||||
if by["node-env.systemd"] != composedSystemd {
|
||||
t.Fatalf("the environment.d file is\n%v", by["node-env.systemd"])
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user