Files
mesh-delivery/modules/mesh-delivery-indexer/judge/rules_test.go
T

6056 lines
303 KiB
Go

// The judge's tests, moved from mesh-sdk go/catalogue at 25a2cec with the rules they test (novox/hq ADR 0578 §4, issue
// 1585); those reading the catalogue read the copy captured in testdata/beside (internal/beside).
package judge
import (
json "encoding/json"
fmt "fmt"
catalogue "git.novox.be/novox/mesh-sdk/go/catalogue"
ast "go/ast"
parser "go/parser"
token "go/token"
beside "mesh-delivery-indexer/internal/beside"
os "os"
filepath "path/filepath"
reflect "reflect"
regexp "regexp"
slices "slices"
strconv "strconv"
strings "strings"
sync "sync"
testing "testing"
)
// ---- moved from catalogue ----
// A backup contribution written by hand still names its module's own directories; one it does not
// declare is refused where it is written rather than reaching the holder as the literal text. (The
// catalogue check refuses a hand-written backup line at all since ADR 0233; parsing still reads one,
// because a module on the shelf was written before.)
func TestABackupNamingAnUndeclaredDirectoryIsRefused(t *testing.T) {
_, err := ParseManifest([]byte(`{"module":"pg","version":"1",
"contributions":[{"seat":"node-backup","kind":"backup","content":"path ${dir:nowhere}"}]}`))
if err == nil || !strings.Contains(err.Error(), "nowhere") {
t.Fatalf("a backup of an undeclared directory was accepted: %v", err)
}
}
// **A language that compiles to a binary must say which system.**
//
// A binary is pinned to one operating system at link time, so a host refuses to touch a machine it
// was not built for. An artifact that says nothing would be compiled for whatever the build machine
// happened to be — which reads as portable and is not, and is the fault this check exists for.
func TestABinaryMustSayWhichSystemItIsFor(t *testing.T) {
got := buildProblemsOf(t, bundleFor("go", ""))
if !strings.Contains(got, "says no system") {
t.Fatalf("a compiled bundle with no system was accepted:\n%s", got)
}
// And the refusal names what it could have said, so a reader is one edit from right.
for _, system := range []string{"alpine", "android", "arch"} {
if !strings.Contains(got, system) {
t.Fatalf("the refusal does not name %q as a choice:\n%s", system, got)
}
}
}
func TestABlockOutsideTheShapeIsRefused(t *testing.T) {
cases := map[string]string{
`{"seat":"node-bar","kind":"block","content":"[[block]]"}`: "has no data",
`{"seat":"node-bar","kind":"block","content":"x","data":{"bar":"top","place":"status","shows":"battery"}}`: "has content",
`{"seat":"node-hotkeys","kind":"trigger","data":{"a":"b"}}`: "receives trigger as text",
`{"seat":"node-bar","kind":"block","data":{"bar":"left","place":"status","shows":"battery"}}`: `bar "left"`,
`{"seat":"node-bar","kind":"block","data":{"bar":"top","place":"middle","shows":"battery"}}`: `place "middle"`,
`{"seat":"node-bar","kind":"block","data":{"bar":"top","place":"status","shows":"gpu"}}`: "shows one of battery, command",
`{"seat":"node-bar","kind":"block","data":{"bar":"top","place":"status","shows":"battery","colour":"red"}}`: `no field "colour"`,
`{"seat":"node-bar","kind":"block","data":{"bar":"top","place":"status","shows":"battery","order":100}}`: "whole number from 0 to 99",
`{"seat":"node-bar","kind":"block","data":{"bar":"top","place":"status","shows":"command"}}`: "needs the option command",
`{"seat":"node-bar","kind":"block","data":{"bar":"top","place":"status","shows":"battery","options":{"x":"y"}}}`: `no option "x"`,
`{"seat":"node-bar","kind":"block","data":{"bar":"top","place":"status","shows":"command","options":{"command":"c","interval":0}}}`: "seconds",
`{"seat":"node-bar","kind":"block","if-capability":"Has Battery","data":{"bar":"top","place":"status","shows":"battery"}}`: "not a capability",
}
for c, want := range cases {
raw := `{"module":"power","contributions":[` + c + `]}`
if _, err := ParseManifest([]byte(raw)); err == nil || !strings.Contains(err.Error(), want) {
t.Errorf("%s: accepted, or refused without %q: %v", c, want, err)
}
}
ok := `{"module":"power","contributions":[{"seat":"node-bar","kind":"block","if-capability":"battery",` +
`"data":{"bar":"bottom","place":"status","order":10,"shows":"command","options":{"command":"c","interval":5}}}]}`
if _, err := ParseManifest([]byte(ok)); err != nil {
t.Errorf("a block in the shape was refused: %v", err)
}
}
func TestABuildContextOnASeatNamesNoForge(t *testing.T) {
m := catalogue.Manifest{Module: "packager", Build: &catalogue.Build{Artifacts: []catalogue.Artifact{
{Name: "server", Kind: "image", From: "Dockerfile",
Context: &catalogue.ArtifactContext{Seat: "git", Repository: "org/controller", Ref: "main"}},
}}}
if got := InstallationProblems(m); len(got) != 0 {
t.Fatalf("a context on a seat was reported: %v", got)
}
m.Build.Artifacts[0].Context = &catalogue.ArtifactContext{Repository: "https://git.mesh-one.be/org/controller.git"}
if got := InstallationProblems(m); len(got) != 1 {
t.Fatalf("a context by URL was not reported: %v", got)
}
}
func TestABuildsBundlesAreCarriedOnTheResolvedManifest(t *testing.T) {
m := aToolsModule(t, "nftables", "tools/index.js")
if len(m.Bundles) != 1 {
t.Fatalf("the resolved manifest carries %d bundle(s), not the one the build made", len(m.Bundles))
}
b := m.Bundles[0]
if b.Name != "tools" || b.Digest != bundleDigest || b.Language != "typescript" ||
b.Source != catalogue.ArtifactStoreScheme+"nftables/tools/blobs/"+bundleDigest ||
len(b.Entrypoints) != 1 || b.Entrypoints[0] != "tools/index.js" {
t.Errorf("the bundle is carried as %+v", b)
}
// A repository manifest may not write what the build derives.
raw := `{"module":"x","version":"1","build":{"artifacts":[{"name":"t","kind":"bundle","language":"typescript"}]},` +
`"bundles":[{"name":"t","source":"s","digest":"` + bundleDigest + `"}]}`
if _, err := ParseManifest([]byte(raw)); err == nil || !strings.Contains(err.Error(), "bundles") {
t.Errorf("a manifest stating its build's output by hand was accepted: %v", err)
}
}
// novox/hq ADR 0192: a tools bundle says what it is given, written with ${dir:…} and ${port:…}, and the judge takes
// it; how the node-engine resolves the words per machine and hands them to the runtime is its own composition's to test
// (ADR 0593 §7).
func TestABundleIsGivenItsWordsResolvedForThisMachine(t *testing.T) {
dash := catalogue.Manifest{Module: "dash", Version: "1", Tools: []string{"status"},
Listens: []catalogue.Listening{{Name: "web", Port: 3000, Protocol: "tcp"}},
OwnSecrets: catalogue.OwnSecrets{"token": {Path: "${dir:mesh-state}/token"}},
Resources: []map[string]any{
{"id": "mesh-state", "type": "directory", "mode": "0700", "place": "mesh"},
{"id": "config", "type": "file", "path": "${dir:mesh-state}/config.json", "mode": "0600", "content": "{}\n"},
{"id": "unrelated", "type": "file", "path": "/etc/dash.conf", "content": "x\n"},
},
Build: &catalogue.Build{Artifacts: []catalogue.Artifact{{Name: "tools", Kind: catalogue.ArtifactBundle, Language: "typescript",
Entrypoints: []string{"tools/index.js"},
Env: map[string]string{
"DASH_CONFIG_FILE": "${dir:mesh-state}/config.json",
"DASH_TOKEN_FILE": "${dir:mesh-state}/token",
"DASH_URL": "http://127.0.0.1:${port:3000}",
"DASH_ADMIN": "mesh-admin",
}}}}}
if problems := buildProblems(dash.Build, dash.Module); len(problems) > 0 {
t.Fatalf("a bundle's words written with ${dir:…} and ${port:…} were refused: %v", problems)
}
}
// novox/hq 04-ISSUES/216: a bundle nothing loads, runs or unpacks is refused at registration; saying
// `loads`, listing `tools`, or a resource naming it admits it.
func TestABundleNothingDeliversIsRefused(t *testing.T) {
base := func() catalogue.Manifest {
return catalogue.Manifest{Module: "baserow", Version: "1", Build: &catalogue.Build{Artifacts: []catalogue.Artifact{
{Name: "tools", Kind: catalogue.ArtifactBundle, Language: "typescript", Entrypoints: []string{"tools/index.js"}}}}}
}
if p := undeliveredBundles(base()); len(p) != 1 || !strings.Contains(p[0], "never reach a machine") {
t.Fatalf("a bundle nothing delivers was admitted: %v", p)
}
loads := base()
loads.Build.Artifacts[0].Loads = []string{"tools/index.js"}
tools := base()
tools.Tools = []string{"baserow_list_rows"}
run := base()
run.Resources = []map[string]any{{"id": "daemon", "type": "process", "artifact": "tools", "run": []any{"node", "tools/index.js"}}}
runtime := base()
runtime.Module = catalogue.RuntimeModule
for name, m := range map[string]catalogue.Manifest{"loads": loads, "tools": tools, "a process": run, "the runtime": runtime} {
if p := undeliveredBundles(m); len(p) != 0 {
t.Errorf("a bundle delivered by %s was refused: %v", name, p)
}
}
}
func TestABundlesWordsAreRefusedWhenTheyAreNotPathsOrConstants(t *testing.T) {
m := catalogue.Manifest{Module: "dash", Version: "1", Build: &catalogue.Build{Artifacts: []catalogue.Artifact{
{Name: "tools", Kind: catalogue.ArtifactBundle, Language: "typescript", Entrypoints: []string{"tools/index.js"},
Env: map[string]string{"DASH_TOKEN": "${secret:token}", catalogue.RuntimeBrokerFile: "/x", "DASH_PEER": "${bound:db:url}"}},
{Name: "runtime", Kind: catalogue.ArtifactImage, From: "Dockerfile", Env: map[string]string{"X": "y"}},
}}}
said := strings.Join(buildProblems(m.Build, m.Module), "\n")
for _, want := range []string{
`"tools" gives DASH_TOKEN the value "${secret:token}"`,
`"tools" gives DASH_PEER the value "${bound:db:url}"`,
`"tools" gives itself ` + catalogue.RuntimeBrokerFile,
`"runtime" is a "image" and says what it is given`,
} {
if !strings.Contains(said, want) {
t.Errorf("not refused: %s\nsaid:\n%s", want, said)
}
}
}
func TestABusyIsHeldToItsShape(t *testing.T) {
if _, err := ParseManifest(busyManifest(t, "agent", map[string]any{"command": "mesh-builder busy"})); err != nil {
t.Fatalf("a well-formed busy was refused: %v", err)
}
if _, err := ParseManifest(busyManifest(t, "agent", map[string]any{"command": "x", "timeout": "30s"})); err != nil {
t.Fatalf("a busy with its timeout was refused: %v", err)
}
for name, c := range map[string]struct {
on string
busy any
says string
}{
"text": {"agent", "mesh-builder busy", "it is an object"},
"no command": {"agent", map[string]any{"timeout": "5s"}, "names no command"},
"two lines": {"agent", map[string]any{"command": "a\nb"}, "one line"},
"a long timeout": {"agent", map[string]any{"command": "x", "timeout": "5m"}, "at most 1m0s"},
"an unknown key": {"agent", map[string]any{"command": "x", "every": "1m"}, `says "every"`},
"on a step": {"seed", map[string]any{"command": "x"}, "not a container that stays up"},
"on a service": {"daemon", map[string]any{"command": "x"}, "not a container that stays up"},
} {
_, err := ParseManifest(busyManifest(t, c.on, c.busy))
if err == nil || !strings.Contains(err.Error(), c.says) {
t.Errorf("%s: want a refusal saying %q, got %v", name, c.says, err)
}
}
}
// The claim may say what it serves for the role instead, when the module's own tools are not the
// seat's verbs (ADR 0160).
func TestAClaimMayNameWhatItServesForTheSeat(t *testing.T) {
m := telegram()
m.Tools = []string{"telegram_send"}
m.Claims = append([]catalogue.Claim(nil), m.Claims...)
for i := range m.Claims {
if m.Claims[i].Name == "telegram-sender" {
m.Claims[i].Serves = []string{"status"}
}
}
if got := problemsFor(t, catalogue.Shelf{"telegram": m}); strings.Contains(got, "does not serve") {
t.Fatalf("a claim naming the seat's verb was refused: %s", got)
}
}
// A claim at the wrong scope is a different seat than the one declared.
func TestAClaimMustMatchTheDeclaredScope(t *testing.T) {
m := telegram()
m.Claims = []catalogue.Claim{{Name: "telegram-sender", Scope: catalogue.ScopeNode}}
got := problemsFor(t, catalogue.Shelf{"telegram": m})
if !strings.Contains(got, "scope") {
t.Fatalf("a claim at the wrong scope was accepted: %s", got)
}
}
// And the same claim is fine once something declares that seat, which is the case the parser
// could not have distinguished.
func TestAClaimOnASeatAnotherModuleDeclaresIsAccepted(t *testing.T) {
claimant, err := ParseManifest(claimed(`[{"name":"the-anything","scope":"node"}]`))
if err != nil {
t.Fatal(err)
}
declarer := catalogue.Manifest{Module: "someone", DefinesSeats: []catalogue.SeatDeclaration{
{Name: "the-anything", Scope: catalogue.ScopeNode, Accepts: []string{"work"}},
}}
if problems := CatalogueProblems(catalogue.Shelf{claimant.Module: claimant, "someone": declarer}); len(problems) != 0 {
t.Fatalf("a claim on a declared seat was refused: %v", problems)
}
// The second of a kind (issue 1952; the lead, 2026-10-29): a holder that lists its seat's event among its own emits
// would say it where no consumer hears it, so the manifest is refused, naming the seat's subject; the seat's
// holder saying nothing of its own is accepted.
holder := func(emits string) []byte {
return []byte(`{"module":"mesh-self-check","version":"1","emits":` + emits + `,"claims":[{"name":"mesh-self-check","scope":"mesh","serves":["doctor","probes"]}]}`)
}
if _, err := ParseManifest(holder(`["checked"]`)); err == nil || !strings.Contains(err.Error(), "mesh.seat.mesh-self-check.event.checked") {
t.Errorf("a seat's event listed as the module's own: %v", err)
}
if _, err := ParseManifest(holder(`[]`)); err != nil {
t.Errorf("a holder saying its seat's event on the seat was refused: %v", err)
}
}
// **The refusal moved, it did not go** (novox/hq ADR 0118, superseding 0110). A module may now
// declare its own seats, so whether a claimed seat exists is a fact about the *catalogue* and not
// about the manifest in front of the parser: a claim on a seat another registered module declares
// is perfectly good, and the parser cannot tell the two cases apart. So the parser accepts it and
// registration refuses it — the same guarantee, at the same moment work would otherwise start,
// from a set nobody maintains by hand.
func TestAClaimOnASeatNobodyDeclaresIsRefusedAtRegistration(t *testing.T) {
m, err := ParseManifest(claimed(`[{"name":"the-anything","scope":"node"}]`))
if err != nil {
t.Fatalf("the parser judged a claim it cannot judge alone: %v", err)
}
problems := CatalogueProblems(catalogue.Shelf{m.Module: m})
if len(problems) == 0 {
t.Fatal("a module invented a seat by claiming it, and registration allowed it")
}
joined := strings.Join(problems, "; ")
if !strings.Contains(joined, "the-anything") || !strings.Contains(joined, "no module declares") {
t.Fatalf("the refusal does not say the seat is nobody's: %v", problems)
}
}
func TestAClaimThatIsMalformedIsRefusedOnceForThat(t *testing.T) {
// Not a second time for being unknown: one mistake, one line.
_, err := ParseManifest(claimed(`[{"name":"Not A Name","scope":"node"}]`))
if err == nil {
t.Fatal("a malformed claim was accepted")
}
if strings.Contains(err.Error(), "not a seat") {
t.Fatalf("a malformed claim was also called unknown: %v", err)
}
}
// A holder declares every path its seat names, as a resource or as a file the mesh renders for it, or
// CanHold refuses it naming the seat and each missing path — the judgement registration and handover share.
func TestAClaimWithoutTheSeatsPathsIsRefused(t *testing.T) {
seat := hostnameSeat(t)
hosts := map[string]any{"id": "hosts", "type": "file", "path": "/etc/hosts", "content": "x"}
name := map[string]any{"id": "name", "type": "file", "path": "/etc/hostname", "content": "x"}
if err := catalogue.CanHold(hostnameHolder(hosts, name), seat); err != nil {
t.Errorf("a holder declaring both paths as resources was refused: %v", err)
}
rendered := hostnameHolder(hosts)
rendered.Facts = map[string]catalogue.RosterFile{"name": {Path: "/etc/hostname", Template: "{{.Node}}\n"}}
if err := catalogue.CanHold(rendered, seat); err != nil {
t.Errorf("a holder declaring a path as a file the mesh renders was refused: %v", err)
}
err := catalogue.CanHold(hostnameHolder(), seat)
if err == nil {
t.Fatal("a holder declaring neither path was accepted")
}
for _, want := range []string{"node-hostname", "/etc/hosts", "/etc/hostname", "ADR 0287"} {
if !strings.Contains(err.Error(), want) {
t.Errorf("the refusal does not name %s: %v", want, err)
}
}
// A file under the operator's home is placed per account: it is not the machine's path.
home := hostnameHolder(hosts)
home.Facts = map[string]catalogue.RosterFile{"name": {Path: "/etc/hostname", Home: true, Template: "x"}}
if err := catalogue.CanHold(home, seat); err == nil {
t.Error("a file in the operator's home counted as the machine's /etc/hostname")
}
// At registration, the same refusal.
if got := strings.Join(CatalogueProblems(catalogue.Shelf{"names": hostnameHolder(hosts)}), "; "); !strings.Contains(got,
"does not declare /etc/hostname") {
t.Errorf("registration let a holder without /etc/hostname through: %q", got)
}
}
func TestACompiledBundleStillMustSayItsSystem(t *testing.T) {
b := &catalogue.Build{Artifacts: []catalogue.Artifact{{
Name: "host", Kind: catalogue.ArtifactBundle, Language: "go", From: "cmd/mesh-host",
}}}
if len(buildProblems(b, "mesh-host")) == 0 {
t.Fatal("a compiled bundle with no system was accepted")
}
}
func TestAContainerCannotBeBothRunOnceAndScheduled(t *testing.T) {
// A container runs once and gates, or on a cadence, or stays up — never two (novox/hq ADR 0053).
digest := "@sha256:" + strings.Repeat("a", 64)
bad := []byte(`{"module":"m","resources":[
{"id":"sync","type":"container","name":"sync","image":"registry.example/x` + digest + `","run-once":true,"schedule":"0 3 * * *"}
]}`)
if _, err := ParseManifest(bad); err == nil {
t.Error("a container that was both run-once and scheduled was accepted")
} else if !strings.Contains(err.Error(), "both run-once and schedule") {
t.Errorf("refused for the wrong reason: %v", err)
}
}
func TestAContributionLocalNameMustBeUsable(t *testing.T) {
for _, bad := range []string{
// Not a usable name.
`{"module":"minio","version":"1","requires":["route"],
"contributes":{"route":{"Not OK":{"label":"files","port":9000}}}}`,
// A local contribution with nothing in it.
`{"module":"minio","version":"1","requires":["route"],
"contributes":{"route":{"api":{}}}}`,
} {
if _, err := ParseManifest([]byte(bad)); err == nil {
t.Errorf("accepted:\n%s", bad)
}
}
}
func TestAContributionPlaceholderOutsideTheHolderIsRefused(t *testing.T) {
raw := `{"module":"laptop","resources":[{"id":"t","type":"file","path":"/etc/t","content":"${contribution:node-hotkeys:trigger}"}]}`
if _, err := ParseManifest([]byte(raw)); err == nil || !strings.Contains(err.Error(), "laptop does not claim node-hotkeys") {
t.Errorf("a placeholder outside the holder was accepted: %v", err)
}
raw = `{"module":"triggerhappy","claims":[{"name":"node-hotkeys"}],"resources":[{"id":"t","type":"file","path":"/etc/t","content":"${contribution:node-hotkeys:keys}"}]}`
if _, err := ParseManifest([]byte(raw)); err == nil || !strings.Contains(err.Error(), "it receives trigger") {
t.Errorf("a placeholder for a kind the seat does not receive was accepted: %v", err)
}
}
func TestAContributionToASeatThatDoesNotReceiveItIsRefused(t *testing.T) {
cases := map[string]string{
`{"seat":"node-hotkeys","kind":"config","content":"x"}`: "it receives trigger",
`{"seat":"node-nothing","kind":"trigger","content":"x"}`: "the mesh does not define",
`{"seat":"node-hotkeys","kind":"trigger","content":" "}`: "has no content",
`{"seat":"node-display-session","kind":"trigger","content":"x"}`: "it receives config",
}
for c, want := range cases {
raw := `{"module":"laptop","contributions":[` + c + `]}`
if _, err := ParseManifest([]byte(raw)); err == nil || !strings.Contains(err.Error(), want) {
t.Errorf("%s: accepted, or refused without %q: %v", c, want, err)
}
}
}
func TestADeclaredStoreParsesAndPassesTheCheck(t *testing.T) {
m := mustParse(t, declaredStore)
if problems := DataProblems(catalogue.Shelf{"pg": m}); len(problems) > 0 {
t.Fatalf("a store declaring its data was refused: %v", problems)
}
if !m.KeepsConsumerData("postgres-database") {
t.Fatal("an irreplaceable consumer class does not keep the consumer's data")
}
if it, _ := m.DataItem("store"); !it.BackedUp() || it.BackupWithin() != catalogue.DefaultBackupWithin || it.ActiveWithin().Hours() != 24 {
t.Fatalf("the store item reads %+v", it)
}
if it, _ := m.DataItem("dumps"); it.BackedUp() {
t.Fatal("a rebuildable item that says none is backed up")
}
}
// The node-engine's rules, for directories and — as the engine judges them since novox/hq issue 495 — files.
func TestADefinitionIsRefusedWhereTheNodeEngineRefusesItsPaths(t *testing.T) {
refused := map[string]string{
"a stated directory in docker's data": `{"module": "sidecar", "version": "1", "resources": [
{"id": "volumes", "type": "directory", "path": "/var/lib/docker/volumes/x"}]}`,
"a file in containerd's data": `{"module": "images", "version": "1", "resources": [
{"id": "f", "type": "file", "path": "/var/lib/containerd/x", "content": "x"}]}`,
"a file beneath a placed directory that climbs out of it": `{"module": "docker", "version": "1", "resources": [
{"id": "state", "type": "directory", "place": "mesh"},
{"id": "f", "type": "file", "path": "${dir:state}/../../containers/x", "content": "x"}]}`,
"a file in /boot": `{"module": "grub", "version": "1", "resources": [
{"id": "cfg", "type": "file", "path": "/boot/grub/custom.cfg", "content": "x"}]}`,
"a directory that is the mesh's whole tree": `{"module": "mesh", "version": "1", "resources": [
{"id": "all", "type": "directory", "place": "."}]}`,
"a directory that holds /etc": `{"module": "x", "version": "1", "resources": [
{"id": "d", "type": "directory", "path": "/"}]}`,
"a directory in the node-engine's own tree, by another module": `{"module": "intruder", "version": "1",
"resources": [{"id": "d", "type": "directory", "path": "/var/lib/mesh-host/x"}]}`,
}
for name, raw := range refused {
if _, err := ParseManifest([]byte(raw)); err == nil || !strings.Contains(err.Error(), "issue 496") {
t.Errorf("%s: accepted, or refused for another reason: %v", name, err)
}
}
// What the engine applies, the mesh's own modules' paths among them (read from every machine's live plan):
// the machine's configuration, run directories, programs below /usr/local, the node-engine's own trees by its
// own module, an account's keys, a module's own root, and — not on the engine's lists, so not refused here —
// below /lib and at /storage, /data, /services and /var/lock.
accepted := map[string]string{
"a unit in /etc": `{"module": "power", "version": "1", "resources": [
{"id": "d", "type": "directory", "path": "/etc/systemd/system/x.service.d"},
{"id": "u", "type": "file", "path": "/etc/systemd/system/x.service.d/a.conf", "content": "x"}]}`,
"a run directory": `{"module": "fail2ban", "version": "1", "resources": [
{"id": "run-dir", "type": "directory", "path": "/var/run/fail2ban"}]}`,
"a program in /usr/local/bin": `{"module": "claude-code", "version": "1", "resources": [
{"id": "start", "type": "file", "path": "/usr/local/bin/claude-agent", "content": "x"}]}`,
"the node-engine's launcher and state, by the node-engine": `{"module": "mesh-host", "version": "1", "resources": [
{"id": "launcher", "type": "file", "path": "/usr/lib/nox-mesh-host/launch", "content": "x"},
{"id": "state", "type": "directory", "path": "/var/lib/mesh-host"}]}`,
"an account's .ssh": `{"module": "ssh-client", "version": "1", "resources": [
{"id": "ssh-dir", "type": "directory", "path": "/home/someone/.ssh"},
{"id": "config", "type": "file", "path": "/home/someone/.ssh/config", "content": "x"}]}`,
"a module's own root": `{"module": "mailu", "version": "1", "resources": [
{"id": "state", "type": "directory", "place": "."}]}`,
"a file below /lib": `{"module": "udev", "version": "1", "resources": [
{"id": "rule", "type": "file", "path": "/lib/udev/rules.d/99-x.rules", "content": "x"}]}`,
"directories at /storage, /data, /services and /var/lock": `{"module": "roots", "version": "1", "resources": [
{"id": "a", "type": "directory", "path": "/storage"}, {"id": "b", "type": "directory", "path": "/data"},
{"id": "c", "type": "directory", "path": "/services"}, {"id": "d", "type": "directory", "path": "/var/lock"}]}`,
}
for name, raw := range accepted {
if _, err := ParseManifest([]byte(raw)); err != nil {
t.Errorf("%s: refused: %v", name, err)
}
}
}
// A definition names no installation (novox/hq ADR 0112, ADR 0155). What the mesh acts on is judged;
// prose is not; the world's registries and resolvers are the world's; a declared exception is a
// reason a reader sees.
func TestADefinitionNamingAnInstallationIsNamedBack(t *testing.T) {
m := catalogue.Manifest{Module: "idp", Resources: []map[string]any{
{"id": "server", "type": "container", "image": "quay.io/keycloak/keycloak@sha256:aa",
"env": map[string]any{"KC_HOSTNAME": "https://login.mesh-one.be"}},
{"id": "env", "type": "file", "content": "REAL_IP_FROM=192.168.1.0/24,127.0.0.0/8,203.0.113.7,51.15.22.9\n"},
}, Listens: []catalogue.Listening{{Port: 8080, From: catalogue.FromMesh, Why: "the login page; login.mesh-one.be is a route grant"}}}
got := strings.Join(InstallationProblems(m), "\n")
for _, want := range []string{
"idp names login.mesh-one.be at resources[0].env.KC_HOSTNAME",
"idp names the public address 51.15.22.9 at resources[1].content",
} {
if !strings.Contains(got, want) {
t.Errorf("missing %q in:\n%s", want, got)
}
}
for _, mustNot := range []string{"quay.io", "why", "203.0.113.7", "192.168.1.0", "127.0.0.0"} {
if strings.Contains(got, mustNot) {
t.Errorf("%q was reported and should not be:\n%s", mustNot, got)
}
}
}
func TestADeliveringSeatIsOnlyHeldByAModuleThatProvides(t *testing.T) {
// Holding it makes the module the mesh's answer for the provision. A module that cannot answer
// would be the answer anyway, and every consumer would be sent to it.
// And refused at registration, where the seat set is the store's: what a seat delivers is
// data, so a compiled copy of it may not be what refuses a build (novox/hq ADR 0122).
raw := []byte(`{"module":"thing","version":"1","claims":[{"name":"git","scope":"mesh"}]}`)
m, err := ParseManifest(raw)
if err != nil {
t.Fatalf("the parser judged what a seat delivers: %v", err)
}
got := strings.Join(CatalogueProblems(catalogue.Shelf{m.Module: m}), "; ")
if !strings.Contains(got, `does not provide "git"`) {
t.Fatalf("the refusal does not say what is missing: %q", got)
}
}
func TestADirectoryBeneathAPlacedOneIsPlacedWithIt(t *testing.T) {
t.Run("anEnvironmentItemIsTypedAndNamesWhatItNeeds", anEnvironmentItemIsTypedAndNamesWhatItNeeds)
t.Run("theMeshsDirectoryForAModuleIsAPlace", theMeshsDirectoryForAModuleIsAPlace)
t.Run("anIdentityServedAsADNSLabelIsBoundedToOne", anIdentityServedAsADNSLabelIsBoundedToOne)
// An adopted layout keeps a subdirectory the predecessor made under the mesh's directory
// (issue 174: a forge's runtime state, a manager's output). Stated as beneath the placed one,
// it moves with it — a node's root moves both, and the definition names no host path.
m := catalogue.Manifest{Module: "gitea", Resources: []map[string]any{
{"id": "mesh-state", "type": "directory", "place": "mesh"},
{"id": "runtime-state", "type": "directory", "path": "${dir:mesh-state}/state"},
{"id": "server", "type": "container", "image": "x@sha256:aa",
"volumes": []any{"${dir:runtime-state}:/data"}},
}}
if got := manifestUnknownDirRefs(m); len(got) != 0 {
t.Fatalf("a path beneath a placed directory is well formed; got %v", got)
}
dirs := dirsFor(m, catalogue.Rendering{DataRoot: "/srv"})
if dirs["runtime-state"] != "/srv/mesh/gitea/state" {
t.Fatalf("the subdirectory follows the placed one; got %v", dirs)
}
}
func TestADirectoryPlacedInDockersDataIsRefusedWhereTheDefinitionIsJudged(t *testing.T) {
// The #205 shape, as it was merged.
_, err := ParseManifest([]byte(`{"module": "docker", "version": "1",
"resources": [{"id": "state", "type": "directory", "place": "."}]}`))
if err == nil {
t.Fatal("a directory resolving to /var/lib/docker was accepted; the node-engine refuses it at apply")
}
for _, said := range []string{"docker", `"state"`, "/var/lib/docker", "issue 496"} {
if !strings.Contains(err.Error(), said) {
t.Errorf("the refusal names the module, the resource, the path and why; %q is missing from %q", said, err)
}
}
}
func TestAFileNamingBothATemplateAndContentIsRefused(t *testing.T) {
raw := strings.Replace(templatedTwin, `"template":"files/tidy.conf"`,
`"template":"files/tidy.conf","content":"x"`, 1)
_, err := ParseManifest([]byte(raw))
if err == nil || !strings.Contains(err.Error(), "main") || !strings.Contains(err.Error(), "never both") {
t.Fatalf("a file with both a template and content was not refused by name: %v", err)
}
}
// A file that asks for a setting says whether what it asks is trusted (novox/hq issue 339): `trusted` is a
// boolean, on a file alone, and a file asking for a setting without it is named.
func TestAFileSaysWhetherItsSettingsAreTrusted(t *testing.T) {
m := catalogue.Manifest{Module: "power", Resources: []map[string]any{
{"id": "said", "type": "file", "path": "/etc/a", "trusted": true, "content": "${setting:a}"},
{"id": "unsaid", "type": "file", "path": "/etc/b", "content": "${setting:b}"},
{"id": "no-setting", "type": "file", "path": "/etc/c", "content": "plain"}}}
if got := strings.Join(catalogue.UnsaidTrust(m), ","); got != "unsaid" {
t.Errorf("unsaid: %s; want unsaid", got)
}
// A mergeable file that names keys asks for them, so it is named too; one that names none asks for nothing.
merged := catalogue.Manifest{Module: "agent", Resources: []map[string]any{
{"id": "settings", "type": "file", "path": "/a", "merge": catalogue.MergeJSON, "content": `{"managed_settings": {}}`},
{"id": "blank", "type": "file", "path": "/b", "merge": catalogue.MergeJSON, "content": `{}`}}}
if got := strings.Join(catalogue.UnsaidTrust(merged), ","); got != "settings" {
t.Errorf("unsaid: %s; want settings", got)
}
for _, bad := range []map[string]any{
{"id": "x", "type": "file", "path": "/etc/x", "trusted": "yes", "content": "${setting:a}"},
{"id": "y", "type": "directory", "trusted": true},
} {
if problems := TrustProblems(catalogue.Manifest{Module: "power", Resources: []map[string]any{bad}}); len(problems) == 0 {
t.Errorf("%v was taken", bad)
}
}
}
func TestAGoBundleMayNameItsCommand(t *testing.T) {
b := &catalogue.Build{Artifacts: []catalogue.Artifact{{
Name: "host-arch", Kind: catalogue.ArtifactBundle, Language: "go", System: "arch",
From: "cmd/mesh-host",
}}}
if p := buildProblems(b, "mesh-host"); len(p) != 0 {
t.Fatalf("a go bundle naming its command was refused: %v", p)
}
}
// novox/hq ADR 0193: a Go tools bundle is served — its binary is what the runtime starts, delivered
// like any tools bundle, named to the runtime where a TypeScript bundle names its launcher.
func TestAGoToolsBundleIsServedByItsBinary(t *testing.T) {
lamp := catalogue.Manifest{Module: "lamp", Version: "1", Tools: []string{"on"},
Build: &catalogue.Build{Artifacts: []catalogue.Artifact{{Name: "tools", Kind: catalogue.ArtifactBundle, Language: "go",
System: "arch", From: "cmd/lamp-tools"}}}}
if p := buildProblems(lamp.Build, "lamp"); len(p) != 0 {
t.Fatalf("a Go tools bundle was refused: %v", p)
}
lamp, err := lamp.Resolve([]catalogue.Built{{Name: "tools", Kind: catalogue.ArtifactBundle,
Reference: catalogue.ArtifactStoreScheme + "lamp/tools/blobs/" + bundleDigest, Digest: bundleDigest}})
if err != nil {
t.Fatal(err)
}
if fmt.Sprint(lamp.Bundles[0].Loads) != "[lamp-tools]" {
t.Fatalf("the runtime loads %v from a Go bundle, want its binary", lamp.Bundles[0].Loads)
}
// An artifact may say it explicitly; naming anything but the binary is refused.
said := catalogue.Manifest{Module: "lamp", Version: "1", Build: &catalogue.Build{Artifacts: []catalogue.Artifact{{Name: "tools",
Kind: catalogue.ArtifactBundle, Language: "go", System: "arch", Binary: "lamp", Loads: []string{"lamp"}}}}}
if p := buildProblems(said.Build, "lamp"); len(p) != 0 {
t.Errorf("loads naming the binary was refused: %v", p)
}
said.Build.Artifacts[0].Loads = []string{"tools/index.js"}
if p := buildProblems(said.Build, "lamp"); len(p) == 0 {
t.Error("a Go bundle loading a file it does not contain was admitted")
}
}
func TestAGuardedPortMustBeAPort(t *testing.T) {
if _, err := ParseManifest([]byte(`{"module":"postgres","guards":[5432]}`)); err != nil {
t.Fatalf("guards is refused: %v", err)
}
if _, err := ParseManifest([]byte(`{"module":"postgres","guards":[0]}`)); err == nil {
t.Fatal("guarding port 0 was accepted")
}
}
// Each entry names the credential form the provider's offer declares (issue 768), from the provider's manifest, so
// the vault makes a first value in that form for a provider it holds no row of: one of its own secrets shared by
// every consumer, a login per consumer when the offer says no credential, issued by its software, or given by a
// person (the offer's own secret issued outside).
func TestAHandedModulesPlanNamesTheCredentialFormOfItsProvidersOffer(t *testing.T) {
storeOffering := func(credential string) catalogue.Manifest {
raw := `{"module":"store","version":"1","provides":[{"name":"postgres-database","scope":"mesh"` +
credential + `}],"serves":{"postgres-database":{"port":5432}}`
if strings.Contains(credential, `"own"`) {
raw += `,"own-secrets":{"admin":{"path":"/var/lib/store/admin","taken":"at-start"},` +
`"token":{"path":"/var/lib/store/token","taken":"at-start","issued-by":"outside"}}`
}
return mustParse(t, raw+`}`)
}
for credential, want := range map[string]string{
// No default (the lead, 2026-10-29): an offer that says nothing has no form, and the catalogue refuses it.
``: "",
`,"credential":{"form":"pair"}`: catalogue.CredentialFormPair,
`,"credential":{"form":"none"}`: catalogue.CredentialFormNone,
`,"credential":{"own":"admin"}`: catalogue.CredentialFormShared,
`,"credential":{"own":"token"}`: catalogue.CredentialFormGiven,
`,"credential":{"issued-by":"outside"}`: catalogue.CredentialFormGiven,
`,"credential":{"issued-by":"software","tool":"store_issued_token"}`: catalogue.CredentialFormIssued,
} {
// The form the provider's offer declares, read from its manifest as the judge reads it; a plan naming it is the
// node-engine's composition (ADR 0593 §7).
if got := storeOffering(credential).CredentialFormOf("postgres-database"); got != want {
t.Errorf("an offer with %q: the form %q, want %q", credential, got, want)
}
// Every form a plan names is in the one list each reader checks (issue 774); an undeclared one is not.
if catalogue.IsCredentialForm(want) != (want != "") {
t.Errorf("the form %q of an offer with %q: in CredentialForms %v", want, credential, catalogue.IsCredentialForm(want))
}
}
if forms := strings.Join(catalogue.CredentialForms(), ","); forms != "shared,pair,issued,given,none" || catalogue.IsCredentialForm("maybe") {
t.Errorf("the credential forms are %s, and maybe is one: %v", forms, catalogue.IsCredentialForm("maybe"))
}
if got := (catalogue.Manifest{Module: "x"}).CredentialFormOf("postgres-database"); got != "" {
t.Errorf("a module offering nothing has the credential form %q", got)
}
// The catalogue refuses an offer saying no form, and a form beside an own secret or not pair or none.
for credential, refused := range map[string]string{``: "says no credential form", `,"credential":{}`: "says no credential form",
`,"credential":{"form":"pair","own":"admin"}`: "beside an own secret", `,"credential":{"form":"maybe"}`: `not "pair" or "none"`,
`,"credential":{"form":"none"}`: ""} {
raw := `{"module":"store","version":"1","provides":[{"name":"postgres-database","scope":"mesh"` + credential + `}]}`
var m catalogue.Manifest
_ = json.Unmarshal([]byte(raw), &m)
problems := strings.Join(CredentialFormProblems(m), "; ")
if (refused == "") != (problems == "") || !strings.Contains(problems, refused) {
t.Errorf("%q: %q, want %q", credential, problems, refused)
}
}
}
// A holder that does not answer what the seat promises is a caller's timeout, found here instead.
func TestAHolderMustServeWhatItsSeatPromises(t *testing.T) {
m := telegram()
m.Tools = nil // declares the seat, serves none of it
got := problemsFor(t, catalogue.Shelf{"telegram": m})
if !strings.Contains(got, "does not serve status") {
t.Fatalf("a holder was accepted that answers nothing its seat promises: %s", got)
}
}
// issue 900, ADR 0394 §2: a holder of node-files runs as the tool runner's account, so it declares no runs-as.
func TestAHolderOfNodeFilesDeclaringRunsAsIsRefused(t *testing.T) {
p := FilesHolderProblems(catalogue.Manifest{Module: "files", RunsAs: "mesh-files", Claims: []catalogue.Claim{{Name: catalogue.FilesSeat}}})
if len(p) != 1 || !strings.Contains(p[0], "0394") || !strings.Contains(p[0], "runs-as") {
t.Errorf("a holder declaring runs-as was not refused: %v", p)
}
if p := FilesHolderProblems(catalogue.Manifest{Module: "files", Claims: []catalogue.Claim{{Name: catalogue.FilesSeat}}}); len(p) != 0 {
t.Errorf("a holder declaring none was refused: %v", p)
}
if p := FilesHolderProblems(catalogue.Manifest{Module: "other", RunsAs: "x"}); len(p) != 0 {
t.Errorf("a non-holder was judged: %v", p)
}
}
// novox/hq ADR 0310 §3: a holder of node-reads reads as an account of its own; one the machine's runtime would
// carry — as the operator's account — is refused at parse.
func TestAHolderOfNodeReadsWithoutAnAccountOfItsOwnIsRefused(t *testing.T) {
_, err := ParseManifest([]byte(`{"module": "reads", "version": "1",
"claims": [{"name": "node-reads", "scope": "node"}]}`))
if err == nil || !strings.Contains(err.Error(), "runs-as") || !strings.Contains(err.Error(), "0310") {
t.Errorf("a holder of node-reads without runs-as was not refused for it: %v", err)
}
if p := ReadsHolderProblems(catalogue.Manifest{Module: "reads", RunsAs: "mesh-reads",
Claims: []catalogue.Claim{{Name: catalogue.ReadsSeat}}}); len(p) != 0 {
t.Errorf("a holder running as its own account was refused: %v", p)
}
}
func TestAHolderPlacesEveryCombinationOnceWithATemplateThatRendersEveryExample(t *testing.T) {
holder := func(template string, contents ...string) string {
var res []string
for i, c := range contents {
b, _ := json.Marshal(c)
res = append(res, `{"id":"f`+string(rune('a'+i))+`","type":"file","path":"/f`+string(rune('a'+i))+`","content":`+string(b)+`}`)
}
tb, _ := json.Marshal(template)
renders := ""
if template != "" {
renders = `,"renders":{"block":` + string(tb) + `}`
}
return `{"module":"a-bar","claims":[{"name":"node-bar"` + renders + `}],"resources":[` + strings.Join(res, ",") + `]}`
}
all := "${contribution:node-bar:block:bar=bottom,place=resources}${contribution:node-bar:block:bar=bottom,place=status}" +
"${contribution:node-bar:block:bar=top,place=resources}${contribution:node-bar:block:bar=top,place=status}"
cases := map[string]string{
holder(barTemplate, "${contribution:node-bar:block:bar=bottom}"): "never places node-bar:block for bar=top,place=resources",
holder(barTemplate, all, "${contribution:node-bar:block:place=status}"): "for bar=bottom,place=status 2 times",
holder("", all): "gives no template",
holder(`{{if eq .shows "battery"}}b{{end}}`, all): "renders nothing",
holder(`{{.nothing}}`, all): "renders nothing",
holder(`{{if}`, all): "does not parse",
holder(barTemplate, "${contribution:node-bar:block:bar=left}"+all): "bar is one of bottom, top",
holder(barTemplate, "${contribution:node-bar:block:colour=red}"+all): "placed by bar, place",
holder(barTemplate, "${contribution:node-hotkeys:trigger:bar=top}"): "does not claim node-hotkeys",
}
for raw, want := range cases {
if _, err := ParseManifest([]byte(raw)); err == nil || !strings.Contains(err.Error(), want) {
t.Errorf("%s:\naccepted, or refused without %q: %v", raw, want, err)
}
}
for _, raw := range []string{holder(barTemplate, all), holder(barTemplate, "${contribution:node-bar:block}"),
// A holder that places no block at all is older than the kind, and is not refused for it.
holder("", "no blocks here")} {
if _, err := ParseManifest([]byte(raw)); err != nil {
t.Errorf("%s: refused: %v", raw, err)
}
}
}
// A jail with no name, no pattern, or a name another jail of the same module already took is refused.
func TestAJailIsNamedOnceAndSaysWhatAFailureLooksLike(t *testing.T) {
m := catalogue.Manifest{Module: "mailu", Jails: []catalogue.Jail{
{Name: "", Failregex: "^.*rip=<HOST>$"},
{Name: "mailu-front", Failregex: " "},
{Name: "mailu-front", Failregex: "^.*rip=<HOST>$"},
}}
problems := strings.Join(manifestJailProblems(m), "\n")
for _, want := range []string{"a jail with no name", "says nothing a failed attempt looks like", "two jails called"} {
if !strings.Contains(problems, want) {
t.Errorf("missing %q in:\n%s", want, problems)
}
}
}
// A jail whose pattern names <HOST> twice is refused by name: fail2ban would read it as a duplicate
// capture group and refuse its whole configuration, leaving the machine with no bans at all — which
// is what happened on the control node the day this was built (novox/hq ADR 0179).
func TestAJailsPatternNamesTheHostOncePerShape(t *testing.T) {
twice := catalogue.Manifest{Module: "route-proxy", Jails: []catalogue.Jail{{
Name: "route-proxy",
Failregex: `^.*(?:refused for <HOST>:\d+|asked from <HOST>:\d+)$`,
Jail: "backend = systemd",
}}}
problems := manifestJailProblems(twice)
if len(problems) != 1 || !strings.Contains(problems[0], "names <HOST> 2 times") {
t.Fatalf("a pattern naming <HOST> twice was not refused by name: %v", problems)
}
if !strings.Contains(problems[0], "no bans at all") {
t.Errorf("the refusal does not say what it costs: %q", problems[0])
}
// The same two shapes, one pattern each, is how it is written — and is accepted.
split := catalogue.Manifest{Module: "route-proxy", Jails: []catalogue.Jail{{
Name: "route-proxy",
Failregex: "^.*refused for <HOST>:\\d+$\n ^.*asked from <HOST>:\\d+$",
Jail: "backend = systemd",
}}}
if problems := manifestJailProblems(split); len(problems) != 0 {
t.Fatalf("one <HOST> per pattern was refused: %v", problems)
}
}
// The operator's own value is told by what a key's name is about: its last word, or its last two as
// a known compound; a plural as its singular; and never a number or a switch.
func TestAKeyIsTheOperatorsByWhatItIsAbout(t *testing.T) {
for _, key := range []string{"max-tokens", "show-hostname", "ghost-opacity", "users-per-page", "mailbox-size",
"font-size", "width", "keyboard-delay", "ipv6-preferred", "client-width", "user-agent", "url-timeout",
"site-title", "cert-renewal-days", "name", "font-name", "allow-resize", "use-gpu", "disable-sender-check",
"max-recipients", "gateway-timeout", "sender-delay", "upstream-resolvers", "mirror-countries",
"pool-region", "proxy-timeout", "listen-backlog", "peer-keepalive", "admin-theme", "log-file",
"cache-path"} {
if w := operatorsWord(key); w != "" {
t.Errorf("%s read as the operator's (%s)", key, w)
}
}
for key, word := range map[string]string{"mail-domain": "domain", "api-key": "key", "admin-password": "password",
"oauth-client-id": "client-id", "db-dsn": "dsn", "public-ip": "ip", "puid": "puid", "dns-zone": "zone",
"webhook": "webhook", "notify_phone": "phone", "cors.origin": "origin", "smtp-pass": "pass",
"backup-passphrase": "passphrase", "data-owner": "owner", "fqdn": "fqdn", "tenant": "tenant", "host": "host",
"allowed-hosts": "host", "admin-emails": "email", "tokens": "token", "hostname": "hostname",
"apikey": "apikey", "servername": "servername", "tls-cert": "cert", "site": "site", "timezone": "timezone",
"bearer": "bearer", "bind-ipv4": "ipv4", "listen-ipv6": "ipv6", "site-name": "site-name",
"server-name": "server-name", "public-name": "public-name", "smtp-relay": "smtp-relay",
"host-name": "host-name", "user-name": "user-name", "domain-name": "domain-name",
"nameserver": "nameserver", "upstream-nameservers": "nameserver", "dns-server": "dns-server",
"dns-servers": "dns-server", "default-gateway": "gateway", "lan-subnet": "subnet", "sender": "sender",
"notify-recipients": "recipient", "contact": "contact", "tls-certificate": "certificate",
"allow-from": "allow-from", "deny-from": "deny-from", "allow-hosts": "host", "use-host": "host",
"trusted": "trusted", "allow-list": "allow-list", "ip-whitelist": "whitelist", "peer": "peer",
"wireguard-peers": "peer", "bind": "bind", "listen": "listen", "upstream": "upstream", "http-proxy": "proxy",
"trusted-proxies": "proxy", "admin": "admin", "notify-admin": "admin", "wake-mac": "mac",
"password-file": "password-file", "key-file": "key-file", "token-path": "token-path",
"secret-file": "secret-file", "cert-path": "cert-path"} {
if w := operatorsWord(key); w != word {
t.Errorf("%s: read %q, want %q", key, w, word)
}
}
}
func TestAKindedBenchNeedsAKindAndNoOtherSeatTakesOne(t *testing.T) {
got := problemsFor(t, catalogue.Shelf{"messenger": router(), "nameless": aChannel("nameless", "")})
if !strings.Contains(got, "claims the kinded bench channel and names no kind") {
t.Fatalf("a claim without a kind stood: %s", got)
}
odd := catalogue.Manifest{Module: "odd", Claims: []catalogue.Claim{{Name: "operator-channel", Scope: catalogue.ScopeMesh, Kind: "telegram"}}}
got = problemsFor(t, catalogue.Shelf{"messenger": router(), "odd": odd})
if !strings.Contains(got, "only a kinded bench takes a kind") {
t.Fatalf("a kind on a seat that is not kinded stood: %s", got)
}
dotted := problemsFor(t, catalogue.Shelf{"messenger": router(), "dotted": aChannel("dotted", "a.b")})
if !strings.Contains(dotted, "not a usable name") {
t.Fatalf("a kind that would widen a subject stood: %s", dotted)
}
}
// A local name names one credential: two requirements may not share it (review C2).
func TestALocalNameIsUniqueAcrossRequirements(t *testing.T) {
_, err := ParseManifest([]byte(`{"module":"x","version":"1","requires":["secret","postgres-database"],
"secrets":{"secret":{"x":"/var/lib/x/a"},"postgres-database":{"x":"/var/lib/x/b"}}}`))
if err == nil || !strings.Contains(err.Error(), "both under") {
t.Fatalf("two requirements under one local name were accepted: %v", err)
}
}
func TestALocalNameMayNotCollideWithWhatTheModuleAlreadyCallsSomething(t *testing.T) {
for _, bad := range []string{
// One of the module's own secrets.
`{"module":"ca","version":"1","requires":["secret"],"own-secrets":{"root-key":"/var/lib/ca/own"},
"secrets":{"secret":{"root-key":"/var/lib/ca/root.key"}}}`,
// Something it requires.
`{"module":"ca","version":"1","requires":["secret","postgres-database"],
"secrets":{"secret":{"postgres-database":"/var/lib/ca/x"}}}`,
// Not a usable name.
`{"module":"ca","version":"1","requires":["secret"],"secrets":{"secret":{"Root Key":"/var/lib/ca/x"}}}`,
// A relative path.
`{"module":"ca","version":"1","requires":["secret"],"secrets":{"secret":{"root-key":"root.key"}}}`,
} {
if _, err := ParseManifest([]byte(bad)); err == nil {
t.Errorf("accepted:\n%s", bad)
}
}
}
// The catalogue check refuses a log outside the caps, with a name that is not one plain token, with a
// field the mesh does not know, or kept by a module whose own name is not one plain token.
func TestALogIsNamedLocallyAndCapped(t *testing.T) {
for _, c := range []struct{ manifest, says string }{
{`{"module":"a","version":"1","logs":["mesh.changes"]}`, `keeps log "mesh.changes": a log is named locally`},
{`{"module":"a","version":"1","logs":["my_changes"]}`, `keeps log "my_changes"`},
{`{"module":"a","version":"1","logs":["Changes"]}`, `keeps log "Changes"`},
{`{"module":"a","version":"1","logs":["c","c"]}`, `keeps log "c" twice`},
{`{"module":"a","version":"1","logs":[{"name":"c","max-mib":8193}]}`, `between 1 and 8192 MiB`},
{`{"module":"a","version":"1","logs":[{"name":"c","max-mib":-1}]}`, `between 1 and 8192 MiB`},
{`{"module":"a","version":"1","logs":[{"name":"c","max-mib":0}]}`, `max-mib is between 1 and 8192, not 0`},
{`{"module":"a","version":"1","logs":[{"name":"c","stream":"LOG_x"}]}`, `{name, max-mib, max-age-days}`},
{`{"module":"a","version":"1","logs":[{"name":"c","max-age-days":0}]}`, `max-age-days is between 1 and 3650, not 0`},
{`{"module":"a.b","version":"1","logs":["c"]}`, `no dot`},
} {
_, err := ParseManifest([]byte(c.manifest))
if c.says == "" {
if err != nil {
t.Errorf("%s was refused: %v", c.manifest, err)
}
continue
}
if err == nil {
t.Errorf("%s was accepted", c.manifest)
continue
}
if !strings.Contains(err.Error(), c.says) {
t.Errorf("%s refused for the wrong reason: %v", c.manifest, err)
}
}
}
func TestAMaintenanceWindowIsRefusedWhereTheDefinitionShowsItCannotMean(t *testing.T) {
for _, c := range []struct {
name string
step map[string]any
says string
}{
{
"on a step with no schedule",
map[string]any{"id": "collect", "type": "container", "name": "c",
"image": "registry@sha256:" + strings.Repeat("a", 64),
"while-stopped": []any{"store"}},
"gates what is declared after it",
},
{
"on a run-once step, which already has order",
map[string]any{"id": "collect", "type": "container", "name": "c",
"image": "registry@sha256:" + strings.Repeat("a", 64),
"run-once": true, "while-stopped": []any{"store"}},
"A maintenance window is for a recurring step",
},
{
"naming a container this module does not declare",
map[string]any{"id": "collect", "type": "container", "name": "c",
"image": "registry@sha256:" + strings.Repeat("a", 64),
"schedule": "30 3 * * *", "while-stopped": []any{"the-broker"}},
"could quiesce a neighbour could stop the mesh",
},
{
"naming itself",
map[string]any{"id": "collect", "type": "container", "name": "c",
"image": "registry@sha256:" + strings.Repeat("a", 64),
"schedule": "30 3 * * *", "while-stopped": []any{"collect"}},
"naming itself",
},
{
"written as something that is not a list",
map[string]any{"id": "collect", "type": "container", "name": "c",
"image": "registry@sha256:" + strings.Repeat("a", 64),
"schedule": "30 3 * * *", "while-stopped": "store"},
"a list of this module's container ids",
},
} {
_, err := ParseManifest(aStoreManifest(c.step))
if err == nil {
t.Errorf("%s was accepted", c.name)
continue
}
if !strings.Contains(err.Error(), c.says) {
t.Errorf("%s: the refusal does not say %q:\n%v", c.name, c.says, err)
}
}
}
// What ADR 0203 §2 allows a contribution to say, refused at parse when it says anything else.
func TestAMalformedEnvironmentIsRefusedAtParse(t *testing.T) {
for _, c := range []struct{ environment, want string }{
{`{"variables":{"1X":"a"}}`, `tool sets the variable "1X", which is not a name a shell accepts`},
{`{"variables":{"MY-VAR":"a"}}`, `tool sets the variable "MY-VAR", which is not a name a shell accepts`},
{`{"variables":{"PATH":"/bin"}}`, `tool sets PATH as a variable; a module adds an entry under environment.path`},
{`{"variables":{"A":"$HOME/x"}}`, `tool sets A to "$HOME/x", which holds a $ that is not one of the machine's ${machine:…} facts`},
{`{"variables":{"A":"${HOME}/x"}}`, `tool sets A to "${HOME}/x", which holds a $`},
{`{"variables":{"A":"it's"}}`, `tool sets A to "it's", which holds a quote`},
{`{"variables":{"A":"say \"hi\""}}`, `which holds a quote`},
{`{"variables":{"A":"a\\b"}}`, `which holds a backslash`},
{`{"variables":{"A":"a\nb"}}`, `which holds a line break`},
{`{"variables":{"A":"a\u0000b"}}`, `which holds a NUL`},
{`{"path":[{"entry":"","at":"start"}]}`, `tool's PATH entry 1 names no directory`},
{`{"path":[{"entry":"/a:/b","at":"start"}]}`, `tool puts "/a:/b" on PATH, which holds a colon`},
{`{"path":[{"entry":"$HOME/bin","at":"start"}]}`, `tool puts "$HOME/bin" on PATH, which holds a $`},
{`{"path":[{"entry":"/a","at":"middle"}]}`, `tool puts "/a" on PATH at "middle"; an entry goes at "start" or "end"`},
{`{"path":[{"entry":"/a"}]}`, `tool puts "/a" on PATH at ""`},
{`{"path":[{"entry":"/a","at":"start"},{"entry":"/a","at":"end"}]}`, `tool puts "/a" on PATH twice`},
{`{"variables":{"A":"x"},"paths":[]}`, `unknown field "paths"`},
} {
_, err := ParseManifest([]byte(`{"module":"tool","environment":` + c.environment + `}`))
if err == nil || !strings.Contains(err.Error(), c.want) {
t.Errorf("%s: want %q, got %v", c.environment, c.want, err)
}
}
// What is allowed: a literal, and the machine's own facts.
if _, err := ParseManifest([]byte(`{"module":"tool","environment":{` +
`"variables":{"GOPATH":"${machine:account-home}/go","DISABLE_X":"1","ANSWER":"a b+c=d"},` +
`"path":[{"entry":"${machine:account-home}/go/bin","at":"start"},{"entry":"/opt/x","at":"end"}]}}`)); err != nil {
t.Fatalf("a well-formed environment was refused: %v", err)
}
}
func TestAMalformedFamilyIsRefused(t *testing.T) {
for name, c := range map[string]struct{ own, says string }{
"made by the mesh": {`"smb-password-*":{"path":"/s/smb-password-*.secret","taken":"at-start"}`, "issued-by"},
"no star in its path": {`"smb-password-*":{"path":"/s/smb-password.secret","issued-by":"outside"}`,
"exactly one *"},
"two stars in its path": {`"smb-password-*":{"path":"/s/*/smb-password-*.secret","issued-by":"outside"}`,
"exactly one *"},
"a star inside a name": {`"smb*password":{"path":"/s/x","issued-by":"outside"}`, "only ends a family"},
"a name that is also a member": {`"smb-password-*":{"path":"/s/p-*","issued-by":"outside"},
"smb-password-games":{"path":"/s/games","issued-by":"outside"}`, "also a member"},
} {
if _, err := familyManifest(t, c.own); err == nil || !strings.Contains(err.Error(), c.says) {
t.Errorf("%s: %v; want a refusal saying %q", name, err, c.says)
}
}
}
// A prompt that is not one short line naming only {member} is refused by the module check (novox/hq issue 789).
func TestAMalformedPromptIsRefused(t *testing.T) {
for name, c := range map[string]struct{ prompt, says string }{
"a newline": {"the password of\nthe mail account {member}", "one line"},
"over 120 characters": {"the password of the mail account {member}, which this mesh needs to log in to " +
"send and receive mail for the operator's account on this machine today", "120 characters"},
"a placeholder other than {member}": {"the password of the mail account {name}", "{member}"},
"two placeholders, one unknown": {"the {kind} secret of the mail account {member}", "{member}"},
} {
_, err := ParseManifest([]byte(`{"module":"mesh-mail","version":"1","own-secrets":{
"mail-credential-*":{"path":"/var/lib/mesh-mail/mail-credential-*.secret","issued-by":"outside",
"prompt":` + jsonString(c.prompt) + `}}}`))
if err == nil || !strings.Contains(err.Error(), c.says) {
t.Errorf("%s: %v; want a refusal saying %q", name, err, c.says)
}
}
}
func TestAMalformedScheduleIsRefused(t *testing.T) {
digest := "@sha256:" + strings.Repeat("a", 64)
bad := []byte(`{"module":"m","resources":[
{"id":"sync","type":"container","name":"sync","image":"registry.example/x` + digest + `","schedule":"every night"}
]}`)
if _, err := ParseManifest(bad); err == nil {
t.Error("a malformed schedule was accepted")
} else if !strings.Contains(err.Error(), "cron") {
t.Errorf("refused for the wrong reason: %v", err)
}
// A schedule out of range is refused for the same reason, near its author.
outOfRange := []byte(`{"module":"m","resources":[
{"id":"sync","type":"container","name":"sync","image":"registry.example/x` + digest + `","schedule":"0 25 * * *"}
]}`)
if _, err := ParseManifest(outOfRange); err == nil {
t.Error("a schedule with an hour out of range was accepted")
}
good := []byte(`{"module":"m","resources":[
{"id":"sync","type":"container","name":"sync","image":"registry.example/x` + digest + `","schedule":"*/15 * * * *"}
]}`)
if _, err := ParseManifest(good); err != nil {
t.Errorf("a valid scheduled container was refused: %v", err)
}
}
// 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 := catalogue.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 manifest key nothing reads is worse than a key that is refused: five HAL manifests carried
// `scope:`, which read as a restriction and restricted nothing (novox/hq 04-ISSUES/003). The host's
// declaration parser has always refused unknown keys; manifests never did.
func TestAManifestKeyNothingReadsIsRefused(t *testing.T) {
_, err := ParseManifest([]byte(`{"module":"web","scope":"mesh"}`))
if err == nil {
t.Fatal("a manifest with a key nothing reads was accepted, which is how scope: survived")
}
if !strings.Contains(err.Error(), "scope") {
t.Fatalf("refused without naming the key, which leaves the author guessing: %v", err)
}
}
// `local-network` is a source a manifest may name (§1, changing ADR 0045 §3), and an unknown source is
// still refused.
func TestAManifestMayListenFromTheLocalNetwork(t *testing.T) {
if _, err := ParseManifest([]byte(`{"module":"mosquitto","version":"1",
"listens":[{"port":1883,"from":"local-network"}]}`)); err != nil {
t.Fatalf("a listen from the local network is refused: %v", err)
}
_, err := ParseManifest([]byte(`{"module":"mosquitto","version":"1",
"listens":[{"port":1883,"from":"lan"}]}`))
if err == nil || !strings.Contains(err.Error(), `from "lan"`) {
t.Fatalf("a source the mesh cannot render is not refused: %v", err)
}
}
// A module uses one form or the other, never both.
func TestAManifestMixingTheConfigAndSettingsFormsIsRefused(t *testing.T) {
m := inConfigForm()
resourceOf(m, "configuration")["content"] = "font = ${config:font-size}\nwidth = ${setting:width}\nfrom = ${config:sender}\n"
refused(t, m, "mixes", "${setting:", "config")
m = inConfigForm()
delete(resourceOf(m, "rules"), "takes-config")
resourceOf(m, "rules")["takes-settings"] = []any{"position", "theme"}
refused(t, m, "mixes", "takes-settings")
m = inConfigForm()
m["settings"] = map[string]any{"colour": map[string]any{"kind": "preference", "default": "red", "why": "red"}}
refused(t, m, "mixes", "settings")
}
func TestAManifestProvidingAmqpIsRefused(t *testing.T) {
raw := []byte(`{"module":"old-broker","version":"1","provides":[{"name":"amqp","scope":"mesh"}]}`)
_, err := ParseManifest(raw)
if err == nil || !strings.Contains(err.Error(), `provides "amqp", which is not a provision`) {
t.Fatalf("a module providing amqp was not refused, or not for the reason: %v", err)
}
}
func TestAManifestRequiringAmqpIsRefused(t *testing.T) {
raw := []byte(`{"module":"forwarder","version":"1","requires":["amqp"]}`)
_, err := ParseManifest(raw)
if err == nil || !strings.Contains(err.Error(), `requires "amqp", which is not a provision`) {
t.Fatalf("a module requiring amqp was not refused, or not for the reason: %v", err)
}
}
// A manifest written against the old name is told what the field became.
//
// `needs` and `secrets` were both name-to-path and differed only in whose secret it was, so
// reaching for the wrong one parsed cleanly and failed somewhere else entirely. Refusing the old
// name with "unknown field" would be correct and unhelpful: whoever wrote it knew what they meant,
// and the mesh knows what it is called now.
func TestAManifestUsingTheOldNameIsToldTheNewOne(t *testing.T) {
_, err := ParseManifest([]byte(
`{"module":"postgres","version":"1","needs":{"superuser":"/var/lib/superuser"}}`))
if err == nil {
t.Fatal("a manifest using the old name was accepted, so two fields now mean one thing")
}
for _, want := range []string{"needs", "own-secrets"} {
if !strings.Contains(err.Error(), want) {
t.Fatalf("the refusal does not mention %q: %v", want, err)
}
}
}
// A manifest in the config form registers with the group a config value fills, and the value is placed (a container's
// groups count as a place for it); a placeholder in any other shape is refused where the manifest is read.
func TestAManifestWhoseContainerTakesItsGroupFromConfigRegisters(t *testing.T) {
m := map[string]any{
"module": "n8n", "version": "1",
"config": map[string]any{"media-gid": map[string]any{"type": "number", "minimum": 1, "required": true,
"description": "the group that owns the media library on this machine"}},
"resources": []any{aContainerWithGroups("${config:media-gid}")},
}
if _, err := parseRaw(t, m); err != nil {
t.Fatalf("refused: %v", err)
}
for _, shape := range []string{"x${config:media-gid}", "${config:media-gid}:1", "${config:other}"} {
m["resources"] = []any{aContainerWithGroups(shape)}
if _, err := parseRaw(t, m); err == nil {
t.Errorf("groups %q registered", shape)
}
}
}
func TestAManifestWithoutUnhealthyIsNotUrgent(t *testing.T) {
m, err := ParseManifest([]byte(`{"module":"notes","version":"1"}`))
if err != nil {
t.Fatal(err)
}
if m.UnhealthyIsUrgent() {
t.Errorf("a manifest saying nothing of its unhealthy reads as urgent")
}
warning := catalogue.Manifest{Module: "notes", Unhealthy: &catalogue.UnhealthyPolicy{Severity: catalogue.UnhealthyWarning, Why: "notes wait"}}
if warning.UnhealthyIsUrgent() || len(manifestUnhealthyProblems(warning)) != 0 {
t.Errorf("a warning said with why reads as urgent or is refused: %v", manifestUnhealthyProblems(warning))
}
}
func TestAMapReferenceToNoDirectoryRefusesAtTheManifest(t *testing.T) {
m := catalogue.Manifest{
Module: "photos",
Resources: []map[string]any{{"id": "state", "type": "directory", "place": "."}},
Binds: map[string]string{"route": "${dir:stat}/route.json"},
}
problems := manifestUnknownDirRefs(m)
if len(problems) != 1 || !strings.Contains(problems[0], `${dir:stat}`) {
t.Fatalf("a map naming no directory is a manifest problem; got %v", problems)
}
}
// The catalogue check — the strict parse registration runs too — refuses each by name, with the
// module and the field it stands in, and says nothing about the shell's own syntax.
func TestAMisspelledPlaceholderIsRefusedAtTheCheck(t *testing.T) {
raw := `{"module":"speller","version":"1","resources":[{"id":"rc","type":"file","path":"/etc/speller.rc",` +
`"content":"a=` + misspelledShell + `\nb=` + undeclaredSetting + `\nc=` + misspelledMachine +
`\nd=` + shellsOwn + `\n"}]}`
_, err := ParseManifest([]byte(raw))
if err == nil {
t.Fatal("a file holding three placeholders no pass consumes was accepted")
}
for _, token := range []string{misspelledShell, undeclaredSetting, misspelledMachine} {
if !strings.Contains(err.Error(), "speller's resource rc holds "+token+" in its content") {
t.Errorf("the refusal does not name %s with its module and field: %v", token, err)
}
}
if strings.Contains(err.Error(), "XDG_CACHE_HOME") {
t.Errorf("the shell's own syntax was refused: %v", err)
}
// A namespace the mesh knows, misspelt in any way its own pattern does not take, and the same
// namespace in a field its pass does not read: refused at the check as at composition.
for _, c := range []struct{ resource, token, field string }{
{`{"id":"rc","type":"file","path":"/etc/rc","content":"${Machine:address}"}`, "${Machine:address}", "content"},
{`{"id":"rc","type":"file","path":"/etc/rc","content":"${machine:.address}"}`, "${machine:.address}", "content"},
{`{"id":"rc","type":"file","path":"/etc/rc","content":"${machine: address}"}`, "${machine: address}", "content"},
{`{"id":"rc","type":"file","path":"/etc/rc","content":"${Dir:x}"}`, "${Dir:x}", "content"},
{`{"id":"rc","type":"file","path":"/etc/rc","content":"a=${machine:address\nb=1"}`, "${machine:address", "content"},
{`{"id":"rc","type":"process","name":"speller","env":{"NAME":"${machine:name}"}}`, "${machine:name}", "env.NAME"},
} {
raw := `{"module":"speller","version":"1","resources":[` + c.resource + `]}`
_, err := ParseManifest([]byte(raw))
if err == nil || !strings.Contains(err.Error(), "speller's resource rc holds "+c.token+" in its "+c.field) {
t.Errorf("%s in its %s was accepted at the check, or not named: %v", c.token, c.field, err)
}
}
// And the shell's syntax alone, beside placeholders every pass knows, is accepted — as is the
// shape a lower-case shell variable takes with an operator after its colon.
raw = `{"module":"speller","version":"1","resources":[{"id":"rc","type":"file","path":"${machine:account-home}/.rc",` +
`"content":"` + shellsOwn + ` ${(%):-%n} ${1:-.} ${count:-} ${trial:+ on trial} ${machine:address} ` +
shellsOperators + `\n"},` +
`{"id":"server","type":"container","name":"server","env":{"PORT":"${PORT:-80}"}}]}`
if _, err := ParseManifest([]byte(raw)); err != nil {
t.Fatalf("the shell's own syntax was refused: %v", err)
}
}
// The prefix is the reservation rule, so there is no list to maintain and none to drift.
func TestAModuleCannotDeclareAMeshSeat(t *testing.T) {
for _, n := range []string{"mesh-broker", "mesh-anything", "mesh-store"} {
m := catalogue.Manifest{Module: "impostor", DefinesSeats: []catalogue.SeatDeclaration{{Name: n, Accepts: []string{"x"}}}}
got := strings.Join(declaredSeatProblems(m), "; ")
if !strings.Contains(got, "reserved to the mesh") {
t.Fatalf("%q was accepted as a module's seat: %q", n, got)
}
}
}
// The whole point: a module contributes a capability without the mesh being changed.
func TestAModuleDeclaresItsOwnSeatAndHoldsIt(t *testing.T) {
shop := catalogue.Manifest{Module: "shop", Uses: []string{"telegram-sender"}}
if got := problemsFor(t, catalogue.Shelf{"telegram": telegram(), "shop": shop}); got != "" {
t.Fatalf("a declared seat and its caller were refused: %s", got)
}
}
// A module may define its own seat and claim it — the mesh enforces exclusivity without knowing
// what it means (novox/hq ADR 0121). But it may not define one in the mesh's own namespace.
func TestAModuleDefinesAndClaimsItsOwnSeat(t *testing.T) {
ok := []byte(`{"module":"showcase","version":"1","seats":[{"name":"the-showcase","scope":"node"}],` +
`"claims":[{"name":"the-showcase","scope":"node"}]}`)
if _, err := ParseManifest(ok); err != nil {
t.Fatalf("a module could not define and claim its own seat: %v", err)
}
// Claiming a name nobody defines is still refused — but **at registration, not here**: with
// seats declared by modules, a claim on a seat *another* module declares is good, and the
// parser cannot tell that from an invented name. See
// TestAClaimOnASeatNobodyDeclaresIsRefusedAtRegistration.
claimant, err := ParseManifest(claimed(`[{"name":"the-anything","scope":"node"}]`))
if err != nil {
t.Fatalf("the parser judged a claim it cannot judge alone: %v", err)
}
if len(CatalogueProblems(catalogue.Shelf{claimant.Module: claimant})) == 0 {
t.Fatal("a module claimed a seat nobody defines")
}
// A module may not carve its seat out of the mesh's own namespace.
bad := []byte(`{"module":"x","version":"1","seats":[{"name":"node-mine","scope":"node"}],` +
`"claims":[{"name":"node-mine","scope":"node"}]}`)
if _, err := ParseManifest(bad); err == nil || !strings.Contains(err.Error(), "own namespace") {
t.Fatalf("a module defined a seat in the mesh's namespace and was not refused: %v", err)
}
}
func TestAModuleIsEitherWrittenOrComputedNotBoth(t *testing.T) {
// Otherwise nobody could say where a given file on a machine came from.
_, err := ParseManifest([]byte(`{"module":"mesh-network","version":"1",
"computed":"mesh-network",
"resources":[{"id":"a","type":"package","package":"wireguard-tools"}]}`))
if err == nil {
t.Fatal("a module that both ships files and has them computed was accepted")
}
if !strings.Contains(err.Error(), "one or the other") {
t.Fatalf("unhelpful refusal: %v", err)
}
}
// A module may not declare an action, and it is refused where it was written.
//
// The host refuses one arriving over the link, which is the bound the whole security argument
// rests on (novox/hq ADR 0005) and is enforced in the right place. But a module's resources reach
// a machine *over* the link, so a manifest carrying an action used to be accepted here, stored,
// resolved and pushed — and then refused on the machine, in a log, with nothing tying it back to
// the manifest. Enforced at the far end only is enforced and not usable.
func TestAModuleMayNotDeclareAnAction(t *testing.T) {
_, err := ParseManifest([]byte(`{"module":"probe","version":"1","resources":[
{"id":"migrate","type":"action","command":["/bin/true"],"verify":["/bin/true"]}]}`))
if err == nil {
t.Fatal("a manifest declaring an action was accepted, and the machine would refuse it")
}
if !strings.Contains(err.Error(), "may not") || !strings.Contains(err.Error(), "0005") {
t.Errorf("the refusal does not say what rule it is: %v", err)
}
// And it says what to do instead, because "you may not" without an alternative is where a
// module author stops.
if !strings.Contains(err.Error(), "ships a program") {
t.Errorf("the refusal names no alternative: %v", err)
}
}
// A provision names what the consumer is coupled to (novox/hq ADR 0027).
//
// The fault this defends against does not look like a fault: resolution succeeds, the module
// deploys, and the first query fails somewhere else entirely. Every earlier test had exactly one
// provider of each name, so no mismatch was expressible and none was caught.
func TestAModuleMayNotProvideAGenericDatabase(t *testing.T) {
for _, hidden := range []string{"database", "db", "sql", "sql-database"} {
_, err := ParseManifest([]byte(`{"module":"postgres","version":"1",
"provides":[{"name":"` + hidden + `","scope":"mesh"}]}`))
if err == nil {
t.Fatalf("providing %q was accepted; a requirement for it matches any engine", hidden)
}
for _, want := range []string{hidden, "postgres-database", "first query"} {
if !strings.Contains(err.Error(), want) {
t.Errorf("refusing %q did not mention %q, so nobody learns what to write:\n%v",
hidden, want, err)
}
}
}
}
func TestAModuleNamedAfterADomainIsNamedBack(t *testing.T) {
got := InstallationProblems(catalogue.Manifest{Module: "mesh-one.be"})
if len(got) != 1 || !strings.Contains(got[0], "named after mesh-one.be") {
t.Fatalf("got %v", got)
}
}
func TestAModuleOffersItsOwnStateOnly(t *testing.T) {
block := `{"seat":"node-bar","kind":"block","data":{"bar":"bottom","place":"status","shows":"state","options":{"state":"%s"}}}`
cases := map[string]string{
`{"module":"power","state":["draw"],"contributions":[` + fmt.Sprintf(block, "other.draw") + `]}`: "which power does not keep",
`{"module":"power","contributions":[` + fmt.Sprintf(block, "power.draw") + `]}`: "which power does not keep",
`{"module":"power","state":["draw"],"contributions":[` + fmt.Sprintf(block, "draw") + `]}`: "does not name a module",
}
for raw, want := range cases {
if _, err := ParseManifest([]byte(raw)); err == nil || !strings.Contains(err.Error(), want) {
t.Errorf("%s: accepted, or refused without %q: %v", raw, want, err)
}
}
ok := `{"module":"power","state":["draw"],"contributions":[` + fmt.Sprintf(block, "power.draw") + `]}`
if _, err := ParseManifest([]byte(ok)); err != nil {
t.Errorf("its own state was refused: %v", err)
}
}
// A module says what its own tools replace, and only for a tool it declares.
func TestAModuleSaysWhatItsOwnToolsReplace(t *testing.T) {
m, err := ParseManifest([]byte(`{"module":"docker","version":"1","tools":["docker_logs","docker_list"],` +
`"replaces":{"docker_logs":["docker logs"],"docker_list":["docker ps"]}}`))
if err != nil {
t.Fatal(err)
}
if m.Replaces["docker_logs"][0] != "docker logs" || m.Replaces["docker_list"][0] != "docker ps" {
t.Fatalf("replaces not read: %v", m.Replaces)
}
for _, c := range []struct{ manifest, says string }{
{`{"module":"docker","version":"1","tools":["docker_logs"],"replaces":{"docker_lgos":["docker logs"]}}`,
"declares no such tool"},
{`{"module":"docker","version":"1","tools":["docker_logs"],"replaces":{"docker_logs":[]}}`, "replaces nothing"},
{`{"module":"docker","version":"1","tools":["docker_logs"],"replaces":{"docker_logs":[" "]}}`, "empty command"},
{`{"module":"docker","version":"1","tools":["docker_logs"],"replaces":{"docker_logs":["docker logs\nrm -rf /"]}}`,
"more than one line"},
{`{"module":"docker","version":"1","tools":["docker_logs"],"replaces":{"docker_logs":["docker logs","docker logs"]}}`,
"twice"},
{`{"module":"docker","version":"1","tools":["docker_logs"],"replaces":{"docker_logs":["` + strings.Repeat("x", catalogue.MaxReplaced+1) + `"]}}`,
"longer than"},
} {
if _, err := ParseManifest([]byte(c.manifest)); err == nil || !strings.Contains(err.Error(), c.says) {
t.Errorf("%s: want a refusal saying %q, got %v", c.manifest, c.says, err)
}
}
}
// A module's own seat names paths under the same rules: a node seat only, literal, and none another seat
// names — the second declaration loses. Its holder declares them, as a mesh seat's does.
func TestAModulesOwnSeatNamesPathsUnderTheSameRules(t *testing.T) {
meshScoped := catalogue.Manifest{Module: "a", Version: "1", DefinesSeats: []catalogue.SeatDeclaration{
{Name: "a-seat", Paths: []string{"/etc/a"}}}}
if got := strings.Join(declaredSeatProblems(meshScoped), "; "); !strings.Contains(got, "only a node seat names paths") {
t.Errorf("a module's mesh seat naming a path was accepted: %q", got)
}
placeholder := catalogue.Manifest{Module: "a", Version: "1", DefinesSeats: []catalogue.SeatDeclaration{
{Name: "a-seat", Scope: catalogue.ScopeNode, Paths: []string{"${dir:state}/a"}}}}
if got := strings.Join(declaredSeatProblems(placeholder), "; "); !strings.Contains(got, "placeholder") {
t.Errorf("a module's seat naming a path with a placeholder was accepted: %q", got)
}
taken := catalogue.Manifest{Module: "b", Version: "1", DefinesSeats: []catalogue.SeatDeclaration{
{Name: "b-seat", Scope: catalogue.ScopeNode, Paths: []string{"/etc/hosts"}}}}
if got := strings.Join(CatalogueProblems(catalogue.Shelf{"b": taken}), "; "); !strings.Contains(got,
`b declares the seat b-seat naming the path "/etc/hosts", which node-hostname already names`) {
t.Errorf("a module's seat naming /etc/hosts was accepted: %q", got)
}
own := catalogue.Manifest{Module: "c", Version: "1",
DefinesSeats: []catalogue.SeatDeclaration{{Name: "c-seat", Scope: catalogue.ScopeNode, Paths: []string{"/etc/c.conf"}}},
Claims: []catalogue.Claim{{Name: "c-seat", Scope: catalogue.ScopeNode}}}
if got := strings.Join(CatalogueProblems(catalogue.Shelf{"c": own}), "; "); !strings.Contains(got,
"c claims c-seat but does not declare /etc/c.conf") {
t.Errorf("a holder of a module's seat without its path was accepted: %q", got)
}
own.Resources = []map[string]any{{"id": "conf", "type": "file", "path": "/etc/c.conf", "content": "x"}}
if got := CatalogueProblems(catalogue.Shelf{"c": own}); len(got) != 0 {
t.Errorf("a holder declaring its own seat's path was refused: %v", got)
}
other := catalogue.Manifest{Module: "d", Version: "1", Resources: []map[string]any{
{"id": "conf", "type": "file", "path": "/etc/c.conf", "content": "x"}}}
if got := strings.Join(CatalogueProblems(catalogue.Shelf{"c": own, "d": other}), "; "); !strings.Contains(got,
`d declares the path "/etc/c.conf", which the seat c-seat names`) {
t.Errorf("a module not claiming c-seat declared its path: %q", got)
}
}
func TestAModuleThatDeclaresASettingNamesItsFilesKeys(t *testing.T) {
// ADR 0326 §3, ADR 0164 §15, novox/hq issue 574: only a module that declares no settings keeps
// today's form. One that declares a preference and owns a silent mergeable file is refused where
// a definition is written — and read and composed where a stored one is read again, so a build kept
// from before the rule is still sent and still put back.
declaring := func(app, sidecar []any) catalogue.Manifest {
m := twoFiles(app, sidecar)
m.Resources[2]["content"] = "WORKERS=${setting:workers}\n"
m.Settings = map[string]catalogue.SettingDeclaration{"workers": {Kind: catalogue.KindPreference, Default: 4.0, Why: "a number"}}
return m
}
m := declaring(nil, nil)
problems := WrittenSettingsProblems(m)
if len(problems) != 2 || !containsText(problems, "mergeable app names no takes-settings") ||
!containsText(problems, "mergeable sidecar names no takes-settings") {
t.Errorf("where it is written: %v", problems)
}
if problems := TakenSettingsProblems(m); len(problems) != 0 {
t.Errorf("what a stored definition is read by refuses it: %v", problems)
}
raw, err := json.Marshal(map[string]any{"module": m.Module, "version": "1", "resources": m.Resources,
"settings": map[string]any{"workers": map[string]any{"kind": catalogue.KindPreference, "default": 4, "why": "a number"}}})
if err != nil {
t.Fatal(err)
}
if _, err := ParseManifest(raw); err != nil {
t.Fatalf("a stored build from before the rule is not read again: %v", err)
}
if problems := WrittenSettingsProblems(declaring([]any{"server"}, []any{})); len(problems) != 0 {
t.Errorf("every mergeable file names its keys, and was refused: %v", problems)
}
if problems := WrittenSettingsProblems(twoFiles(nil, nil)); len(problems) != 0 {
t.Errorf("a module declaring no setting was refused: %v", problems)
}
// One naming file and one silent: §2 names the silent one, and §3 does not name it again.
mixed := declaring([]any{"server"}, nil)
if written, taken := WrittenSettingsProblems(mixed), TakenSettingsProblems(mixed); len(written) != 0 || len(taken) != 1 {
t.Errorf("the silent file is reported %d times by §3 and %d by §2; want once, by §2: %v %v",
len(written), len(taken), written, taken)
}
}
// A module whose own code the mesh cannot find has nothing to ask, and saying so where the manifest
// is read beats a declaration that quietly prepares nothing.
func TestAModuleThatPreparesAndRunsNoneOfItsOwnCodeIsRefused(t *testing.T) {
m := catalogue.Manifest{
Module: "gitea",
Prepares: true,
Resources: []map[string]any{
// Only the upstream server it packages: nothing here runs gitea's own code.
{"id": "server", "type": "container", "name": "mesh-gitea-server", "image": "gitea/gitea@" + digest},
},
}
raw, err := json.Marshal(m)
if err != nil {
t.Fatal(err)
}
if _, err := ParseManifest(raw); err == nil {
t.Fatal("a module that prepares its state with nothing of its own to run was accepted")
}
}
func TestAMountNothingDeclaresIsRefused(t *testing.T) {
_, err := ParseManifest(manifestMounting("/services/store/data", ""))
if err == nil {
t.Fatal("a container mounting a path no resource declares was accepted; the runtime " +
"would create it as root and the module's owner and mode would never apply")
}
if !strings.Contains(err.Error(), "/services/store/data") {
t.Fatalf("refused without naming the path, which leaves the author guessing: %v", err)
}
}
func TestAMovedModulesMarkIsJudged(t *testing.T) {
for name, raw := range map[string]string{
"marked moved and naming no template": `{"module":"bare","version":"1","moved":true,
"resources":[{"id":"conf","type":"file","path":"/etc/bare","content":"x"}]}`,
"handed written in a repository": `{"module":"bare","version":"1","moved":true,
"handed":{"source":"s","digest":"d","templates":{}},
"build":{"artifacts":[{"name":"files","kind":"archive","from":"files"}]},
"resources":[{"id":"conf","type":"file","path":"/etc/bare","template":"files/bare"}]}`,
"handed and not moved": `{"module":"bare","version":"1",
"handed":{"source":"s","digest":"d","templates":{}},
"resources":[{"id":"conf","type":"file","path":"/etc/bare","content":"x"}]}`,
} {
if _, err := ParseManifest([]byte(raw)); err == nil {
t.Errorf("%s: accepted", name)
}
}
}
// A manifest states an access's extent, the way a listening port states its source. An absolute
// path and a mode of read or read-write; anything else is refused rather than acted on wrongly.
func TestAnAccessIsValidated(t *testing.T) {
good := []byte(`{"module":"m","accesses":[{"path":"/services/media","mode":"read-write"}]}`)
if _, err := ParseManifest(good); err != nil {
t.Fatalf("a valid access was refused: %v", err)
}
// Mode is optional and narrows to read — the safe default, because the danger with an access
// is being given more than was meant, not less.
bare := []byte(`{"module":"m","accesses":[{"path":"/services/media"}]}`)
m, err := ParseManifest(bare)
if err != nil {
t.Fatalf("an access with no mode was refused: %v", err)
}
if m.Accesses[0].At() != catalogue.AccessRead {
t.Errorf("an access with no mode is %q, want %q", m.Accesses[0].At(), catalogue.AccessRead)
}
relative := []byte(`{"module":"m","accesses":[{"path":"services/media","mode":"read"}]}`)
if _, err := ParseManifest(relative); err == nil {
t.Error("a relative access path was accepted")
}
wrong := []byte(`{"module":"m","accesses":[{"path":"/services/media","mode":"append"}]}`)
if _, err := ParseManifest(wrong); err == nil {
t.Error("an access with an unknown mode was accepted")
}
}
func TestANameMeantOnPurposeIsDeclaredWithItsReason(t *testing.T) {
// The federation's public directory in a homeserver's config: the world's, said so, and a name
// the map does not cover is still reported.
m := catalogue.Manifest{Module: "homeserver", Resources: []map[string]any{
{"id": "conf", "type": "file", "content": "trusted_key_servers: matrix.org\nwell_known: https://mesh-one.be\n",
catalogue.NamesOnPurpose: map[string]any{"matrix.org": "the federation's public key server, the world's"}},
}}
got := InstallationProblems(m)
if len(got) != 1 || !strings.Contains(got[0], "mesh-one.be") {
t.Fatalf("got %v", got)
}
}
func TestAnArtifactBuiltFromOutsideItsRepositoryIsRefused(t *testing.T) {
// A build reads its own repository and nothing else. A path leaving it would make what gets
// built depend on whatever happens to be on the machine doing the building.
for _, from := range []string{"/etc/passwd", "../elsewhere"} {
_, err := ParseManifest([]byte(`{"module":"a","version":"1","build":{"artifacts":[
{"name":"x","kind":"archive","from":"` + from + `"}]}}`))
if err == nil {
t.Fatalf("%q was accepted as a build input", from)
}
if !strings.Contains(err.Error(), "outside its own repository") {
t.Fatalf("refused for the wrong reason: %v", err)
}
}
}
// An artifact names the paths it is built from with `paths` (novox/hq ADR 0499 clause 1): a package, or a bundle in a
// language other than Go, may; a Go program's artifact, an archive, an image and an upstream artifact may not, and the
// refusal names the artifact and its kind. Each path stays inside the module's directory: no `..`, no absolute path,
// no pattern.
func TestAnArtifactNamesThePathsItIsBuiltFrom(t *testing.T) {
m, err := ParseManifest([]byte(`{"module":"mesh-sdk","version":"1","slug":"sdk","build":{"artifacts":[
{"name":"lib","kind":"package","language":"typescript",
"paths":["src","test","package.json","package-lock.json","tsconfig.json"]}]},"resources":[]}`))
if err != nil {
t.Fatalf("a package naming its paths was refused: %v", err)
}
if got := strings.Join(m.Build.Artifacts[0].Paths, ","); got != "src,test,package.json,package-lock.json,tsconfig.json" {
t.Fatalf("the paths were not kept as written: %q", got)
}
if _, err := ParseManifest([]byte(`{"module":"a","version":"1","build":{"artifacts":[
{"name":"tools","kind":"bundle","language":"typescript","entrypoints":["index.js"],"loads":["index.js"],"paths":["src","package.json"]}]}}`)); err != nil {
t.Fatalf("a TypeScript bundle naming its paths was refused: %v", err)
}
for _, c := range []struct{ artifact, says string }{
{`{"name":"x","kind":"bundle","language":"go","system":"linux-amd64","paths":["cmd"]}`, `"x" is a Go program`},
{`{"name":"x","kind":"archive","from":"files","paths":["files"]}`, `"x" is a "archive"`},
{`{"name":"x","kind":"image","from":"Dockerfile","paths":["src"]}`, `"x" is a "image"`},
{`{"name":"x","kind":"upstream","from":"nats:2.10","paths":["src"]}`, `"x" is a "upstream"`},
{`{"name":"x","kind":"package","language":"typescript","paths":["../other"]}`, "leaves the module's directory"},
{`{"name":"x","kind":"package","language":"typescript","paths":["src/../../x"]}`, "leaves the module's directory"},
{`{"name":"x","kind":"package","language":"typescript","paths":["/etc"]}`, "leaves the module's directory"},
{`{"name":"x","kind":"package","language":"typescript","paths":["src/*.ts"]}`, "is a pattern"},
{`{"name":"x","kind":"package","language":"typescript","paths":["src/[ab]"]}`, "is a pattern"},
{`{"name":"x","kind":"package","language":"typescript","paths":[""]}`, "names an empty path"},
{`{"name":"x","kind":"package","language":"typescript","paths":["."]}`, "names the module's whole directory"},
{`{"name":"x","kind":"package","language":"typescript","paths":[]}`, "names no path"},
} {
_, err := ParseManifest([]byte(`{"module":"a","version":"1","build":{"artifacts":[` + c.artifact + `]}}`))
if err == nil || !strings.Contains(err.Error(), c.says) {
t.Errorf("%s: %v, wanted a refusal saying %q", c.artifact, err, c.says)
}
}
}
func TestAnArtifactOfAnUnknownKindIsRefused(t *testing.T) {
_, err := ParseManifest([]byte(`{"module":"a","version":"1","build":{"artifacts":[
{"name":"x","kind":"binary","from":"main.go"}]}}`))
if err == nil {
t.Fatal("an artifact of an unknown kind was accepted")
}
}
func TestANeedIsAnAbsolutePath(t *testing.T) {
_, err := ParseManifest([]byte(`{"module":"postgres","version":"1",
"own-secrets":{"superuser":"superuser.txt"}}`))
if err == nil {
t.Fatal("a relative path was accepted")
}
if !strings.Contains(err.Error(), "absolute path") {
t.Fatalf("unhelpful refusal: %v", err)
}
}
// A need naming nothing node-scope — undeclared, or declared at another scope — is the whole
// catalogue's to say (novox/hq ADR 0378 §8).
func TestANeedMustNameANodeSeatSomewhereInTheCatalogue(t *testing.T) {
mail := catalogue.Manifest{Module: "mail", DefinesSeats: []catalogue.SeatDeclaration{{
Name: "mail-notifier", Scope: catalogue.ScopeNode, Needs: []string{"desk-beeper"}}}}
// Nobody declares desk-beeper at all.
got := problemsFor(t, catalogue.Shelf{"mail": mail})
if !strings.Contains(got, "desk-beeper") || !strings.Contains(got, "no module declares") {
t.Fatalf("needing an undeclared seat was accepted: %q", got)
}
// Declared, but at mesh scope: still not a node seat to hold beside it.
notNode := catalogue.Manifest{Module: "dunst", DefinesSeats: []catalogue.SeatDeclaration{{Name: "desk-beeper", Scope: catalogue.ScopeMesh}}}
got = problemsFor(t, catalogue.Shelf{"mail": mail, "dunst": notNode})
if !strings.Contains(got, "desk-beeper") || !strings.Contains(got, "not a node seat") {
t.Fatalf("needing a mesh-scope seat was accepted: %q", got)
}
// Declared at node scope: accepted.
dunst := catalogue.Manifest{Module: "dunst", DefinesSeats: []catalogue.SeatDeclaration{{Name: "desk-beeper", Scope: catalogue.ScopeNode}}}
if got := problemsFor(t, catalogue.Shelf{"mail": mail, "dunst": dunst}); got != "" {
t.Fatalf("needing a node seat another module declares was refused: %q", got)
}
}
func TestAnEmptyContributionIsRefused(t *testing.T) {
// Either a mistake or a requirement written the long way round, and both are better said.
_, err := ParseManifest([]byte(`{"module":"board","version":"1",
"contributes":{"reverse-proxy":{}}}`))
if err == nil {
t.Fatal("a module contributed nothing and was accepted")
}
if !strings.Contains(err.Error(), "require it") {
t.Fatalf("the refusal does not say what to do instead: %v", err)
}
}
// A name that is not a name is refused where it is written: it ends up in something a person types.
func TestAnEndpointNameIsHeldToItsShape(t *testing.T) {
for _, wrong := range []string{"Web", "web port", "3000", "-web", "web_surface"} {
m := catalogue.Manifest{Module: "odd", Listens: []catalogue.Listening{{Name: wrong, Port: 80, From: catalogue.FromMesh}}}
if got := strings.Join(endpointNameProblems(m), "\n"); !strings.Contains(got, "a name is lowercase") {
t.Fatalf("%q was accepted as an endpoint name:\n%s", wrong, got)
}
}
}
func anEnvironmentItemIsTypedAndNamesWhatItNeeds(t *testing.T) {
item := func(attrs map[string]string) []string {
return environmentItemProblems("x", catalogue.Item{Collection: "environment", Attributes: attrs})
}
for _, ok := range []map[string]string{
{"kind": "variable", "name": "GTK_THEME"},
{"kind": "path", "at": "end", "order": "10"},
} {
if p := item(ok); len(p) != 0 {
t.Errorf("%v is refused: %v", ok, p)
}
}
for _, bad := range []map[string]string{
nil, {"kind": "other"}, {"kind": "variable"}, {"kind": "variable", "name": "PATH"},
{"kind": "path"}, {"kind": "path", "at": "middle"}, {"kind": "path", "at": "start", "name": "X"},
{"kind": "variable", "name": "A", "color": "red"},
} {
if p := item(bad); len(p) == 0 {
t.Errorf("%v is taken", bad)
}
}
if !attributeFits("environment", "name", "GTK_THEME") || attributeFits("shell:zsh:normal", "name", "GTK_THEME") ||
attributeFits("environment", "kind", "Variable") {
t.Error("an upper-case name is the environment's name alone")
}
}
// An identity served as a DNS label is never truncated, so the offer's bound must keep it in one.
func anIdentityServedAsADNSLabelIsBoundedToOne(t *testing.T) {
serves := map[string]map[string]any{"bucket": {"name": "${consumer:as:dns}"}}
wide := catalogue.Manifest{Module: "wide", Serves: serves, Provides: []catalogue.Offer{{Name: "bucket", Scope: catalogue.ScopeMesh,
Identity: &catalogue.OfferIdentity{Max: 255, In: "a client id"}}}}
if got := CheckServes(wide); len(got) != 1 || !strings.Contains(got[0], "a label keeps 63") {
t.Fatalf("a 255-character bound on a DNS label passed: %q", got)
}
unsaid := catalogue.Manifest{Module: "unsaid", Serves: serves, Provides: []catalogue.Offer{{Name: "bucket", Scope: catalogue.ScopeMesh}}}
if got := CheckServes(unsaid); len(got) != 0 {
t.Fatalf("the default bound (20) on a DNS label was refused: %q", got)
}
}
func TestAnImageFromAnInstallationsRegistryNeedsAReason(t *testing.T) {
bare := catalogue.Manifest{Module: "site", Resources: []map[string]any{
{"id": "server", "type": "container", "image": "registry.mesh-one.be/org/site@sha256:cc"},
}}
if got := InstallationProblems(bare); len(got) != 1 || !strings.Contains(got[0], "registry.mesh-one.be") {
t.Fatalf("an image on an installation's registry was not named: %v", got)
}
excepted := catalogue.Manifest{Module: "site", Resources: []map[string]any{
{"id": "server", "type": "container", "image": "registry.mesh-one.be/org/site@sha256:cc",
catalogue.NamesOnPurpose: map[string]any{"registry.mesh-one.be": "built outside the mesh until the site's repository is a build source here"}},
}}
if got := InstallationProblems(excepted); len(got) != 0 {
t.Fatalf("a declared exception was still reported: %v", got)
}
}
func TestAnInterpretedBundleStillMayNotNameASource(t *testing.T) {
b := &catalogue.Build{Artifacts: []catalogue.Artifact{{
Name: "tools", Kind: catalogue.ArtifactBundle, Language: "typescript", From: "src",
}}}
p := buildProblems(b, "something")
if len(p) == 0 {
t.Fatal("an interpreted bundle naming what it is built from was accepted")
}
}
// And an ordinary unknown key is still refused as one, rather than being guessed at.
func TestAnInventedKeyIsNotTreatedAsARename(t *testing.T) {
_, err := ParseManifest([]byte(`{"module":"postgres","version":"1","nonsense":{}}`))
if err == nil {
t.Fatal("an invented key was accepted")
}
if strings.Contains(err.Error(), "is now called") {
t.Fatalf("an invented key was reported as a rename: %v", err)
}
}
// An entry that names a module and no tool is refused at parse, in the manifest's words, rather than
// at the composition of the bus's user list where it would stop the file for everybody.
func TestAnInvokeThatNamesNoToolIsRefusedAtParse(t *testing.T) {
_, err := ParseManifest([]byte(`{"module":"mesh-console","version":"1","invokes":["shop"]}`))
if err == nil {
t.Fatal("an invoke naming no tool was accepted")
}
if !strings.Contains(err.Error(), `invokes "shop", which does not name a tool`) {
t.Fatalf("refused for the wrong reason: %v", err)
}
}
// A provider with one credential shares it (novox/hq ADR 0158): the offer names the own secret, the
// secret says how it is taken, and a consumer's need carries the name so the plan mints its copy
// from the provider's value.
func TestAnOfferMayNameAnOwnSecretAsItsCredential(t *testing.T) {
m, err := ParseManifest([]byte(`{"module":"downloader","version":"1",
"own-secrets":{"password":{"path":"/var/lib/mesh/downloader/password","taken":"at-start"}},
"provides":[{"name":"downloader-api","credential":{"own":"password"}}],
"serves":{"downloader-api":{"port":8080,"username":"admin"}}}`))
if err != nil {
t.Fatal(err)
}
if own, shared := m.SharedCredentialOf("downloader-api"); !shared || own != "password" {
t.Fatalf("the offer's credential was not read: %v %v", own, shared)
}
if got := m.ProvisionsSharing("password"); len(got) != 1 || got[0] != "downloader-api" {
t.Fatalf("the provisions sharing the secret: %v", got)
}
for want, raw := range map[string]string{
"declares no such secret": `{"module":"d","version":"1","provides":[{"name":"d-api","credential":{"own":"password"}}]}`,
"must say how the module takes it": `{"module":"d","version":"1","own-secrets":{"password":"/p"},
"provides":[{"name":"d-api","credential":{"own":"password"}}]}`,
"names no own secret": `{"module":"d","version":"1","provides":[{"name":"d-api","credential":{"own":""}}]}`,
} {
if _, err := ParseManifest([]byte(raw)); err == nil || !strings.Contains(err.Error(), want) {
t.Errorf("expected a refusal saying %q, got %v", want, err)
}
}
}
// A provision's credential may be one only a person outside the mesh can give (novox/hq ADR 0337 §1, issue 164):
// the offer says `issued-by: outside` and names neither an own secret nor a tool, and the parser refuses it beside
// either. An offer that says `software` keeps ADR 0305's shape.
func TestAnOfferMaySayAPersonOutsideTheMeshIssuesItsCredential(t *testing.T) {
m, err := ParseManifest([]byte(`{"module":"tv","version":"1",
"provides":[{"name":"tv-api","scope":"mesh","credential":{"issued-by":"outside"}}],
"serves":{"tv-api":{"port":8989}}}`))
if err != nil {
t.Fatal(err)
}
if !m.IssuedOutsideOf("tv-api") {
t.Fatal("the offer's credential issued outside was not read")
}
if m.IssuedOutsideOf("other") {
t.Fatal("a provision the module does not offer reads as issued outside")
}
if _, issued := m.IssuedCredentialOf("tv-api"); issued {
t.Fatal("a credential issued outside was read as issued by the software")
}
if _, shared := m.SharedCredentialOf("tv-api"); shared {
t.Fatal("a credential issued outside was read as an own secret shared")
}
for want, raw := range map[string]string{
"names its own secret": `{"module":"d","version":"1","own-secrets":{"key":{"path":"/p","taken":"at-start"}},
"provides":[{"name":"d-api","credential":{"issued-by":"outside","own":"key"}}]}`,
"names the tool": `{"module":"d","version":"1",
"provides":[{"name":"d-api","credential":{"issued-by":"outside","tool":"d_key"}}]}`,
"the issuers an offer may name": `{"module":"d","version":"1",
"provides":[{"name":"d-api","credential":{"issued-by":"a-person"}}]}`,
} {
if _, err := ParseManifest([]byte(raw)); err == nil || !strings.Contains(err.Error(), want) {
t.Errorf("expected a refusal saying %q, got %v", want, err)
}
}
}
// A provision's credential may be issued by its provider's software (novox/hq ADR 0305, issue 526): the offer
// names the tool that answers it and no own secret, and every other shape is refused at parse.
func TestAnOfferMaySayItsSoftwareIssuesTheCredential(t *testing.T) {
m, err := ParseManifest([]byte(`{"module":"media","version":"1",
"provides":[{"name":"media-api","scope":"mesh","credential":{"issued-by":"software","tool":"media_issued_credential"}}],
"serves":{"media-api":{"port":32400}}}`))
if err != nil {
t.Fatal(err)
}
if tool, issued := m.IssuedCredentialOf("media-api"); !issued || tool != "media_issued_credential" {
t.Fatalf("the offer's issued credential was not read: %q %v", tool, issued)
}
if _, shared := m.SharedCredentialOf("media-api"); shared {
t.Fatal("an issued credential was read as an own secret shared")
}
if _, issued := m.IssuedCredentialOf("other"); issued {
t.Fatal("a provision the module does not offer reads as issued")
}
for want, raw := range map[string]string{
"the issuers an offer may name": `{"module":"d","version":"1",
"provides":[{"name":"d-api","credential":{"issued-by":"vendor","tool":"d_token"}}]}`,
"names no tool that answers it": `{"module":"d","version":"1",
"provides":[{"name":"d-api","credential":{"issued-by":"software"}}]}`,
"names its own secret": `{"module":"d","version":"1","own-secrets":{"token":{"path":"/p","taken":"at-start"}},
"provides":[{"name":"d-api","credential":{"issued-by":"software","tool":"d_token","own":"token"}}]}`,
"the issuers an offer may name ": `{"module":"d","version":"1",
"provides":[{"name":"d-api","credential":{"tool":"d_token"}}]}`,
} {
want = strings.TrimSpace(want)
if _, err := ParseManifest([]byte(raw)); err == nil || !strings.Contains(err.Error(), want) {
t.Errorf("expected a refusal saying %q, got %v", want, err)
}
}
}
// The field reads as written and writes back the same, and refuses what says nothing.
func TestAnOffersIdentityIsParsedStrictly(t *testing.T) {
for _, raw := range []string{
`{"name":"s3-bucket","scope":"mesh","identity":{"max":20,"in":"an S3 access key"}}`,
`{"name":"wildcard-resolution","scope":"mesh","identity":false}`,
`{"name":"redis-cache","scope":"mesh","identity":{"in":"a Redis ACL user"}}`,
} {
var o catalogue.Offer
if err := json.Unmarshal([]byte(raw), &o); err != nil {
t.Fatalf("%s: %v", raw, err)
}
back, err := json.Marshal(o)
if err != nil || string(back) != raw {
t.Errorf("did not round-trip:\n%s\n%s (%v)", raw, back, err)
}
}
for _, raw := range []string{
`{"name":"x","identity":true}`,
`{"name":"x","identity":{"max":20,"in":"y","most":3}}`,
} {
var o catalogue.Offer
if err := json.Unmarshal([]byte(raw), &o); err == nil {
t.Errorf("accepted %s", raw)
}
}
bad := catalogue.Manifest{Module: "bad", Version: "1", Provides: []catalogue.Offer{
{Name: "unsaid", Scope: catalogue.ScopeMesh, Identity: &catalogue.OfferIdentity{Max: 20}},
{Name: "tiny", Scope: catalogue.ScopeMesh, Identity: &catalogue.OfferIdentity{Max: 4, In: "nothing usable"}},
}}
raw, err := json.Marshal(bad)
if err != nil {
t.Fatal(err)
}
_, err = ParseManifest(raw)
if err == nil || !strings.Contains(err.Error(), "without saying what keeps them") ||
!strings.Contains(err.Error(), "the shortest the mesh makes") {
t.Fatalf("a bound with no `in`, or too short for any identity, was accepted: %v", err)
}
}
// A value named after the operator's own (a url, a user, an address) is `required` where nothing falls back when
// it is unset; where a mergeable file takes it and no placeholder asks for it, the file's own content (or the
// module's code reading the file) falls back, so it keeps a default and stays optional, as it is today: the
// operator's decision on issue 664's second question (issue 735).
func TestAnOperatorNamedValueAFileFallsBackOnMayHaveADefault(t *testing.T) {
m := inConfigForm()
m["config"].(map[string]any)["url"] = map[string]any{"type": "string",
"description": "the address the notifier posts to, a URL; empty for the one the file's software picks",
"default": ""}
resourceOf(m, "rules")["takes-config"] = []any{"position", "theme", "url"}
if _, err := parseRaw(t, m); err != nil {
t.Fatalf("a taken value named after the operator's, with a default, was refused: %v", err)
}
// A placeholder asking for it has no fallback but the default, which would put a value in the definition
// where the operator's belongs (ADR 0112).
asked := inConfigForm()
asked["config"].(map[string]any)["url"] = map[string]any{"type": "string", "description": "a URL", "default": ""}
resourceOf(asked, "rules")["takes-config"] = []any{"position", "theme", "url"}
resourceOf(asked, "configuration")["content"] = "font = Inter ${config:font-size}\nwidth = ${config:width}\n" +
"from = ${config:sender}\npost = ${config:url}\n"
refused(t, asked, `"url"`, `a key naming "url" is the operator's value`)
// Read by the module's code alone, nothing in the definition says it falls back.
runtime := inConfigForm()
runtime["config"].(map[string]any)["url"] = map[string]any{"type": "string", "description": "a URL",
"default": "", "runtime": true}
refused(t, runtime, `"url"`, `a key naming "url" is the operator's value`)
}
// A key asked only in a template is not refused while the manifest still names its templates (issue 817), and
// is judged once they are filled: WithTemplatesFrom over a tree whose template does not ask for it refuses it.
func TestAnOwnerSettingAskedOnlyInATemplateIsJudgedOnceFilled(t *testing.T) {
raw := `{"module":"sonarr","version":"1",
"build":{"artifacts":[{"name":"files","kind":"archive","from":"files"}]},
"resources":[{"id":"recycle-bin","type":"directory","path":"/srv/sonarr/recycle","mode":"0755","owner":"${setting:puid}:${setting:pgid}"},
{"id":"identity","type":"file","path":"/srv/sonarr/identity.env","mode":"0644","template":"files/identity.env"}]}`
m, err := ParseManifest([]byte(raw))
if err != nil {
t.Fatalf("a templated manifest was refused: %v", err)
}
write := func(text string) string {
dir := t.TempDir()
if err := os.MkdirAll(filepath.Join(dir, "files"), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(dir, "files", "identity.env"), []byte(text), 0o644); err != nil {
t.Fatal(err)
}
return dir
}
if _, err := WithTemplatesFrom(m, write("PUID=${setting:puid}\nPGID=${setting:pgid}\n")); err != nil {
t.Errorf("a template asking for both keys was refused: %v", err)
}
_, err = WithTemplatesFrom(m, write("NOTHING=1\n"))
if err == nil || !strings.Contains(err.Error(), "asks for \"puid\" nowhere else") {
t.Errorf("a template not asking for puid was not refused: %v", err)
}
}
// Where the manifest is read: an owner holding a setting is a whole placeholder or a whole number on
// each side of the colon, and every key is one the module asks for elsewhere or declares a default for.
func TestAnOwnerSettingIsJudgedWhereTheManifestIsRead(t *testing.T) {
ok := []string{"${setting:puid}:${setting:pgid}", "${setting:puid}", "${setting:puid}:1000", "1000:${setting:pgid}",
"1000:1000", "media:media"}
bad := []string{
"${setting:puid}x",
"x${setting:puid}",
"${setting:puid}:${setting:pgid}:1",
"${setting:puid}${setting:pgid}",
"media:${setting:pgid}",
"${setting:puid}:",
"${setting:other}:${setting:pgid}",
}
for _, owner := range append(ok, bad...) {
m := recycler()
m.Resources[0] = copied(m.Resources[0])
m.Resources[0]["owner"] = owner
err := parsed(t, m)
refused := err != nil && strings.Contains(err.Error(), "owner")
want := false
for _, b := range bad {
want = want || b == owner
}
if refused != want {
t.Errorf("%s: refused %v, want %v (%v)", owner, refused, want, err)
}
}
// A key with a default is declared, though nothing else asks for it.
m := recycler()
m.Resources = m.Resources[:1]
m.Resources[0] = copied(m.Resources[0])
m.Resources[0]["owner"] = "${setting:workers}"
m.Settings = map[string]catalogue.SettingDeclaration{"workers": {Kind: catalogue.KindPreference, Default: float64(4), Why: "a test"}}
if err := parsed(t, m); err != nil && strings.Contains(err.Error(), "owner") {
t.Errorf("a declared key was refused: %v", err)
}
// A file's owner takes no setting: the sweep says so.
f := recycler()
f.Resources[1] = copied(f.Resources[1])
f.Resources[1]["owner"] = "${setting:puid}"
if err := parsed(t, f); err == nil || !strings.Contains(err.Error(), "not filled in this field") {
t.Errorf("a file's owner setting passed: %v", err)
}
}
// A secret an outside party issues says so (novox/hq ADR 0228), is written back as it was read, and
// is the one at-start secret the mesh may not make; any other word for who issued it is refused.
func TestAnOwnSecretSaysWhenAnOutsidePartyIssuesIt(t *testing.T) {
m, err := ParseManifest([]byte(`{"module":"letta","version":"1","own-secrets":{
"server-password":{"path":"/var/lib/letta/server-password","taken":"at-start"},
"openai-api-key":{"path":"/var/lib/letta/openai-api-key","taken":"at-start","issued-by":"outside"},
"telegram-token":{"path":"/var/lib/letta/telegram-token","issued-by":"outside"},
"logflare":{"path":"/var/lib/letta/logflare","taken":"applied"},
"broker":"/var/lib/mesh/letta/broker"}}`))
if err != nil {
t.Fatal(err)
}
for name, may := range map[string]bool{"server-password": true, "openai-api-key": false,
"telegram-token": false, "logflare": false, "broker": false} {
if got := m.OwnSecrets[name].MeshMayMake(); got != may {
t.Errorf("%s: the mesh may make it = %v, want %v", name, got, may)
}
}
out, err := json.Marshal(m.OwnSecrets)
if err != nil {
t.Fatal(err)
}
var again catalogue.OwnSecrets
if err := json.Unmarshal(out, &again); err != nil {
t.Fatal(err)
}
if again["openai-api-key"] != m.OwnSecrets["openai-api-key"] || again["telegram-token"] != m.OwnSecrets["telegram-token"] {
t.Fatalf("the round trip lost who issued it: %s", out)
}
if strings.Contains(string(out), `"taken":""`) {
t.Fatalf("a secret that says only who issued it gained an empty taken: %s", out)
}
_, err = ParseManifest([]byte(`{"module":"letta","version":"1","own-secrets":{
"openai-api-key":{"path":"/var/lib/letta/openai-api-key","issued-by":"openai"}}}`))
if err == nil || !strings.Contains(err.Error(), `issued by "openai"`) {
t.Fatalf("an unknown word for who issued a secret was accepted: %v", err)
}
}
// 1170/AC2: an owner naming an account the module does not make, root, or the operator account's placeholder is
// refused, naming the secret and the account.
func TestAnOwnSecretsOwnerIsRefusedWhenItIsNotAnAccountTheModuleMakes(t *testing.T) {
for name, c := range map[string]struct{ owner, says string }{
"root": {"root", "root"},
"the operator account": {"${machine:account}", "operator account"},
"an account not declared": {"not-made", "not-made"},
} {
_, err := ParseManifest([]byte(`{"module":"mesh-mail","version":"1","own-secrets":{` +
`"mail-credential-*":{"path":"/var/lib/mesh-mail/mail-credential-*.secret","issued-by":"outside","owner":"` +
c.owner + `"}},"resources":[{"id":"account","type":"user","name":"mesh-mail-send"}]}`))
if err == nil || !strings.Contains(err.Error(), "mail-credential-*") || !strings.Contains(err.Error(), c.says) {
t.Errorf("%s: %v; want a refusal naming the secret and %q", name, err, c.says)
}
}
}
func TestAnOwnSecretTakenSomeOtherWayIsRefused(t *testing.T) {
_, err := ParseManifest([]byte(`{"module":"idp","version":"1","own-secrets":{
"admin":{"path":"/var/lib/idp/admin.secret","taken":"sometimes"}}}`))
if err == nil || !strings.Contains(err.Error(), `taken "sometimes"`) {
t.Fatalf("an unknown word for how a secret is taken was accepted: %v", err)
}
_, err = ParseManifest([]byte(`{"module":"idp","version":"1","own-secrets":{
"admin":{"path":"/var/lib/idp/admin.secret","rotate":"yes"}}}`))
if err == nil {
t.Fatal("an unknown field on an own secret was accepted")
}
}
// A path is compared as the machine writes it: cleaned. Each spelling of /etc/hosts is /etc/hosts.
func TestAnUncleanSpellingOfASeatsPathIsTheSeatsPath(t *testing.T) {
for _, spelled := range []string{"/etc//hosts", "/etc/./hosts", "/etc/../etc/hosts", "/etc/hosts/"} {
stranger := catalogue.Manifest{Module: "stranger", Version: "1", Resources: []map[string]any{
{"id": "hosts", "type": "file", "path": spelled, "content": "10.0.0.1 x\n"}}}
if got := strings.Join(CatalogueProblems(catalogue.Shelf{"stranger": stranger}), "; "); !strings.Contains(got,
`stranger declares the path "/etc/hosts", which the seat node-hostname names`) {
t.Errorf("%s from a module not claiming node-hostname was not refused: %q", spelled, got)
}
holder := hostnameHolder(map[string]any{"id": "hosts", "type": "file", "path": spelled, "content": "x"},
map[string]any{"id": "name", "type": "file", "path": "/etc/hostname", "content": "x"})
if err := catalogue.CanHold(holder, hostnameSeat(t)); err != nil {
t.Errorf("a holder declaring %s was refused: %v", spelled, err)
}
}
}
func TestAnUnhealthySeverityOtherThanUrgentOrWarningIsRefused(t *testing.T) {
if _, err := ParseManifest([]byte(`{"module":"mail","version":"1","unhealthy":{"severity":"loud","why":"mail stops"}}`)); err == nil ||
!strings.Contains(err.Error(), `not "loud"`) {
t.Errorf("a manifest declaring its unhealthy loud parsed: %v", err)
}
}
func TestAnUnhealthyWithoutWhyIsRefused(t *testing.T) {
for _, why := range []string{"", " "} {
if problems := manifestUnhealthyProblems((catalogue.Manifest{Module: "mail", Unhealthy: &catalogue.UnhealthyPolicy{Severity: catalogue.UnhealthyUrgent, Why: why}})); len(problems) == 0 {
t.Errorf("urgent with why %q was accepted", why)
}
}
if _, err := ParseManifest([]byte(`{"module":"mail","version":"1","unhealthy":{"severity":"urgent"}}`)); err == nil ||
!strings.Contains(err.Error(), "unhealthy says why") {
t.Errorf("a manifest declaring its unhealthy urgent without why parsed: %v", err)
}
}
// A reference to an access the definition does not declare is refused where the author is.
func TestAnUnknownAccessReferenceIsRefusedAtParse(t *testing.T) {
_, err := ParseManifest([]byte(`{
"module": "arr", "version": "1",
"accesses": [{"id": "series", "mode": "read-write"}],
"resources": [
{"id": "state", "type": "directory", "place": ".", "mode": "0700"},
{"id": "server", "type": "container", "name": "arr", "image": "arr:1",
"volumes": ["${access:movies}:/movies"]}
]}`))
if err == nil || !strings.Contains(err.Error(), "${access:movies}") {
t.Fatalf("a reference to an undeclared access was accepted: %v", err)
}
_, err = ParseManifest([]byte(`{"module": "arr", "version": "1", "accesses": [{"mode": "read"}]}`))
if err == nil || !strings.Contains(err.Error(), "neither an id nor a path") {
t.Fatalf("an access with no id and no path was accepted: %v", err)
}
}
// A key nobody renders is refused, not left in the file as a literal.
func TestAnUnknownPlaceholderKeyIsRefused(t *testing.T) {
for _, c := range []struct{ content, want string }{
{"${environment:foo}", "names ${environment:foo}; the environment is ${environment:posix} or ${environment:systemd}"},
{"${shell:zsh:middle}", "names ${shell:zsh:middle}; shell code is ${shell:<shell>:<slot>}"},
{"${shell:tcsh:first}", "names ${shell:tcsh:first}; shell code is ${shell:<shell>:<slot>}"},
{"${shell:zsh}", "names ${shell:zsh}; shell code is ${shell:<shell>:<slot>}"},
} {
raw := `{"module":"holder","claims":[{"name":"node-environment","scope":"node"},{"name":"node-login-shell","scope":"node"}],` +
`"resources":[{"id":"rc","type":"file","path":"/etc/rc","content":"` + c.content + `"}]}`
if _, err := ParseManifest([]byte(raw)); err == nil || !strings.Contains(err.Error(), c.want) {
t.Errorf("%s was accepted: %v", c.content, err)
}
}
// And outside a file's content, where nothing could be placed.
raw := `{"module":"holder","claims":[{"name":"node-environment","scope":"node"}],` +
`"resources":[{"id":"rc","type":"file","path":"/etc/${environment:posix}","content":"x"}]}`
if _, err := ParseManifest([]byte(raw)); err == nil ||
!strings.Contains(err.Error(), "names ${environment:posix} in its path; the environment and the shell's code are placed only in a file's content") {
t.Errorf("a placeholder in a path was accepted: %v", err)
}
}
// **Every endpoint in the catalogue is unnamed today, and must stay valid.** The word ships one
// release before anything uses it.
func TestAnUnnamedEndpointIsStillValid(t *testing.T) {
m := catalogue.Manifest{Module: "ordinary", Listens: []catalogue.Listening{{Port: 443, From: catalogue.FromEverywhere}}}
if got := endpointNameProblems(m); len(got) != 0 {
t.Fatalf("an unnamed endpoint was refused: %v", got)
}
if got := RouteProblems(m); len(got) != 0 {
t.Fatalf("a module with no route was refused: %v", got)
}
}
func TestAnUpstreamFileFromAnAddressOrAReservedNameIsRefused(t *testing.T) {
for _, u := range []string{"https://git.novox.internal/r.tar.xz", "https://10.1.2.3/r.tar.xz",
"https://203.0.113.9/r.tar.xz", "https://[2001:db8::1]/r.tar.xz", "https://nas.lan/r.tar.xz", "https://vendor/r.tar.xz"} {
_, err := ParseManifest([]byte(upstreamManifest(`{"name":"x","kind":"archive","url":"` + u + `","sha256":"` +
pinnedSum + `","version":"1"}`)))
if err == nil || !strings.Contains(err.Error(), "published under a public name") {
t.Errorf("%s: %v", u, err)
}
}
}
func TestAnUpstreamFilesVendorIsNamedOnPurposeAndAnInstallationsIsRefused(t *testing.T) {
// The vendor's host is judged like any name, and passes through names-on-purpose; a version of four dotted
// numbers is not an address, and a name in it is still judged.
m, err := ParseManifest([]byte(upstreamManifest(`{"name":"owl","kind":"archive","url":"https://vendor.be/owl/owl.xpi","sha256":"` +
pinnedSum + `","version":"1.5.6.1","file":"owl.xpi","names-on-purpose":{"vendor.be":"Owl's vendor publishes it there"}}`)))
if err != nil {
t.Fatal(err)
}
if problems := InstallationProblems(m); len(problems) != 0 {
t.Fatalf("the declared vendor and the version were taken for an installation: %v", problems)
}
m.Build.Artifacts[0].NamesOnPurpose = nil
if problems := InstallationProblems(m); len(problems) != 1 || !strings.Contains(problems[0], "vendor.be") {
t.Fatalf("an undeclared vendor host is judged: %v", problems)
}
m.Build.Artifacts[0].Version = "git.novox.be 10.0.0.1"
if problems := InstallationProblems(m); len(problems) != 2 {
t.Fatalf("a name in the version is judged: %v", problems)
}
m.Build.Artifacts[0].URL, m.Build.Artifacts[0].Version = "https://mail.novox.be/owl.xpi", "1"
if problems := InstallationProblems(m); len(problems) != 1 || !strings.Contains(problems[0], "mail.novox.be") {
t.Fatalf("an installation's host in the url is judged: %v", problems)
}
}
func TestAnUpstreamFileThatIsNotPinnedIsRefused(t *testing.T) {
cases := map[string]string{
// No digest: what the address serves changes under the mesh.
`{"name":"x","kind":"archive","url":"https://vendor.invalid/r.tar.xz","version":"1"}`: "without a pinned sha256",
`{"name":"x","kind":"archive","url":"https://vendor.invalid/r.tar.xz","sha256":"ABC","version":"1"}`: "without a pinned sha256",
// No version: a person reading the pin cannot tell which release.
`{"name":"x","kind":"archive","url":"https://vendor.invalid/r.tar.xz","sha256":"` + pinnedSum + `"}`: "says no version",
// Not https.
`{"name":"x","kind":"archive","url":"http://vendor.invalid/r.tar.xz","sha256":"` + pinnedSum + `","version":"1"}`: "over https",
// Both a directory of the repository and a URL.
`{"name":"x","kind":"archive","from":"files","url":"https://vendor.invalid/r.tar.xz","sha256":"` + pinnedSum +
`","version":"1"}`: "one or the other",
// A pin without a URL.
`{"name":"x","kind":"archive","from":"files","sha256":"` + pinnedSum + `"}`: "no url",
// Only an archive is mirrored from a file.
`{"name":"x","kind":"image","from":"Dockerfile","url":"https://vendor.invalid/r.tar.xz","sha256":"` + pinnedSum +
`","version":"1"}`: "only an archive",
// A file name that is a path, a directory within that leaves the archive, and both at once.
`{"name":"x","kind":"archive","url":"https://vendor.invalid/a.xpi","sha256":"` + pinnedSum +
`","version":"1","file":"../a.xpi"}`: "not a plain relative file name",
`{"name":"x","kind":"archive","url":"https://vendor.invalid/r.tar.xz","sha256":"` + pinnedSum +
`","version":"1","within":"../up"}`: "not a plain relative directory",
`{"name":"x","kind":"archive","url":"https://vendor.invalid/r.tar.xz","sha256":"` + pinnedSum +
`","version":"1","within":"r","file":"r.tar.xz"}`: "a file is kept whole",
}
for artifact, want := range cases {
_, err := ParseManifest([]byte(upstreamManifest(artifact)))
if err == nil {
t.Errorf("%s was accepted", artifact)
continue
}
if !strings.Contains(err.Error(), want) {
t.Errorf("%s was refused for another reason than %q: %v", artifact, want, err)
}
}
}
func TestAPackageParsesLikeABundleAndNeedsALanguage(t *testing.T) {
// The SDK's shape: a package built from the module's own directory, naming a language.
m, err := ParseManifest([]byte(`{"module":"mesh-sdk","version":"1","slug":"sdk",
"build":{"artifacts":[{"name":"lib","kind":"package","language":"typescript"}]},
"resources":[]}`))
if err != nil {
t.Fatalf("the SDK's package manifest did not parse: %v", err)
}
if m.Build.Artifacts[0].Kind != catalogue.ArtifactPackage {
t.Fatalf("expected a package artifact, got %q", m.Build.Artifacts[0].Kind)
}
// A package that names what it is built from is refused, exactly as a bundle is: it is built
// from the module's own directory.
if _, err := ParseManifest([]byte(`{"module":"a","version":"1","build":{"artifacts":[
{"name":"lib","kind":"package","language":"typescript","from":"Dockerfile"}]}}`)); err == nil {
t.Fatal("a package naming a source was accepted")
}
// A package with no language cannot choose a toolchain.
if _, err := ParseManifest([]byte(`{"module":"a","version":"1","build":{"artifacts":[
{"name":"lib","kind":"package"}]}}`)); err == nil {
t.Fatal("a package with no language was accepted")
}
}
func TestAPathThroughAnAccessIsJudgedWithTheAccessFilledIn(t *testing.T) {
// A file's path may name an access; the access's default path is the definition's, so it is judged with it.
_, err := ParseManifest([]byte(`{"module": "backup", "version": "1",
"accesses": [{"id": "images", "path": "/var/lib/docker/volumes"}],
"resources": [{"id": "marker", "type": "file", "path": "${access:images}/marker", "content": "x"}]}`))
if err == nil || !strings.Contains(err.Error(), "/var/lib/docker/volumes/marker") ||
!strings.Contains(err.Error(), "issue 496") {
t.Fatalf("a file reaching Docker's data through an access's default path was accepted: %v", err)
}
// An access the definition gives no path is placed by a setting, which Places and AccessPlaces judge, and on
// the machine by the node-engine: nothing here to resolve it against, so nothing is refused for it.
if _, err := ParseManifest([]byte(`{"module": "backup", "version": "1",
"accesses": [{"id": "images"}],
"resources": [{"id": "marker", "type": "file", "path": "${access:images}/marker", "content": "x"}]}`)); err != nil {
t.Fatalf("an access placed only by a setting cannot be judged at the definition: %v", err)
}
}
// ADR 0203 §5 and ADR 0204 §3: a placeholder outside the seat's holder is refused by the parser, which is what the
// catalogue check and registration run (its refusal again at composition is the node-engine's, ADR 0593 §7).
func TestAPlaceholderOutsideTheHolderIsRefused(t *testing.T) {
for _, c := range []struct{ content, want string }{
{"${environment:posix}", "toolchain's resource rc names ${environment:posix} and toolchain does not claim node-environment"},
{"${shell:zsh:normal}", "toolchain's resource rc names ${shell:zsh:normal} and toolchain does not claim node-login-shell"},
} {
raw := `{"module":"toolchain","resources":[{"id":"rc","type":"file","path":"/etc/rc","content":"` + c.content + `"}]}`
if _, err := ParseManifest([]byte(raw)); err == nil || !strings.Contains(err.Error(), c.want) {
t.Errorf("the catalogue check accepted %s outside its holder: %v", c.content, err)
}
}
}
// **A language that runs anywhere must not name a system.** A system that decides nothing reads as
// though it did, which is the same fault as a restriction that restricts nothing (novox/hq ADR 0045); a compiled
// bundle names one, and a portable bundle naming none is an ordinary one.
func TestABundleNamesASystemOnlyWhereItCompiles(t *testing.T) {
for _, c := range []struct{ language, system, refused string }{
{"go", "arch", ""},
{"typescript", "", ""},
{"typescript", "arch", "runs anywhere"},
} {
got := buildProblemsOf(t, bundleFor(c.language, c.system))
if (c.refused == "") != (got == "") || !strings.Contains(got, c.refused) {
t.Errorf("a %s bundle naming %q: %q, want %q", c.language, c.system, got, c.refused)
}
}
}
// A rule with no source is open, and must say so rather than appear to restrict something.
func TestAPortWithNoSourceIsRefused(t *testing.T) {
_, err := ParseManifest([]byte(`{"module":"web","listens":[{"port":443}]}`))
if err == nil {
t.Fatal("a port that never said who may reach it was accepted; it reads as a restriction")
}
if !strings.Contains(err.Error(), "from where") {
t.Fatalf("refused without saying what is missing: %v", err)
}
}
// 1179/AC2: a bus on a process with no user, user root, or the operator account is refused naming the
// process and the account rule.
func TestAProcessBusAccountMustBeTheModulesOwn(t *testing.T) {
for name, c := range map[string]struct{ user, says string }{
"no user": {"", "send"},
"user root": {"root", "root"},
"the operator account": {"${machine:account}", "operator"},
} {
raw := `{"module":"mesh-mail","version":"1","emits":["mail.send.sent"],"state":["sends"],` +
`"streams":[{"name":"mail-sends","events":["mail.send.approved"],"retention":"work-queue",` +
`"max-deliveries":5}],"resources":[{"id":"account","type":"user","name":"mesh-mail-send"},` +
`{"id":"send","type":"process","run":["./mesh-mail","send"]`
if c.user != "" {
raw += `,"user":"` + c.user + `"`
}
raw += `,"bus":{"emits":["mail.send.sent"]}}]}`
_, err := ParseManifest([]byte(raw))
if err == nil || !strings.Contains(err.Error(), "send") || !strings.Contains(err.Error(), c.says) {
t.Errorf("%s: %v; want a refusal naming the process and %q", name, err, c.says)
}
}
}
// 1179/AC3: a bus naming a stream, state or event the module does not declare as its own is refused naming
// it.
func TestAProcessBusNamesOnlyTheModulesOwn(t *testing.T) {
for name, c := range map[string]struct{ bus, says string }{
"another module's event": {`{"emits":["billing.paid"]}`, "billing.paid"},
"another module's queue": {`{"queues":["other-queue"]}`, "other-queue"},
"another module's state": {`{"state":["other-state"]}`, "other-state"},
} {
_, err := ParseManifest([]byte(mailSenderProcessManifest(t, c.bus)))
if err == nil || !strings.Contains(err.Error(), c.says) {
t.Errorf("%s: %v; want a refusal naming %q", name, err, c.says)
}
}
}
// 1179/AC4: a bus declaring serves, holds, claims or a seat is refused naming the key; a bus with invokes of
// a tool or a seat verb is accepted.
func TestAProcessBusNeverServesOrHoldsASeat(t *testing.T) {
for name, c := range map[string]struct{ bus, says string }{
"serves": {`{"emits":["mail.send.sent"],"serves":["send"]}`, "serves"},
"holds": {`{"emits":["mail.send.sent"],"holds":["mail-sender"]}`, "holds"},
"claims": {`{"emits":["mail.send.sent"],"claims":["mail-sender"]}`, "claims"},
"seat": {`{"emits":["mail.send.sent"],"seat":"mail-sender"}`, "seat"},
} {
_, err := ParseManifest([]byte(mailSenderProcessManifest(t, c.bus)))
if err == nil || !strings.Contains(err.Error(), c.says) {
t.Errorf("%s: %v; want a refusal naming %q", name, err, c.says)
}
}
accepted := `{"emits":["mail.send.sent","mail.send.failed"],"invokes":["seat:operator-channel.tell","mail.status"]}`
if _, err := ParseManifest([]byte(mailSenderProcessManifest(t, accepted))); err != nil {
t.Fatalf("a bus invoking a tool and a seat verb was refused: %v", err)
}
}
// 1179/AC1: a well-formed bus on a process whose account the module makes is accepted, and the catalogue
// gives it whole to the grant composition (ProcessBuses, which 1180 reads).
func TestAProcessDeclaresItsBusAndIsAccepted(t *testing.T) {
m, err := ParseManifest([]byte(mailSenderProcessManifest(t, mailSenderBus)))
if err != nil {
t.Fatalf("a well-formed process bus was refused: %v", err)
}
buses := m.ProcessBuses()
b, ok := buses["send"]
if !ok {
t.Fatalf("the process's bus was not given to the grant composition: %+v", buses)
}
if !strSliceEqual(b.Emits, []string{"mail.send.sent", "mail.send.failed"}) ||
!strSliceEqual(b.Queues, []string{"mail-sends"}) || !strSliceEqual(b.State, []string{"sends"}) ||
!b.Asks || !strSliceEqual(b.Invokes, []string{"seat:operator-channel.tell"}) {
t.Fatalf("the bus read as %+v", b)
}
// 1952 S5 (novox/hq ADR 0564 Limits 4; the lead, 2026-10-29): a tools bundle declares its own bus account, read as a
// process's, named after its binary and kept through resolution; a removal is from the closed set, alone, and the
// bus module's (mesh-broker) alone.
toolsIn := func(claims, bus, mode, where string) string {
return `{"module":"nats","version":"1","claims":` + claims + `,"own-secrets":{"broker":"${dir:mesh-state}/broker",` +
`"broker-nats-tools":"${dir:tools-secret}/broker-nats-tools"},"resources":[{"id":"mesh-state","type":"directory",` +
`"mode":"0700","place":"mesh","unknown":"program"},{"id":"tools-secret","type":"directory","mode":"` + mode +
`","path":"` + where + `","unknown":"program"}],"build":{"artifacts":[{"name":"tools","kind":"bundle",` +
`"language":"go","system":"arch","from":"cmd/nats-tools","binary":"nats-tools","loads":["nats-tools"],"bus":` + bus + `}]}}`
}
tools := func(claims, bus string) string { return toolsIn(claims, bus, "0711", "/var/lib/mesh/nats-tools") }
broker := `[{"name":"mesh-broker","scope":"mesh"}]`
for _, c := range []struct {
name, manifest, refused string
}{
{name: "the bus module's tools removing", manifest: tools(broker, `{"removes":["mesh-host_acts","controller-leftovers"]}`)},
{name: "a removal outside the set", manifest: tools(broker, `{"removes":["EVENTS.a_m"]}`), refused: "not one of"},
{name: "a removal beside a grant", manifest: tools(broker, `{"removes":["mesh-host_acts"],"invokes":["nats.nats_streams"]}`), refused: "with nothing else"},
{name: "another module's tools removing", manifest: tools(`[]`, `{"removes":["mesh-host_acts"]}`), refused: "only the bus module"},
{name: "a tools bundle consuming a queue", manifest: tools(broker, `{"queues":["x"]}`), refused: "holds no consumer"},
{name: "a process removing", manifest: mailSenderProcessManifest(t, `{"removes":["mesh-host_acts"]}`), refused: "only a tools bundle"},
// The runtime's account reads the account's secret: it lies where others may pass (the host agent, 2026-10-29).
{name: "a tools secret the runtime cannot reach", manifest: toolsIn(broker, `{"removes":["mesh-host_acts"]}`, "0700",
"/var/lib/mesh/nats-tools"), refused: "cannot reach"},
// Its parents too: under the module's mesh place, /var/lib/mesh/nats is 0700 (the lead, read on novox 2026-10-29).
{name: "a tools secret beneath the module's mesh place", manifest: toolsIn(broker, `{"removes":["mesh-host_acts"]}`, "0711",
"${dir:mesh-state}/tools"), refused: "cannot reach"},
} {
m, err := ParseManifest([]byte(c.manifest))
if c.refused != "" {
if err == nil || !strings.Contains(err.Error(), c.refused) {
t.Errorf("%s: %v, want refused saying %q", c.name, err, c.refused)
}
continue
}
if err != nil {
t.Fatalf("%s: %v", c.name, err)
}
accounts := m.BusAccounts()
if len(accounts) != 1 || accounts[0].ID != "nats-tools" || !accounts[0].Tools ||
!strSliceEqual(accounts[0].Bus.Removes, []string{catalogue.RemovesEngineActs, catalogue.RemovesControllerLeftovers}) {
t.Errorf("%s: the accounts are %+v", c.name, accounts)
}
// Resolved, the build goes and its bundles keep the bus (build.go).
resolved := catalogue.Manifest{Module: "nats", Bundles: []catalogue.Bundle{{Name: "tools", Binary: "nats-tools", Bus: m.Build.Artifacts[0].Bus}}}
if got := resolved.BusAccounts(); len(got) != 1 || got[0].ID != "nats-tools" {
t.Errorf("%s: resolved, the accounts are %+v", c.name, got)
}
}
if got := m.BusAccounts(); len(got) != 1 || got[0].ID != "send" || got[0].Tools {
t.Errorf("the process's account is %+v", got)
}
}
// {member} is replaced only for a family's member: a single secret's prompt may not hold it (novox/hq issue 789).
func TestAPromptHoldingMemberOnASingleSecretIsRefused(t *testing.T) {
_, err := ParseManifest([]byte(`{"module":"mesh-mail","version":"1","own-secrets":{
"smtp-relay-key":{"path":"/var/lib/mesh-mail/smtp-relay-key","taken":"at-start",
"prompt":"For {member}"}}}`))
if err == nil || !strings.Contains(err.Error(), "{member} is replaced only for a family's member") {
t.Fatalf("a single secret's prompt holding {member}: %v; want a refusal saying so", err)
}
}
// What `module check` and the merge gate judge: the provider read from its tree, its text inline. Judged by the same
// rules, a provider naming a provision it does not provide is refused there too, and one that does is taken.
func TestAProviderReadFromItsTreeIsJudgedByTheSameRules(t *testing.T) {
read := func(text string) (catalogue.Manifest, error) {
dir := t.TempDir()
if err := os.MkdirAll(filepath.Join(dir, "files"), 0o755); err != nil {
t.Fatal(err)
}
if err := os.WriteFile(filepath.Join(dir, "files", "consumers.json"), []byte(text), 0o644); err != nil {
t.Fatal(err)
}
m, err := ParseManifest([]byte(handedProvider))
if err != nil {
t.Fatal(err)
}
return WithTemplatesFrom(m, dir)
}
if _, err := read(consumersText); err != nil {
t.Fatalf("a provider read from its tree is refused: %v", err)
}
if _, err := read("${consumers:cache}"); err == nil || !strings.Contains(err.Error(), "does not provide") {
t.Fatalf("a provider read from its tree naming a provision it does not provide: %v", err)
}
}
func TestAReferenceToNoDirectoryRefusesAtTheManifest(t *testing.T) {
m := placedModule()
m.Resources[2]["volumes"] = []any{"${dir:date}:/data"} // a typo, the likely shape
problems := manifestUnknownDirRefs(m)
if len(problems) != 1 || !strings.Contains(problems[0], `${dir:date}`) {
t.Fatalf("a reference naming no directory is a manifest problem; got %v", problems)
}
if got := manifestUnknownDirRefs(placedModule()); len(got) != 0 {
t.Fatalf("a correct definition has none; got %v", got)
}
}
func TestARenamedEntryIsRefusedWhereItCouldNotHold(t *testing.T) {
entry := func(m map[string]any, e any) map[string]any {
resourceOf(m, "rules")["takes-config"] = []any{"position", "theme", e}
return m
}
refused(t, entry(renaming(), map[string]any{"key": "url"}), "notifier's rules", `"key" and "as"`)
refused(t, entry(renaming(), map[string]any{"key": "url", "as": "NOTIFIER_URL", "when": "set"}), `"key" and "as"`)
refused(t, entry(renaming(), map[string]any{"key": "URL", "as": "NOTIFIER_URL"}), "not a usable key")
refused(t, entry(renaming(), map[string]any{"key": "url", "as": "NOTIFIER URL"}), "not a usable name")
refused(t, entry(renaming(), map[string]any{"key": "url", "as": "endpoints"}), `"endpoints"`, "the mesh reads")
refused(t, entry(renaming(), map[string]any{"key": "url", "as": "theme"}), `"theme"`, "twice")
refused(t, entry(renaming(), map[string]any{"key": "theme", "as": "THEME"}), `"theme"`, "twice")
refused(t, entry(renaming(), map[string]any{"key": "nothing", "as": "NOTHING"}), `"nothing"`, "declares no such value")
kept := entry(renaming(), map[string]any{"key": "url", "as": "NOTIFIER_URL"})
resourceOf(kept, "rules")["protected"] = []any{"NOTIFIER_URL"}
refused(t, kept, `"NOTIFIER_URL"`, "protected")
// Laid only when set, so the code's own fallback holds: never required.
required := renaming()
declaration(required, "url")["required"] = true
delete(declaration(required, "url"), "default")
refused(t, required, `"url"`, "only when it is set", "never required")
}
func TestARenamedEntryIsTheConfigFormsOnly(t *testing.T) {
m := map[string]any{
"module": "notifier", "version": "1",
"resources": []any{
map[string]any{"id": "rules", "type": "file", "path": "/x/rules.json", "merge": "json", "content": "{}",
"takes-settings": []any{map[string]any{"key": "url", "as": "NOTIFIER_URL"}}},
},
}
refused(t, m, "notifier's rules", "config form")
}
// Tonight's case (issue 263): networkmanager, no slug, requiring the mesh's resolver provision on a
// machine with a six-character name. Under ADR 0049's one bound it was refused, and its provider's
// whole machine with it; the resolver keeps no name, so it is not refused at all.
func TestARequirementOnAKeylessProvisionComposesWithALongName(t *testing.T) {
shelf := catalogue.Shelf{
"resolver": {Module: "resolver", Provides: []catalogue.Offer{{Name: "wildcard-resolution", Scope: catalogue.ScopeMesh}}},
"networkmanager": {Module: "networkmanager", Requires: []string{"wildcard-resolution"}},
}
if catalogue.CheckIdentity("laptop", "networkmanager") == nil {
t.Fatal("the regression is not reproduced: mesh_laptop_networkmanager fits the old global bound")
}
if got := IdentityProblems(shelf, 6); len(got) != 0 {
t.Fatalf("a requirement on a keyless provision was refused for its length: %q", got)
}
// Whether the composed requirement overflows on the machine is the node-engine's composition to say (ADR 0593 §7).
}
// A resolved manifest keeps a built artifact's reference in the store-relative form, never the
// address the builder reached the store by (novox/hq ADR 0155): on 2026-10-02 the first bundle
// resolved on the mesh carried the store's host in bundles[0].source and registration refused it
// as naming an installation. Archive resources are the same kind of reference and get the same.
func TestAResolvedReferenceIsKeptNotRouted(t *testing.T) {
m, err := ParseManifest([]byte(`{
"module": "sample", "version": "1",
"tools": ["one"],
"build": {"artifacts": [
{"name": "code", "kind": "bundle", "language": "typescript", "entrypoints": ["tools/index.js"]},
{"name": "files", "kind": "archive", "from": "files"}
]},
"resources": [{"id": "packed", "type": "archive", "path": "/opt/sample", "artifact": "files"}]
}`))
if err != nil {
t.Fatal(err)
}
digest := "sha256:" + strings.Repeat("ab", 32)
resolved, err := m.Resolve([]catalogue.Built{
{Name: "code", Kind: catalogue.ArtifactBundle, Reference: "http://store.example:5100/v2/sample/code/blobs/" + digest, Digest: digest},
{Name: "files", Kind: catalogue.ArtifactArchive, Reference: "http://store.example:5100/v2/sample/files/blobs/" + digest, Digest: digest},
})
if err != nil {
t.Fatal(err)
}
if got, want := resolved.Bundles[0].Source, catalogue.ArtifactStoreScheme+"sample/code/blobs/"+digest; got != want {
t.Errorf("bundle source %q, want the kept form %q", got, want)
}
if got, want := resolved.Resources[0]["source"], catalogue.ArtifactStoreScheme+"sample/files/blobs/"+digest; got != want {
t.Errorf("archive source %q, want the kept form %q", got, want)
}
if problems := InstallationProblems(resolved); len(problems) != 0 {
t.Errorf("a resolved manifest names an installation: %v", problems)
}
}
// A route naming an endpoint the module does not declare reaches nothing, and is refused where it is
// written rather than resolving to no port and serving nothing.
func TestARouteNamingAnEndpointTheModuleLacksIsRefused(t *testing.T) {
m := aMediaServer()
m.Contributes["route"][catalogue.RouteEndpoint] = "absent"
got := strings.Join(RouteProblems(m), "\n")
if !strings.Contains(got, "does not declare") {
t.Fatalf("a route naming an absent endpoint was accepted:\n%s", got)
}
}
func TestARunOnceMustBeABoolean(t *testing.T) {
// A value that is not true or false is refused here, not sent to a machine that would then have
// to guess what a string means.
digest := "@sha256:" + strings.Repeat("a", 64)
bad := []byte(`{"module":"m","resources":[
{"id":"seed","type":"container","name":"seed","image":"registry.example/x` + digest + `","run-once":"yes"}
]}`)
if _, err := ParseManifest(bad); err == nil {
t.Error("a run-once that is not a boolean was accepted")
} else if !strings.Contains(err.Error(), "run-once") {
t.Errorf("refused for the wrong reason: %v", err)
}
good := []byte(`{"module":"m","resources":[
{"id":"seed","type":"container","name":"seed","image":"registry.example/x` + digest + `","run-once":true}
]}`)
if _, err := ParseManifest(good); err != nil {
t.Errorf("a valid run-once container was refused: %v", err)
}
}
func TestARuntimeHealthIsHeldToTheHealthRules(t *testing.T) {
for _, c := range []struct {
health any
says string
}{
{map[string]any{"kind": "tool", "tool": "keeper_restart"}, "does not serve"},
{map[string]any{"kind": "tool", "tool": "keeper_health", "interval": "2s"}, "no more often"},
{map[string]any{"kind": "exec", "command": "true"}, "only a container"},
{map[string]any{"kind": "tool", "tool": "keeper_health", "port": 80}, "names a port"},
{"tool", "is an object"},
} {
_, err := ParseManifest(ownAccountManifest(t, true, c.health))
if err == nil || !strings.Contains(err.Error(), c.says) {
t.Errorf("%v: %v, want it refused saying %q", c.health, err, c.says)
}
}
}
func TestAScheduleThatIsNotAStringIsRefused(t *testing.T) {
digest := "@sha256:" + strings.Repeat("a", 64)
bad := []byte(`{"module":"m","resources":[
{"id":"sync","type":"container","name":"sync","image":"registry.example/x` + digest + `","schedule":true}
]}`)
if _, err := ParseManifest(bad); err == nil {
t.Error("a schedule that is not a string was accepted")
} else if !strings.Contains(err.Error(), "schedule") {
t.Errorf("refused for the wrong reason: %v", err)
}
}
// A seat needing itself names nothing (novox/hq ADR 0378 §8).
func TestASeatCannotNeedItself(t *testing.T) {
m := catalogue.Manifest{Module: "mail", DefinesSeats: []catalogue.SeatDeclaration{{
Name: "mail-notifier", Scope: catalogue.ScopeNode, Needs: []string{"mail-notifier"}}}}
got := strings.Join(declaredSeatProblems(m), "; ")
if !strings.Contains(got, "needing itself") {
t.Fatalf("a seat needing itself was accepted: %q", got)
}
}
// **At registration, not in the parser** (novox/hq ADR 0122): a seat's scope is a property of the
// set, the set is the store's, and the parser also runs on a build machine that has no store.
func TestASeatClaimedAtAnotherScopeIsRefused(t *testing.T) {
m, err := ParseManifest(claimed(`[{"name":"npm-package-registry","scope":"node"}]`))
if err != nil {
t.Fatalf("the parser judged a scope it reads from data it may not have: %v", err)
}
got := strings.Join(CatalogueProblems(catalogue.Shelf{m.Module: m}), "; ")
if !strings.Contains(got, "mesh seat") {
t.Fatalf("the refusal does not say which scope the seat is: %q", got)
}
}
// A seat names a role's own path, never the root or a top-level directory.
func TestASeatNamesNoRootOrTopLevelDirectory(t *testing.T) {
for _, p := range []string{"/", "/etc"} {
m := catalogue.Manifest{Module: "a", Version: "1", DefinesSeats: []catalogue.SeatDeclaration{
{Name: "a-seat", Scope: catalogue.ScopeNode, Paths: []string{p}}}}
if got := strings.Join(declaredSeatProblems(m), "; "); !strings.Contains(got, "top-level directory") {
t.Errorf("a module's seat naming %s was accepted: %q", p, got)
}
}
}
// A seat with no protocol is a marker: which module is this node's showcase, or its packet filter.
// Most node-scoped seats are markers, so refusing one would refuse the majority of the set.
func TestASeatWithoutAProtocolIsAMarkerNotAMistake(t *testing.T) {
m := catalogue.Manifest{Module: "vague", DefinesSeats: []catalogue.SeatDeclaration{{Name: "something", Scope: catalogue.ScopeNode}}}
if got := strings.Join(declaredSeatProblems(m), "; "); got != "" {
t.Fatalf("a marker seat was refused: %s", got)
}
}
func TestASecondClaimOfOneKindIsRefused(t *testing.T) {
got := problemsFor(t, catalogue.Shelf{"messenger": router(), "telegram": aChannel("telegram", "telegram"),
"telegram-two": aChannel("telegram-two", "telegram")})
if !strings.Contains(got, `of kind "telegram", which telegram already claims`) {
t.Fatalf("a second holder of one kind stood: %s", got)
}
}
func TestAServedNameIsCheckedWhenTheDefinitionIsRead(t *testing.T) {
for bad, want := range map[string]string{
`"public-root":"https://${bound:route:name-api}/x"`: "what forge is called through route is name, internal-name, name-web, internal-name-web",
`"public-root":"https://${bound:postgres-database:name}/x"`: `forge contributes to "route"`,
`"public-root":"https://${bound:route:name-web|default:x}/x"`: "takes no default",
`"public-root":"https://${bound:route:at}/x"`: "what forge is called through route",
} {
raw := strings.Replace(aRegistryForge, `"public-root":"https://${bound:route:name-web}/api/packages/novox/npm/"`, bad, 1)
_, err := ParseManifest([]byte(raw))
if err == nil || !strings.Contains(err.Error(), want) {
t.Errorf("%s: want a refusal saying %q, got %v", bad, want, err)
}
}
nested := strings.Replace(aRegistryForge, `"public-root":"https://${bound:route:name-web}/api/packages/novox/npm/"`,
`"roots":["https://${bound:route:name-web}/x"]`, 1)
if _, err := ParseManifest([]byte(nested)); err == nil || !strings.Contains(err.Error(), "inside a list or an object") {
t.Errorf("a nested own name is refused, not served as the placeholder: %v", err)
}
if _, err := ParseManifest([]byte(aRegistryForge)); err != nil {
t.Fatalf("its own name is accepted: %v", err)
}
}
// A fact or an alphabet the mesh does not have is refused where the definition is, not where a
// consumer happens to be resolved — and the refusal says what may be said instead.
func TestAServedValueNamingSomethingTheMeshDoesNotHaveIsRefused(t *testing.T) {
for _, c := range []struct{ value, says string }{
{"${consumer:node}", "as"},
{"${consumer:as:punycode}", "dns"},
} {
m := store()
m.Serves["s3-bucket"]["bucket"] = c.value
raw, err := json.Marshal(m)
if err != nil {
t.Fatal(err)
}
_, err = ParseManifest(raw)
if err == nil {
t.Fatalf("%s was accepted", c.value)
}
if !strings.Contains(err.Error(), c.value) {
t.Errorf("the refusal of %s does not quote it: %v", c.value, err)
}
if !strings.Contains(err.Error(), c.says) {
t.Errorf("the refusal of %s does not say what may be said (%q): %v", c.value, c.says, err)
}
}
}
// Third review: a setting may name only a template's instance — right after the `@`, before the final
// suffix, and the whole instance — so a value can never make the unit another unit, nor another kind.
// Refused where the manifest is read, near its author.
func TestASettingInAUnitNameIsOnlyATemplatesInstance(t *testing.T) {
ok := []string{"zfs-scrub-weekly@${setting:scrub-pool}.timer", "getty@${setting:tty}.service"}
bad := []string{
"${setting:unit}",
"${setting:name}.timer",
"zfs-scrub-${setting:cadence}@storage.timer",
"zfs-scrub@${setting:pool}-x.timer",
"zfs-scrub@x-${setting:pool}.timer",
"zfs-scrub@${setting:pool}.${setting:kind}",
"zfs-scrub@${setting:pool}",
"zfs-scrub@${setting:a}${setting:b}.timer",
}
for _, unit := range append(ok, bad...) {
m := catalogue.Manifest{Module: "zfs", Resources: []map[string]any{{"id": "t", "type": "service", "unit": unit, "state": "stopped"}}}
err := parsed(t, m)
refused := err != nil && strings.Contains(err.Error(), "instance")
want := false
for _, b := range bad {
want = want || b == unit
}
if refused != want {
t.Errorf("%s: refused %v, want %v (%v)", unit, refused, want, err)
}
}
}
func TestASiteScopedProvisionIsRefused(t *testing.T) {
// Meaningful for a claim — one DHCP server per segment — and not yet meaningful for a
// provision, because nothing knows how to reach "the one at my site".
_, err := ParseManifest([]byte(
`{"module":"dns","version":"1","provides":[{"name":"resolver","scope":"site"}]}`))
if err == nil {
t.Fatal("a site-scoped provision was accepted")
}
if !strings.Contains(err.Error(), "site") {
t.Fatalf("unhelpful refusal: %v", err)
}
}
func TestAskingForTheRuleSetWithNowhereToPutItIsRefused(t *testing.T) {
_, err := ParseManifest([]byte(`{"module":"firewall","filtering":{"into":""}}`))
if err == nil {
t.Fatal("a module asked for the rule set and named no path; it would receive nothing")
}
}
func TestASoftwareSpelledKeyIsUsable(t *testing.T) {
// A file's keys are its software's: nodered's timeZone is not lower case and is still a key.
m := catalogue.Manifest{Module: "flows", Resources: []map[string]any{{"id": "settings", "type": "file",
"path": "/x", "merge": catalogue.MergeJSON, "content": "{}", catalogue.TakesSettingsField: []any{"timeZone"}}}}
if problems := TakenSettingsProblems(m); len(problems) != 0 {
t.Errorf("timeZone was refused: %v", problems)
}
}
func TestASourceNobodyDefinedIsRefused(t *testing.T) {
_, err := ParseManifest([]byte(`{"module":"web","listens":[{"port":443,"from":"lan"}]}`))
if err == nil {
t.Fatal("a source the mesh cannot render was accepted, and would be silently dropped")
}
}
// A name the bus could not hold, or that would let two modules derive one bucket, is refused at
// registration in the manifest's words.
func TestAStateNameIsLocalAndOneToken(t *testing.T) {
for _, c := range []struct{ manifest, says string }{
{`{"module":"a","version":"1","state":["mesh.servers"]}`, `keeps state "mesh.servers": a state is named locally`},
{`{"module":"a","version":"1","state":["my_servers"]}`, `keeps state "my_servers"`},
{`{"module":"a","version":"1","state":["s","s"]}`, `keeps state "s" twice`},
{`{"module":"a","version":"1","state":[{"name":"s","history":65}]}`, `a key keeps 0 or 1 to 64 (0 is taken as 1)`},
{`{"module":"a","version":"1","state":[{"name":"s","history":0}]}`, ``}, // 0 passes, as before (ADR 0579 §1)
// 1966/AC9: one name, one form
{`{"module":"a","version":"1","state":["x"],"requires":[{"name":"x","storage":"key-value","template":"state"}]}`,
`a's bus state "x": the name is also kept in the old section state`},
{`{"module":"a.b","version":"1","state":["s"]}`, `no dot`},
{`{"module":"a","version":"1","reads":["bindings"]}`, `a read is <module>.<name>`},
{`{"module":"a","version":"1","reads":["a.s"]}`, `which is its own state`},
{`{"module":"a","version":"1","state":[{"name":"s","shared":true}]}`, `{name, history, ttl-seconds, per-machine, key-ttl}`},
} {
_, err := ParseManifest([]byte(c.manifest))
if c.says == "" {
if err != nil {
t.Errorf("%s was refused: %v", c.manifest, err)
}
continue
}
if err == nil {
t.Errorf("%s was accepted", c.manifest)
continue
}
if !strings.Contains(err.Error(), c.says) {
t.Errorf("%s refused for the wrong reason: %v", c.manifest, err)
}
}
}
// A stored manifest with a key this reader does not know, at the top or inside any block, is decoded with the key set
// aside and named, and its module is composed, never left out (novox/hq ADR 0578 §6, which changes ADR 0262); the judge
// refuses it. One case per block whose decoder wraps the decoder's words in its own.
func TestAStoredManifestWithAnUnknownKeyIsSetAsideAndTheJudgeRefusesIt(t *testing.T) {
t.Run("it still provides what it provides", aModuleWithAFieldSetAsideStillProvidesWhatItProvides)
for where, raw := range map[string]string{
"the top": `{"module": "later", "version": "2", "a-field-from-later": {"x": 1}}`,
"a state": `{"module": "later", "version": "2", "state": [{"name": "s", "a-field-from-later": 1}]}`,
"a provided name": `{"module": "later", "version": "2", "provides": [{"name": "p", "a-field-from-later": 1}]}`,
"an offer's identity": `{"module": "later", "version": "2", "provides": [{"name": "p", "identity": {"in": "x", "a-field-from-later": 1}}]}`,
"a backup": `{"module": "later", "version": "2", "data": {"own": [{"id": "d", "path": "${dir:d}", "class": "valuable", "backup": {"dump": "x", "into": "y", "a-field-from-later": 1}}]}}`,
"an own secret": `{"module": "later", "version": "2", "own-secrets": {"s": {"path": "/x", "a-field-from-later": 1}}}`,
"a seat's verb": `{"module": "later", "version": "2", "seats": [{"name": "later-seat", "serves": [{"name": "v", "a-field-from-later": 1}]}]}`,
} {
m, err := catalogue.Decode([]byte(raw))
if err != nil {
t.Errorf("%s: the stored manifest was not read: %v", where, err)
continue
}
if m.Module != "later" || m.Version != "2" || !strings.Contains(m.SetAside(), `"a-field-from-later"`) || m.ReadNoFurther() {
t.Errorf("%s: read as %q %q, unknown %q", where, m.Module, m.Version, m.UnknownField())
continue
}
if where != "the top" && !strings.Contains(m.UnknownField(), ": json: unknown field") {
t.Errorf("%s: the block's decoder did not say it: %q", where, m.UnknownField())
}
if _, err := ParseManifest([]byte(raw)); err == nil || !strings.Contains(err.Error(), "a-field-from-later") {
t.Errorf("%s: the module check took it: %v", where, err)
}
}
var known catalogue.Manifest
if err := json.Unmarshal([]byte(`{"module": "now", "state": [{"name": "s"}]}`), &known); err != nil || known.UnknownField() != "" {
t.Fatalf("a known manifest: %v %q", err, known.UnknownField())
}
// A malformed manifest is still refused: only an unknown key is read past.
var bad catalogue.Manifest
if err := json.Unmarshal([]byte(`{"module": "bad", "state": [{"name": 3}]}`), &bad); err == nil {
t.Fatal("a malformed stored manifest was read")
}
}
// Registration refuses a stream the mesh could not hold or would leave ambiguous (ADR 0363 §1, §2, §4).
func TestAStreamIsNamedBoundedAndGrantedByATrustedConfigValue(t *testing.T) {
grants := `"grant":{"config":"grants","events":"mail.{key}.**"}`
for _, c := range []struct{ manifest, says string }{
{mailManifest(t, `[{"name":"Mail","events":["mail.**"]}]`, ""), `a stream is named locally`},
{mailManifest(t, `[{"name":"mail.x","events":["mail.**"]}]`, ""), `a stream is named locally`},
{mailManifest(t, `[{"name":"events","events":["mail.**"]}]`, ""), `the mesh's own stream EVENTS`},
{mailManifest(t, `[{"name":"log-x","events":["mail.**"]}]`, ""), `starts with LOG_`},
{mailManifest(t, `[{"name":"mail","events":["mail.**"]},{"name":"mail","events":["x.**"]}]`, ""), `twice`},
{mailManifest(t, `[{"name":"mail","events":[]}]`, ""), `with no events`},
{mailManifest(t, `[{"name":"mail","events":["mail.**.x"]}]`, ""), `nothing may follow it`},
{mailManifest(t, `[{"name":"mail","events":["mail.**"],"max-age-days":31}]`, ""), `between 1 and 30`},
{mailManifest(t, `[{"name":"mail","events":["mail.**"],"max-mib":9000}]`, ""), `between 1 and 8192`},
{mailManifest(t, `[{"name":"mail","events":["mail.**"],"stream":"MAIL"}]`, ""), `{name, events`},
{mailManifest(t, `[{"name":"mail","events":["mail.**"]}]`, `"emits":["mail.mesh.message.added"],`),
`which its stream "mail" carries`},
{mailManifest(t, `[{"name":"mail","events":["mail.**"]},{"name":"more","events":["mail.x.*"]}]`, ""),
`both carry`},
{mailManifest(t, `[{"name":"mail","events":["mail.**"],"grant":{"config":"tags","events":"mail.{key}.**"}}]`, ""),
`is not a trusted config value`},
{mailManifest(t, `[{"name":"mail","events":["mail.**"],"grant":{"config":"grants","events":"mail.**"}}]`, ""),
`exactly once`},
{mailManifest(t, `[{"name":"mail","events":["mail.**"],"grant":{"config":"grants","events":"{key}.{key}"}}]`, ""),
`exactly once`},
{mailManifest(t, `[{"name":"mail","events":["mail.**"],`+grants+`,"why":1}]`, ""), `{name, events`},
{`{"module":"a.b","version":"1","streams":[{"name":"x","events":["x"]}]}`, `no dot`},
} {
_, err := ParseManifest([]byte(c.manifest))
if err == nil {
t.Errorf("%s was accepted", c.manifest)
continue
}
if !strings.Contains(err.Error(), c.says) {
t.Errorf("%s refused for the wrong reason: %v", c.manifest, err)
}
}
}
// A system the mesh does not build for is refused where it is written. These are the host's own
// names, not a compiler's: the difference between two of them is a C library rather than a kernel,
// so a value that looks like an operating system to a toolchain is still wrong here.
func TestASystemTheMeshDoesNotBuildForIsRefused(t *testing.T) {
for _, wrong := range []string{"linux", "debian", "darwin"} {
got := buildProblemsOf(t, bundleFor("go", wrong))
if !strings.Contains(got, "and a system is") {
t.Fatalf("%q was accepted as a system:\n%s", wrong, got)
}
}
}
func TestATemplatedModuleWhoseBuildPacksNoFilesIsRefused(t *testing.T) {
for _, build := range []string{
``,
`"build":{"artifacts":[{"name":"look","kind":"archive","from":"theme"}]},`,
`"build":{"artifacts":[{"name":"a","kind":"archive","from":"files"},{"name":"b","kind":"archive","from":"./files/"}]},`,
} {
raw := strings.Replace(templatedTwin,
`"build":{"artifacts":[{"name":"files","kind":"archive","from":"files"}]},`, build, 1)
_, err := ParseManifest([]byte(raw))
if err == nil || !strings.Contains(err.Error(), "files/") {
t.Errorf("a templated module with the build %q was not refused: %v", build, err)
}
}
}
func TestATemplateOnAResourceThatIsNotAFileIsRefused(t *testing.T) {
raw := strings.Replace(templatedTwin, `{"id":"conf","type":"directory","path":"${dir:conf}"}`,
`{"id":"conf","type":"directory","path":"${dir:conf}","template":"files/tidy.conf"}`, 1)
_, err := ParseManifest([]byte(raw))
if err == nil || !strings.Contains(err.Error(), "conf is a directory and names a template") {
t.Fatalf("a directory naming a template was not refused: %v", err)
}
}
func TestATemplateOutsideFilesIsRefusedByName(t *testing.T) {
for _, bad := range []string{"../tidy.conf", "/etc/tidy.conf", "files", "files/", "files/../tidy.conf",
"files/./tidy.conf", "other/tidy.conf", "tidy.conf", "files//tidy.conf", `files\\tidy.conf`} {
raw := strings.Replace(templatedTwin, `"files/tidy.conf"`, `"`+bad+`"`, 1)
_, err := ParseManifest([]byte(raw))
if err == nil || !strings.Contains(err.Error(), "main") || !strings.Contains(err.Error(), "not a file under files/") {
t.Errorf("the template %q was not refused by name: %v", bad, err)
}
}
}
func TestATemplateOutsideTheNamedTemplatesDirectoryIsRefused(t *testing.T) {
raw := strings.Replace(unpackingTemplated, `"template":"templates/tidy.conf"`, `"template":"files/tidy.conf"`, 1)
if _, err := ParseManifest([]byte(raw)); err == nil || !strings.Contains(err.Error(), "files/tidy.conf") ||
!strings.Contains(err.Error(), "templates/") {
t.Fatalf("a template outside the named templates directory was not refused by name: %v", err)
}
}
func TestATemplatesDirectoryNoArchiveIsBuiltFromIsRefused(t *testing.T) {
raw := strings.Replace(unpackingTemplated, `,
{"name":"templates","kind":"archive","from":"templates"}`, "", 1)
if _, err := ParseManifest([]byte(raw)); err == nil || !strings.Contains(err.Error(), "no archive from templates/") {
t.Fatalf("a module whose build packs no archive from its templates directory was not refused: %v", err)
}
}
func TestATemplatesDirectoryOnAModuleNamingNoTemplateIsRefused(t *testing.T) {
raw := strings.Replace(unpackingInline, `"version":"1",`, `"version":"1","templates":"templates",`, 1)
if _, err := ParseManifest([]byte(raw)); err == nil || !strings.Contains(err.Error(), "names no template") {
t.Fatalf("a templates directory read by nothing was not refused: %v", err)
}
}
func TestATemplatesDirectoryThatIsNotAPlainRelativeDirectoryIsRefused(t *testing.T) {
for _, dir := range []string{"/etc", "../other", "..", "templates/../files", "./templates", ".", "a\\b", "templates/"} {
raw := strings.Replace(unpackingTemplated, `"templates":"templates"`, `"templates":`+jsonString(dir), 1)
if _, err := ParseManifest([]byte(raw)); err == nil || !strings.Contains(err.Error(), "templates directory") {
t.Errorf("the templates directory %q was not refused: %v", dir, err)
}
}
}
// novox/hq ADR 0259 §8: a module saying warrants, or speaking for a kind that proves its sender, runs as an
// account of its own — never carried by the machine's runtime, which runs as the operator's account.
func TestATrustedHolderMustRunAsAnAccountOfItsOwn(t *testing.T) {
r := router()
r.RunsAs = ""
if got := problemsFor(t, catalogue.Shelf{"messenger": r}); !strings.Contains(got, "messenger must run as an account of its own") {
t.Errorf("a router on the machine's runtime stood: %s", got)
}
tg := aChannel("telegram", "telegram")
tg.Claims[0].Capabilities = []string{"choice", "verified-sender"}
if got := problemsFor(t, catalogue.Shelf{"messenger": router(), "telegram": tg}); !strings.Contains(got, "telegram must run as an account of its own") {
t.Errorf("a verified channel on the machine's runtime stood: %s", got)
}
desk := aChannel("desk-channel", "desktop")
desk.Claims[0].Capabilities = []string{"choice"}
if got := problemsFor(t, catalogue.Shelf{"messenger": router(), "desk-channel": desk}); got != "" {
t.Errorf("a channel proving nothing was held to it: %s", got)
}
// A kind that is `private` shows a link's code, which links an account as the operator: its holder is
// trusted with it, so it runs as its own account too (the confirmation review of 2026-10-09).
private := aChannel("desk-channel", "desktop")
private.Claims[0].Capabilities = []string{"choice", "private"}
if got := problemsFor(t, catalogue.Shelf{"messenger": router(), "desk-channel": private}); !strings.Contains(got, "desk-channel must run as an account of its own") {
t.Errorf("a private channel on the machine's runtime stood: %s", got)
}
}
// A misspelt key inside a verb's definition is refused, like one anywhere else in the manifest.
func TestAVerbWithAnUnknownKeyIsRefused(t *testing.T) {
_, err := ParseManifest([]byte(`{"module":"till","version":"1",` +
`"seats":[{"name":"shop-till","serves":[{"name":"price","descripton":"typo"}]}]}`))
if err == nil || !strings.Contains(err.Error(), "descripton") {
t.Fatalf("a verb with a misspelt key was accepted: %v", err)
}
}
// A family is named one by one, never by a wildcard, and only by the tool runner's module (ADR 0367 §2, §6):
// each refusal is said in the manifest's words, at parse, rather than at the composition of the bus's users.
func TestAWaitIsRefusedUnlessItNamesOneFamilyOfTheToolRunner(t *testing.T) {
for manifest, want := range map[string]string{
`{"module":"shop","version":"1","waits":["mesh-knowledge.reviewed"]}`: "only node-tools",
`{"module":"node-tools","version":"1","waits":["mesh-knowledge.*"]}`: "never a wildcard",
`{"module":"node-tools","version":"1","waits":["**"]}`: "never a wildcard",
`{"module":"node-tools","version":"1","waits":["mesh-knowledge.>"]}`: "never a wildcard",
`{"module":"node-tools","version":"1","waits":["*.reviewed"]}`: "never a wildcard",
`{"module":"node-tools","version":"1","waits":["reviewed"]}`: "does not name an emitter and an event",
`{"module":"node-tools","version":"1","waits":["mesh-knowledge..reviewed"]}`: "does not name an emitter and an event",
`{"module":"node-tools","version":"1","waits":["Mesh-Knowledge.reviewed x"]}`: "does not name an emitter and an event",
`{"module":"node-tools","version":"1","waits":["a.b","a.b"]}`: "twice",
} {
_, err := ParseManifest([]byte(manifest))
if err == nil {
t.Errorf("accepted: %s", manifest)
continue
}
if !strings.Contains(err.Error(), want) {
t.Errorf("%s refused for the wrong reason (want %q): %v", manifest, want, err)
}
}
}
// 1178/AC2: a work-queue stream is read by exactly one process of its module and by no other module's consumes: one
// process naming it is accepted; none, two, or another module's consumes is refused, naming it.
func TestAWorkQueueStreamIsReadByExactlyOneProcessOfItsModule(t *testing.T) {
if got := WorkQueueStreamProblems(mailSenderManifestWithQueue("mail-sends")); len(got) != 0 {
t.Fatalf("a work-queue stream one process names was refused: %v", got)
}
if got := strings.Join(WorkQueueStreamProblems(mailSenderManifestWithQueue("")), "; "); !strings.Contains(got, "mail-sends") ||
!strings.Contains(got, "no process names it") {
t.Errorf("an unnamed work-queue stream: %q", got)
}
two := mailSenderManifestWithQueue("mail-sends")
two.Resources = append(two.Resources, map[string]any{"id": "resend", "type": "process",
"run": []any{"./mesh-mail", "send"}, "user": "mesh-mail-send",
"bus": map[string]any{"emits": []any{"mail.send.sent"}, "queues": []any{"mail-sends"}}})
if got := strings.Join(WorkQueueStreamProblems(two), "; "); !strings.Contains(got, "mail-sends") || !strings.Contains(got, "2 processes name") {
t.Errorf("a work-queue stream two processes name: %q", got)
}
reader := catalogue.Manifest{Module: "reader", Version: "1", Consumes: []string{"mesh-mail.mail.send.approved"}}
got := strings.Join(WorkQueueConsumedElsewhereProblems(catalogue.Shelf{"mesh-mail": mailSenderManifestWithQueue("mail-sends"), "reader": reader}), "; ")
if !strings.Contains(got, "reader") || !strings.Contains(got, "MAIL_SENDS") {
t.Errorf("another module's consumes of a work-queue stream: %q", got)
}
}
// 1177/AC2, ADR 0592 §6: a stream whose work-queue or history values are out of range, contradictory, or carries a grant
// where it may not is refused naming the stream and the rule.
func TestAWorkQueueStreamsValuesAreBounded(t *testing.T) {
for name, c := range map[string]struct{ streams, says string }{
"max-deliveries 0": {`[{"name":"mail-sends","events":["mail.send.approved"],"retention":"work-queue",` +
`"max-deliveries":0,"duplicates":24}]`, "between 1 and 10"},
"max-deliveries 11": {`[{"name":"mail-sends","events":["mail.send.approved"],"retention":"work-queue",` +
`"max-deliveries":11,"duplicates":24}]`, "between 1 and 10"},
"duplicates longer than its maximum age": {`[{"name":"mail-sends","events":["mail.send.approved"],` +
`"max-age-days":1,"retention":"work-queue","max-deliveries":5,"duplicates":25}]`, "longer than its maximum age"},
"an unknown retention": {`[{"name":"mail-sends","events":["mail.send.approved"],"retention":"sometimes"}]`,
"retention"},
"max-deliveries without retention work-queue": {`[{"name":"mail-sends","events":["mail.send.approved"],` +
`"max-deliveries":5}]`, "without retention"},
"duplicates without retention work-queue": {`[{"name":"mail-sends","events":["mail.send.approved"],` +
`"duplicates":24}]`, "without retention"},
"a work-queue stream carrying a grant": {`[{"name":"mail-sends","events":["mail.send.approved"],` +
`"retention":"work-queue","max-deliveries":5,` +
`"grant":{"config":"grants","events":"mail-sends.{key}.**"}}]`, "carrying a grant"},
// ADR 0592 §6: a stream that keeps its history has no maximum age, deliveries or duplicate window.
"a history stream with a maximum age": {`[{"name":"mail-sends","events":["mail.send.approved"],` +
`"retention":"history","max-age-days":30}]`, "no maximum age"},
"a history stream with max-deliveries": {`[{"name":"mail-sends","events":["mail.send.approved"],` +
`"retention":"history","max-deliveries":3}]`, "without retention"},
"a history stream with duplicates": {`[{"name":"mail-sends","events":["mail.send.approved"],` +
`"retention":"history","duplicates":2}]`, "without retention"},
"a history stream over its bound": {`[{"name":"mail-sends","events":["mail.send.approved"],` +
`"retention":"history","max-mib":8193}]`, "between"},
} {
_, err := ParseManifest([]byte(mailManifest(t, c.streams, "")))
if err == nil || !strings.Contains(err.Error(), "mail-sends") || !strings.Contains(err.Error(), c.says) {
t.Errorf("%s: %v; want a refusal naming the stream and %q", name, err, c.says)
}
}
}
// A zone names the listen that answers it, or there is nothing to forward to (novox/hq ADR 0199).
func TestAZoneMustNameOneOfTheModulesListens(t *testing.T) {
_, err := ParseManifest([]byte(`{"module":"lab","version":"1",
"listens":[{"name":"dns","port":5353,"protocol":"udp","from":"mesh","why":"the lab's names"}],
"zone":{"name":"${setting:zone}","listen":"web"}}`))
if err == nil || !strings.Contains(err.Error(), `has no listen of that name`) {
t.Fatalf("a zone answered by a listen the module does not have was accepted: %v", err)
}
if _, err := ParseManifest([]byte(`{"module":"lab","version":"1",
"listens":[{"name":"dns","port":5353,"protocol":"udp","from":"mesh","why":"the lab's names"}],
"zone":{"name":"${setting:zone}","listen":"dns"}}`)); err != nil {
t.Fatalf("a well-formed zone was refused: %v", err)
}
}
func TestBindingWhatYouDoNotRequireIsRefused(t *testing.T) {
_, err := ParseManifest([]byte(`{"module":"app","version":"1",
"binds":{"postgres-database":"/etc/app/db.json"}}`))
if err == nil {
t.Fatal("a module was told about something it never asked for")
}
if !strings.Contains(err.Error(), "does not require") {
t.Fatalf("unhelpful refusal: %v", err)
}
}
// A channel's capabilities come from the fixed vocabulary, and only a kinded claim carries any.
func TestCapabilitiesAreTheVocabularysAndOnlyOnAKindedClaim(t *testing.T) {
good := aChannel("telegram", "telegram")
good.Claims[0].Capabilities = []string{"deliver", "choice", "verified-sender", "max-length:4096"}
good.RunsAs = "telegram"
if got := problemsFor(t, catalogue.Shelf{"messenger": router(), "telegram": good}); got != "" {
t.Fatalf("the vocabulary was refused: %s", got)
}
bad := aChannel("telegram", "telegram")
bad.Claims[0].Capabilities = []string{"trusted", "max-length:lots"}
got := problemsFor(t, catalogue.Shelf{"messenger": router(), "telegram": bad})
for _, w := range []string{`"trusted"`, `"max-length:lots"`} {
if !strings.Contains(got, w+", which channel-capabilities/1 does not have") {
t.Errorf("%s was not refused: %s", w, got)
}
}
odd := catalogue.Manifest{Module: "odd", Claims: []catalogue.Claim{{Name: "operator-channel", Scope: catalogue.ScopeMesh, Capabilities: []string{"deliver"}}}}
if got := problemsFor(t, catalogue.Shelf{"messenger": router(), "odd": odd}); !strings.Contains(got, "only a kinded bench's claim carries them") {
t.Errorf("capabilities on a seat that is not kinded stood: %s", got)
}
}
func TestCodeForAPowerMomentDependsOnThePowerSeat(t *testing.T) {
for _, moment := range powerMoments {
m := mod("laptop", nil, nil, nil)
m.Shell = []catalogue.ShellCode{{For: moment, Slot: "normal", Code: "true"}}
if got := catalogue.DependsOn(nil, m); !reflect.DeepEqual(got, []string{catalogue.PowerSeat}) {
t.Errorf("code for %s depends on %v, want %s", moment, got, catalogue.PowerSeat)
}
if p := manifestShellProblems(m); len(p) != 0 {
t.Errorf("code for %s is refused: %v", moment, p)
}
}
m := mod("laptop", nil, nil, nil)
m.Shell = []catalogue.ShellCode{{For: "after-lunch", Slot: "normal", Code: "true"}}
if p := manifestShellProblems(m); len(p) == 0 {
t.Error("code for a moment that does not exist was accepted")
}
if s, ok := catalogue.SeatNamed(catalogue.PowerSeat); !ok || s.Scope != catalogue.ScopeNode {
t.Errorf("%s is not a node seat of the mesh's own: %+v %v", catalogue.PowerSeat, s, ok)
}
}
func TestCollectionsAndItemsAreJudged(t *testing.T) {
for name, c := range map[string]struct{ raw, says string }{
"an item of a module not moved": {`{"module":"m","version":"1",
"items":[{"collection":"shell:zsh:normal","template":"files/x"}],
"build":{"artifacts":[{"name":"files","kind":"archive","from":"files"}]}}`, "is marked moved"},
"two items to one collection": {`{"module":"m","version":"1","moved":true,
"items":[{"collection":"a","template":"files/x"},{"collection":"a","template":"files/y"}],
"build":{"artifacts":[{"name":"files","kind":"archive","from":"files"}]}}`, "contributes to a twice"},
"an item to no slot": {`{"module":"m","version":"1","moved":true,
"items":[{"collection":"shell:zsh:middle","template":"files/x"}],
"build":{"artifacts":[{"name":"files","kind":"archive","from":"files"}]}}`, "is no slot"},
"an item outside the templates": {`{"module":"m","version":"1","moved":true,
"items":[{"collection":"a","template":"x"}],
"build":{"artifacts":[{"name":"files","kind":"archive","from":"files"}]}}`, "is not a file under files/"},
"an item naming a secret": {`{"module":"m","version":"1",
"items":[{"collection":"a","content":"t=${secret:token}"}]}`, "where no secret is"},
"a collection not declared": {`{"module":"m","version":"1",
"resources":[{"id":"f","type":"file","path":"/f","content":"${collected:a}"}]}`, "does not collect"},
"an old namespace declared": {`{"module":"m","version":"1","collections":[{"name":"shell:zsh:normal"}],
"resources":[{"id":"f","type":"file","path":"/f","content":"x"}]}`, "read from its placeholder"},
"a collection placed nowhere": {`{"module":"m","version":"1","collections":[{"name":"a"}],
"resources":[{"id":"f","type":"file","path":"/f","content":"x"}]}`, "no file of its places it"},
"a collection in a path": {`{"module":"m","version":"1","collections":[{"name":"a"}],
"resources":[{"id":"f","type":"file","path":"/${collected:a}","content":"${collected:a}"}]}`,
"only in a file's content"},
} {
_, err := ParseManifest([]byte(c.raw))
if err == nil || !strings.Contains(err.Error(), c.says) {
t.Errorf("%s: %v, want it to say %q", name, err, c.says)
}
}
if _, err := ParseManifest([]byte(collectorZsh)); err != nil {
t.Errorf("a collector: %v", err)
}
}
// Rule 3: a default failing its validation.
func TestConfigRefusesADefaultFailingItsValidation(t *testing.T) {
for _, c := range []struct {
key string
value any
said string
}{
{"font-size", 4, "below its minimum"},
{"font-size", 41, "above its maximum"},
{"font-size", "10", "not a number"},
{"position", "bottom", "not one of"},
{"theme", "Dark", "does not match"},
} {
m := inConfigForm()
declaration(m, c.key)["default"] = c.value
refused(t, m, `"`+c.key+`"`, c.said)
}
}
// Rule 10: a moved module's mergeable file not naming its keys under takes-config.
func TestConfigRefusesAMergeableFileNotNamingItsKeys(t *testing.T) {
m := inConfigForm()
delete(resourceOf(m, "rules"), "takes-config")
// position and theme are then placed nowhere; say they are read at runtime so only the file's rule speaks.
declaration(m, "position")["runtime"] = true
declaration(m, "theme")["runtime"] = true
refused(t, m, "rules", "names no takes-config")
}
// Rule 11: a mesh word, puid or pgid in takes-config.
func TestConfigRefusesAMeshWordOrAnAccountFactInTakesConfig(t *testing.T) {
for _, word := range []string{"ports", "expose", "reach", "endpoints", "places", "accesses", "networks", "puid", "pgid"} {
m := inConfigForm()
resourceOf(m, "rules")["takes-config"] = []any{"position", "theme", word}
refused(t, m, `"`+word+`"`, "takes-config")
}
}
// Rule 4: an object or a list not trusted.
func TestConfigRefusesAnObjectOrListNotTrusted(t *testing.T) {
for _, kind := range []string{"object", "list"} {
m := inConfigForm()
def := any(map[string]any{"a": 1})
if kind == "list" {
def = []any{"a"}
}
m["config"].(map[string]any)["rules"] = map[string]any{"type": kind, "description": "the rules, as " + kind,
"default": def, "runtime": true}
refused(t, m, `"rules"`, "is "+kind+" and not trusted")
m["config"].(map[string]any)["rules"].(map[string]any)["trusted"] = true
if _, err := parseRaw(t, m); err != nil {
t.Fatalf("a trusted %s is refused: %v", kind, err)
}
}
}
// Rule 6: an untrusted value falling back to a trusted predefined key.
func TestConfigRefusesAnUntrustedValueFallingBackToATrustedKey(t *testing.T) {
withPredefined(t, nil)
m := inConfigForm()
declaration(m, "theme")["predefined"] = "theme"
refused(t, m, `"theme"`, "is not present")
withPredefined(t, map[string]catalogue.PredefinedKey{"theme": {Type: "string", Trusted: true}})
refused(t, m, `"theme"`, "trusted", "is not")
declaration(m, "theme")["trusted"] = true
if _, err := parseRaw(t, m); err != nil {
t.Fatalf("a trusted value falling back to a trusted key is refused: %v", err)
}
}
// Rule 8: a placeholder naming an undeclared value.
func TestConfigRefusesAPlaceholderNamingAnUndeclaredValue(t *testing.T) {
m := inConfigForm()
resourceOf(m, "configuration")["content"] = "font = ${config:font-size}\nwidth = ${config:width}\n" +
"from = ${config:sender}\ncolour = ${config:colour}\n"
refused(t, m, "${config:colour}", "declares no such value")
}
// Rule 5: a predefined key not on the list, or of another type. Where no list is present, a value naming one is
// refused saying so.
func TestConfigRefusesAPredefinedKeyNotOnTheListOrOfAnotherType(t *testing.T) {
withPredefined(t, nil)
m := inConfigForm()
declaration(m, "font-size")["predefined"] = "font-size"
refused(t, m, `"font-size"`, "the list of predefined keys", "is not present")
withPredefined(t, map[string]catalogue.PredefinedKey{"font-size": {Type: "number"}, "accent": {Type: "string"}})
if _, err := parseRaw(t, m); err != nil {
t.Fatalf("a value falling back to a listed key of its type is refused: %v", err)
}
m = inConfigForm()
declaration(m, "font-size")["predefined"] = "font-scale"
refused(t, m, `"font-size"`, `"font-scale"`, "not on the list")
m = inConfigForm()
declaration(m, "font-size")["predefined"] = "accent"
refused(t, m, `"font-size"`, `"accent"`, "is a string")
}
// Rule 9: a reload-on or restart-on naming a file the module does not declare.
func TestConfigRefusesAReloadOrRestartOnNamingAnUndeclaredFile(t *testing.T) {
m := inConfigForm()
resourceOf(m, "notifier")["reload-on"] = []any{"configuration", "dunstrc"}
refused(t, m, "reload-on", `"dunstrc"`, "declares no such")
m = inConfigForm()
resourceOf(m, "notifier")["restart-on"] = []any{"rule"}
refused(t, m, "restart-on", `"rule"`)
// Another module's file, and one the mesh makes for the module, are not the module's to declare.
m = inConfigForm()
resourceOf(m, "notifier")["restart-on"] = []any{"rules", "resolver.fact-zones", "secret-mail", "composed-jails"}
if _, err := parseRaw(t, m); err != nil {
t.Fatalf("refused: %v", err)
}
}
// A key a mergeable file takes is a config value like every other: one not declared under config is refused by
// name (ADR 0348 §4, review of mesh-controller#320).
func TestConfigRefusesATakesConfigKeyNotDeclared(t *testing.T) {
m := inConfigForm()
resourceOf(m, "rules")["takes-config"] = []any{"position", "theme", "urgency"}
refused(t, m, `"urgency"`, "takes-config", "declares no such value under config")
}
// Rule 7: a value neither placed in a template nor read at runtime.
func TestConfigRefusesAValueNeitherPlacedNorReadAtRuntime(t *testing.T) {
m := inConfigForm()
delete(declaration(m, "history"), "runtime")
refused(t, m, `"history"`, "${config:history}", `"runtime": true`)
// Taken by a mergeable file is placed.
m = inConfigForm()
if _, err := parseRaw(t, m); err != nil {
t.Fatal(err)
}
}
// Rule 2: a value with neither a default nor required.
func TestConfigRefusesAValueWithNeitherDefaultNorRequired(t *testing.T) {
m := inConfigForm()
delete(declaration(m, "width"), "default")
refused(t, m, `"width"`, "neither a default nor required")
m = inConfigForm()
declaration(m, "sender")["default"] = "noreply@example.org"
refused(t, m, `"sender"`, "both a default and required")
}
// Rule 1: a value without type or description.
func TestConfigRefusesAValueWithoutTypeOrDescription(t *testing.T) {
m := inConfigForm()
delete(declaration(m, "width"), "type")
refused(t, m, `notifier's config value "width"`, "no type")
m = inConfigForm()
declaration(m, "width")["type"] = "colour"
refused(t, m, `"width"`, `type "colour"`)
m = inConfigForm()
delete(declaration(m, "width"), "description")
refused(t, m, `"width"`, "no description")
}
// 1658/AC2: a declared value placed nowhere, with an item that does not say it, is still refused.
func TestConfigStillRefusesAValueNoItemPlaces(t *testing.T) {
m := map[string]any{
"module": "m", "version": "1",
"config": map[string]any{
"k": map[string]any{"type": "string", "description": "a word nothing says", "default": "x"},
},
"items": []any{map[string]any{"collection": "a", "content": "k = fixed\n"}},
}
refused(t, m, `"k"`, "placed nowhere")
}
// Consumer data said to live in a provision the module requires is protected only as well as that
// provision's provider protects its consumers' data.
func TestConsumerDataInARequiredProvisionIsJudgedAgainstItsProvider(t *testing.T) {
idp := mustParse(t, `{"module":"idp","version":"1","requires":["postgres-database"],
"provides":[{"name":"oidc-client","scope":"mesh"}],"grants":{"oidc-client":"${dir:g}"},
"resources":[{"id":"g","type":"directory","mode":"0700"}],
"data":{"consumers":{"oidc-client":{"class":"irreplaceable","in":"postgres-database"}}}}`)
cachy := mustParse(t, strings.Replace(declaredStore, `"consumers":{"postgres-database":{"class":"irreplaceable","in":"store"}}`,
`"consumers":{"postgres-database":{"class":"cache"}}`, 1))
if p := strings.Join(DataProblems(catalogue.Shelf{"idp": idp, "pg": cachy}), "\n"); !strings.Contains(p, "not protected as irreplaceable") {
t.Fatalf("irreplaceable data kept in a cache passed: %s", p)
}
if p := DataProblems(catalogue.Shelf{"idp": idp, "pg": mustParse(t, declaredStore)}); len(p) > 0 {
t.Fatalf("refused against a provider that keeps its consumers' data: %v", p)
}
}
// The settings form keeps its own declaration; the config form's has no kind or why.
func TestEachFormKeepsItsOwnDeclaration(t *testing.T) {
m := inConfigForm()
declaration(m, "width")["why"] = "fits a title"
refused(t, m, `"width"`, "why")
err := parsed(t, catalogue.Manifest{Module: "notifier",
Settings: map[string]catalogue.SettingDeclaration{"width": {Kind: catalogue.KindPreference, Default: float64(250),
Why: "fits a title", Type: "number"}},
Resources: []map[string]any{{"id": "c", "type": "file", "path": "/x", "content": "${setting:width}"}}})
if err == nil || !strings.Contains(err.Error(), "under config") {
t.Fatalf("a typed value under settings: %v", err)
}
}
// TestEveryCatalogueIdentityFitsWhatItRequires is ADR 0225's check over the real catalogue: every
// module's identity, on the longest machine name, fits the bound of every provision it wants. An
// overflow fails here, in the pull request that introduces it, rather than on the provider's machine
// the first time a real machine's name meets the module's (issue 263).
func TestEveryCatalogueIdentityFitsWhatItRequires(t *testing.T) {
_, found := catalogueManifests(t)
shelf := catalogue.Shelf{}
for _, r := range found {
m, err := r.manifest, r.err
if err != nil {
t.Fatalf("%s: %v", r.path, err)
}
shelf[m.Module] = m
}
for _, p := range IdentityProblems(shelf, catalogue.DefaultLongestMachine) {
t.Error(p)
}
// The night it was found: the resolver provision bounds nothing, and the object store still 20.
if dns, ok := shelf["dnsmasq"]; ok {
if b := dns.IdentityBoundOf("wildcard-resolution"); b.Bounded() {
t.Errorf("the resolver provision bounds its consumers' identities: %+v", b)
}
}
if store, ok := shelf["minio"]; ok {
if b := store.IdentityBoundOf("s3-bucket"); b.Max != 20 {
t.Errorf("the object store's access key is not bounded at 20: %+v", b)
}
}
}
// TestEveryCatalogueModuleDeclaresItsData is ADR 0233's check over the real catalogue (it replaced ADR
// 0214's store list): every provider that grants says what it keeps for its consumers, nothing writes a
// backup line by hand, every directory a container writes is declared, and every irreplaceable item is
// backed up — so a store added is a store backed up, without a list of stores to keep in step.
func TestEveryCatalogueModuleDeclaresItsData(t *testing.T) {
_, found := catalogueManifests(t)
shelf := catalogue.Shelf{}
granting := 0
for _, r := range found {
m, err := r.manifest, r.err
if err != nil {
continue // TestEveryCatalogueManifestParses says why
}
if len(m.Grants) > 0 {
granting++
}
shelf[m.Module] = m
}
for _, problem := range DataProblems(shelf) {
t.Error(problem)
}
if granting == 0 {
t.Fatal("no module in the catalogue grants a provision, so this proved nothing")
}
}
// **Every module's event names are what design 29 says, across the whole catalogue.**
//
// The rule above holds by construction and turned out to be weaker than it reads: a manifest holds
// no subject, and every manifest in the catalogue still held the old bus's routing key, which
// derives into a namespace nobody owns (novox/hq 04-ISSUES/127). Nothing failed — the services
// started and none of them reacted. This is the check that was missing.
func TestEveryManifestsEventNamesAreLocal(t *testing.T) {
manifests := theCatalogue(t)
var problems []string
for _, m := range manifests {
problems = append(problems, EventProblems(m)...)
}
if len(problems) > 0 {
t.Fatalf("the catalogue holds %d event name(s) the mesh would derive wrongly:\n %s",
len(problems), strings.Join(problems, "\n "))
}
}
// **Across the whole catalogue**: every log is named locally and capped within the mesh's caps.
func TestEveryManifestsLogsAreLocalAndCapped(t *testing.T) {
var problems []string
for _, m := range theCatalogue(t) {
problems = append(problems, LogProblems(m)...)
}
if len(problems) > 0 {
t.Fatalf("the catalogue's logs are not what ADR 0297 says:\n %s", strings.Join(problems, "\n "))
}
}
// **Across the whole catalogue**: every state name is local and every read names a module's state.
func TestEveryManifestsStateIsLocalAndEveryReadIsKept(t *testing.T) {
manifests := theCatalogue(t)
var problems []string
for _, m := range manifests {
problems = append(problems, StateProblems(m)...)
}
if len(problems) > 0 {
t.Fatalf("the catalogue's state is not what ADR 0201 says:\n %s", strings.Join(problems, "\n "))
}
}
func TestEveryOutOfBoundsHealthIsRefusedByName(t *testing.T) {
cases := []struct {
name string
on string
health map[string]any
says string
}{
{"no kind", "server", map[string]any{"endpoint": "web"}, "with no kind"},
{"an unknown kind", "server", map[string]any{"kind": "ping"}, `of kind "ping"`},
{"a port", "server", map[string]any{"kind": "tcp", "port": 8080}, "names a port"},
{"an address", "server", map[string]any{"kind": "http", "endpoint": "web", "address": "127.0.0.1"}, "names a address"},
{"a url", "server", map[string]any{"kind": "http", "url": "http://localhost:8080/"}, "names a url"},
{"an unknown key", "server", map[string]any{"kind": "tcp", "endpoint": "web", "retries": 3}, `"retries"`},
{"no endpoint", "server", map[string]any{"kind": "http"}, "names no endpoint"},
{"an undeclared endpoint", "server", map[string]any{"kind": "tcp", "endpoint": "admin"}, "does not declare"},
{"a udp endpoint", "server", map[string]any{"kind": "tcp", "endpoint": "beacon"}, "a check connects over tcp"},
{"an interval under the floor", "server", map[string]any{"kind": "tcp", "endpoint": "web", "interval": "5s", "timeout": "1s"}, "no more often than every 10s"},
{"a timeout of the interval", "server", map[string]any{"kind": "tcp", "endpoint": "web", "interval": "10s", "timeout": "10s"}, "under its interval"},
{"one failing look", "server", map[string]any{"kind": "tcp", "endpoint": "web", "looks": 1}, "at least 2"},
{"a grace and looks past five minutes", "server", map[string]any{"kind": "tcp", "endpoint": "web", "grace": "4m", "looks": 3, "interval": "30s"}, "at most 5m0s"},
{"a duration that is not one", "server", map[string]any{"kind": "tcp", "endpoint": "web", "interval": "often"}, "is a duration"},
{"looks that are not a number", "server", map[string]any{"kind": "tcp", "endpoint": "web", "looks": "three"}, "whole number"},
{"a path without a slash", "server", map[string]any{"kind": "http", "endpoint": "web", "path": "health"}, "starts with /"},
{"a status that is not one", "server", map[string]any{"kind": "http", "endpoint": "web", "status": 700}, "is not one"},
{"a scheme that is not one", "server", map[string]any{"kind": "http", "endpoint": "web", "scheme": "ftp"}, "http or https"},
{"a status on a tcp check", "server", map[string]any{"kind": "tcp", "endpoint": "web", "status": 200}, "only an http check has"},
{"an exec with no command", "server", map[string]any{"kind": "exec"}, "says no command"},
{"a command on an http check", "server", map[string]any{"kind": "http", "endpoint": "web", "command": "true"}, "only an exec check runs"},
{"a runtime check on a service", "daemon", map[string]any{"kind": "runtime"}, "only a container has"},
{"a unit check on a container", "server", map[string]any{"kind": "unit"}, "a service's or a process's"},
{"a tool the module does not serve", "server", map[string]any{"kind": "tool", "tool": "app_admin"}, "does not serve"},
{"a tool check alone", "server", map[string]any{"kind": "tool", "tool": "app_status"}, "judges itself only by its own tool"},
{"needs not required", "server", map[string]any{"kind": "tcp", "endpoint": "web", "needs": "redis"}, "does not require"},
{"health on a step", "seed", map[string]any{"kind": "runtime"}, "does not stay up"},
{"health on a schedule", "sweep", map[string]any{"kind": "runtime"}, "does not stay up"},
{"health that is not an object", "server", nil, "is an object"},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
raw := healthManifest(t, c.on, c.health)
if c.health == nil {
raw = []byte(strings.Replace(string(raw), `"name":"app-server"`, `"name":"app-server","health":"tcp"`, 1))
}
_, err := ParseManifest(raw)
if err == nil {
t.Fatalf("%s was accepted", c.name)
}
if !strings.Contains(err.Error(), c.says) {
t.Errorf("refused, but not for saying %q: %v", c.says, err)
}
})
}
}
// **A module that hears a restricted stream runs as an account of its own** (ADR 0376 §2): registration refuses a
// `consumes` naming a module and asking for events its restricted stream carries, unless the manifest declares
// `runs-as`; a stream without a grant, an event the provider says on EVENTS, and a wildcard emitter stay as they
// were, and a consumer whose provider is not on the shelf is judged once it is.
func TestHearingARestrictedStreamNeedsAnAccountOfItsOwn(t *testing.T) {
provider, err := ParseManifest([]byte(mailManifest(t, mailStream, `"emits":["account.added"],`)))
if err != nil {
t.Fatal(err)
}
feed := catalogue.Manifest{Module: "feed", Streams: []catalogue.StreamDeclaration{{Name: "items", Events: []string{"item.**"}}}}
carried := catalogue.Manifest{Module: "reader", Consumes: []string{"mesh-mail.mail.*.message.added"}}
got := RestrictedHearingProblems(catalogue.Shelf{"mesh-mail": provider, "reader": carried})
if len(got) != 1 || !strings.Contains(got[0], "reader consumes mesh-mail.mail.*.message.added") ||
!strings.Contains(got[0], "MAIL") || !strings.Contains(got[0], "runs-as") || !strings.Contains(got[0], "ADR 0376") {
t.Fatalf("a carried module hearing MAIL was not refused as it should be: %v", got)
}
if all := strings.Join(CatalogueProblems(catalogue.Shelf{"mesh-mail": provider, "reader": carried}), "; "); !strings.Contains(all, got[0]) {
t.Fatalf("the catalogue's check does not refuse it: %s", all)
}
own := carried
own.RunsAs = "reader"
if got := RestrictedHearingProblems(catalogue.Shelf{"mesh-mail": provider, "reader": own}); len(got) != 0 {
t.Fatalf("a module of its own account was refused: %v", got)
}
for _, m := range []catalogue.Manifest{
{Module: "events", Consumes: []string{"mesh-mail.account.added"}},
{Module: "items", Consumes: []string{"feed.item.**"}},
{Module: "audit", Consumes: []string{"**", "*.mail.**"}},
} {
if got := RestrictedHearingProblems(catalogue.Shelf{"mesh-mail": provider, "feed": feed, m.Module: m}); len(got) != 0 {
t.Errorf("%s was refused: %v", m.Module, got)
}
}
if got := RestrictedHearingProblems(catalogue.Shelf{"reader": carried}); len(got) != 0 {
t.Fatalf("judged without its provider: %v", got)
}
}
// 1354/AC3: a contribution placed only where the machine reports the user namespaces capability (the name issue 1308 has
// the node-engine report) is accepted, and a misspelt name is still refused with today's words. The manifests are the
// shape of TestIfACapabilityNamesOneTheNodeEngineDetectsOnAnOfferedKindOnly (mesh-control 6c2a8d62
// internal/catalogue/seat_data_test.go:105), which stays in mesh-controller with the tests that read its other packages.
func TestIfACapabilityMayNameUserNamespaces(t *testing.T) {
const piece = `{"module":"power","contributions":[{"seat":"node-bar","kind":"block","if-capability":%q,"data":{"bar":"top","place":"status","shows":"battery"}}]}`
if _, err := ParseManifest([]byte(strings.Replace(piece, "%q", `"user-namespaces"`, 1))); err != nil {
t.Fatalf("if-capability user-namespaces was refused: %v", err)
}
for name, want := range map[string]string{
"user-namespace": "which no machine reports",
"userns": "which no machine reports",
"user_namespaces": "is not a capability's name",
} {
_, err := ParseManifest([]byte(strings.Replace(piece, "%q", `"`+name+`"`, 1)))
if err == nil || !strings.Contains(err.Error(), want) {
t.Errorf("%s: accepted, or refused without today's words %q: %v", name, want, err)
}
}
if !oneOf(catalogue.KnownCapabilities, "user-namespaces") {
t.Error("user-namespaces is not a known capability")
}
for _, name := range []string{"battery", "power-meter", "seat", "container-runtime"} {
if !oneOf(catalogue.KnownCapabilities, name) {
t.Errorf("%s is no longer a known capability", name)
}
}
}
func TestIfACapabilityNamesOneTheNodeEngineDetectsOnAnOfferedKindOnly(t *testing.T) {
cases := map[string]string{
// A name nothing detects would leave the piece out on every machine.
`{"seat":"node-bar","kind":"block","if-capability":"batery","data":{"bar":"top","place":"status","shows":"battery"}}`: "which no machine reports",
// A kind the holder depends on is never left out for want of something.
`{"seat":"node-hotkeys","kind":"trigger","if-capability":"battery","content":"KEY_BATTERY 1 x"}`: "only a kind the seat offers",
`{"seat":"node-backup","kind":"backup","if-capability":"seat","content":"path /x"}`: "only a kind the seat offers",
}
for c, want := range cases {
raw := `{"module":"power","contributions":[` + c + `]}`
if _, err := ParseManifest([]byte(raw)); err == nil || !strings.Contains(err.Error(), want) {
t.Errorf("%s: accepted, or refused without %q: %v", c, want, err)
}
}
for _, name := range []string{"battery", "power-meter", "seat"} {
if !oneOf(catalogue.KnownCapabilities, name) {
t.Errorf("%s is not a known capability", name)
}
}
}
// Data a consumer keeps with a provider as irreplaceable must be protected there: the check refuses a
// provider that keeps its consumers' data as a cache, or in an item with no protection.
func TestIrreplaceableDataKeptWithAProviderMustBeProtectedThere(t *testing.T) {
photos := mustParse(t, `{"module":"photos","version":"1","requires":["s3-bucket"],
"data":{"kept-by":{"s3-bucket":{"class":"irreplaceable"}}}}`)
store := func(backup string) catalogue.Manifest {
return mustParse(t, `{"module":"objects","version":"1","provides":[{"name":"s3-bucket","scope":"mesh"}],
"grants":{"s3-bucket":"${dir:g}"},
"data":{"own":[{"id":"data","path":"${dir:data}","class":"valuable","backup":"`+backup+`"}],
"consumers":{"s3-bucket":{"class":"valuable","in":"data"}}},
"resources":[{"id":"g","type":"directory","mode":"0700"},{"id":"data","type":"directory","mode":"0700"}]}`)
}
if p := DataProblems(catalogue.Shelf{"photos": photos, "objects": store("copy")}); len(p) > 0 {
t.Fatalf("a provider backing its consumers' data up was refused: %v", p)
}
if p := strings.Join(DataProblems(catalogue.Shelf{"photos": photos, "objects": store("none")}), "\n"); !strings.Contains(p, "neither backed up") {
t.Fatalf("irreplaceable photos in an unprotected store passed: %s", p)
}
}
func TestItemNamesAttributesAndCapabilitiesAreJudged(t *testing.T) {
item := func(extra string) string {
return `{"module":"p","moved":true,"build":{"artifacts":[{"name":"files","kind":"archive","from":"files"}]},` +
`"items":[` + extra + `]}`
}
for items, want := range map[string]string{
`{"collection":"c","name":"a","template":"files/a"},{"collection":"c","template":"files/b"}`: "has no name",
`{"collection":"c","name":"a","template":"files/a"},{"collection":"c","name":"a","template":"files/b"}`: "twice",
`{"collection":"c","name":"Big","template":"files/a"}`: "is not a name",
`{"collection":"c","attributes":{"order":"100"},"template":"files/a"}`: "0 to 99",
`{"collection":"c","attributes":{"bar":"a b"},"template":"files/a"}`: "is not a value",
`{"collection":"c","if-capability":"batery","template":"files/a"}`: "no machine reports",
`{"collection":"contribution:node-bar:block","attributes":{"bar":"left"},"template":"files/a"}`: "bar=left",
`{"collection":"contribution:node-bar:block","attributes":{"colour":"red"},"template":"files/a"}`: `"colour" is none of them`,
`{"collection":"c","name":"a","template":"files/a","colour":"red"}`: "colour",
} {
_, err := ParseManifest([]byte(item(items)))
if err == nil || !strings.Contains(err.Error(), want) {
t.Errorf("%s: %v, want %q", items, err, want)
}
}
ok := item(`{"collection":"contribution:node-bar:block","name":"a","attributes":{"bar":"top","place":"status","order":"5"},"if-capability":"battery","template":"files/a"}`)
if _, err := ParseManifest([]byte(ok)); err != nil {
t.Fatal(err)
}
}
func TestKeepsMustBeAnAbsolutePath(t *testing.T) {
_, err := ParseManifest([]byte(`{"module":"mesh-vault","version":"1","keeps":"root"}`))
if err == nil || !strings.Contains(err.Error(), "keeps") {
t.Fatalf("a relative keeps path was not refused: %v", err)
}
if _, err := ParseManifest([]byte(`{"module":"mesh-vault","version":"1","keeps":"/var/lib/mesh-vault/root"}`)); err != nil {
t.Fatalf("an absolute keeps path was refused: %v", err)
}
}
func TestMalformedShellCodeIsRefusedAtParse(t *testing.T) {
for _, c := range []struct{ shell, want string }{
{`[{"for":"tcsh","slot":"normal","code":"x"}]`, `tool's shell code 1 is for "tcsh"; the shells are zsh, bash, fish`},
{`[{"for":"zsh","slot":"middle","code":"x"}]`, `tool's shell code 1 goes in the slot "middle"; the slots are first, normal, last`},
{`[{"for":"zsh","slot":"last","code":"x"},{"for":"zsh","slot":"last","code":" \n"}]`, `tool's shell code 2 has no code`},
{`[{"for":"zsh","slot":"last","code":"x","order":1}]`, `unknown field "order"`},
} {
_, err := ParseManifest([]byte(`{"module":"tool","shell":` + c.shell + `}`))
if err == nil || !strings.Contains(err.Error(), c.want) {
t.Errorf("%s: want %q, got %v", c.shell, c.want, err)
}
}
// The code itself is never judged: a shell's own `${…}` is not the mesh's.
if _, err := ParseManifest([]byte(`{"module":"tool","shell":[{"for":"zsh","slot":"first",` +
`"code":"source \"${XDG_CACHE_HOME:-$HOME/.cache}/p10k-instant-prompt-${(%):-%n}.zsh\""}]}`)); err != nil {
t.Fatalf("shell code was judged as if it were the mesh's: %v", err)
}
}
// TestNoCatalogueManifestNamesAnInstallation is ADR 0112's check, run over the real catalogue: no
// definition names a domain or a public address the mesh acts on, and every value that must for now
// carries its reason (novox/hq ADR 0155, issue 134). The list it prints is the one that shrinks.
func TestNoCatalogueManifestNamesAnInstallation(t *testing.T) {
_, found := catalogueManifests(t)
var named []string
for _, r := range found {
m, err := r.manifest, r.err
if err != nil {
t.Errorf("%s: %v", r.path, err)
continue
}
named = append(named, InstallationProblems(m)...)
}
if len(named) > 0 {
t.Fatalf("%d value(s) name an installation:\n %s", len(named), strings.Join(named, "\n "))
}
}
// 1887/A9a: the catalogue defines no seat node-build-agent, and a manifest claiming it is refused naming its
// replacement; the build modules' seats accept the three kinds in their pull order and serve current, pause, resume.
func TestNodeBuildAgentIsRetiredForTheBuildSeats(t *testing.T) {
for _, gone := range []string{"node-build-agent", "mesh-build-machine"} {
if _, ok := catalogue.SeatNamed(gone); ok {
t.Errorf("the mesh still defines %s", gone)
}
}
_, err := ParseManifest([]byte(`{"module":"build-agent","version":"1","claims":[{"name":"node-build-agent","scope":"node"}]}`))
if err == nil || !strings.Contains(err.Error(), `"node-build-agent", which is retired`) || !strings.Contains(err.Error(), "node-build-go") {
t.Fatalf("the claim is not refused naming its replacement: %v", err)
}
// And over a catalogue whose store still holds no row for it, a use of it is refused the same way.
user := catalogue.Manifest{Module: "watcher", Uses: []string{"node-build-agent"}}
if joined := strings.Join(CatalogueProblems(catalogue.Shelf{user.Module: user}), "; "); !strings.Contains(joined, "which is retired: its work is node-build-go") {
t.Fatalf("a use is not refused naming the replacement: %s", joined)
}
for _, name := range []string{"node-build-go", "node-build-node", "node-build-image"} {
s, ok := catalogue.SeatNamed(name)
if !ok || s.Scope != catalogue.ScopeNode || !s.PulledByKind || strings.Join(s.Accepts, ",") != "delivery,check,recheck" {
t.Fatalf("%s = %+v", name, s)
}
if !s.Yields {
t.Errorf("%s does not yield (ADR 0553)", name)
}
var verbs []string
for _, v := range s.Serves {
verbs = append(verbs, v.Name)
}
if strings.Join(verbs, ",") != "current,pause,resume" {
t.Errorf("%s serves %v", name, verbs)
}
}
// The build seats are the seats that yield, and no other (ADR 0553 §1).
var yielding []string
for _, s := range catalogue.Seats() {
if s.Yields {
yielding = append(yielding, s.Name)
}
}
if strings.Join(yielding, ",") != "node-build-go,node-build-node,node-build-image" {
t.Errorf("the seats that yield are %v", yielding)
}
}
// §2: a module may claim one of them, and may not declare it as its own.
func TestNoModuleMayDeclareAGraphicalSessionSeat(t *testing.T) {
raw := `{"module":"xorg","seats":[{"name":"node-display-server","scope":"node"}]}`
if _, err := ParseManifest([]byte(raw)); err == nil || !strings.Contains(err.Error(), "mesh's own namespace") {
t.Fatalf("a module declared node-display-server as its own: %v", err)
}
}
// ADR 0204 §1: the login shell is the mesh's, so no module declares it — neither under the mesh's
// name nor under the name a module gave it before.
func TestNoModuleMayDeclareTheLoginShell(t *testing.T) {
for _, n := range []string{"login-shell", "node-login-shell", "node-environment"} {
raw := `{"module":"zsh","seats":[{"name":"` + n + `","scope":"node","serves":["execute"]}],"tools":["execute"]}`
_, err := ParseManifest([]byte(raw))
if err == nil {
t.Errorf("a module declaring %q was accepted", n)
}
}
got := strings.Join(declaredSeatProblems(catalogue.Manifest{Module: "zsh",
DefinesSeats: []catalogue.SeatDeclaration{{Name: "login-shell", Scope: catalogue.ScopeNode}}}), "; ")
if !strings.Contains(got, `zsh declares a seat named "login-shell"; the login shell is the mesh's own seat node-login-shell`) {
t.Fatalf("declaring login-shell was not refused by name: %q", got)
}
// And a shell module claiming the mesh's seat, serving execute, is what the seat is for.
m, err := ParseManifest([]byte(`{"module":"zsh","tools":["execute"],` +
`"claims":[{"name":"node-login-shell","scope":"node"}]}`))
if err != nil {
t.Fatal(err)
}
if err := catalogue.CanHold(m, catalogue.Seat{Name: catalogue.LoginShellSeat, Scope: catalogue.ScopeNode, Serves: loginShellVerbs()}); err != nil {
t.Fatalf("a shell module claiming the seat cannot hold it: %v", err)
}
}
// One component, one artifact per system: the shape the mesh's own binaries are declared in, and the
// reason the target is the artifact's rather than the recipe's.
func TestOneArtifactPerSystemIsAccepted(t *testing.T) {
m := catalogue.Manifest{Module: "the-host", Build: &catalogue.Build{Artifacts: []catalogue.Artifact{
{Name: "arch", Kind: catalogue.ArtifactBundle, Language: "go", System: "arch"},
{Name: "alpine", Kind: catalogue.ArtifactBundle, Language: "go", System: "alpine"},
{Name: "android", Kind: catalogue.ArtifactBundle, Language: "go", System: "android"},
}}}
if got := buildProblemsOf(t, m); got != "" {
t.Fatalf("one artifact per system was refused:\n%s", got)
}
}
func TestOneNamingFileMakesEveryMergeableFileName(t *testing.T) {
// ADR 0326 §2: a module with one naming and one silent mergeable file is refused, naming the
// silent one — the silent one would otherwise still take every key.
m := twoFiles([]any{"server"}, nil)
problems := TakenSettingsProblems(m)
if len(problems) != 1 || !strings.Contains(problems[0], "sidecar merges and names no takes-settings") {
t.Fatalf("problems: %v", problems)
}
raw, err := json.Marshal(map[string]any{"module": m.Module, "version": "1", "resources": m.Resources})
if err != nil {
t.Fatal(err)
}
if _, err := ParseManifest(raw); err == nil || !strings.Contains(err.Error(), "sidecar merges and names no") {
t.Errorf("the module check does not say it: %v", err)
}
}
// Only a claimant declares a seat's path: a module claiming no seat that names /etc/hosts and declaring
// it is refused at registration naming the seat; the uplink's holder and the resolver's both declaring
// the resolver file are each a claimant of a seat naming it. Reading a seat's path is access, and allowed.
func TestOnlyAClaimantDeclaresASeatsPath(t *testing.T) {
stranger := catalogue.Manifest{Module: "stranger", Version: "1", Resources: []map[string]any{
{"id": "hosts", "type": "file", "path": "/etc/hosts", "into": "block", "content": "10.0.0.1 x\n"}}}
got := strings.Join(CatalogueProblems(catalogue.Shelf{"stranger": stranger}), "; ")
if !strings.Contains(got, `stranger declares the path "/etc/hosts", which the seat node-hostname names`) {
t.Errorf("a module declaring /etc/hosts without claiming node-hostname was not refused: %q", got)
}
// However the path is spelled (novox/hq issue 412 review).
unclean := catalogue.Manifest{Module: "unclean", Version: "1", Resources: []map[string]any{
{"id": "hosts", "type": "file", "path": "/etc/../etc/hosts", "content": "10.0.0.1 x\n"}}}
if got := strings.Join(CatalogueProblems(catalogue.Shelf{"unclean": unclean}), "; "); !strings.Contains(got, "node-hostname") {
t.Errorf("/etc/../etc/hosts from a module not claiming node-hostname was not refused: %q", got)
}
reader := catalogue.Manifest{Module: "reader", Version: "1", Accesses: []catalogue.Access{{ID: "hosts", Path: "/etc/hosts"}}}
if got := CatalogueProblems(catalogue.Shelf{"reader": reader}); len(got) != 0 {
t.Errorf("reading /etc/hosts was refused: %v", got)
}
uplink := catalogue.Manifest{Module: "uplink", Version: "1", Claims: []catalogue.Claim{{Name: "node-uplink", Scope: catalogue.ScopeNode}},
Facts: map[string]catalogue.RosterFile{"resolvers": {Path: catalogue.ResolverFile, Template: "nameserver 10.42.0.1\n"}}}
resolver := localResolver()
resolver.Claims[0].Serves = []string{"routes", "route", "unroute"}
if got := CatalogueProblems(catalogue.Shelf{"uplink": uplink, resolver.Module: resolver}); len(got) != 0 {
t.Errorf("the uplink's holder and the resolver's declaring the resolver file were refused: %v", got)
}
}
// An image names the Go package its recipe compiles (novox/hq ADR 0267): within the tree it is built in,
// and only an image says one.
func TestOnlyAnImageNamesThePackageItCompilesWithinItsTree(t *testing.T) {
m, err := ParseManifest([]byte(`{"module":"a","version":"1","build":{"artifacts":[
{"name":"x","kind":"image","from":"Dockerfile","compiles":"cmd/x"}]}}`))
if err != nil || m.Build.Artifacts[0].Compiles != "cmd/x" {
t.Fatalf("an image naming its package was refused or lost it: %v", err)
}
for _, c := range []struct{ artifact, says string }{
{`{"name":"x","kind":"archive","from":"files","compiles":"cmd/x"}`, "Only an image"},
{`{"name":"x","kind":"image","from":"Dockerfile","compiles":"../x"}`, "leaves the tree"},
{`{"name":"x","kind":"image","from":"Dockerfile","compiles":"/x"}`, "leaves the tree"},
{`{"name":"x","kind":"image","from":"Dockerfile","compiles":"a/../../x"}`, "leaves the tree"},
} {
_, err := ParseManifest([]byte(`{"module":"a","version":"1","build":{"artifacts":[` + c.artifact + `]}}`))
if err == nil || !strings.Contains(err.Error(), c.says) {
t.Errorf("%s: %v, wanted a refusal saying %q", c.artifact, err, c.says)
}
}
}
// Only a node seat may carry needs (novox/hq ADR 0378 §8): a per-mesh seat's holder is not on any
// one node to depend on anything beside it there.
func TestOnlyANodeSeatMayCarryNeeds(t *testing.T) {
m := catalogue.Manifest{Module: "mail", DefinesSeats: []catalogue.SeatDeclaration{{
Name: "mail-notifier", Scope: catalogue.ScopeMesh, Needs: []string{"node-notifier"}}}}
got := strings.Join(declaredSeatProblems(m), "; ")
if !strings.Contains(got, "needing") || !strings.Contains(got, "node seat") {
t.Fatalf("a mesh-scope seat's needs were accepted: %q", got)
}
}
func TestOnlyChannelAndIntakeAreKinded(t *testing.T) {
m := catalogue.Manifest{Module: "x", DefinesSeats: []catalogue.SeatDeclaration{{Name: "pager", Kinded: true, Accepts: []string{"page"}}}}
if got := strings.Join(declaredSeatProblems(m), "; "); !strings.Contains(got, "only channel and intake are kinded") {
t.Fatalf("another kinded bench was declared: %s", got)
}
}
// The indexer and the builder hold no published list of predefined keys; a manifest whose config values fall back to
// one (claude-code, dunst, xterm at mesh-catalog 835bf22b) is still read for its build's and fingerprint's inputs
// (issue 1751).
func TestParseManifestForInputsReadsAValueFallingBackToAPredefinedKey(t *testing.T) { // issue 1751
was := catalogue.ReplacePublished(nil) // the indexer's and the builder's case: no published list
defer catalogue.ReplacePublished(was)
for _, module := range []string{"claude-code", "dunst", "xterm"} {
raw, err := os.ReadFile("testdata/catalogue-835bf22b-" + module + ".module.json")
if err != nil {
t.Fatal(err)
}
if _, err := ParseManifest(raw); err == nil {
t.Fatalf("%s: ParseManifest reads it with no list; the test proves nothing", module)
}
if m, err := ParseManifestForInputs(raw); err != nil || m.Module != module {
t.Errorf("%s: %v", module, err)
}
}
}
func TestPlaceIsValidatedAtTheManifest(t *testing.T) {
both := catalogue.Manifest{Module: "x", Resources: []map[string]any{
{"id": "d", "type": "directory", "place": ".", "path": "/somewhere"},
}}
if got := manifestUnknownDirRefs(both); len(got) != 1 || !strings.Contains(got[0], "both path and place") {
t.Fatalf("path beside place refuses; got %v", got)
}
elsewhere := catalogue.Manifest{Module: "x", Resources: []map[string]any{
{"id": "f", "type": "file", "place": ".", "path": "/somewhere", "content": ""},
}}
if got := manifestUnknownDirRefs(elsewhere); len(got) != 1 || !strings.Contains(got[0], "not a directory") {
t.Fatalf("place on a file refuses; got %v", got)
}
wrong := catalogue.Manifest{Module: "x", Resources: []map[string]any{
{"id": "d", "type": "directory", "place": "sub/dir"},
}}
if got := manifestUnknownDirRefs(wrong); len(got) != 1 || !strings.Contains(got[0], `the places are "."`) {
t.Fatalf("a place that is neither root refuses; got %v", got)
}
}
func TestReceivingWhatYouDoNotProvideIsRefused(t *testing.T) {
// It would create a file nobody ever writes to, on a machine where nothing asked for it.
_, err := ParseManifest([]byte(`{"module":"traefik","version":"1",
"receives":{"reverse-proxy":"/etc/traefik/mesh.json"}}`))
if err == nil {
t.Fatal("a module received contributions to something it does not provide")
}
if !strings.Contains(err.Error(), "does not provide") {
t.Fatalf("unhelpful refusal: %v", err)
}
}
// Replay of novox/hq issue 412 (2026-10-10): which module owns /etc/hosts was enforced only after the fact,
// when two modules on one machine declared it; a module that did not claim node-hostname could declare
// /etc/hosts, and a module could claim node-hostname declaring none of its paths, and nothing refused either.
// Written only with what the catalogue had before its fix (mesh-controller #267): CatalogueProblems, Shelf,
// SeatNamed and CanHold, so it is laid over the commit before.
func TestReplay412(t *testing.T) {
stranger := catalogue.Manifest{Module: "stranger", Version: "1", Resources: []map[string]any{
{"id": "hosts", "type": "file", "path": "/etc/hosts", "into": "block", "content": "10.0.0.1 x\n"}}}
got := strings.Join(CatalogueProblems(catalogue.Shelf{"stranger": stranger}), "; ")
if !strings.Contains(got, "node-hostname") {
t.Errorf("a module declaring /etc/hosts without claiming node-hostname was not refused: %q", got)
}
seat, ok := catalogue.SeatNamed("node-hostname")
if !ok {
t.Fatal("no node-hostname seat")
}
holder := catalogue.Manifest{Module: "names", Version: "1", Claims: []catalogue.Claim{{Name: "node-hostname", Scope: catalogue.ScopeNode,
Serves: []string{"entries", "add", "remove"}}}}
err := catalogue.CanHold(holder, seat)
if err == nil {
t.Fatal("a holder of node-hostname declaring neither /etc/hosts nor /etc/hostname was accepted")
}
for _, want := range []string{"/etc/hosts", "/etc/hostname"} {
if !strings.Contains(err.Error(), want) {
t.Errorf("the refusal does not name %s: %v", want, err)
}
}
}
func TestRunsAsIsAnAccountOfTheModulesOwn(t *testing.T) {
ok := catalogue.Manifest{Module: "telegram", RunsAs: "telegram", SecretsOwner: "telegram",
OwnSecrets: catalogue.OwnSecrets{"broker": {Path: "/var/lib/telegram/broker"}},
Resources: []map[string]any{{"id": "account", "type": "user", "name": "telegram"}}}
if got := RunsAsProblems(ok); len(got) != 0 {
t.Fatalf("a sound runs-as was refused: %v", got)
}
for want, change := range map[string]func(*catalogue.Manifest){
"never root": func(m *catalogue.Manifest) { m.RunsAs, m.SecretsOwner = "root", "root" },
"not an account name": func(m *catalogue.Manifest) { m.RunsAs = "${machine:account}" },
"which it does not make": func(m *catalogue.Manifest) { m.Resources = nil },
"declares no own secret": func(m *catalogue.Manifest) { m.OwnSecrets = nil },
"they are the account's own": func(m *catalogue.Manifest) { m.SecretsOwner = "" },
} {
m := ok
m.Resources = append([]map[string]any(nil), ok.Resources...)
m.OwnSecrets = catalogue.OwnSecrets{"broker": {Path: "/x"}}
change(&m)
if got := strings.Join(RunsAsProblems(m), "; "); !strings.Contains(got, want) {
t.Errorf("want %q, got %q", want, got)
}
}
}
func TestRuntimeHealthIsRefusedWithoutAnAccountOfItsOwn(t *testing.T) {
_, err := ParseManifest(ownAccountManifest(t, false, map[string]any{"kind": "tool", "tool": "keeper_health"}))
if err == nil || !strings.Contains(err.Error(), "runtime-health") || !strings.Contains(err.Error(), "runs-as") {
t.Errorf("runtime-health without runs-as: %v", err)
}
}
func TestServingWhatYouDoNotProvideIsRefused(t *testing.T) {
_, err := ParseManifest([]byte(`{"module":"app","version":"1",
"serves":{"postgres-database":{"port":5432}}}`))
if err == nil {
t.Fatal("a module served something it does not provide")
}
if !strings.Contains(err.Error(), "does not provide") {
t.Fatalf("unhelpful refusal: %v", err)
}
}
func TestTakesSettingsIsRefusedWhereItCannotMeanAnything(t *testing.T) {
// ADR 0326 §4.
cases := map[string]struct {
resource map[string]any
says string
}{
"on a file that does not merge": {
map[string]any{"id": "plain", "type": "file", "path": "/x", "content": "x",
catalogue.TakesSettingsField: []any{"a"}},
"does not merge"},
"not a list": {
map[string]any{"id": "conf", "type": "file", "path": "/x", "merge": catalogue.MergeJSON, "content": "{}",
catalogue.TakesSettingsField: "server"},
"is a list of setting keys"},
"a mesh word": {
map[string]any{"id": "conf", "type": "file", "path": "/x", "merge": catalogue.MergeJSON, "content": "{}",
catalogue.TakesSettingsField: []any{"endpoints"}},
"the mesh reads"},
"a protected key": {
map[string]any{"id": "conf", "type": "file", "path": "/x", "merge": catalogue.MergeJSON, "content": "{}",
"protected": []any{"secret"}, catalogue.TakesSettingsField: []any{"secret"}},
"keeps it as protected"},
"named twice": {
map[string]any{"id": "conf", "type": "file", "path": "/x", "merge": catalogue.MergeJSON, "content": "{}",
catalogue.TakesSettingsField: []any{"a", "a"}},
"twice"},
"not a key": {
map[string]any{"id": "conf", "type": "file", "path": "/x", "merge": catalogue.MergeJSON, "content": "{}",
catalogue.TakesSettingsField: []any{"two words"}},
"not a usable setting key"},
}
for name, c := range cases {
problems := TakenSettingsProblems(catalogue.Manifest{Module: "m", Resources: []map[string]any{c.resource}})
if !containsText(problems, c.says) {
t.Errorf("%s: want a problem saying %q, got %v", name, c.says, problems)
}
}
}
// The module that provides the artifact store may not be delivered through it.
//
// Building publishes to the store and the builder will not start without one, so a module that
// provides the store and also builds something asks the mesh to put an artifact into the thing
// that artifact is needed to create. On a mesh new enough to have no registry, that is a build
// that never returns (novox/hq 04-ISSUES/029).
func TestTheArtifactStoreCannotBeDeliveredThroughItself(t *testing.T) {
_, err := ParseManifest([]byte(`{"module":"registry","version":"1",` +
`"provides":[{"name":"artifact-store","scope":"mesh"}],` +
`"build":{"artifacts":[{"name":"registry","kind":"upstream","from":"registry:2"}]},` +
`"resources":[{"id":"store","type":"container","name":"mesh-registry",` +
`"artifact":"registry","ports":["5000:5000"]}]}`))
if err == nil {
t.Fatal("a registry module that builds its own image was accepted; the build has " +
"nowhere to publish until the module it belongs to is already running")
}
if !strings.Contains(err.Error(), "artifact-store") {
t.Fatalf("refused without naming the provision the cycle turns on: %v", err)
}
}
// The module holding mesh-broker is the bus, and its account is granted the bus's snapshot API and
// nothing else (novox/hq ADR 0235). Anything it declared to say or hear on the bus would be granted
// nothing, so it is refused at the parser rather than silently dropped.
func TestTheBussAccountSaysNothingOnTheBus(t *testing.T) {
raw := []byte(`{"module":"bus","version":"1","provides":[{"name":"mesh-bus","scope":"mesh"}],
"claims":[{"name":"mesh-broker","scope":"mesh"}],"own-secrets":{"broker":"/run/broker"},
"emits":["something.happened"]}`)
_, err := ParseManifest(raw)
if err == nil || !strings.Contains(err.Error(), "granted the bus's snapshot API and nothing else") {
t.Fatalf("a bus module declaring what it emits was not refused, or not for the reason: %v", err)
}
// Nor what it invokes, with no bundle for the runtime to ask on its behalf.
unbundledInvoke := []byte(`{"module":"bus","version":"1","provides":[{"name":"mesh-bus","scope":"mesh"}],
"claims":[{"name":"mesh-broker","scope":"mesh"}],"own-secrets":{"broker":"/run/broker"},
"invokes":["seat:mesh-hand-acts.hand-act"]}`)
if _, err := ParseManifest(unbundledInvoke); err == nil || !strings.Contains(err.Error(), "its invokes would be granted nothing") {
t.Fatalf("a bus module with no bundle declaring what it invokes was not refused, or not for the reason: %v", err)
}
// Its tools bundle asks what the module invokes (novox/hq ADR 0357; issue 774): the bundle asks through the
// node's runtime, which is granted every tool and seat verb (mesh-sdk go/buscompose), so a bus module that
// ships one may declare them. The shape of mesh-catalog's modules/nats/module.json at fc5b849d, cut to the
// parts the rule reads, was refused and held every bus user list from 2026-11-02 01:35.
nats := []byte(`{"module":"nats","version":"1","provides":[{"name":"mesh-bus","scope":"mesh","credential":{"form":"none"}}],
"claims":[{"name":"mesh-broker","scope":"mesh"}],
"own-secrets":{"broker":"/run/broker","broker-nats-tools":"${dir:tools-secret}/broker-nats-tools"},
"resources":[{"id":"tools-secret","type":"directory","path":"/var/lib/mesh/nats-tools","mode":"0711","unknown":"program"}],
"emits":["consumer-removed","bucket-keys-removed","stream-removed","bucket-expiry-set"],
"invokes":["seat:mesh-hand-acts.hand-act"],"consumes":[],
"tools":["nats_server","nats_bucket_expiry"],
"build":{"artifacts":[{"name":"tools","kind":"bundle","language":"go","system":"arch","from":"cmd/nats-tools",
"binary":"nats-tools","loads":["nats-tools"],"bus":{"removes":["controller-leftovers"]}}]}}`)
nm, err := ParseManifest(nats)
if err != nil {
t.Fatalf("the bus module whose tools bundle invokes a seat verb was refused: %v", err)
}
nbuilt, err := nm.Resolve([]catalogue.Built{{Name: "tools", Kind: catalogue.ArtifactBundle, Reference: "store/tools:1", Digest: "sha256:1"}})
if err != nil {
t.Fatal(err)
}
if raw, err = json.Marshal(nbuilt); err != nil {
t.Fatal(err)
}
if _, err := ParseManifestForInputs(raw); err != nil {
t.Fatalf("the built manifest of the bus module whose tools bundle invokes a seat verb was refused: %v", err)
}
// Its tools bundle says what it emits (novox/hq ADR 0357): the node's runtime carries the bundle and is
// granted the module's events, so a bus module that ships one may declare them.
bundled := []byte(`{"module":"bus","version":"1","provides":[{"name":"mesh-bus","scope":"mesh"}],
"claims":[{"name":"mesh-broker","scope":"mesh"}],"own-secrets":{"broker":"/run/broker"},
"emits":["bus.slow-consumer"],"tools":["bus_streams"],
"build":{"artifacts":[{"name":"tools","kind":"bundle","language":"go","system":"arch","from":"cmd/t","binary":"t"}]}}`)
m, err := ParseManifest(bundled)
if err != nil {
t.Fatalf("a bus module whose tools bundle declares what it emits was refused: %v", err)
}
// And so does its built manifest, its bundle in Bundles and its build read away (issue 774: nats's built manifest
// was refused, and with it every plan of the control-node).
built, err := m.Resolve([]catalogue.Built{{Name: "tools", Kind: catalogue.ArtifactBundle, Reference: "store/tools:1", Digest: "sha256:1"}})
if err != nil {
t.Fatal(err)
}
raw, err = json.Marshal(built)
if err != nil {
t.Fatal(err)
}
if _, err := ParseManifestForInputs(raw); err != nil {
t.Fatalf("the built manifest of a bus module whose tools bundle declares what it emits was refused: %v", err)
}
quiet := []byte(`{"module":"bus","version":"1","provides":[{"name":"mesh-bus","scope":"mesh"}],
"claims":[{"name":"mesh-broker","scope":"mesh"}],"own-secrets":{"broker":"/run/broker"},
"tools":["bus_streams"]}`)
m, err = ParseManifest(quiet)
if err != nil {
t.Fatalf("a bus module with an account and tools was refused: %v", err)
}
if !m.ClaimsSeat(catalogue.BrokerSeat) {
t.Fatal("it does not read as holding the broker seat")
}
}
// The catalogue check: a granting provider says what it keeps, says it in one place, writes no backup
// line by hand, and every directory a container writes is declared (novox/hq ADR 0233).
func TestTheCatalogueCheckAsksEveryModuleForItsData(t *testing.T) {
unsaid := mustParse(t, `{"module":"q","version":"1","provides":[{"name":"queue","scope":"mesh"}],
"grants":{"queue":"${dir:g}"},"resources":[{"id":"g","type":"directory","mode":"0700"}]}`)
onTheOffer := mustParse(t, `{"module":"r","version":"1","provides":[{"name":"resolver","scope":"mesh","keeps-consumer-data":false}]}`)
byHand := mustParse(t, `{"module":"h","version":"1","resources":[{"id":"d","type":"directory","mode":"0700"}],
"contributions":[{"seat":"node-backup","kind":"backup","content":"path ${dir:d}"}]}`)
writes := mustParse(t, `{"module":"w","version":"1","resources":[{"id":"d","type":"directory","mode":"0700"},
{"id":"c","type":"directory","mode":"0700"},
{"id":"app","type":"container","image":"x@sha256:aa","volumes":["${dir:d}:/data","${dir:c}:/etc/app:ro","${dir:d}/f:/f"]}]}`)
problems := strings.Join(DataProblems(catalogue.Shelf{"q": unsaid, "r": onTheOffer, "h": byHand, "w": writes}), "\n")
for _, want := range []string{
"q grants queue and does not say what it keeps",
"r says keeps-consumer-data on its offer",
"h's contribution 1 is a backup line written by hand",
`w mounts its directory "d" into a container to be written`,
} {
if !strings.Contains(problems, want) {
t.Errorf("the check does not say %q:\n%s", want, problems)
}
}
if strings.Contains(problems, `"c"`) {
t.Errorf("a read-only mount was taken for written data:\n%s", problems)
}
// The same module declaring the directory, as a cache, passes.
declared := mustParse(t, `{"module":"w","version":"1","data":{"own":[{"id":"d","path":"${dir:d}","class":"cache"}]},
"resources":[{"id":"d","type":"directory","mode":"0700"},
{"id":"app","type":"container","image":"x@sha256:aa","volumes":["${dir:d}:/data"]}]}`)
if p := DataProblems(catalogue.Shelf{"w": declared}); len(p) > 0 {
t.Fatalf("a declared directory is still refused: %v", p)
}
}
// Refused before merge: the catalogue check judges each module's identity, on the longest machine
// name, against the bound of every provision it wants — and names the module and the slug to set.
func TestTheCatalogueCheckRefusesAnIdentityThatOverflowsWhatItRequires(t *testing.T) {
shelf := catalogue.Shelf{
"objects": {Module: "objects", Provides: []catalogue.Offer{{Name: "s3-bucket", Scope: catalogue.ScopeMesh,
Identity: &catalogue.OfferIdentity{Max: 20, In: "an S3 access key"}}},
Receives: map[string]string{"s3-bucket": "/var/lib/mesh/objects/mesh.json"}},
"photoalbum": {Module: "photoalbum", Requires: []string{"s3-bucket"}},
"files": {Module: "files", Requires: []string{"s3-bucket"}},
}
problems := IdentityProblems(shelf, 6)
if len(problems) != 1 || !strings.Contains(problems[0], "photoalbum wants s3-bucket") ||
!strings.Contains(problems[0], `"mesh_nnnnnn_photoalbum", 22`) ||
!strings.Contains(problems[0], "`slug` of at most 8 characters") {
t.Fatalf("one overflow, named with its remedy, was expected: %q", problems)
}
// A longer machine name refuses more: the check is about the mesh's machines, not one.
if got := IdentityProblems(shelf, 10); len(got) != 2 {
t.Fatalf("on a 10-character name both overflow (mesh_nnnnnnnnnn_files is 21): %q", got)
}
// And a slug is the remedy it names.
album := shelf["photoalbum"]
album.Slug = "album"
shelf["photoalbum"] = album
if got := IdentityProblems(shelf, 6); len(got) != 0 {
t.Fatalf("a slug that fits is still refused: %q", got)
}
}
// The catalogue as it is passes: no holder of a seat naming a path is missing one, and nobody else
// declares one.
func TestTheCataloguePassesTheSeatPathRules(t *testing.T) {
shelf := catalogue.Shelf{}
for _, m := range theCatalogue(t) {
shelf[m.Module] = m
}
for _, p := range CatalogueProblems(shelf) {
if strings.Contains(p, "ADR 0287") {
t.Errorf("the catalogue breaks ADR 0287: %s", p)
}
}
}
// The catalogue pass no longer judges one module's reads against another's manifest (novox/hq ADR 0495 clauses 1 and 5,
// issue 1790): what an owner keeps is what it announces on the bus, and each module's own announcement test holds that.
func TestTheCataloguePassJudgesNoReadAgainstTheOwnersManifest(t *testing.T) {
shelf := catalogue.Shelf{
"licence-manager": {Module: "licence-manager", State: []catalogue.StateDeclaration{{Name: "bindings"}}},
"claude-code": {Module: "claude-code", Reads: []string{"licence-manager.tokens"}},
}
for _, p := range CatalogueProblems(shelf) {
if strings.Contains(p, "keeps no state") {
t.Fatalf("the catalogue pass judged a read against the owner's manifest: %s", p)
}
}
}
// A moved bar is rendered by the provisioner's client, not by the controller, so the rule that its template renders
// every example of the shape is no longer judged here (issue 915, which also covers placementProblems not reading the
// collected form). What the controller still owns is that the bar's files place the blocks at all: a moved bar's
// carry the collected placement, and one that stopped placing them fails here rather than being skipped (issue 432).
func TestTheCataloguesBarPlacesBlocksAndItsShapeCoversEveryShow(t *testing.T) {
holder := catalogueManifest(t, "i3status-rust")
s, _ := catalogue.SeatNamed(catalogue.BarSeat)
if holder.Moved {
placed := "${collected:contribution:" + catalogue.BarSeat + ":" + catalogue.BarKindBlock + ":"
found := false
for _, res := range holder.Resources {
if content, _ := res["content"].(string); strings.Contains(content, placed) {
found = true
}
}
if !found {
t.Fatalf("the catalogue's moved bar places no blocks: none of its files carries %s…}", placed)
}
} else {
if !placesKind(holder, s, catalogue.BarKindBlock) {
// It places them since the catalogue's bar took the seat; a skip here hid a bar that stopped
// (novox/hq issue 432).
t.Fatal("the catalogue's bar places no blocks, so none of the shape's examples would render")
}
tmpl, err := holderTemplate(holder, s, catalogue.BarKindBlock)
if err != nil {
t.Fatal(err)
}
for _, ex := range s.Receives[0].Shape.Examples {
out, err := renderPiece(tmpl, "example", "a-machine", ex)
if err != nil || !strings.Contains(out, "[[block]]") {
t.Errorf("the catalogue's bar renders %v as %q: %v", ex, out, err)
}
}
}
for shows := range barShows {
covered := false
for _, ex := range s.Receives[0].Shape.Examples {
covered = covered || ex["shows"] == shows
}
if !covered {
t.Errorf("the shape has no example that shows %s, so no holder is made to render it", shows)
}
}
}
// The compiled set keeps the rules: node seats only, absolute and literal, one seat per path without
// stepping back, and a stepping back that cannot cycle.
func TestTheCompiledSeatPathsKeepTheRules(t *testing.T) {
if p := SeatPathProblems(catalogue.DefaultSeats()); len(p) != 0 {
t.Fatalf("the compiled set breaks ADR 0287: %v", p)
}
for _, c := range []struct {
why string
set []catalogue.Seat
want string
}{
{"a mesh seat naming a path", []catalogue.Seat{{Name: "mesh-x", Scope: catalogue.ScopeMesh, Paths: []catalogue.SeatPath{{Path: "/etc/x"}}}},
"only a node seat names paths"},
{"a placeholder", []catalogue.Seat{{Name: "node-x", Scope: catalogue.ScopeNode, Paths: []catalogue.SeatPath{{Path: "/etc/${setting:x}"}}}},
"placeholder"},
{"a relative path", []catalogue.Seat{{Name: "node-x", Scope: catalogue.ScopeNode, Paths: []catalogue.SeatPath{{Path: "etc/x"}}}},
"not a clean absolute path"},
{"two owners", []catalogue.Seat{
{Name: "node-a", Scope: catalogue.ScopeNode, Paths: []catalogue.SeatPath{{Path: "/etc/x"}}},
{Name: "node-b", Scope: catalogue.ScopeNode, Paths: []catalogue.SeatPath{{Path: "/etc/x"}}}}, "no two seats name one path"},
{"a cycle", []catalogue.Seat{
{Name: "node-a", Scope: catalogue.ScopeNode, Paths: []catalogue.SeatPath{{Path: "/etc/x", StepsBackFor: "node-b"}}},
{Name: "node-b", Scope: catalogue.ScopeNode, Paths: []catalogue.SeatPath{{Path: "/etc/x", StepsBackFor: "node-a"}}}},
"does not own that path itself"},
{"stepping back for nobody", []catalogue.Seat{
{Name: "node-a", Scope: catalogue.ScopeNode, Paths: []catalogue.SeatPath{{Path: "/etc/x", StepsBackFor: "node-z"}}}},
"not a node seat"},
} {
if got := strings.Join(SeatPathProblems(c.set), "; "); !strings.Contains(got, c.want) {
t.Errorf("%s: %q does not say %q", c.why, got, c.want)
}
}
}
// Every rule of the section itself, refused at parse in the words of the module.
func TestTheDataSectionIsRefusedWhereItIsWrong(t *testing.T) {
for _, c := range []struct{ name, data, want string }{
{"a class it does not know", `{"own":[{"id":"a","path":"${dir:d}","class":"precious"}]}`, "not one the mesh protects by"},
{"an access it does not have", `{"own":[{"id":"a","path":"${access:media}","class":"irreplaceable","redundancy":"an array"}]}`, "declares no access"},
{"irreplaceable and protected by nothing", `{"own":[{"id":"a","path":"${dir:d}","class":"irreplaceable","backup":"none"}]}`, "protected one way or the other"},
{"a cache on redundancy", `{"own":[{"id":"a","path":"${dir:d}","class":"cache","redundancy":"an array"}]}`, "a cache is not protected"},
{"kept with something not required", `{"kept-by":{"s3-bucket":{"class":"irreplaceable"}}}`, "does not require"},
{"a measure it does not know", `{"own":[{"id":"a","path":"${dir:d}","class":"cache","measure":"du"}]}`, "walk, dataset or shallow"},
{"a cache expecting writes", `{"own":[{"id":"a","path":"${dir:d}","class":"cache","active":"1d"}]}`, "only irreplaceable and valuable"},
{"a machine path", `{"own":[{"id":"a","path":"/srv/a","class":"cache"}]}`, "names no machine path"},
{"a directory it does not declare", `{"own":[{"id":"a","path":"${dir:nowhere}","class":"cache"}]}`, "declares no directory"},
{"a cache backed up", `{"own":[{"id":"a","path":"${dir:d}","class":"cache","backup":"copy"}]}`, "disposable"},
{"a dump into nothing", `{"own":[{"id":"a","path":"${dir:d}","class":"irreplaceable","backup":{"dump":"x","into":"b"}}]}`, "not one of the module's data items"},
{"a dump into itself", `{"own":[{"id":"a","path":"${dir:d}","class":"irreplaceable","backup":{"dump":"x","into":"a"}}]}`, "into itself"},
{"a bad duration", `{"own":[{"id":"a","path":"${dir:d}","class":"irreplaceable","within":"soon"}]}`, "not a duration"},
{"the same id twice", `{"own":[{"id":"a","path":"${dir:d}","class":"cache"},{"id":"a","path":"${dir:d}/x","class":"cache"}]}`, "declared twice"},
{"own data of class none", `{"own":[{"id":"a","path":"${dir:d}","class":"none"}]}`, "only a provision"},
{"consumers of something not provided", `{"consumers":{"s3-bucket":{"class":"cache"}}}`, "does not provide"},
{"consumer data that lives nowhere", `{"consumers":{"q":{"class":"irreplaceable"}}}`, "says nowhere it lives"},
{"consumer data in an item less precious", `{"own":[{"id":"a","path":"${dir:d}","class":"rebuildable"}],"consumers":{"q":{"class":"irreplaceable","in":"a"}}}`, "which is only rebuildable"},
{"consumer data in something unknown", `{"consumers":{"q":{"class":"rebuildable","in":"elsewhere"}}}`, "neither one of its data items"},
} {
raw := `{"module":"m","version":"1","provides":["q"],"resources":[{"id":"d","type":"directory","mode":"0700"}],"data":` + c.data + `}`
_, err := ParseManifest([]byte(raw))
if err == nil || !strings.Contains(err.Error(), c.want) {
t.Errorf("%s: %v, want %q", c.name, err, c.want)
}
}
}
// A default is a value the mesh writes, so the installation check reads it, even under a setting
// called `description` or `why`; a setting's own why is prose and is not read.
func TestTheInstallationCheckReadsADefault(t *testing.T) {
m := notifier()
m.Settings["relay"] = catalogue.SettingDeclaration{Kind: catalogue.KindPreference, Default: "relay.acme.be", Why: "as at relay.acme.be"}
m.Settings["description"] = catalogue.SettingDeclaration{Kind: catalogue.KindPreference, Default: "notes.acme.be", Why: "x"}
problems := strings.Join(InstallationProblems(m), "; ")
for _, want := range []string{"relay.acme.be at settings.relay.default", "notes.acme.be at settings.description.default"} {
if !strings.Contains(problems, want) {
t.Errorf("not reported: %q in %s", want, problems)
}
}
if strings.Contains(problems, "settings.relay.why") {
t.Errorf("a why was read as a value: %s", problems)
}
}
func TestTheMachinesReachIsAProvisionsOnlyReachAndIsNodeScoped(t *testing.T) {
for _, c := range []struct{ provides, want string }{
{`{"name":"x11-display","reach":"internal"}`, `with reach "internal"; a provision's reach is "machine" or nothing`},
{`{"name":"x11-display","scope":"mesh","reach":"machine"}`, `at scope "mesh" with the machine's reach`},
} {
_, err := ParseManifest([]byte(`{"module":"xorg","provides":[` + c.provides + `]}`))
if err == nil || !strings.Contains(err.Error(), c.want) {
t.Errorf("%s: want %q, got %v", c.provides, c.want, err)
}
}
m, err := ParseManifest([]byte(`{"module":"xorg","provides":[{"name":"x11-display","reach":"machine"}]}`))
if err != nil {
t.Fatal(err)
}
if !m.Provides[0].MachineReach() {
t.Fatal("the reach was not read")
}
back, _ := json.Marshal(m.Provides[0])
if string(back) != `{"name":"x11-display","reach":"machine"}` {
t.Errorf("written back as %s", back)
}
}
func theMeshsDirectoryForAModuleIsAPlace(t *testing.T) {
// novox/hq issue 174. What the mesh writes for a module — its bus credential, its bindings —
// is the mesh's plumbing under <root>/mesh/<module>, and the definition names it by id.
m := catalogue.Manifest{Module: "umami",
Resources: []map[string]any{
{"id": "mesh-state", "type": "directory", "place": "mesh"},
{"id": "state", "type": "directory", "place": "."},
{"id": "server", "type": "container", "image": "x@sha256:aa",
"volumes": []any{"${dir:mesh-state}/broker:/run/secrets/broker:ro"}},
},
OwnSecrets: catalogue.OwnSecrets{"broker": {Path: "${dir:mesh-state}/broker"}},
Binds: map[string]string{"route": "${dir:state}/route.json"},
}
if got := manifestUnknownDirRefs(m); len(got) != 0 {
t.Fatalf("place %q is a place; got %v", "mesh", got)
}
dirs := dirsFor(m, catalogue.Rendering{})
if dirs["mesh-state"] != "/var/lib/mesh/umami" || dirs["state"] != "/var/lib/umami" {
t.Fatalf("the mesh's directory sits beside the module's, not in it; got %v", dirs)
}
dirs = dirsFor(m, catalogue.Rendering{DataRoot: "/srv"})
if dirs["mesh-state"] != "/srv/mesh/umami" {
t.Fatalf("a node's root moves the mesh's files with the module's; got %v", dirs)
}
}
// The mesh's own seats still work, and are not shadowed by the derived half.
func TestTheMeshsOwnSeatsAreStillClaimable(t *testing.T) {
// It delivers the bus, so its holder provides the bus — the rule this check now enforces.
m := catalogue.Manifest{Module: "nats",
Provides: []catalogue.Offer{{Name: "mesh-bus", Scope: catalogue.ScopeMesh, Credential: &catalogue.OfferCredential{Form: catalogue.CredentialFormNone}}},
Claims: []catalogue.Claim{{Name: "mesh-broker", Scope: catalogue.ScopeMesh}}}
if got := problemsFor(t, catalogue.Shelf{"nats": m}); got != "" {
t.Fatalf("a mesh seat was refused by the derived check: %s", got)
}
}
func TestTheModuleCheckRefusesAnOptionalPlaceholderThatCouldRenderEmpty(t *testing.T) {
refused := map[string]string{
"a placeholder without a default": `{"id":"ds","type":"file","path":"/a","mode":"0644",
"content":"url: ${bound:influxdb-api:at}"}`,
"a secret outside an only-when-bound file": `{"id":"s","type":"file","path":"/a","mode":"0400",
"content":"${secret:influxdb-api}"}`,
"only-when-bound on a hard requirement": `{"id":"r","type":"file","path":"/a","mode":"0644",
"content":"x","only-when-bound":"route"}`,
"only-when-bound on something not required": `{"id":"r","type":"file","path":"/a","mode":"0644",
"content":"x","only-when-bound":"mqtt-topic"}`,
"only-when-bound on a directory": `{"id":"d","type":"directory","mode":"0700",
"only-when-bound":"influxdb-api"}`,
"a default on a hard requirement": `{"id":"r","type":"file","path":"/a","mode":"0644",
"content":"${bound:route:name|default:x}"}`,
}
for what, resource := range refused {
if _, err := ParseManifest([]byte(optionalModule(resource))); err == nil {
t.Errorf("%s passed the module check", what)
} else if what == "a placeholder without a default" && !strings.Contains(err.Error(), "|default:") {
t.Errorf("the refusal does not say how to write it: %v", err)
}
}
accepted := map[string]string{
"a placeholder with a default": `{"id":"ds","type":"file","path":"/a","mode":"0644",
"content":"url: ${bound:influxdb-api:at|default:none}"}`,
"a placeholder and a secret inside an only-when-bound file": `{"id":"ds","type":"file","path":"/a",
"mode":"0400","content":"url: ${bound:influxdb-api:at}\npassword: ${secret:influxdb-api}",
"only-when-bound":"influxdb-api"}`,
}
for what, resource := range accepted {
if _, err := ParseManifest([]byte(optionalModule(resource))); err != nil {
t.Errorf("%s was refused: %v", what, err)
}
}
}
func TestTheNewRulesAreHeldToWhatTheSeatSays(t *testing.T) {
m := catalogue.Manifest{Module: "x", DefinesSeats: []catalogue.SeatDeclaration{{Name: "thing", Accepts: []string{"do"},
ByCaller: []string{"undo"}, Proofs: []string{"code"}, Records: []string{"nothing"}}}}
got := strings.Join(declaredSeatProblems(m), "; ")
for _, want := range []string{"names thing.undo by its caller, which the seat neither accepts nor emits",
"declares proofs on thing, which is not kinded", `read its records "nothing", which it keeps no state of`} {
if !strings.Contains(got, want) {
t.Errorf("not refused: %q in %s", want, got)
}
}
}
// **A build machine has no store, so it may not judge a seat.** The set is data the control plane
// owns (novox/hq ADR 0122), and `ParseManifest` runs on the build machine too, against whatever set
// that binary was compiled with. When the two disagreed, a valid holder of the bus seat was refused
// mid-rollout — the compiled row said the seat delivered one provision, the store's row said
// another, and the build failed on the copy rather than the truth. The parser judges the manifest;
// the seat set judges the claim, where it is loaded.
func TestTheParserDoesNotJudgeWhatOnlyTheStoreKnows(t *testing.T) {
was := catalogue.Seats()
t.Cleanup(func() { catalogue.UseSeats(was) })
// A store whose bus seat delivers something this module does provide.
catalogue.UseSeats([]catalogue.Seat{{Name: "mesh-broker", Scope: catalogue.ScopeMesh, Delivers: "mesh-bus", Decision: "test"}})
raw := []byte(`{"module":"a-bus","version":"1",` +
`"provides":[{"name":"mesh-bus","scope":"mesh","credential":{"form":"none"}}],` +
`"claims":[{"name":"mesh-broker","scope":"mesh"}]}`)
m, err := ParseManifest(raw)
if err != nil {
t.Fatalf("the parser refused a claim only the seat set can judge: %v", err)
}
if got := CatalogueProblems(catalogue.Shelf{m.Module: m}); len(got) != 0 {
t.Fatalf("a holder that provides what the store says the seat delivers was refused: %v", got)
}
// And with the store saying the seat delivers something else, registration is what refuses it.
catalogue.UseSeats([]catalogue.Seat{{Name: "mesh-broker", Scope: catalogue.ScopeMesh, Delivers: "other-bus", Decision: "test"}})
if _, err := ParseManifest(raw); err != nil {
t.Fatalf("the parser judged it the second time: %v", err)
}
got := strings.Join(CatalogueProblems(catalogue.Shelf{m.Module: m}), "; ")
if !strings.Contains(got, `does not provide "other-bus"`) {
t.Fatalf("registration did not refuse a holder that cannot answer for the seat: %q", got)
}
}
func TestTheParserTakesAPreferenceAndRefusesTheRest(t *testing.T) {
if err := parsed(t, notifier()); err != nil {
t.Fatalf("a preference with a default was refused: %v", err)
}
for _, c := range []struct {
name string
key string
d catalogue.SettingDeclaration
refuse string
}{
{"no kind", "font-size", catalogue.SettingDeclaration{Default: float64(10), Why: "x"}, `only a "preference" has a default`},
{"another kind", "font-size", catalogue.SettingDeclaration{Kind: "operator", Default: float64(10), Why: "x"}, `only a "preference"`},
{"no default", "font-size", catalogue.SettingDeclaration{Kind: catalogue.KindPreference, Why: "x"}, "no default"},
{"an empty default", "font-size", catalogue.SettingDeclaration{Kind: catalogue.KindPreference, Default: " ", Why: "x"}, "an empty default"},
{"an object", "font-size", catalogue.SettingDeclaration{Kind: catalogue.KindPreference, Default: map[string]any{"a": 1.0}, Why: "x"}, "a string, a number or a boolean"},
{"no why", "font-size", catalogue.SettingDeclaration{Kind: catalogue.KindPreference, Default: float64(10)}, "no why"},
{"the operator's", "mail-domain", catalogue.SettingDeclaration{Kind: catalogue.KindPreference, Default: "example.tld", Why: "x"}, "is the operator's value"},
{"a secret", "api-token", catalogue.SettingDeclaration{Kind: catalogue.KindPreference, Default: "x", Why: "x"}, "is the operator's value"},
{"the mesh's word", catalogue.PortsSetting, catalogue.SettingDeclaration{Kind: catalogue.KindPreference, Default: float64(1), Why: "x"}, "the mesh reads"},
{"read by nothing", "height", catalogue.SettingDeclaration{Kind: catalogue.KindPreference, Default: float64(300), Why: "x"}, "reaches nothing"},
} {
m := notifier()
m.Resources[0]["content"] = m.Resources[0]["content"].(string) + "x = ${setting:" + c.key + "}\n"
if c.name == "read by nothing" {
m.Resources[0]["content"] = "font = Inter ${setting:font-size}\nwidth = ${setting:width}\n"
}
m.Settings[c.key] = c.d
err := parsed(t, m)
if err == nil || !strings.Contains(err.Error(), c.refuse) {
t.Errorf("%s: parsed %v, want %q", c.name, err, c.refuse)
}
}
}
// A refusal that reorders itself between runs is a refusal nobody can diff.
func TestTheProblemsAreStable(t *testing.T) {
shelf := catalogue.Shelf{"telegram": telegram(), "shop": {Module: "shop", Uses: []string{"nope"}},
"other": {Module: "other", Uses: []string{"also-nope"}}}
first, second := problemsFor(t, shelf), problemsFor(t, shelf)
if first != second {
t.Fatalf("unstable:\n%s\n%s", first, second)
}
}
// ADR 0310 §3: the account that reads is in no group beside its own; one put in a group is refused.
func TestTheReadsAccountIsInNoGroup(t *testing.T) {
m := catalogue.Manifest{Module: "reads", RunsAs: "mesh-reads", Claims: []catalogue.Claim{{Name: catalogue.ReadsSeat}},
Resources: []map[string]any{{"id": "account", "type": "user", "name": "mesh-reads", "groups": []any{"docker"}}}}
if p := ReadsHolderProblems(m); len(p) != 1 || !strings.Contains(p[0], "docker") {
t.Errorf("an account in docker was not refused: %v", p)
}
m.Resources[0]["groups"] = []any{}
if p := ReadsHolderProblems(m); len(p) != 0 {
t.Errorf("an account in no group was refused: %v", p)
}
}
// The controller refuses a definition's resolved path by the node-engine's own rules, no more (novox/hq issue
// 496): the same lists, read here from mesh-host's internal/apply/placement_guard.go — in a merge check the clone
// beside it at the commit the mesh runs, elsewhere the copy captured in testdata/beside. When the engine's lists
// move, this fails until the controller's move with them, so the gate never refuses what the engine applies nor
// passes what it refuses by path alone.
func TestTheResolvedPathRulesAreTheNodeEnginesOwn(t *testing.T) {
file := filepath.Join(beside.Dir(t, "mesh-host"), "internal", "apply", "placement_guard.go")
parsed, err := parser.ParseFile(token.NewFileSet(), file, nil, 0)
if err != nil {
t.Fatalf("the node-engine's guard does not parse: %v", err)
}
lists := map[string][]string{}
consts := map[string]string{}
ast.Inspect(parsed, func(n ast.Node) bool {
spec, ok := n.(*ast.ValueSpec)
if !ok {
return true
}
for i, name := range spec.Names {
if i >= len(spec.Values) {
continue
}
switch v := spec.Values[i].(type) {
case *ast.CompositeLit:
for _, elt := range v.Elts {
if lit, ok := elt.(*ast.BasicLit); ok && lit.Kind == token.STRING {
s, _ := strconv.Unquote(lit.Value)
lists[name.Name] = append(lists[name.Name], s)
}
}
case *ast.BasicLit:
if v.Kind == token.STRING {
consts[name.Name], _ = strconv.Unquote(v.Value)
}
}
}
return true
})
for name, ours := range map[string][]string{
"protectedRoots": protectedRoots, "forbiddenBelow": forbiddenBelow, "engineTrees": engineTrees,
} {
theirs := lists[name]
if len(theirs) == 0 {
t.Errorf("the node-engine's guard names no %s any more; read it and say where its rule went", name)
continue
}
a, b := slices.Clone(ours), slices.Clone(theirs)
slices.Sort(a)
slices.Sort(b)
if !slices.Equal(a, b) {
t.Errorf("%s differs from the node-engine's:\n controller %v\n node-engine %v", name, a, b)
}
}
if consts["engineModule"] != engineModule {
t.Errorf("the node-engine's own module is %q there and %q here", consts["engineModule"], engineModule)
}
}
// Whom agents run as is the runtime's, told by the node-engine's composition (its own to test, ADR 0593 §7): a bundle
// may not say it.
func TestTheRuntimeIsToldTheAgentAccount(t *testing.T) {
if problems := bundleEnvProblems("x", catalogue.Artifact{Name: "b", Kind: catalogue.ArtifactBundle, Loads: []string{"x"},
Env: map[string]string{catalogue.RuntimeAgentAccount: "me"}}); len(problems) == 0 {
t.Error("a bundle may tell the runtime whom agents run as")
}
}
// The machine's facility — the container runtime's socket — is granted by the capability that names
// it, and declaring it as the module's own directory would be a lie the host would act on. This is
// the case the first version of this check refused, and was withdrawn for: the builder.
func TestTheRuntimeSocketIsGrantedByTheCapabilityAndNotOtherwise(t *testing.T) {
granted := `{"module":"builder","capabilities":["container-runtime"],"resources":[` +
strings.Replace(aContainerMounting, "%s", "/var/run/docker.sock", 1) + `]}`
if _, err := ParseManifest([]byte(granted)); err != nil {
t.Fatalf("a module with the container-runtime capability may mount its socket: %v", err)
}
if _, err := ParseManifest(manifestMounting("/var/run/docker.sock", "")); err == nil {
t.Fatal("a module mounted the container runtime's socket without declaring the capability, and was accepted")
}
}
// The set is closed, and changing it is a decision.
//
// **The count is asserted, and every entry names the record that made it a seat**, so the next
// person changing the set finds the argument rather than a number to edit — the pattern the host's
// vocabulary test follows. If this fails because a seat was added, the fix is a record in novox/hq
// and a row in to-be 26, not a new number here.
func TestTheSeatsAreAClosedSetAndEachNamesItsDecision(t *testing.T) {
// A seat a later record extends names both, "novox/hq ADR 0166, ADR 0207": the one that defined
// it and the one that seeded it.
record := regexp.MustCompile(`^novox/hq ADR \d{4}(, ADR \d{4})*$`)
seen := map[string]bool{}
delivered := map[string]string{}
for _, s := range catalogue.Seats() {
if seen[s.Name] {
t.Errorf("%s is in the set twice", s.Name)
}
seen[s.Name] = true
if !record.MatchString(s.Decision) {
t.Errorf("%s names %q as its decision; every seat names the record that made it one",
s.Name, s.Decision)
}
switch s.Scope {
case catalogue.ScopeNode, catalogue.ScopeSite, catalogue.ScopeMesh:
default:
t.Errorf("%s is held per %q, which is not a scope", s.Name, s.Scope)
}
if s.Delivers != "" {
// Two seats answering for one provision would put the question "which one?" back,
// which is the question a seat exists to answer.
if other, twice := delivered[s.Delivers]; twice {
t.Errorf("%s and %s both deliver %q", other, s.Name, s.Delivers)
}
delivered[s.Delivers] = s.Name
}
}
// Forty-seven with mesh-placement (novox/hq ADR 0567), serving its five verbs: adopt and hand-back went with the
// controller (novox/hq ADR 0590, issue 1126).
if s, ok := catalogue.SeatNamed(catalogue.PlacementSeat); !ok || s.Scope != catalogue.ScopeMesh || !reflect.DeepEqual(catalogue.VerbNames(s.Serves),
[]string{"placement", "proposals", "propose", "withdraw", "again"}) {
t.Errorf("the placement seat is %v, %v", ok, s)
}
// The checker's seat serves the try, which replaces the shell's merge check (ADR 0547 §10, ADR 0245).
var checkerVerbs []string
for _, s := range catalogue.Seats() {
if s.Name == catalogue.CheckerSeat {
for _, v := range s.Serves {
checkerVerbs = append(checkerVerbs, v.Name)
if v.Name == "try" && strings.Join(v.Replaces, "|") != "sh merge-check.sh|docker run postgres" {
t.Errorf("try replaces %v", v.Replaces)
}
}
}
}
if strings.Join(checkerVerbs, ",") != "try,try-read,try-end" {
t.Errorf("the checker seat serves %v", checkerVerbs)
}
// The four health seats (novox/hq ADR 0564 clause 1): at mesh scope, each held once, each naming ADR 0564, with the
// verbs and events the record names; the events keep the controller's names (clause 3).
for _, c := range []struct{ seat, verbs, emits string }{
{catalogue.ConditionsSeat, "conditions,silence,resync", "condition-raised,condition-changed,condition-cleared"},
{catalogue.WatchdogsSeat, "signals,stuck,presence,dead-letters,resync", ""},
{catalogue.SelfCheckSeat, "doctor,probes", "checked"},
{catalogue.HealersSeat, "healers,heal,drill", "healer-acted"},
// And mesh-config's config; its switch went with the controller (ADR 0590 clause 3).
{"mesh-config", "config", ""},
// And ADR 0565 clause 1's two.
{catalogue.WantsSeat, "upgrade,bus", "policy-changed"},
{catalogue.HandActsSeat, "hand-act,hand-acts,warranted", "act-recorded"},
// ADR 0571 clause 2: the artifact store's sweep.
{catalogue.ArtifactSweepSeat, "artifacts,held,collect,mirrors,images", "artifacts-gone"},
} {
s, ok := catalogue.SeatNamed(c.seat)
if !ok || s.Scope != catalogue.ScopeMesh || s.Replicated || s.Delivers != "" || !strings.Contains(s.Decision, "ADR 05") ||
strings.Join(catalogue.VerbNames(s.Serves), ",") != c.verbs || strings.Join(s.Emits, ",") != c.emits {
t.Errorf("the health seat %s is %v, %+v", c.seat, ok, s)
}
}
// The node seat node-engine is the node's engine's (novox/hq ADR 0563 §1): at node scope, delivering nothing, its verbs
// (compose-candidate since ADR 0593 §7) and three events, held by no assignment, and a module's claim of it refused.
if engine, ok := catalogue.SeatNamed(catalogue.EngineSeat); !ok || engine.Scope != catalogue.ScopeNode || engine.Delivers != "" || !engine.HeldByTheEngine {
t.Errorf("node-engine is %+v, not a node seat delivering nothing that the engine holds", engine)
} else {
var verbs []string
for _, v := range engine.Serves {
verbs = append(verbs, v.Name)
}
// acts reads one act by its id too (ADR 0563 §9: the seat's tools are its command line).
if props, _ := engine.Serves[5].Input["properties"].(map[string]any); props["id"] == nil {
t.Errorf("acts takes no id: %v", engine.Serves[5].Input)
}
if strings.Join(verbs, ",") != "hand-over,access-act,judge-directories,setuid-search,root-free,acts,packages,composition,compose,compose-candidate" ||
strings.Join(engine.Emits, ",") != "asked,done,refused" {
t.Errorf("node-engine serves %v and emits %v", verbs, engine.Emits)
}
}
if _, err := ParseManifest([]byte(`{"module":"thing","version":"1","claims":[{"name":"node-engine","scope":"node"}]}`)); err == nil ||
!strings.Contains(err.Error(), "node-engine") {
t.Errorf("a module's claim of node-engine was not refused: %v", err)
}
// Forty-six with the three build seats replacing node-build-agent and the retired mesh-build-machine (novox/hq
// ADR 0525 §2, §10); forty-five with node-files (novox/hq ADR 0394); forty-four again once node-reads goes.
// Forty-four with node-provisioner (novox/hq ADR 0349 clause 3); forty-three with mesh-provisioner (novox/hq ADR 0349 clause 3); forty-two with mesh-config (novox/hq ADR 0348 §9); forty-one with node-reads (novox/hq ADR 0310); forty with node-nfs-server and node-mounts (novox/hq ADR 0263); thirty-eight with node-resolver (novox/hq ADR 0247); thirty-seven with mesh-delivery (novox/hq ADR 0239); thirty-six since node-resolver-config retired
// into node-uplink (novox/hq ADR 0223); thirty-seven
// since the retired node-dns-resolver went (novox/hq ADR 0220); thirty-eight with
// node-backup (novox/hq ADR 0214); thirty-seven with node-message-bus (novox/hq ADR 0215);
// thirty-six with mesh-dns-resolver (novox/hq ADR 0194) and node-hosts-file (ADR 0199, now
// node-hostname); thirty-four
// with node-hotkeys (ADR 0212); thirty-three with node-power (ADR 0211); thirty-two since the
// graphical session's eleven (ADR 0208); twenty-one with node-package-manager and
// node-container-runtime (ADR 0207); nineteen with node-environment and node-login-shell (ADR 0203,
// ADR 0204); seventeen with node-build-agent (ADR 0190). One fewer once the retired
// mesh-build-machine row goes, when no registered manifest claims it.
// Forty-eight with mesh-delivery-checker (novox/hq ADR 0547 §10, issue 774); forty-nine with node-engine (novox/hq ADR 0563 §1);
// fifty-three with the four health seats (novox/hq ADR 0564 clause 1); fifty-five with mesh-wants and mesh-hand-acts
// (novox/hq ADR 0565 clause 1); fifty-six with mesh-artifact-sweep (novox/hq ADR 0571 clause 2);
// fifty-seven with mesh-grants (ADR 0582 clause 1); fifty-eight with mesh-enrolment (novox/hq ADR 0587 §1); fifty-six
// without the controller's seat and mesh-grants, which served its lease write alone (novox/hq ADR 0590, issue 1126).
if len(catalogue.Seats()) != 56 {
t.Errorf("the mesh defines %d seats rather than 56; the set is closed, so a change here is "+
"a decision (novox/hq ADR 0110): %s", len(catalogue.Seats()), seatNames())
}
}
// §4: xinitrc and xresources slots, placed by the display server's holder alone: the judge takes a session
// contribution and refuses its placeholder outside the holder (the files the holder's composition places are the
// node-engine's to test, ADR 0593 §7).
func TestTheSessionsFilesArePlacedByTheDisplayServersHolderAlone(t *testing.T) {
// The contributions parse; the placeholders parse only in the holder.
if _, err := ParseManifest([]byte(`{"module":"i3","shell":[{"for":"xinitrc","slot":"last","code":"exec i3"},` +
`{"for":"xresources","slot":"normal","code":"i3.font: x"}]}`)); err != nil {
t.Fatalf("a session contribution was refused: %v", err)
}
for _, c := range []struct{ claims, content, want string }{
{``, "${shell:xinitrc:normal}", "does not claim node-display-server; every module's xinitrc is placed by the display server's holder alone"},
{`{"name":"node-login-shell"}`, "${shell:xresources:normal}", "does not claim node-display-server"},
{`{"name":"node-display-server"}`, "${shell:zsh:normal}", "does not claim node-login-shell"},
{`{"name":"node-display-server"}`, "${shell:xsession:normal}", "the session's file one of xinitrc, xresources"},
} {
raw := `{"module":"holder","claims":[` + c.claims + `],"resources":[{"id":"rc","type":"file","path":"/etc/rc","content":"` +
c.content + `"}]}`
if _, err := ParseManifest([]byte(raw)); err == nil || !strings.Contains(err.Error(), c.want) {
t.Errorf("%s with claims [%s]: want %q, got %v", c.content, c.claims, c.want, err)
}
}
if _, err := ParseManifest([]byte(`{"module":"xorg","claims":[{"name":"node-display-server"}],` +
`"resources":[{"id":"rc","type":"file","path":"/home/op/.xinitrc","content":"${shell:xinitrc:last}"}]}`)); err != nil {
t.Errorf("the display server's holder could not place the session's slots: %v", err)
}
}
// The stored form reads on a controller older than optional requirements (review of mesh-controller
// #281). Inventory.Catalogue stops at the first manifest it cannot read, so a long-form entry inside
// `requires` would have stopped every plan after a rollback. Stored, `requires` is plain names, as main's
// manifest type reads it, and which are optional sits beside it in a key such a controller sets aside.
func TestTheStoredFormReadsWhereRequiresIsAListOfNames(t *testing.T) {
m := mustParse(t, `{"module":"grafana","version":"1",
"requires":["route",{"name":"influxdb-api","optional":true}]}`)
raw, err := json.Marshal(m)
if err != nil {
t.Fatal(err)
}
// main's field, exactly: `Requires []string`.
var older struct {
Requires []string `json:"requires"`
}
if err := json.Unmarshal(raw, &older); err != nil {
t.Fatalf("a controller reading requires as names cannot read the stored form: %v\n%s", err, raw)
}
if strings.Join(older.Requires, " ") != "route influxdb-api" {
t.Fatalf("it reads the requirements as %v", older.Requires)
}
var keys map[string]json.RawMessage
if err := json.Unmarshal(raw, &keys); err != nil {
t.Fatal(err)
}
if string(keys[catalogue.OptionalRequiresKey]) != `["influxdb-api"]` {
t.Fatalf("which are optional is stored as %s", keys[catalogue.OptionalRequiresKey])
}
// And this controller reads the stored form back the same, leniently and strictly.
var back catalogue.Manifest
if err := json.Unmarshal(raw, &back); err != nil {
t.Fatal(err)
}
strict, err := ParseManifest(raw)
if err != nil {
t.Fatal(err)
}
for _, got := range []catalogue.Manifest{back, strict} {
if !got.IsOptional("influxdb-api") || got.IsOptional("route") || len(got.Requires) != 2 {
t.Fatalf("read back as %v optional %v", got.Requires, got.OptionalRequires)
}
}
// A stored list naming something the module does not require is refused.
if _, err := ParseManifest([]byte(`{"module":"x","version":"1","requires":["route"],"optional-requires":["mqtt-topic"]}`)); err == nil {
t.Error("an optional requirement the module does not require passed the check")
}
}
// novox/hq ADR 0117: a machine's uplink is a seat, held per machine, and delivers nothing.
//
// **Nothing, because nothing may be required of it.** A holder only keeps its network manager from
// contradicting the mesh; a requirement resolving to it would make the manager the mesh's answer
// for something, and the manager's link is the one thing the mesh must never be able to break.
func TestTheUplinkIsANodeSeatThatDeliversNothing(t *testing.T) {
seat, known := catalogue.SeatNamed("node-uplink")
if !known {
t.Fatalf("the uplink is not a seat; the seats are: %s", seatNames())
}
if seat.Scope != catalogue.ScopeNode || seat.Delivers != "" || seat.Decision != "novox/hq ADR 0117" {
t.Fatalf("the uplink is %+v, not a node seat delivering nothing by ADR 0117", seat)
}
// And a manager's module can hold it without providing anything.
raw := []byte(`{"module":"networkmanager","version":"1","claims":[{"name":"node-uplink","scope":"node"}]}`)
if _, err := ParseManifest(raw); err != nil {
t.Fatalf("a network manager's module could not hold the uplink: %v", err)
}
}
func TestTheWorldsNamesAreNotAnInstallations(t *testing.T) {
m := catalogue.Manifest{Module: "resolver", Resources: []map[string]any{
{"id": "conf", "type": "file", "content": "server=1.1.1.1\nserver=8.8.8.8\nlisten=127.0.0.55\n"},
{"id": "proxy", "type": "container", "image": "docker.io/library/traefik@sha256:bb"},
{"id": "adapter", "type": "file", "content": "{\"machine\": \"host.docker.internal\"}\n"},
{"id": "doc", "type": "file", "content": "root = https://git.example.tld/\n"},
}}
if got := InstallationProblems(m); len(got) != 0 {
t.Fatalf("the world's names were reported: %v", got)
}
}
func TestTwoArtifactsWithOneNameAreRefused(t *testing.T) {
_, err := ParseManifest([]byte(`{"module":"a","version":"1","build":{"artifacts":[
{"name":"x","kind":"image","from":"Dockerfile"},
{"name":"x","kind":"archive","from":"files"}]}}`))
if err == nil {
t.Fatal("two artifacts with one name were accepted")
}
if !strings.Contains(err.Error(), "could mean either") {
t.Fatalf("unhelpful refusal: %v", err)
}
}
func TestTwoChannelsOfDifferentKindsHoldTheBenches(t *testing.T) {
shelf := catalogue.Shelf{"messenger": router(), "telegram": aChannel("telegram", "telegram"),
"desk-channel": aChannel("desk-channel", "desktop")}
if got := problemsFor(t, shelf); got != "" {
t.Fatalf("two kinds were refused: %s", got)
}
for _, m := range shelf {
if got := declaredSeatProblems(m); len(got) > 0 {
t.Fatalf("%s: %v", m.Module, got)
}
}
}
// **Two endpoints called the same would make an assignment configure whichever was read last.** The
// point of a name is that it identifies one thing.
func TestTwoEndpointsWithOneNameAreRefused(t *testing.T) {
m := catalogue.Manifest{Module: "twice", Listens: []catalogue.Listening{
{Name: "web", Port: 80, From: catalogue.FromMesh},
{Name: "web", Port: 8080, From: catalogue.FromMesh},
}}
got := strings.Join(endpointNameProblems(m), "\n")
if !strings.Contains(got, "could mean either") {
t.Fatalf("two endpoints with one name were accepted:\n%s", got)
}
}
// A stream name has one owner across the catalogue (ADR 0363 §1).
func TestTwoModulesCannotDeclareOneStream(t *testing.T) {
a := catalogue.Manifest{Module: "a", Streams: []catalogue.StreamDeclaration{{Name: "mail", Events: []string{"x"}}}}
b := catalogue.Manifest{Module: "b", Streams: []catalogue.StreamDeclaration{{Name: "mail", Events: []string{"y"}}}}
got := strings.Join(CatalogueProblems(catalogue.Shelf{"a": a, "b": b}), "; ")
if !strings.Contains(got, "b declares the stream MAIL, which a already declares") {
t.Fatalf("a second owner of MAIL was not refused: %s", got)
}
if got := StreamNameProblems(catalogue.Shelf{"a": a}); len(got) != 0 {
t.Fatalf("one owner refused: %v", got)
}
// And a stream of its own is outside the bus's predefined forms (the lead, 2026-10-29): refused, naming it, unless it
// is one kept by name until its module moves it.
if !strings.Contains(got, `a keeps the stream "mail", outside the bus's predefined forms`) {
t.Fatalf("a stream of its own was not refused: %s", got)
}
if got := PredefinedFormProblems(catalogue.Manifest{Module: "mesh-mail", Streams: a.Streams}); len(got) != 0 {
t.Fatalf("a stream kept until it moves was refused: %v", got)
}
}
// A seat name meaning two protocols is the failure nobody could diagnose afterwards.
func TestTwoModulesCannotDeclareTheSameSeat(t *testing.T) {
other := catalogue.Manifest{Module: "aardvark", DefinesSeats: []catalogue.SeatDeclaration{{
Name: "telegram-sender", Scope: catalogue.ScopeMesh, Accepts: []string{"something-else"}}}}
got := problemsFor(t, catalogue.Shelf{"telegram": telegram(), "aardvark": other})
if !strings.Contains(got, "already declares") {
t.Fatalf("both declarations stood: %s", got)
}
// The first declarer keeps it; only the second is refused.
if strings.Count(got, "already declares") != 1 {
t.Fatalf("expected exactly one refusal: %s", got)
}
}
// Where ADR 0110's guarantee lands under a derived set: a typo is refused, not resolved to
// nothing at runtime. And a seat verb a module invokes is one a holder serves (issue 774, sweep item 1: messenger
// invoked a verb nothing answered, and Telegram held every ask silently).
func TestUsingASeatNobodyDeclaresIsRefused(t *testing.T) {
unclaimed := telegram()
unclaimed.Claims = nil
for _, row := range []struct {
name, want string
shop catalogue.Manifest
seat catalogue.Manifest
note bool // over some manifests alone it is a note (IsUndeclaredSeat)
}{
{"a misspelled seat used", `uses the seat "telegram-sendr", ` + catalogue.UndeclaredSeat,
catalogue.Manifest{Module: "shop", Uses: []string{"telegram-sendr"}}, telegram(), true},
{"a declared verb invoked of a held seat", "", catalogue.Manifest{Module: "shop", Invokes: []string{"seat:telegram-sender.status"}}, telegram(), false},
{"a verb the seat does not promise", `the seat telegram-sender serves no verb "resend"`,
catalogue.Manifest{Module: "shop", Invokes: []string{"seat:telegram-sender.resend"}}, telegram(), false},
{"a verb of a misspelled seat", `a verb of the seat "telegram-sendr", ` + catalogue.UndeclaredSeat,
catalogue.Manifest{Module: "shop", Invokes: []string{"seat:telegram-sendr.status"}}, telegram(), true},
{"a verb of a declared seat nobody claims", `a verb of the seat "telegram-sender", ` + catalogue.UnheldSeat,
catalogue.Manifest{Module: "shop", Invokes: []string{"seat:telegram-sender.status"}}, unclaimed, true},
{"a verb of a mesh seat whose holder lives in another repository", "",
catalogue.Manifest{Module: "shop", Invokes: []string{"seat:mesh-watchdogs.signals"}}, telegram(), false},
{"a verb a mesh seat does not promise", `the seat mesh-placement serves no verb "adopt"`,
catalogue.Manifest{Module: "shop", Invokes: []string{"seat:mesh-placement.adopt"}}, telegram(), false},
{"the controller's seat is gone (ADR 0590)", `a verb of the seat "mesh-controller", ` + catalogue.UndeclaredSeat,
catalogue.Manifest{Module: "shop", Invokes: []string{"seat:mesh-controller.root-free"}}, telegram(), true},
{"the engine's verb: every node's engine holds its seat", "",
catalogue.Manifest{Module: "shop", Invokes: []string{"seat:node-engine.root-free"}}, telegram(), false},
{"a verb the engine's seat does not promise", `the seat node-engine serves no verb "root-freed"`,
catalogue.Manifest{Module: "shop", Invokes: []string{"seat:node-engine.root-freed"}}, telegram(), false},
} {
got := CatalogueProblems(catalogue.Shelf{"telegram": row.seat, "shop": row.shop})
joined := strings.Join(got, "; ")
if row.want == "" && joined != "" || row.want != "" && (len(got) != 1 || !strings.Contains(joined, row.want) ||
catalogue.IsUndeclaredSeat(got[0]) != row.note) {
t.Errorf("%s: %s", row.name, joined)
}
}
}
func TestWhatAHandedModuleCannotHaveFromTheVaultIsRefused(t *testing.T) {
// A family and a secret issued outside the mesh are not handed by name yet: the mark is refused.
for why, raw := range map[string]string{
"a family": strings.Replace(botMoved(), `"own-secrets":{"token":"${dir:state}/token"}`, `"own-secrets":{"token":"${dir:state}/token","key-*":"${dir:state}/keys"}`, 1),
"issued outside": strings.Replace(botMoved(), `"own-secrets":{"token":"${dir:state}/token"}`, `"own-secrets":{"token":{"path":"${dir:state}/token","issued-by":"outside"}}`, 1),
} {
if _, err := ParseManifest([]byte(raw)); err == nil || !strings.Contains(err.Error(), "design 57 V4") {
t.Errorf("a moved module with %s: %v", why, err)
}
}
}
func TestWhatReplacesMayNameIsRefusedNearItsAuthor(t *testing.T) {
for what, resource := range map[string]string{
"a container": `{"id":"c","type":"container","name":"c","image":"x@` + aServiceDigest + `","replaces":["old"]}`,
"a step": `{"id":"p","type":"process","name":"p","run":["./p"],"run-once":true,"replaces":["old"]}`,
"something declared": `{"id":"p","type":"process","name":"p","run":["./p"],"replaces":["p"]}`,
"another module's": `{"id":"p","type":"process","name":"p","run":["./p"],"replaces":["other.old"]}`,
"not a list": `{"id":"p","type":"process","name":"p","run":["./p"],"replaces":"old"}`,
} {
raw := `{"module":"m","version":"1","resources":[` + resource + `]}`
if _, err := ParseManifest([]byte(raw)); err == nil || !strings.Contains(err.Error(), "replace") {
t.Errorf("replaces on %s was accepted: %v", what, err)
}
}
ok := `{"module":"m","version":"1","resources":[{"id":"p","type":"process","name":"p","run":["./p"],"replaces":["old"]}]}`
if _, err := ParseManifest([]byte(ok)); err != nil {
t.Errorf("a process replacing what its module no longer declares was refused: %v", err)
}
}
// A module rolls out by default; it records where it says so with why, where it keeps irreplaceable
// data, and always where it is the bus (novox/hq ADR 0236).
func TestWhereAModulesUpgradePolicyComesFrom(t *testing.T) {
cases := []struct {
name string
m catalogue.Manifest
policy, from string
}{
{"default", catalogue.Manifest{Module: "app"}, catalogue.PolicyRoll, catalogue.FromDefault},
{"says record", catalogue.Manifest{Module: "db", Upgrade: &catalogue.UpgradePolicy{Policy: catalogue.PolicyRecord, Why: "a restart costs"}},
catalogue.PolicyRecord, catalogue.FromModule},
{"says together", catalogue.Manifest{Module: "dns", Upgrade: &catalogue.UpgradePolicy{Policy: catalogue.PolicyTogether, Why: "one zone"}},
catalogue.PolicyTogether, catalogue.FromModule},
{"irreplaceable data", catalogue.Manifest{Module: "media", Data: &catalogue.Data{Own: []catalogue.DataItem{{ID: "library", Class: catalogue.ClassIrreplaceable}}}},
catalogue.PolicyRecord, catalogue.FromData},
{"valuable data rolls", catalogue.Manifest{Module: "notes", Data: &catalogue.Data{Own: []catalogue.DataItem{{ID: "db", Class: catalogue.ClassValuable}}}},
catalogue.PolicyRoll, catalogue.FromDefault},
{"irreplaceable but says roll", catalogue.Manifest{Module: "photos", Upgrade: &catalogue.UpgradePolicy{Policy: catalogue.PolicyRoll},
Data: &catalogue.Data{Own: []catalogue.DataItem{{ID: "originals", Class: catalogue.ClassIrreplaceable}}}}, catalogue.PolicyRoll, catalogue.FromModule},
{"the bus, whatever it says", catalogue.Manifest{Module: "nats", Provides: []catalogue.Offer{{Name: "mesh-bus"}},
Upgrade: &catalogue.UpgradePolicy{Policy: catalogue.PolicyRoll}}, catalogue.PolicyRecord, catalogue.FromBus},
}
for _, c := range cases {
policy, from, _ := catalogue.DerivedUpgrade(c.m)
if policy != c.policy || from != c.from {
t.Errorf("%s: %s from %s, want %s from %s", c.name, policy, from, c.policy, c.from)
}
// ADR 0565 clause 7 (issue 1953 L5-S3): a person's choice wins over each source but the bus; none leaves the
// derivation as it was.
person := &catalogue.PersonPolicy{V: catalogue.PersonPolicyVersion, Module: c.m.Module, Policy: catalogue.PolicyTogether, Why: "one zone", By: "person.jochen"}
policy, from, _, by := catalogue.Upgrade(c.m, person)
wantPolicy, wantFrom, wantBy := catalogue.PolicyTogether, catalogue.FromPerson, "person.jochen"
if c.from == catalogue.FromBus {
wantPolicy, wantFrom, wantBy = c.policy, catalogue.FromBus, ""
}
if policy != wantPolicy || from != wantFrom || by != wantBy {
t.Errorf("%s with a person's choice: %s from %s by %q", c.name, policy, from, by)
}
if policy, from, _, _ := catalogue.Upgrade(c.m, nil); policy != c.policy || from != c.from {
t.Errorf("%s with no choice: %s from %s", c.name, policy, from)
}
}
for _, p := range []catalogue.PersonPolicy{{Module: "m", Policy: "sometimes", Why: "x", By: "p"}, {Module: "m", Policy: catalogue.PolicyRecord, By: "p"},
{Module: "m", Policy: catalogue.PolicyRecord, Why: "x"}} {
if p.Check() == nil {
t.Errorf("%+v was accepted", p)
}
}
// A policy other than roll says why, and an unknown one is refused.
for _, u := range []catalogue.UpgradePolicy{{Policy: catalogue.PolicyRecord}, {Policy: catalogue.PolicyTogether}, {Policy: "sometimes", Why: "x"}} {
if problems := manifestUpgradeProblems((catalogue.Manifest{Module: "m", Upgrade: &u})); len(problems) == 0 {
t.Errorf("%+v was accepted", u)
}
}
if problems := manifestUpgradeProblems((catalogue.Manifest{Module: "m", Upgrade: &catalogue.UpgradePolicy{Policy: catalogue.PolicyRecord, Why: "a restart costs"}})); len(problems) != 0 {
t.Errorf("a record with why was refused: %v", problems)
}
if _, err := ParseManifest([]byte(`{"module":"m","upgrade":{"policy":"record"}}`)); err == nil ||
!strings.Contains(err.Error(), "says why") {
t.Errorf("a manifest recording without why parsed: %v", err)
}
}
// ---- copied from catalogue: helpers composition keeps too ----
func buildProblemsOf(t *testing.T, m catalogue.Manifest) string {
t.Helper()
return strings.Join(buildProblems(m.Build, m.Module), "\n")
}
// bundleFor is a manifest whose one artifact is a bundle in the given language and system.
func bundleFor(language, system string) catalogue.Manifest {
return catalogue.Manifest{Module: "a-component", Build: &catalogue.Build{Artifacts: []catalogue.Artifact{
{Name: "binary", Kind: catalogue.ArtifactBundle, Language: language, System: system},
}}}
}
// aToolsModule is a module whose tools come as a compiled bundle and nothing else — the shape every
// module takes once its tool container goes (to-be 38 WP4).
func aToolsModule(t *testing.T, name string, entrypoints ...string) catalogue.Manifest {
t.Helper()
m := catalogue.Manifest{Module: name, Version: "1", Tools: []string{"status"},
Build: &catalogue.Build{Artifacts: []catalogue.Artifact{
{Name: "tools", Kind: catalogue.ArtifactBundle, Language: "typescript", Entrypoints: entrypoints},
}}}
resolved, err := m.Resolve([]catalogue.Built{{Name: "tools", Kind: catalogue.ArtifactBundle,
Reference: catalogue.ArtifactStoreScheme + name + "/tools/blobs/" + bundleDigest, Digest: bundleDigest}})
if err != nil {
t.Fatal(err)
}
return resolved
}
var bundleDigest = "sha256:" + strings.Repeat("b", 64)
func busyManifest(t *testing.T, on string, busy any) []byte {
t.Helper()
extra := map[string]any{"id": "agent", "type": "container", "name": "app-agent",
"image": "registry.example/app" + healthDigest}
var m map[string]any
if err := json.Unmarshal(healthManifest(t, "", nil, extra), &m); err != nil {
t.Fatal(err)
}
for _, r := range m["resources"].([]any) {
if r := r.(map[string]any); r["id"] == on {
r[catalogue.BusyField] = busy
}
}
out, err := json.Marshal(m)
if err != nil {
t.Fatal(err)
}
return out
}
func telegram() catalogue.Manifest {
return catalogue.Manifest{Module: "telegram", Tools: []string{"status"}, DefinesSeats: []catalogue.SeatDeclaration{{
Name: "telegram-sender", Scope: catalogue.ScopeMesh,
Accepts: []string{"send"}, Emits: []string{"delivered", "failed"}, Serves: []catalogue.Verb{{Name: "status"}},
}}, Claims: []catalogue.Claim{{Name: "telegram-sender", Scope: catalogue.ScopeMesh}}}
}
func problemsFor(t *testing.T, shelf catalogue.Shelf) string {
t.Helper()
return strings.Join(CatalogueProblems(shelf), "; ")
}
func claimed(claims string) []byte {
return []byte(`{"module":"thing","version":"1","provides":[{"name":"npm-package-registry","scope":"mesh","credential":{"form":"none"}}],"claims":` + claims + `}`)
}
func hostnameSeat(t *testing.T) catalogue.Seat {
t.Helper()
s, ok := catalogue.SeatNamed("node-hostname")
if !ok {
t.Fatal("no node-hostname seat")
}
return s
}
func hostnameHolder(resources ...map[string]any) catalogue.Manifest {
return catalogue.Manifest{Module: "names", Version: "1", Claims: []catalogue.Claim{{Name: "node-hostname", Scope: catalogue.ScopeNode,
Serves: []string{"entries", "add", "remove"}}}, Resources: resources}
}
func mustParse(t *testing.T, raw string) catalogue.Manifest {
t.Helper()
m, err := ParseManifest([]byte(raw))
if err != nil {
t.Fatalf("refused: %v", err)
}
return m
}
const declaredStore = `{"module":"pg","version":"1",
"provides":[{"name":"postgres-database","scope":"mesh"}],
"grants":{"postgres-database":"${dir:grants}"},
"data":{
"own":[
{"id":"store","path":"${dir:store}","class":"irreplaceable","backup":{"dump":"dump-all > ${dir:dumps}/all","into":"dumps"},"active":"1d"},
{"id":"dumps","path":"${dir:dumps}","class":"rebuildable","backup":"none"}],
"consumers":{"postgres-database":{"class":"irreplaceable","in":"store"}}},
"resources":[
{"id":"grants","type":"directory","mode":"0700"},
{"id":"store","type":"directory","path":"/srv/store","mode":"0700"},
{"id":"dumps","type":"directory","path":"${dir:store}/dumps","mode":"0700"},
{"id":"server","type":"container","image":"x@sha256:aa","volumes":["${dir:store}:/var/lib/postgresql/data"]}]}`
func shallowCopy(resource map[string]any) map[string]any {
copied := map[string]any{}
for k, v := range resource {
copied[k] = v
}
return copied
}
const templatedTwin = `{"module":"tidy","version":"1",
"build":{"artifacts":[{"name":"files","kind":"archive","from":"files"}]},
"resources":[
{"id":"conf","type":"directory","path":"${dir:conf}"},
{"id":"main","type":"file","path":"${dir:conf}/tidy.conf","mode":"0644","template":"files/tidy.conf"},
{"id":"extra","type":"file","path":"${dir:conf}/extra.conf","template":"files/deeper/extra.conf"}]}`
const barTemplate = `{{if eq .shows "battery"}}[[block]]
block = "battery"
{{with .options}}{{with .device}}device = {{quote .}}
{{end}}{{end}}{{else if eq .shows "command"}}[[block]]
block = "custom"
command = {{quote .options.command}}
interval = {{.options.interval}}
{{else if eq .shows "state"}}[[block]]
block = "custom"
command = {{quote (printf "show %s %s" .options.state (or .options.key .machine))}}
{{end}}`
// The router of novox/hq ADR 0259: it declares the operator's seat and the two kinded benches.
func router() catalogue.Manifest {
return catalogue.Manifest{Module: "messenger", Tools: []string{"open", "history", "notify"},
State: []catalogue.StateDeclaration{{Name: "asks"}}, RunsAs: "messenger", SecretsOwner: "messenger",
DefinesSeats: []catalogue.SeatDeclaration{
{Name: "operator-channel", Scope: catalogue.ScopeMesh, Accepts: []string{"ask", "cancel"}, Emits: []string{"decided"},
ByCaller: []string{"ask", "cancel", "decided"}, Records: []string{"asks"},
Serves: []catalogue.Verb{{Name: "open"}, {Name: "history"}, {Name: "notify"}}},
{Name: "channel", Scope: catalogue.ScopeMesh, Kinded: true, Accepts: []string{"show", "edit", "send"}},
{Name: "intake", Scope: catalogue.ScopeMesh, Kinded: true, Emits: []string{"choice", "link"}, Proofs: []string{"code"}},
},
Claims: []catalogue.Claim{{Name: "operator-channel", Scope: catalogue.ScopeMesh}},
Uses: []string{"channel"}}
}
func aChannel(module, kind string) catalogue.Manifest {
return catalogue.Manifest{Module: module, Claims: []catalogue.Claim{
{Name: "channel", Scope: catalogue.ScopeMesh, Kind: kind}, {Name: "intake", Scope: catalogue.ScopeMesh, Kind: kind}}}
}
func aStoreManifest(step map[string]any) []byte {
m := map[string]any{
"module": "distribution", "version": "1",
"resources": []any{
map[string]any{"id": "store", "type": "container", "name": "mesh-registry",
"image": "registry@sha256:" + strings.Repeat("a", 64)},
step,
},
}
raw, _ := json.Marshal(m)
return raw
}
func familyManifest(t *testing.T, ownSecrets string) (catalogue.Manifest, error) {
t.Helper()
return ParseManifest([]byte(`{"module":"mounts","version":"1","own-secrets":{` + ownSecrets + `}}`))
}
// jsonString quotes a Go string the way encoding/json would, for building a literal into a test's manifest JSON.
func jsonString(s string) string {
out, err := json.Marshal(s)
if err != nil {
panic(err)
}
return string(out)
}
// withPredefined is the list of predefined keys the mesh published, for the length of a test.
func withPredefined(t *testing.T, keys catalogue.PredefinedList) {
t.Helper()
was := catalogue.ReplacePublished(keys)
t.Cleanup(func() { catalogue.ReplacePublished(was) })
}
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}
}`
// 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
}
// inConfigForm is a module in the config form that registers: every value typed, described, defaulted or
// required, placed in a file or read at runtime, and its mergeable file naming its keys.
func inConfigForm() map[string]any {
return map[string]any{
"module": "notifier",
"version": "1",
"config": map[string]any{
"font-size": map[string]any{"type": "number", "description": "the notification's font size, in points",
"minimum": 6, "maximum": 40, "default": 10},
"width": map[string]any{"type": "number", "description": "the notification's width, in pixels",
"default": 250},
"position": map[string]any{"type": "string", "description": "the corner it shows in, a word",
"enum": []any{"top-right", "top-left"}, "default": "top-right"},
"theme": map[string]any{"type": "string", "description": "the colour theme, a lower-case name",
"pattern": "[a-z]+", "default": "dark"},
"history": map[string]any{"type": "number", "description": "how many notifications it keeps, a count",
"default": 20, "runtime": true},
"sender": map[string]any{"type": "string", "description": "the address it mails a digest from",
"required": true},
},
"resources": []any{
map[string]any{"id": "configuration", "type": "file", "path": "/x/notifierrc",
"content": "font = Inter ${config:font-size}\nwidth = ${config:width}\nfrom = ${config:sender}\n"},
map[string]any{"id": "rules", "type": "file", "path": "/x/rules.json", "merge": "json",
"takes-config": []any{"position", "theme"}, "content": "{\"position\": \"top-right\"}\n"},
map[string]any{"id": "notifier", "type": "service", "unit": "notifier.service",
"reload-on": []any{"configuration"}, "restart-on": []any{"rules"}},
},
}
}
// declaration is one value of inConfigForm's config, to change in place.
func declaration(m map[string]any, key string) map[string]any {
return m["config"].(map[string]any)[key].(map[string]any)
}
func resourceOf(m map[string]any, id string) map[string]any {
for _, r := range m["resources"].([]any) {
if r.(map[string]any)["id"] == id {
return r.(map[string]any)
}
}
return nil
}
// refused is that the manifest is refused with every one of these words.
func refused(t *testing.T, m map[string]any, words ...string) {
t.Helper()
_, err := parseRaw(t, m)
if err == nil {
t.Fatalf("registered; wanted a refusal saying %q", words)
}
for _, w := range words {
if !strings.Contains(err.Error(), w) {
t.Fatalf("refused without %q:\n%v", w, err)
}
}
}
func aContainerWithGroups(groups ...any) map[string]any {
return map[string]any{"id": "server", "type": "container", "name": "n8n", "image": "x@sha256:" + strings.Repeat("a", 64),
"groups": groups}
}
func parseRaw(t *testing.T, m map[string]any) (catalogue.Manifest, error) {
t.Helper()
raw, err := json.Marshal(m)
if err != nil {
t.Fatal(err)
}
return ParseManifest(raw)
}
const misspelledShell = "${shel:zsh:first}"
const undeclaredSetting = "${setting:Undeclared}"
const misspelledMachine = "${machnie:address}"
const shellsOwn = "${XDG_CACHE_HOME:-x}"
const shellsOperators = "${PORT:-8080} ${SHELL:-/bin/sh} ${SECRET:?unset} ${dir:-/tmp}"
// 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) catalogue.Manifest {
appFile := map[string]any{"id": "app", "type": "file", "path": "/etc/app.json", "merge": catalogue.MergeJSON,
"content": `{"server":{"port":8080},"search":{"safe":1}}`}
sideFile := map[string]any{"id": "sidecar", "type": "file", "path": "/etc/sidecar.json", "merge": catalogue.MergeJSON,
"content": `{"url":"http://127.0.0.1:8080"}`}
if app != nil {
appFile[catalogue.TakesSettingsField] = app
}
if sidecar != nil {
sideFile[catalogue.TakesSettingsField] = sidecar
}
return catalogue.Manifest{Module: "search", Resources: []map[string]any{
appFile, sideFile,
{"id": "env", "type": "file", "path": "/etc/search.env", "content": "SITE=${setting:site-name}\n"},
}}
}
func containsText(list []string, text string) bool {
for _, s := range list {
if strings.Contains(s, text) {
return true
}
}
return false
}
var digest = "sha256:" + strings.Repeat("d", 64)
func manifestMounting(from string, beside string) []byte {
res := aContainerMounting
if beside != "" {
res = beside + "," + res
}
return []byte(`{"module":"store","resources":[` + strings.Replace(res, "%s", from, 1) + `]}`)
}
func recycler() catalogue.Manifest {
return catalogue.Manifest{Module: "sonarr",
Resources: []map[string]any{
{"id": "recycle-bin", "type": "directory", "path": "/srv/sonarr/recycle", "mode": "0755",
"owner": "${setting:puid}:${setting:pgid}"},
{"id": "identity", "type": "file", "path": "/srv/sonarr/identity.env", "mode": "0644",
"content": "PUID=${setting:puid}\nPGID=${setting:pgid}\n"},
},
}
}
func copied(r map[string]any) map[string]any {
out := map[string]any{}
for k, v := range r {
out[k] = v
}
return out
}
func parsed(t *testing.T, m catalogue.Manifest) error {
t.Helper()
raw, err := json.Marshal(m)
if err != nil {
t.Fatal(err)
}
_, err = ParseManifest(raw)
return err
}
func upstreamManifest(artifact string) string {
return `{"module":"a","version":"1","build":{"artifacts":[` + artifact + `]}}`
}
const pinnedSum = "38dd24890a66b4cf9a45bc24e15192e00c26b929e1bb8df7974c16a4539129e2"
// A process resource may declare `bus` (novox/hq ADR 0436 §1): what it emits, the module's own work-queue
// streams it consumes, the module's own state it reads and writes, whether it reads the router's records of
// this module's own asks, and the tools and seat verbs it calls. The fixture is mesh-mail's future manifest
// shape (ADR 0436 §7, §8), copied from mesh-catalog/modules/mesh-mail/module.json and extended as settled by
// the lead: process id "send" is the manifest's own `id` (internal/catalogue/runtime.go:72-74 is the model for
// a process id); the account file uses the controller's real JSON tags (modules.go:1041-1056 issueWith).
func mailSenderProcessManifest(t *testing.T, bus string) string {
t.Helper()
process := `{"id":"send","type":"process","run":["./mesh-mail","send"],"user":"mesh-mail-send"`
if bus != "" {
process += `,"bus":` + bus
}
process += `}`
// The stream itself is plain here, not retention work-queue: 1179 checks only that a process names a
// stream the module owns, never its retention — that rule is 1178's (WorkQueueStreamProblems), and is
// tested against its own fixture there (work_queue_consumer_test.go) so this one stays accepted whether
// or not a process currently names "mail-sends" in bus.queues.
return `{"module":"mesh-mail","version":"1","emits":["mail.send.sent","mail.send.failed"],` +
`"state":["sends"],"streams":[{"name":"mail-sends","events":["mail.send.approved"],"max-age-days":1,` +
`"max-mib":64}],"resources":[{"id":"account","type":"user","name":"mesh-mail-send"},` + process + `]}`
}
const mailSenderBus = `{"emits":["mail.send.sent","mail.send.failed"],"queues":["mail-sends"],` +
`"state":["sends"],"asks":true,"invokes":["seat:operator-channel.tell"]}`
func strSliceEqual(a, b []string) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if a[i] != b[i] {
return false
}
}
return true
}
const handedProvider = `{"module":"db","version":"1","moved":true,
"provides":[{"name":"postgres-database","scope":"mesh","identity":{"max":63,"in":"a PostgreSQL role"}}],
"build":{"artifacts":[{"name":"files","kind":"archive","from":"files"}]},
"resources":[
{"id":"state","type":"directory","mode":"0700","place":"."},
{"id":"consumers","type":"file","path":"${dir:state}/consumers.json","mode":"0600","template":"files/consumers.json"}]}`
const consumersText = "${consumers:postgres-database}"
func placedModule() catalogue.Manifest {
return catalogue.Manifest{
Module: "photos",
Resources: []map[string]any{
{"id": "data", "type": "directory", "mode": "0700"},
{"id": "server-env", "type": "file", "path": "${dir:data}/server.env",
"content": "STORE=${dir:data}/objects\n"},
{"id": "server", "type": "container", "name": "photos-server",
"volumes": []any{"${dir:data}:/data"},
"env": map[string]any{"DATA": "${dir:data}/objects"},
"env-file": []any{"${dir:data}/server.env"}},
},
}
}
// renaming is inConfigForm with its rules file taking `url` as NOTIFIER_URL, a value the code reading the file
// falls back on when the file lacks it.
func renaming() map[string]any {
m := inConfigForm()
m["config"].(map[string]any)["url"] = map[string]any{"type": "string",
"description": "the address the notifier posts to, a URL; unset, the notifier's own environment says it",
"default": ""}
resourceOf(m, "rules")["takes-config"] = []any{"position", "theme",
map[string]any{"key": "url", "as": "NOTIFIER_URL"}}
return m
}
// aMediaServer is the shape one port number per key cannot express: two endpoints of different kinds.
// A web surface a proxy serves under a subdomain, and a protocol port clients dial directly because
// the client expects that number.
func aMediaServer() catalogue.Manifest {
return catalogue.Manifest{
Module: "media",
Listens: []catalogue.Listening{
{Name: "web", Port: 80, From: catalogue.FromMesh, Why: "the app, behind the proxy"},
{Name: "stream", Port: 32400, From: catalogue.FromEverywhere, Fixed: true,
Why: "the client dials this number; the protocol chose it"},
},
Contributes: map[string]map[string]any{
"route": {"label": "media", catalogue.RouteEndpoint: "web"},
},
// Both endpoints are published by its container, which is what lets a machine port be given
// for either: the mesh moves a port the module publishes, never one it merely listens on.
Resources: []map[string]any{
{"id": "server", "type": "container", "name": "media",
"ports": []any{"80", "32400"}},
},
}
}
// ownAccountManifest is a module of its own account with nothing long-running but its own runtime, given
// runtime-health (or none, when health is nil), and running as no account when runsAs is false.
func ownAccountManifest(t *testing.T, runsAs bool, health any) []byte {
t.Helper()
m := map[string]any{"module": "keeper", "tools": []any{"keeper_status", "keeper_health"},
"own-secrets": map[string]any{"broker": map[string]any{"path": "/var/lib/keeper/broker"}},
"resources": []any{map[string]any{"id": "account", "type": "user", "name": "keeper"}}}
if runsAs {
m["runs-as"], m["secrets-owner"] = "keeper", "keeper"
}
if health != nil {
m["runtime-health"] = health
}
raw, err := json.Marshal(m)
if err != nil {
t.Fatal(err)
}
return raw
}
const aRegistryForge = `{"module":"forge","version":"1","requires":["route"],
"provides":[{"name":"npm-package-registry","scope":"mesh"}],
"contributes":{"route":{"web":{"label":"git","port":3000}}},
"serves":{"npm-package-registry":{"scheme":"http","port":3000,"npm-path":"/api/packages/novox/npm/",
"public-root":"https://${bound:route:name-web}/api/packages/novox/npm/"}}}`
// store is an object store in the shape minio has: it serves a region and a port to everyone, and
// a bucket named for whoever is asking.
func store() catalogue.Manifest {
return catalogue.Manifest{
Module: "store", Version: "1",
Provides: catalogue.FromAnywhere("s3-bucket"),
Listens: []catalogue.Listening{{Port: 9000, Protocol: "tcp", From: catalogue.FromMesh}},
Serves: map[string]map[string]any{"s3-bucket": {
"region": "eu-west",
"bucket": "${consumer:as:dns}",
}},
Receives: map[string]string{"s3-bucket": "/var/lib/store/grants/mesh.json"},
Grants: map[string]string{"s3-bucket": "/var/lib/store/grants"},
Resources: []map[string]any{{
"id": "server", "type": "container", "name": "store", "ports": []any{"9000"},
}},
}
}
// A module left out for a key this controller does not know, inside an entry, is read past that key
// alone: it still provides what it provides, its other entries are whole, and a key of the same name
// that another entry knows is kept (novox/hq ADR 0262).
func aModuleWithAFieldSetAsideStillProvidesWhatItProvides(t *testing.T) {
var m catalogue.Manifest
raw := `{"module": "later", "version": "2",
"provides": [{"name": "db", "scope": "mesh"}, {"name": "cache", "a-field-from-later": 1}],
"state": [{"name": "s", "history": 3}],
"data": {"own": [{"id": "d", "path": "${dir:d}", "class": "valuable", "backup": {"dump": "x", "into": "d", "class": "later"}}]},
"resources": [{"id": "d", "type": "directory", "mode": "0700"}]}`
if err := json.Unmarshal([]byte(raw), &m); err != nil {
t.Fatal(err)
}
if len(m.Provides) != 2 || m.Provides[0].Name != "db" || m.Provides[1].Name != "cache" {
t.Fatalf("provides: %+v", m.Provides)
}
if len(m.State) != 1 || m.State[0].History != 3 {
t.Fatalf("state: %+v", m.State)
}
if m.Data == nil || len(m.Data.Own) != 1 || m.Data.Own[0].Class != "valuable" {
t.Fatalf("data: the item's own class was taken for the backup's unknown one: %+v", m.Data)
}
for _, want := range []string{"provides[1].a-field-from-later", "data.own[0].backup.class"} {
if !strings.Contains(m.UnknownField(), want) {
t.Errorf("unknown %q does not say it read without %s", m.UnknownField(), want)
}
}
}
func notifier() catalogue.Manifest {
return catalogue.Manifest{Module: "notifier",
Settings: map[string]catalogue.SettingDeclaration{
"font-size": {Kind: catalogue.KindPreference, Default: float64(10), Why: "readable at a scale of one"},
"width": {Kind: catalogue.KindPreference, Default: float64(250), Why: "fits a title of forty characters"},
},
Resources: []map[string]any{{"id": "configuration", "type": "file", "path": "/x/notifierrc",
"content": "font = Inter ${setting:font-size}\nwidth = ${setting:width}\n"}},
}
}
// mailManifest is a module owning a restricted stream, as mesh-mail declares it (novox/hq ADR 0363), with
// the trusted mergeable file that takes its grants setting; extra is spliced in at the top.
func mailManifest(t *testing.T, streams, extra string) string {
t.Helper()
return `{"module":"mesh-mail","version":"1",` + extra + `"streams":` + streams + `,` +
`"resources":[{"id":"state","type":"directory","mode":"0700","place":"."},` +
`{"id":"grants","trusted":true,"type":"file","path":"${dir:state}/grants.json","mode":"0600",` +
`"merge":"json","takes-settings":["grants"],"content":"{\"grants\": {}}"}]}`
}
const unpackingTemplated = `{"module":"tidy","version":"1",
"templates":"templates",
"build":{"artifacts":[{"name":"scripts","kind":"archive","from":"files"},
{"name":"templates","kind":"archive","from":"templates"}]},
"resources":[
{"id":"scripts","type":"archive","path":"/usr/local/lib/tidy","artifact":"scripts"},
{"id":"conf","type":"directory","path":"${dir:conf}"},
{"id":"main","type":"file","path":"/etc/tidy.conf","mode":"0644","template":"templates/tidy.conf"},
{"id":"extra","type":"file","path":"/etc/extra.conf","template":"templates/deeper/extra.conf"}]}`
const unpackingInline = `{"module":"tidy","version":"1",
"build":{"artifacts":[{"name":"scripts","kind":"archive","from":"files"}]},
"resources":[
{"id":"scripts","type":"archive","path":"/usr/local/lib/tidy","artifact":"scripts"},
{"id":"conf","type":"directory","path":"${dir:conf}"},
{"id":"main","type":"file","path":"/etc/tidy.conf","mode":"0644","content":"` + mainText + `"},
{"id":"extra","type":"file","path":"/etc/extra.conf","content":"` + extraText + `"}]}`
// mailSenderManifestWithQueue is the mesh-mail fixture (ADR 0436 §7, §8) with its process's bus.queues set
// as given, so each 1178/AC2 case can vary just that.
func mailSenderManifestWithQueue(queues string) catalogue.Manifest {
return catalogue.Manifest{
Module: "mesh-mail", Version: "1", Emits: []string{"mail.send.sent", "mail.send.failed"},
State: []catalogue.StateDeclaration{{Name: "sends"}},
Streams: []catalogue.StreamDeclaration{{Name: "mail-sends", Events: []string{"mail.send.approved"},
Retention: catalogue.StreamRetentionWorkQueue, MaxDeliveries: 5}},
Resources: []map[string]any{
{"id": "account", "type": "user", "name": "mesh-mail-send"},
{"id": "send", "type": "process", "run": []any{"./mesh-mail", "send"}, "user": "mesh-mail-send",
"bus": map[string]any{"emits": []any{"mail.send.sent", "mail.send.failed"}, "queues": queueList(queues)}},
},
}
}
func mod(name string, provides, requires, capabilities []string, claims ...catalogue.Claim) catalogue.Manifest {
return catalogue.Manifest{Module: name, Provides: catalogue.Offers(provides...), Requires: requires,
Capabilities: capabilities, Claims: claims}
}
const collectorZsh = `{"module":"zsh","version":"1","moved":true,
"claims":[{"name":"node-login-shell","scope":"node"}],
"collections":[{"name":"widgets","comment":"#"}],
"build":{"artifacts":[{"name":"files","kind":"archive","from":"files"}]},
"resources":[{"id":"rc","type":"file","path":"${machine:account-home}/.zshrc","owner":"${machine:account}",
"mode":"0644","template":"files/zshrc"}]}`
// catalogueManifests is every manifest of the catalogue (catalogueRoot), read through ReadTreeManifest once
// per test binary and not once per test: the checks over the real catalogue each read and parsed every
// manifest again, and together took 1.5 s of mesh-sdk's unit bound of 2 s (issue 1756). What it returns is
// shared and read-only: a test that changes a manifest copies it first.
func catalogueManifests(t *testing.T) (string, []readManifest) {
t.Helper()
root := catalogueRoot(t)
readOnce.Do(func() {
readRoot = root
found, err := filepath.Glob(filepath.Join(root, "modules", "*", "module.json"))
if err != nil {
return
}
for _, p := range found {
m, err := ReadTreeManifest(root, filepath.Dir(p))
readAll = append(readAll, readManifest{path: p, manifest: m, err: err})
}
})
if len(readAll) == 0 {
t.Fatalf("no manifests under %s", root)
}
return readRoot, readAll
}
// theCatalogue is every manifest of the catalogue internal/beside finds, parsed the way registration
// parses one.
func theCatalogue(t *testing.T) []catalogue.Manifest {
t.Helper()
root := beside.Catalogue(t)
entries, err := os.ReadDir(root)
if err != nil {
t.Fatal(err)
}
var out []catalogue.Manifest
for _, e := range entries {
if !e.IsDir() {
continue
}
raw, err := os.ReadFile(filepath.Join(root, e.Name(), "module.json"))
if err != nil {
continue
}
var m catalogue.Manifest
if err := json.Unmarshal(raw, &m); err != nil {
t.Fatalf("%s: %v", e.Name(), err)
}
out = append(out, m)
}
if len(out) == 0 {
t.Fatalf("no manifests under %s, so this proved nothing", root)
}
return out
}
// healthManifest is a module of one web container, one running service and one step, with the health
// given on the container (or on the resource named by on).
func healthManifest(t *testing.T, on string, health map[string]any, more ...map[string]any) []byte {
t.Helper()
resources := []map[string]any{
{"id": "server", "type": "container", "name": "app-server", "image": "registry.example/app" + healthDigest,
"ports": []any{"8080"}},
{"id": "daemon", "type": "service", "unit": "app.service", "state": "running"},
{"id": "seed", "type": "container", "name": "app-seed", "image": "registry.example/app" + healthDigest,
"run-once": true},
{"id": "sweep", "type": "container", "name": "app-sweep", "image": "registry.example/app" + healthDigest,
"schedule": "0 3 * * *"},
}
resources = append(resources, more...)
for _, r := range resources {
if r["id"] == on && health != nil {
r["health"] = health
}
}
m := map[string]any{"module": "app", "requires": []any{"postgres-database"}, "tools": []any{"app_status"},
"listens": []any{
map[string]any{"name": "web", "port": 8080, "from": "mesh"},
map[string]any{"name": "beacon", "port": 9999, "protocol": "udp", "from": "mesh"},
},
"resources": resources}
raw, err := json.Marshal(m)
if err != nil {
t.Fatal(err)
}
return raw
}
const mailStream = `[{"name":"mail","events":["mail.*.message.added","mail.*.message.changed",` +
`"mail.*.message.removed"],"max-age-days":7,"grant":{"config":"grants","events":"mail.{key}.**"}}]`
// localResolver is a stand-in holder: it claims the seat and renders the resolver file naming the
// machine's own address. What is under test is the controller's rule, not the catalogue's module.
func localResolver() catalogue.Manifest {
return catalogue.Manifest{Module: "local-resolver", Version: "1",
Claims: []catalogue.Claim{{Name: catalogue.ResolverSeat, Scope: catalogue.ScopeNode}},
Facts: map[string]catalogue.RosterFile{"resolvers": {Path: catalogue.ResolverFile,
Template: "# Managed by the mesh\n{{range .Machines}}{{if eq .Name $.Node}}nameserver {{.Address}}\n{{end}}{{end}}"}}}
}
func catalogueManifest(t *testing.T, module string) catalogue.Manifest {
t.Helper()
root, dir := moduleTree(t, module)
m, err := ReadTreeManifest(root, dir)
if err != nil {
t.Fatalf("%s does not parse:\n%v", module, err)
}
return m
}
func optionalModule(resources string) string {
return `{"module":"grafana","version":"1",
"requires":["route",{"name":"influxdb-api","optional":true}],
"secrets":{"influxdb-api":"/var/lib/grafana/influxdb-api"},
"resources":[` + resources + `]}`
}
const aContainerMounting = `{"id":"server","type":"container","name":"store","image":"x@sha256:` +
`0000000000000000000000000000000000000000000000000000000000000000","volumes":["%s:/inside"]}`
func botMoved() string {
return strings.Replace(handedBot, `"version":"1",`, `"version":"1","moved":true,`, 1)
}
// held is a manifest as registration holds it: resolved with what was built, then read again strictly.
func held(t *testing.T, raw string, built []catalogue.Built) catalogue.Manifest {
t.Helper()
m, err := ParseManifest([]byte(raw))
if err != nil {
t.Fatal(err)
}
resolved, err := m.Resolve(built)
if err != nil {
t.Fatal(err)
}
again, err := json.Marshal(resolved)
if err != nil {
t.Fatal(err)
}
out, err := ParseManifest(again)
if err != nil {
t.Fatal(err)
}
return out
}
func builtBot(text string) []catalogue.Built {
return []catalogue.Built{{Name: "files", Kind: catalogue.ArtifactArchive,
Reference: catalogue.ArtifactStoreScheme + "bot/files/blobs/sha256:" + strings.Repeat("e", 64),
Digest: "sha256:" + strings.Repeat("e", 64), Texts: map[string]string{"bot.conf": text}}}
}
const botText = "token = ${secret:token}\nhome = ${machine:account-home}\n"
const aServiceDigest = "sha256:dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd"
const healthDigest = "@sha256:aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa"
const handedNotifier = `{"module":"notify","version":"1",
"config":{"width":{"type":"number","description":"How wide a notification is, in pixels.","default":250}},
"build":{"artifacts":[{"name":"files","kind":"archive","from":"files"}]},
"resources":[
{"id":"configuration-dir","type":"directory","path":"${machine:account-home}/.config/notify","owner":"${machine:account}","mode":"0755"},
{"id":"configuration","type":"file","path":"${machine:account-home}/.config/notify/rc","owner":"${machine:account}","mode":"0644","template":"files/rc"},
{"id":"legacy","type":"file","path":"/etc/notify.conf","content":"plain = yes\n"},
{"id":"notifier","type":"service","unit":"notify.service","scope":"user","user":"${machine:account}","reload-on":["configuration"]}]}`
func notifierMoved() string {
return strings.Replace(handedNotifier, `"version":"1",`, `"version":"1","moved":true,`, 1)
}
const mainText = `# tidy\nroot = ${dir:conf}\nquoted = \"yes\" é\n`
const extraText = `one\ntwo\n`
func queueList(q string) []any {
if q == "" {
return nil
}
out := []any{}
for _, s := range strings.Split(q, ",") {
out = append(out, s)
}
return out
}
type readManifest struct {
path string // the module.json
manifest catalogue.Manifest
err error
}
// TestEveryCatalogueManifestParses runs the real catalogue through the real gate.
//
// Not a fixture: the point is whether the manifests as written are accepted by the control plane that
// will read them, and a copy of one manifest proves nothing about the other seventy-one.
// catalogueRoot is the catalogue these checks run over: in a merge check the clone the build seat put
// beside this one, elsewhere the copy captured in testdata/beside (internal/beside). A check that only
// ran when somebody remembered a variable was a check nobody ran (novox/hq issue 134), and one that read
// whatever checkout sat beside judged the machine, not the change (novox/hq issue 432): it never skips.
func catalogueRoot(t *testing.T) string {
t.Helper()
return beside.Dir(t, "mesh-catalog")
}
var readOnce sync.Once
var readRoot string
var readAll []readManifest
// The catalogue's foundation modules as they are — in a merge check the catalogue cloned beside it,
// elsewhere the copy captured in testdata/beside (internal/beside, novox/hq issue 432) — parsed by the
// real parser (novox/hq ADR 0100):
// the store and the broker say which of their ports the mesh guards on an adopted node, and the
// filter module loads its table through a unit of its own whose stop deletes only that table.
// moduleTree is where a module is read from: the catalogue beside, with the catalogue as the root. In
// mesh-controller it first looked in the repository's own modules/<module>, where build-agent and
// route-proxy live whole (novox/hq ADR 0420 §1, issue 1019); mesh-sdk has no such directory, and the
// tests that read those two modules stay in mesh-controller (issue 1379).
func moduleTree(t *testing.T, module string) (root, dir string) {
t.Helper()
return beside.Dir(t, "mesh-catalog"), filepath.Join(beside.Catalogue(t), module)
}
const handedBot = `{"module":"bot","version":"1",
"own-secrets":{"token":"${dir:state}/token"},
"build":{"artifacts":[{"name":"files","kind":"archive","from":"files"}]},
"resources":[
{"id":"state","type":"directory","mode":"0700","place":"."},
{"id":"conf","type":"file","path":"/etc/bot/bot.conf","mode":"0640","template":"files/bot.conf"},
{"id":"daemon","type":"process","name":"bot","run":["bot"],"env":{"BOT_TOKEN_FILE":"${dir:state}/token"}}]}`
const notifierText = "width = ${config:width}\nhome = ${machine:account-home}\n"
type hub struct{}
func (hub) Resources(string) ([]map[string]any, bool, error) {
return []map[string]any{{"id": "config", "type": "file", "path": "/etc/wireguard/mesh0.conf",
"content": "[Interface]\n"}}, true, nil
}
func (hub) Listens(string) ([]catalogue.Listening, error) {
return []catalogue.Listening{{Port: 51820, Protocol: "udp", From: catalogue.FromEverywhere}}, nil
}