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"]) } }