Issue 1585, S19: the strict parser, every manifest problem function and the judge's tables (192 declarations, about 5,200 lines) and their 244 tests leave; composition keeps its own refusals and the helpers it uses. The catalogue's tests decode the manifests they build. ParseManifest, ParseManifestWith, ParseManifestForInputs, CatalogueProblems and the rest are gone; mesh-delivery-indexer/judge holds them. Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
123 lines
4.8 KiB
Go
123 lines
4.8 KiB
Go
package catalogue
|
|
|
|
import (
|
|
"encoding/json"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// twoFiles is a module that owns two configurable files, which issue 168 said no module could:
|
|
// every key of every layer reached both.
|
|
func twoFiles(app, sidecar []any) Manifest {
|
|
appFile := map[string]any{"id": "app", "type": "file", "path": "/etc/app.json", "merge": MergeJSON,
|
|
"content": `{"server":{"port":8080},"search":{"safe":1}}`}
|
|
sideFile := map[string]any{"id": "sidecar", "type": "file", "path": "/etc/sidecar.json", "merge": MergeJSON,
|
|
"content": `{"url":"http://127.0.0.1:8080"}`}
|
|
if app != nil {
|
|
appFile[TakesSettingsField] = app
|
|
}
|
|
if sidecar != nil {
|
|
sideFile[TakesSettingsField] = sidecar
|
|
}
|
|
return Manifest{Module: "search", Resources: []map[string]any{
|
|
appFile, sideFile,
|
|
{"id": "env", "type": "file", "path": "/etc/search.env", "content": "SITE=${setting:site-name}\n"},
|
|
}}
|
|
}
|
|
|
|
func settledContent(t *testing.T, resource map[string]any, layers []Layer) map[string]any {
|
|
t.Helper()
|
|
out, err := ApplySettings(resource, layers)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if _, kept := out[TakesSettingsField]; kept {
|
|
t.Errorf("%v still carries %s; the host knows no such field", out["id"], TakesSettingsField)
|
|
}
|
|
var parsed map[string]any
|
|
if err := json.Unmarshal([]byte(out["content"].(string)), &parsed); err != nil {
|
|
t.Fatalf("%v is not JSON after settling: %v", out["id"], err)
|
|
}
|
|
return parsed
|
|
}
|
|
|
|
func TestEachMergeableFileTakesOnlyTheKeysItNames(t *testing.T) {
|
|
// novox/hq ADR 0326 §1, issue 168: two configurable files, each taking its own keys. A key for
|
|
// the other file, for a ${setting:…} elsewhere or for the mesh itself lands in neither.
|
|
m := twoFiles([]any{"server", "search"}, []any{"url"})
|
|
layers := []Layer{
|
|
{From: "the mesh", Values: map[string]any{
|
|
"server": map[string]any{"base_url": "https://search.example.tld/"},
|
|
"url": "http://127.0.0.1:9090", "site-name": "Search",
|
|
"endpoints": map[string]any{"web": map[string]any{"reach": "public"}}}},
|
|
{From: "node-a", Values: map[string]any{"search": map[string]any{"safe": 0.0}}},
|
|
}
|
|
|
|
app := settledContent(t, m.Resources[0], layers)
|
|
for _, stray := range []string{"url", "site-name", "endpoints"} {
|
|
if _, in := app[stray]; in {
|
|
t.Errorf("app took %q, which it does not name: %v", stray, app)
|
|
}
|
|
}
|
|
server, _ := app["server"].(map[string]any)
|
|
if server["base_url"] != "https://search.example.tld/" || server["port"] != 8080.0 {
|
|
t.Errorf("app's server block is %v; the setting should merge into the file's own", server)
|
|
}
|
|
if search, _ := app["search"].(map[string]any); search["safe"] != 0.0 {
|
|
t.Errorf("app's search block is %v; the node's layer should win", search)
|
|
}
|
|
|
|
side := settledContent(t, m.Resources[1], layers)
|
|
if side["url"] != "http://127.0.0.1:9090" {
|
|
t.Errorf("sidecar's url is %v; it names url", side["url"])
|
|
}
|
|
if len(side) != 1 {
|
|
t.Errorf("sidecar took keys it does not name: %v", side)
|
|
}
|
|
}
|
|
|
|
func TestAnEmptyListTakesNoSetting(t *testing.T) {
|
|
// ADR 0164 §12: a file that names no setting takes none.
|
|
m := twoFiles([]any{"server"}, []any{})
|
|
side := settledContent(t, m.Resources[1], []Layer{{From: "the mesh", Values: map[string]any{
|
|
"server": map[string]any{"port": 1.0}, "url": "x"}}})
|
|
if len(side) != 1 || side["url"] != "http://127.0.0.1:8080" {
|
|
t.Errorf("a file taking none was changed: %v", side)
|
|
}
|
|
}
|
|
|
|
func TestAFileThatNamesNothingKeepsTodaysForm(t *testing.T) {
|
|
// ADR 0326 §3, ADR 0164 §15: a module none of whose files names keys is left as it was.
|
|
m := twoFiles(nil, nil)
|
|
app := settledContent(t, m.Resources[0], []Layer{{From: "the mesh", Values: map[string]any{"url": "x"}}})
|
|
if app["url"] != "x" {
|
|
t.Errorf("a silent file should still take every key until its module names them: %v", app)
|
|
}
|
|
if unused := UnusedSettings(m, []Layer{{From: "the mesh", Values: map[string]any{"anything": 1}}}); len(unused) != 0 {
|
|
t.Errorf("today's form has nothing stray, and said: %v", unused)
|
|
}
|
|
}
|
|
|
|
func TestAKeyNoFileNamesIsStray(t *testing.T) {
|
|
// ADR 0326 §5: once the files name their keys, a key none of them names — and nothing else
|
|
// takes — is stray, and is said with what each file takes.
|
|
m := twoFiles([]any{"server", "search"}, []any{"url"})
|
|
unused := UnusedSettings(m, []Layer{{From: "the mesh", Values: map[string]any{
|
|
"server": 1, "url": "x", "site-name": "Search", "ui": map[string]any{"theme": "dark"}}}})
|
|
if len(unused) != 1 || !strings.Contains(unused[0], `"ui"`) {
|
|
t.Fatalf("only ui reaches nothing; named: %v", unused)
|
|
}
|
|
if !strings.Contains(unused[0], "app takes server, search") || !strings.Contains(unused[0], "sidecar takes url") {
|
|
t.Errorf("the stray key's message does not say what each file takes: %s", unused[0])
|
|
}
|
|
}
|
|
|
|
func containsText(list []string, text string) bool {
|
|
for _, s := range list {
|
|
if strings.Contains(s, text) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|