From 660649eeed1cf35fd5f0f6e5289ea6f25b0e9c38 Mon Sep 17 00:00:00 2001 From: mesh-admin Date: Tue, 13 Oct 2026 13:19:30 +0000 Subject: [PATCH] Test config end to end into a composed file, and hold the settings tests to no whole-mesh module layer (hq issue 630) --- cmd/mesh-controller/config_test.go | 187 ++++++++++++++++++ cmd/mesh-controller/facts_test.go | 2 +- .../settings_effective_test.go | 6 +- cmd/mesh-controller/terminal_settings_test.go | 20 +- 4 files changed, 206 insertions(+), 9 deletions(-) create mode 100644 cmd/mesh-controller/config_test.go diff --git a/cmd/mesh-controller/config_test.go b/cmd/mesh-controller/config_test.go new file mode 100644 index 00000000..4b79cc2c --- /dev/null +++ b/cmd/mesh-controller/config_test.go @@ -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()) + } +} diff --git a/cmd/mesh-controller/facts_test.go b/cmd/mesh-controller/facts_test.go index d5b3ac63..5e3eb51e 100644 --- a/cmd/mesh-controller/facts_test.go +++ b/cmd/mesh-controller/facts_test.go @@ -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) } diff --git a/cmd/mesh-controller/settings_effective_test.go b/cmd/mesh-controller/settings_effective_test.go index f248e2b4..bf024d3b 100644 --- a/cmd/mesh-controller/settings_effective_test.go +++ b/cmd/mesh-controller/settings_effective_test.go @@ -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) } diff --git a/cmd/mesh-controller/terminal_settings_test.go b/cmd/mesh-controller/terminal_settings_test.go index 783ca251..c354ac6e 100644 --- a/cmd/mesh-controller/terminal_settings_test.go +++ b/cmd/mesh-controller/terminal_settings_test.go @@ -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) }