mesh/merge-gate head 1cc588ebc060 merged into trunk 1203f96a24c8 — pass: builds mesh-host → ace, g14, novox, shanks; no bus step; every machine composes wi…
mesh/repo-check head 1cc588ebc060 merged into trunk 1203f96a24c8 — pass: its merge-check.sh passed
mesh/delivery delivered
The provisioner (ADR 0349 §5) needs each file it places to say whether it is owned, a seed or managed, and the node-engine needs to know which paths it fills. The words map onto today's forms (drift written back, create-once, into), so no write changes; the reader accepts the fields and states OwnershipContract before any controller sends them. Issue 667. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
131 lines
5.8 KiB
Go
131 lines
5.8 KiB
Go
package declaration
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// Defends novox/hq ADR 0349 §5 (issue 667): a file the provisioner places says who owns it at runtime
|
|
// — owned, seed or managed — and the declaration names the files the provisioner fills. The words are
|
|
// read and judged here before any controller sends them.
|
|
|
|
func TestAFileSaysWhoOwnsItAtRuntime(t *testing.T) {
|
|
d, err := Parse([]byte(`{"declaration":1,"resources":[
|
|
{"id":"o","type":"file","path":"/etc/o","content":"a","owner-at-runtime":"owned"},
|
|
{"id":"s","type":"file","path":"/etc/s","content":"a","owner-at-runtime":"seed"},
|
|
{"id":"s2","type":"file","path":"/etc/s2","content":"a","owner-at-runtime":"seed","create-once":true},
|
|
{"id":"mj","type":"file","path":"/etc/mj","into":"json","content":"{\"k\":1}","owner-at-runtime":"managed"},
|
|
{"id":"mb","type":"file","path":"/etc/mb","into":"block","content":"a\n","owner-at-runtime":"managed"}
|
|
]}`))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
want := map[string]string{"o": RuntimeOwned, "s": RuntimeSeed, "s2": RuntimeSeed, "mj": RuntimeManaged,
|
|
"mb": RuntimeManaged}
|
|
for _, r := range d.Resources {
|
|
f := r.(*File)
|
|
if f.OwnerAtRuntime != want[f.ID] || f.Ownership() != want[f.ID] {
|
|
t.Errorf("%s: read as %q, ownership %q; want %q", f.ID, f.OwnerAtRuntime, f.Ownership(), want[f.ID])
|
|
}
|
|
}
|
|
}
|
|
|
|
// A file that does not say is owned by its old form, so the word means exactly what that form does.
|
|
func TestAFileThatDoesNotSayIsOwnedByItsOldForm(t *testing.T) {
|
|
d, err := Parse([]byte(`{"declaration":1,"resources":[
|
|
{"id":"o","type":"file","path":"/etc/o","content":"a"},
|
|
{"id":"s","type":"file","path":"/etc/s","content":"a","create-once":true},
|
|
{"id":"mj","type":"file","path":"/etc/mj","into":"json","content":"{}"},
|
|
{"id":"mb","type":"file","path":"/etc/mb","into":"block","content":"a\n"}
|
|
]}`))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
want := map[string]string{"o": RuntimeOwned, "s": RuntimeSeed, "mj": RuntimeManaged, "mb": RuntimeManaged}
|
|
for _, r := range d.Resources {
|
|
f := r.(*File)
|
|
if f.OwnerAtRuntime != "" || f.Ownership() != want[f.ID] {
|
|
t.Errorf("%s: said %q, ownership %q; want unsaid and %q", f.ID, f.OwnerAtRuntime, f.Ownership(), want[f.ID])
|
|
}
|
|
if f.Seeded() != (want[f.ID] == RuntimeSeed) {
|
|
t.Errorf("%s: seeded %v", f.ID, f.Seeded())
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAnOwnershipThatContradictsTheFileIsRefused(t *testing.T) {
|
|
for name, c := range map[string]struct{ resource, refusal string }{
|
|
"another word": {`{"id":"f","type":"file","path":"/etc/x","content":"a","owner-at-runtime":"mine"}`,
|
|
`owner-at-runtime "mine"`},
|
|
"owned and create-once": {`{"id":"f","type":"file","path":"/etc/x","content":"a","owner-at-runtime":"owned","create-once":true}`,
|
|
"create-once"},
|
|
"owned and into": {`{"id":"f","type":"file","path":"/etc/x","into":"json","content":"{}","owner-at-runtime":"owned"}`,
|
|
"into"},
|
|
"seed and into": {`{"id":"f","type":"file","path":"/etc/x","into":"block","content":"a","owner-at-runtime":"seed"}`,
|
|
"into"},
|
|
"managed and whole": {`{"id":"f","type":"file","path":"/etc/x","content":"a","owner-at-runtime":"managed"}`,
|
|
"into"},
|
|
"managed and create-once": {`{"id":"f","type":"file","path":"/etc/x","into":"json","content":"{}","owner-at-runtime":"managed","create-once":true}`,
|
|
"create-once"},
|
|
// The rule that was there before stays: a file written into is never sealed or created once.
|
|
"managed and sealed": {`{"id":"f","type":"file","path":"/etc/x","into":"json","content":"{}","sealed":"x","owner-at-runtime":"managed"}`,
|
|
"sealed"},
|
|
} {
|
|
_, err := Parse([]byte(`{"declaration":1,"resources":[` + c.resource + `]}`))
|
|
if err == nil || !strings.Contains(err.Error(), c.refusal) {
|
|
t.Errorf("%s: want a refusal naming %q, got %v", name, c.refusal, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestTheDeclarationNamesTheFilesTheProvisionerFills(t *testing.T) {
|
|
d, err := Parse([]byte(`{"declaration":1,"filled_files":["/etc/a","/etc/b"],"resources":[
|
|
{"id":"a","type":"file","path":"/etc/a","content":"a","owner-at-runtime":"owned"},
|
|
{"id":"b","type":"file","path":"/etc/b","into":"block","content":"b\n","owner-at-runtime":"managed"},
|
|
{"id":"c","type":"file","path":"/etc/c","content":"c"}
|
|
]}`))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(d.Filled) != 2 || d.Filled[0] != "/etc/a" || d.Filled[1] != "/etc/b" {
|
|
t.Fatalf("filled files read as %v", d.Filled)
|
|
}
|
|
if !d.Fills("/etc/a") || !d.Fills("/etc/b") || d.Fills("/etc/c") {
|
|
t.Errorf("Fills: a %v, b %v, c %v", d.Fills("/etc/a"), d.Fills("/etc/b"), d.Fills("/etc/c"))
|
|
}
|
|
|
|
// A declaration that names none fills none, as every declaration before this did.
|
|
d, err = Parse([]byte(`{"declaration":1,"resources":[{"id":"c","type":"file","path":"/etc/c","content":"c"}]}`))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if d.Filled != nil || d.Fills("/etc/c") {
|
|
t.Errorf("a declaration naming no filled files fills %v", d.Filled)
|
|
}
|
|
}
|
|
|
|
func TestAFilledFileMustBeADeclaredFileThatSaysItsOwner(t *testing.T) {
|
|
for name, c := range map[string]struct{ filled, resources, refusal string }{
|
|
"not declared": {`["/etc/none"]`,
|
|
`{"id":"a","type":"file","path":"/etc/a","content":"a","owner-at-runtime":"owned"}`,
|
|
`"/etc/none"`},
|
|
"a directory, not a file": {`["/etc/d"]`,
|
|
`{"id":"d","type":"directory","path":"/etc/d"}`,
|
|
`"/etc/d"`},
|
|
"says no owner": {`["/etc/a"]`,
|
|
`{"id":"a","type":"file","path":"/etc/a","content":"a"}`,
|
|
"owner-at-runtime"},
|
|
"named twice": {`["/etc/a","/etc/a"]`,
|
|
`{"id":"a","type":"file","path":"/etc/a","content":"a","owner-at-runtime":"owned"}`,
|
|
"twice"},
|
|
"no path": {`[""]`,
|
|
`{"id":"a","type":"file","path":"/etc/a","content":"a","owner-at-runtime":"owned"}`,
|
|
"no path"},
|
|
} {
|
|
_, err := Parse([]byte(`{"declaration":1,"filled_files":` + c.filled + `,"resources":[` + c.resources + `]}`))
|
|
if err == nil || !strings.Contains(err.Error(), c.refusal) {
|
|
t.Errorf("%s: want a refusal naming %q, got %v", name, c.refusal, err)
|
|
}
|
|
}
|
|
}
|