Test config end to end into a composed file, and hold the settings tests to no whole-mesh module layer (hq issue 630)
mesh/delivery checking
mesh/merge-gate out of date: judged against trunk 6f1a05366c1d, the trunk is now 8ddf8b700558; judged again on a recheck or when it is merged
mesh/repo-check out of date: judged against trunk 6f1a05366c1d, the trunk is now 8ddf8b700558; judged again on a recheck or when it is merged
mesh/delivery checking
mesh/merge-gate out of date: judged against trunk 6f1a05366c1d, the trunk is now 8ddf8b700558; judged again on a recheck or when it is merged
mesh/repo-check out of date: judged against trunk 6f1a05366c1d, the trunk is now 8ddf8b700558; judged again on a recheck or when it is merged
This commit is contained in:
@@ -0,0 +1,187 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/novox/mesh-controller/internal/catalogue"
|
||||
)
|
||||
|
||||
// The config verb's line: a listing without key, a set or clear with why, and a module value refused
|
||||
// without a node (novox/hq issue 630).
|
||||
func TestTheConfigVerbsLine(t *testing.T) {
|
||||
for _, c := range []struct {
|
||||
args map[string]any
|
||||
want string
|
||||
}{
|
||||
{map[string]any{}, "config list"},
|
||||
{map[string]any{"node": "g14", "module": "dunst"}, "config list --node g14 --module dunst"},
|
||||
{map[string]any{"key": "font-size", "value": "12", "why": "bigger"}, "config set font-size 12 --why bigger"},
|
||||
{map[string]any{"key": "font-size", "value": "13", "node": "g14", "why": "dense"},
|
||||
"config set font-size 13 --node g14 --why dense"},
|
||||
{map[string]any{"key": "font-size", "value": "14", "node": "g14", "module": "dunst", "why": "x"},
|
||||
"config set font-size 14 --node g14 --module dunst --why x"},
|
||||
{map[string]any{"key": "font-size", "clear": "true", "node": "g14", "why": "back"},
|
||||
"config clear font-size --node g14 --why back"},
|
||||
{map[string]any{"key": "font-size", "history": "true"}, "config history font-size"},
|
||||
} {
|
||||
argv, err := argvFor("config", c.args)
|
||||
if err != nil || strings.Join(argv, " ") != c.want {
|
||||
t.Errorf("%v: %v %v, want %s", c.args, argv, err, c.want)
|
||||
}
|
||||
}
|
||||
for want, args := range map[string]map[string]any{
|
||||
"no module layer for the whole mesh": {"key": "font-size", "value": "12", "module": "dunst", "why": "x"},
|
||||
"why": {"key": "font-size", "value": "12"},
|
||||
"names its key": {"value": "12"},
|
||||
"not both": {"key": "font-size", "value": "12", "clear": "true", "why": "x"},
|
||||
} {
|
||||
if _, err := argvFor("config", args); err == nil || !strings.Contains(err.Error(), want) {
|
||||
t.Errorf("%v: %v, want %q", args, err, want)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// End to end through the verb, as the operator's acceptance reads it: cleared → the default; mesh font-size,
|
||||
// then the node's, then dunst's own on the node — each in the node's dunstrc as composed (novox/hq issue 630).
|
||||
func TestConfigReachesTheComposedFileFromEachSource(t *testing.T) {
|
||||
open := aMesh(t)
|
||||
ctx := t.Context()
|
||||
if err := open.inventory.SetPredefined(ctx, catalogue.Predefined{
|
||||
"font-size": {Type: catalogue.TypeNumber, Why: "the size text is drawn at"}}, "", ""); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
register(t, open, catalogue.Manifest{Module: "dunst", Version: "1",
|
||||
Preferences: map[string]catalogue.Preference{
|
||||
"font-size": {Type: catalogue.TypeNumber, Default: float64(10), Predefined: "font-size", Why: "100 DPI"},
|
||||
},
|
||||
Resources: []map[string]any{{"id": "dunstrc", "type": "file", "path": "/etc/dunst/dunstrc", "mode": "0644", "trusted": false,
|
||||
"content": "font = Inter ${setting:font-size}\n"}}})
|
||||
for _, n := range []string{"laptop", "anchor"} {
|
||||
if _, err := assign(ctx, open, n, "dunst"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
dunstrc := func(node string) string {
|
||||
t.Helper()
|
||||
gens, err := generators(ctx, open)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
declared, problems, err := composedAndValidated(ctx, open, node, gens, Allocating)
|
||||
if err != nil {
|
||||
t.Fatalf("%s does not compose: %v %v", node, err, problems)
|
||||
}
|
||||
for _, r := range declared.Resources {
|
||||
if r["path"] == "/etc/dunst/dunstrc" {
|
||||
return fmt.Sprint(r["content"])
|
||||
}
|
||||
}
|
||||
t.Fatalf("%s has no dunstrc: %v", node, problems)
|
||||
return ""
|
||||
}
|
||||
want := func(node, size string) {
|
||||
t.Helper()
|
||||
if got := dunstrc(node); got != "font = Inter "+size+"\n" {
|
||||
t.Fatalf("%s's dunstrc: %q, want size %s", node, got, size)
|
||||
}
|
||||
}
|
||||
verb := func(args map[string]any) error {
|
||||
t.Helper()
|
||||
return throughVerb(t, "config", args)
|
||||
}
|
||||
want("laptop", "10")
|
||||
|
||||
if err := verb(map[string]any{"key": "font-size", "value": "11", "why": "a larger default"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want("laptop", "11")
|
||||
want("anchor", "11")
|
||||
if err := verb(map[string]any{"key": "font-size", "value": "13", "node": "laptop", "why": "a dense screen"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want("laptop", "13")
|
||||
want("anchor", "11")
|
||||
if err := verb(map[string]any{"key": "font-size", "value": "15", "node": "laptop", "module": "dunst",
|
||||
"why": "dunst alone larger"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want("laptop", "15")
|
||||
|
||||
listing := stdoutOf(t, func() error { return verb(map[string]any{}) })
|
||||
for _, said := range []string{"font-size (number): the size text is drawn at", "mesh config: 11",
|
||||
"node config (laptop): 13", "laptop: 15 (dunst on laptop)", "anchor: 11 (mesh config)",
|
||||
"falls back to the predefined key font-size"} {
|
||||
if !strings.Contains(listing, said) {
|
||||
t.Errorf("the listing does not say %q:\n%s", said, listing)
|
||||
}
|
||||
}
|
||||
|
||||
// Cleared, each falls back to the next source down.
|
||||
for _, step := range []struct {
|
||||
args map[string]any
|
||||
size string
|
||||
}{
|
||||
{map[string]any{"key": "font-size", "clear": "true", "node": "laptop", "module": "dunst", "why": "x"}, "13"},
|
||||
{map[string]any{"key": "font-size", "clear": "true", "node": "laptop", "why": "x"}, "11"},
|
||||
{map[string]any{"key": "font-size", "clear": "true", "why": "x"}, "10"},
|
||||
} {
|
||||
if err := verb(step.args); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
want("laptop", step.size)
|
||||
}
|
||||
|
||||
// Refused: a key not on the list for mesh config, a module value without a node, a type not the key's.
|
||||
if err := verb(map[string]any{"key": "colour", "value": "blue", "why": "x"}); err == nil ||
|
||||
!strings.Contains(err.Error(), "predefined") {
|
||||
t.Fatalf("mesh config for a key not on the list: %v", err)
|
||||
}
|
||||
if err := runLine(t, []string{"config", "set", "font-size", "12", "--module", "dunst", "--why", "x"}); err == nil ||
|
||||
!strings.Contains(err.Error(), "no module layer for the whole mesh") {
|
||||
t.Fatalf("a module value without a node: %v", err)
|
||||
}
|
||||
if err := verb(map[string]any{"key": "font-size", "value": `"large"`, "node": "laptop", "module": "dunst",
|
||||
"why": "x"}); err == nil || !strings.Contains(err.Error(), "is a number") {
|
||||
t.Fatalf("a string for a number: %v", err)
|
||||
}
|
||||
if err := verb(map[string]any{"key": "nothing", "value": "1", "node": "laptop", "module": "dunst",
|
||||
"why": "x"}); err == nil || !strings.Contains(err.Error(), "declares no preference") {
|
||||
t.Fatalf("a module value for a key it does not declare: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// The module check reads the catalogue's predefined.json at the repository's root and refuses a preference
|
||||
// falling back to a key not on it (novox/hq issue 630).
|
||||
func TestTheModuleCheckJudgesPreferencesAgainstTheCataloguesList(t *testing.T) {
|
||||
root := t.TempDir()
|
||||
for _, dir := range []string{".git", "modules/dunst"} {
|
||||
if err := os.MkdirAll(filepath.Join(root, dir), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
manifest := filepath.Join(root, "modules/dunst/module.json")
|
||||
write := func(path, body string) {
|
||||
t.Helper()
|
||||
if err := os.WriteFile(path, []byte(body), 0o644); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
write(manifest, `{"module": "dunst", "version": "1",
|
||||
"preferences": {"font-size": {"type": "number", "default": 10, "predefined": "font-size", "why": "100 DPI"}},
|
||||
"resources": [{"id": "rc", "type": "file", "path": "/etc/rc", "mode": "0644", "trusted": false,
|
||||
"content": "font ${setting:font-size}"}]}`)
|
||||
write(filepath.Join(root, "predefined.json"), `{"theme": {"type": "string", "why": "light or dark"}}`)
|
||||
var out strings.Builder
|
||||
if err := moduleCheck([]string{manifest}, &out); err == nil || !strings.Contains(out.String(), "not on the catalogue's list") {
|
||||
t.Fatalf("a key not on the list passed: %v\n%s", err, out.String())
|
||||
}
|
||||
write(filepath.Join(root, "predefined.json"), `{"font-size": {"type": "number", "why": "the size text is drawn at"}}`)
|
||||
out.Reset()
|
||||
if err := moduleCheck([]string{manifest}, &out); err != nil {
|
||||
t.Fatalf("a key on the list was refused: %v\n%s", err, out.String())
|
||||
}
|
||||
}
|
||||
@@ -31,7 +31,7 @@ func aMeshWithSecrets(t *testing.T) (*stores, []string) {
|
||||
}
|
||||
}
|
||||
secrets := []string{"Hunter2-Is-Not-A-Password-9f8e7d", "0123456789abcdefABCDEF0123456789zz"}
|
||||
if err := open.inventory.SetSettings(ctx, "", "files", map[string]any{
|
||||
if err := open.inventory.KeepSettings(ctx, "", "files", map[string]any{
|
||||
"admin_password": secrets[0], "upstream": "10.77.0.9", "hub": "anchor"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@ func TestSettingsSayWhereEachValueComesFrom(t *testing.T) {
|
||||
{Key: "width", Value: float64(250), From: catalogue.DefaultLayer, FromDefault: true, Default: float64(250), HasDefault: true},
|
||||
})
|
||||
want := "dunst on laptop, every value and where it comes from:\n" +
|
||||
" font-size = 13 (laptop; the default is 10)\n" +
|
||||
" font-size = 13 (dunst on laptop; the default is 10)\n" +
|
||||
" width = 250 (default)\n"
|
||||
if got != want {
|
||||
t.Fatalf("said\n%s\nwant\n%s", got, want)
|
||||
@@ -66,7 +66,7 @@ func TestPreferencesSayEachMachinesValueAndItsSource(t *testing.T) {
|
||||
want := "dunst (on desk, laptop)\n" +
|
||||
" font-size, default 10: readable at 100 DPI\n" +
|
||||
" desk: 10 (default)\n" +
|
||||
" laptop: 16 (the node)\n" +
|
||||
" laptop: 16 (dunst on laptop)\n" +
|
||||
" width, default 250: forty characters\n" +
|
||||
" desk: 300 (the mesh)\n" +
|
||||
" laptop: 250 (default)\n"
|
||||
@@ -96,7 +96,7 @@ func TestPreferencesListedFromTheStores(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
all := stdoutOf(t, func() error { return settingsCommand(ctx, []string{"preferences"}) })
|
||||
if !strings.Contains(all, "notes (on laptop)") || !strings.Contains(all, "laptop: 16 (the node)") ||
|
||||
if !strings.Contains(all, "notes (on laptop)") || !strings.Contains(all, "laptop: 16 (notes on laptop)") ||
|
||||
!strings.Contains(all, "font-size, default 10: readable at 100 DPI") {
|
||||
t.Fatalf("the listing:\n%s", all)
|
||||
}
|
||||
|
||||
@@ -263,8 +263,18 @@ func TestTheAgentsManagedSettingsAreRefusedThroughAVerb(t *testing.T) {
|
||||
if err := atTheTerminal(t, "settings", "set", "claude-code", allow, "--node", "laptop"); err != nil {
|
||||
t.Fatalf("the managed settings at the terminal: %v", err)
|
||||
}
|
||||
if err := atTheTerminal(t, "settings", "set", "claude-code", server); err != nil {
|
||||
t.Fatalf("a tool server for the mesh at the terminal: %v", err)
|
||||
// There is no module layer for the whole mesh (novox/hq issue 630), at the terminal either; one the mesh
|
||||
// still holds from before is kept as it is, and is the terminal's as before.
|
||||
if err := atTheTerminal(t, "settings", "set", "claude-code", server); err == nil ||
|
||||
!strings.Contains(err.Error(), "no module layer for the whole mesh") {
|
||||
t.Fatalf("a tool server for the whole mesh at the terminal: %v", err)
|
||||
}
|
||||
var held map[string]any
|
||||
if err := json.Unmarshal([]byte(server), &held); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := open.inventory.KeepSettings(ctx, "", "claude-code", held); err != nil {
|
||||
t.Fatalf("a whole-mesh layer held from before: %v", err)
|
||||
}
|
||||
kept := layer("laptop")
|
||||
// Through a verb they are neither changed, dropped nor cleared.
|
||||
@@ -359,15 +369,15 @@ func TestOnlyATrustedFilesNamedKeyIsRefusedThroughAVerb(t *testing.T) {
|
||||
if _, err := assign(ctx, open, "anchor", "mailer"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := throughVerb(t, "settings", map[string]any{"module": "mailer", "values": `{"tags":{"todo":{"name":"To do"}}}`}); err != nil {
|
||||
if err := throughVerb(t, "settings", map[string]any{"module": "mailer", "node": "anchor", "values": `{"tags":{"todo":{"name":"To do"}}}`}); err != nil {
|
||||
t.Fatalf("a key of a file nothing trusts is the verb's: %v", err)
|
||||
}
|
||||
err := throughVerb(t, "settings", map[string]any{"module": "mailer", "replace": "true",
|
||||
err := throughVerb(t, "settings", map[string]any{"module": "mailer", "node": "anchor", "replace": "true",
|
||||
"values": `{"tags":{"todo":{"name":"To do"}},"accounts":{"mail":{"address":"me@mail.example"}}}`})
|
||||
if err == nil || !strings.Contains(err.Error(), "controller's terminal") || !strings.Contains(err.Error(), "accounts") {
|
||||
t.Fatalf("a key of a trusted file is the terminal's: %v", err)
|
||||
}
|
||||
values, _, err := open.inventory.Layer(ctx, "", "mailer")
|
||||
values, _, err := open.inventory.Layer(ctx, "anchor", "mailer")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user