mesh/delivery-group group pkg/57-k2-predefined-keys rejected: a member's own check failed
mesh/merge-gate head fbcf44702cd6 merged into trunk 9932adfa00fe — fail: head fbcf44702cd6 does not merge cleanly into trunk 9932adfa00fe: in conflict test…
mesh/repo-check head fbcf44702cd6 merged into trunk 9932adfa00fe — fail: head fbcf44702cd6 does not merge cleanly into trunk 9932adfa00fe: in conflict test…
mesh/delivery superseded: a newer head of the same pull request
Part 4: the tests of the list's reading and of the controller's commands on it (hq ADR 0348 clause 7, issue 661). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
176 lines
7.0 KiB
Go
176 lines
7.0 KiB
Go
package catalogue
|
|
|
|
import (
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// The list of predefined keys (novox/hq ADR 0348 §7, design 57 package K2, issue 661): read strictly, and every
|
|
// manifest naming a key judged against the list of the tree it is read in.
|
|
|
|
// theFirstKeys is the catalogue's first list: font-size and agent-managed-settings.
|
|
const theFirstKeys = `{
|
|
"font-size": {"type": "number", "description": "the size text is shown in, in points", "minimum": 6, "maximum": 72},
|
|
"agent-managed-settings": {"type": "object", "description": "the managed settings of every agent session, a JSON object", "trusted": true}
|
|
}`
|
|
|
|
func TestTheFirstListIsRead(t *testing.T) {
|
|
list, err := ParsePredefined([]byte(theFirstKeys))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if got := strings.Join(list.Keys(), ","); got != "agent-managed-settings,font-size" {
|
|
t.Fatalf("keys %s", got)
|
|
}
|
|
if k := list["font-size"]; k.Type != ConfigNumber || *k.Minimum != 6 || *k.Maximum != 72 || k.Trusted {
|
|
t.Fatalf("font-size read as %+v", k)
|
|
}
|
|
if k := list["agent-managed-settings"]; k.Type != ConfigObject || !k.Trusted {
|
|
t.Fatalf("agent-managed-settings read as %+v", k)
|
|
}
|
|
// One list is one text, however it was written.
|
|
a, _ := list.Canonical()
|
|
again, _ := ParsePredefined(a)
|
|
b, _ := again.Canonical()
|
|
if string(a) != string(b) {
|
|
t.Fatalf("canonical text moved:\n%s\n%s", a, b)
|
|
}
|
|
}
|
|
|
|
func TestAListIsRefusedWholeForAnyWrongKey(t *testing.T) {
|
|
for _, c := range []struct{ file, says string }{
|
|
{`[]`, "not a JSON object of keys"},
|
|
{`null`, "not a JSON object of keys"},
|
|
{`{"font-size": {"type": "number", "description": "points", "default": 10}}`, `unknown field "default"`},
|
|
{`{"font-size": {"description": "points"}}`, "no type"},
|
|
{`{"font-size": {"type": "decimal", "description": "points"}}`, `type "decimal"`},
|
|
{`{"font-size": {"type": "number"}}`, "no description"},
|
|
{`{"accent": {"type": "object", "description": "colours"}}`, "not trusted"},
|
|
{`{"font-size": {"type": "number", "description": "points", "minimum": 9, "maximum": 6}}`, "above its maximum"},
|
|
{`{"accent": {"type": "string", "description": "a name", "pattern": "("}}`, "not a regular expression"},
|
|
{`{"accent": {"type": "boolean", "description": "on or off", "minimum": 1}}`, "minimum or maximum is for a number"},
|
|
{`{"Font Size": {"type": "number", "description": "points"}}`, "not a usable key"},
|
|
{`{"places": {"type": "string", "description": "where"}}`, "placement"},
|
|
{`{"puid": {"type": "number", "description": "an id"}}`, "placement"},
|
|
} {
|
|
if _, err := ParsePredefined([]byte(c.file)); err == nil || !strings.Contains(err.Error(), c.says) {
|
|
t.Errorf("%s: wanted a refusal saying %q, got %v", c.file, c.says, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
// fallingBackTo is inConfigForm with its font size falling back to a predefined key.
|
|
func fallingBackTo(t *testing.T, key string) []byte {
|
|
t.Helper()
|
|
m := inConfigForm()
|
|
declaration(m, "font-size")["predefined"] = key
|
|
raw, err := json.Marshal(m)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return raw
|
|
}
|
|
|
|
// A manifest naming font-size registers against the list; one naming a key not on it, or of another type, is
|
|
// refused — judged against the list given, whatever the mesh published.
|
|
func TestAManifestIsJudgedAgainstTheListOfItsTree(t *testing.T) {
|
|
withPredefined(t, nil)
|
|
list, err := ParsePredefined([]byte(theFirstKeys))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := ParseManifestWith(fallingBackTo(t, "font-size"), list); err != nil {
|
|
t.Fatalf("a value falling back to font-size is refused: %v", err)
|
|
}
|
|
if _, err := ParseManifestWith(fallingBackTo(t, "font-scale"), list); err == nil ||
|
|
!strings.Contains(err.Error(), `"font-scale", which is not on the list`) {
|
|
t.Fatalf("a key not on the list: %v", err)
|
|
}
|
|
m := inConfigForm()
|
|
declaration(m, "theme")["predefined"] = "font-size"
|
|
raw, _ := json.Marshal(m)
|
|
if _, err := ParseManifestWith(raw, list); err == nil || !strings.Contains(err.Error(), "which is a number") {
|
|
t.Fatalf("a key of another type: %v", err)
|
|
}
|
|
// Where no tree is read, the published list judges: none published refuses.
|
|
if _, err := ParseManifest(fallingBackTo(t, "font-size")); err == nil || !strings.Contains(err.Error(), "is not present") {
|
|
t.Fatalf("judged against no list: %v", err)
|
|
}
|
|
withPredefined(t, list)
|
|
if _, err := ParseManifest(fallingBackTo(t, "font-size")); err != nil {
|
|
t.Fatalf("judged against the published list: %v", err)
|
|
}
|
|
}
|
|
|
|
// A template's text filled from the tree keeps the list the manifest was judged against: read again, it is not
|
|
// judged against the published one.
|
|
func TestTemplatesFilledKeepTheListTheManifestWasJudgedAgainst(t *testing.T) {
|
|
withPredefined(t, nil)
|
|
list, _ := ParsePredefined([]byte(theFirstKeys))
|
|
dir := t.TempDir()
|
|
if err := os.MkdirAll(filepath.Join(dir, TemplatesDir), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(dir, TemplatesDir, "rc"), []byte("font = ${config:font-size}\n"), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
m := inConfigForm()
|
|
declaration(m, "font-size")["predefined"] = "font-size"
|
|
r := resourceOf(m, "configuration")
|
|
delete(r, "content")
|
|
r["template"] = TemplatesDir + "/rc"
|
|
m["build"] = map[string]any{"artifacts": []any{map[string]any{"name": "files", "kind": "archive", "from": "files"}}}
|
|
declaration(m, "width")["runtime"] = true
|
|
declaration(m, "sender")["runtime"] = true
|
|
raw, _ := json.Marshal(m)
|
|
parsed, err := ParseManifestWith(raw, list)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := WithTemplatesFrom(parsed, dir); err != nil {
|
|
t.Fatalf("filled from the tree, it was judged against another list: %v", err)
|
|
}
|
|
}
|
|
|
|
// module check reads the list at the root of the repository above the manifest; no repository, no list.
|
|
func TestTheListIsFoundAtTheRepositoryRootAboveAManifest(t *testing.T) {
|
|
root := t.TempDir()
|
|
module := filepath.Join(root, "modules", "dunst")
|
|
if err := os.MkdirAll(module, 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if list, err := PredefinedAbove(module); err != nil || list != nil {
|
|
t.Fatalf("outside a repository: %v %v", list, err)
|
|
}
|
|
if err := os.Mkdir(filepath.Join(root, ".git"), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if list, err := PredefinedAbove(module); err != nil || list != nil {
|
|
t.Fatalf("a repository without the file: %v %v", list, err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(root, PredefinedFile), []byte(theFirstKeys), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
list, err := PredefinedAbove(module)
|
|
if err != nil || len(list) != 2 {
|
|
t.Fatalf("the list above: %v %v", list, err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(root, PredefinedFile), []byte(`{"font-size": {}}`), 0o644); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, err := PredefinedAbove(module); err == nil {
|
|
t.Fatal("a wrong list above was read")
|
|
}
|
|
if err := os.Remove(filepath.Join(root, PredefinedFile)); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.Symlink("/etc/hostname", filepath.Join(root, PredefinedFile)); err == nil {
|
|
if _, err := PredefinedAbove(module); err == nil || !strings.Contains(err.Error(), "not a plain file") {
|
|
t.Fatalf("a link read as the list: %v", err)
|
|
}
|
|
}
|
|
}
|