The review of d5e8458 found that mesh-wireguard, provided by the mesh and never wanted at a fingerprint, kept the kept
declaration read on every node for ever, against ADR 0550 §2 once the lease is written. A selfcompose row shows the
node's own composes it from delivery.ProvidedManifest with nothing running, so the kept read holds nothing of it.
Quick fix: name mesh-wireguard in allHave. Thorough fix: allHave counts every module delivery.ProvidedByMesh says the
mesh provides as covered. Chose thorough: one list of provided modules, mesh-sdk's.
236 lines
11 KiB
Go
236 lines
11 KiB
Go
package main
|
|
|
|
import (
|
|
"context"
|
|
"crypto/ed25519"
|
|
"encoding/json"
|
|
"errors"
|
|
"path/filepath"
|
|
"sort"
|
|
"strings"
|
|
"testing"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/declaration"
|
|
"git.novox.be/novox/mesh-sdk/go/delivery"
|
|
"git.novox.be/novox/mesh-sdk/go/overlay"
|
|
|
|
"github.com/novox/mesh-host/internal/agent"
|
|
"github.com/novox/mesh-host/internal/liveness"
|
|
"github.com/novox/mesh-host/internal/store"
|
|
)
|
|
|
|
func aFileOf(module, id string) json.RawMessage {
|
|
raw, _ := json.Marshal(map[string]any{"id": module + "." + id, "type": "file", "path": "/tmp/r6/" + module + "/" + id,
|
|
"content": "x", "module": module})
|
|
return raw
|
|
}
|
|
|
|
// What runs is the agent's compositions and the kept declaration's resources for each module the agent does not hold;
|
|
// the node's own parts are the kept declaration's until the agent holds the node's composition (ADR 0539 §9). One module
|
|
// under the agent must not make the rest invisible to the liveness, units, accounts, libraries, filling and the take act.
|
|
func TestWhatRunsIsTheAgentsCompositionsAndTheKeptRest(t *testing.T) {
|
|
kept := &declaration.Declaration{Identities: []declaration.Identity{{Module: "db", Name: "mesh-db"}},
|
|
Libraries: []declaration.Library{{Library: "media"}}}
|
|
everything := []delivery.Composed{
|
|
{Node: "n1", Module: "web", Resources: []json.RawMessage{aFileOf("web", "a")}},
|
|
{Node: "n1", Module: "db", Resources: []json.RawMessage{aFileOf("db", "a")}}, // still the kept declaration's
|
|
}
|
|
d, err := whatRuns(everything, false, func() (*declaration.Declaration, error) { return kept, nil })
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(d.Resources) != 2 || len(d.Identities) != 1 || len(d.Libraries) != 1 {
|
|
t.Fatalf("%d resources, %d identities, %d libraries: the kept modules and the node's kept parts are missing",
|
|
len(d.Resources), len(d.Identities), len(d.Libraries))
|
|
}
|
|
|
|
// Once the agent holds the node's composition, its parts are the node's.
|
|
node := delivery.Composed{Node: "n1", Own: delivery.NodeOwn{Identities: []declaration.Identity{{Module: "web", Name: "mesh-web"}}}}
|
|
d, err = whatRuns(append(everything, node), true, func() (*declaration.Declaration, error) { return nil, errors.New("not read") })
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if len(d.Identities) != 1 || d.Identities[0].Name != "mesh-web" || len(d.Libraries) != 0 {
|
|
t.Fatalf("the node's parts: %+v %+v", d.Identities, d.Libraries)
|
|
}
|
|
}
|
|
|
|
// What the take act, the judgement of directories and the filling read is what runs, not the frozen kept declaration.
|
|
func TestDeclaredNowReadsWhatRunsWhenTheAgentIsRunning(t *testing.T) {
|
|
setRunsHere(func(string, ed25519.PublicKey) (*declaration.Declaration, error) {
|
|
return &declaration.Declaration{Version: 1, Resources: nil, Provisioner: "from-agent"}, nil
|
|
})
|
|
defer setRunsHere(nil)
|
|
d, err := declaredNow("/nowhere", nil)
|
|
if err != nil || d.Provisioner != "from-agent" {
|
|
t.Fatalf("declaredNow read %+v (%v), not what runs", d, err)
|
|
}
|
|
|
|
// And the health loop's judges are handed what runs once a module settles (g14 and shanks, 2026-10-30 16:14):
|
|
// restic's composition dropped its process restic.measure, which stayed watched and said down on every look.
|
|
j, err := liveness.Open(filepath.Join(t.TempDir(), "liveness.json"), fakeRuntime{})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
judging = j
|
|
defer func() { judging = nil }()
|
|
process := func(name string) json.RawMessage {
|
|
raw, _ := json.Marshal(map[string]any{"id": "restic." + name, "type": "process", "name": "restic-" + name,
|
|
"source": "https://store.invalid/restic", "digest": "sha256:" + strings.Repeat("a", 64),
|
|
"run": []string{"./restic"}, "module": "restic"})
|
|
return raw
|
|
}
|
|
for _, row := range []struct {
|
|
runs []string
|
|
watch string
|
|
}{{[]string{"night", "measure"}, "restic.measure restic.night"}, {[]string{"night"}, "restic.night"}} {
|
|
var raws []json.RawMessage
|
|
for _, name := range row.runs {
|
|
raws = append(raws, process(name))
|
|
}
|
|
runs, err := whatRuns([]delivery.Composed{{Node: "n1", Module: "restic", Resources: raws}}, false,
|
|
func() (*declaration.Declaration, error) { return nil, errors.New("none kept") })
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
setRunsHere(func(string, ed25519.PublicKey) (*declaration.Declaration, error) { return runs, nil })
|
|
judgeWhatRunsNow(filepath.Join(t.TempDir(), "state.json"), nil)
|
|
st, _ := j.Look(context.Background())
|
|
var watched []string
|
|
for _, r := range st.Resources {
|
|
watched = append(watched, r.ID)
|
|
}
|
|
sort.Strings(watched)
|
|
if got := strings.Join(watched, " "); got != row.watch {
|
|
t.Errorf("running %v, the judge watches %q, want %q", row.runs, got, row.watch)
|
|
}
|
|
}
|
|
|
|
// L3 s9 (ADR 0550 §2): a node whose placed modules all have a have reads the kept declaration no more, though the
|
|
// file is there and readable and the controller's key is dropped; until then it reads it.
|
|
state := filepath.Join(t.TempDir(), "state", "state.json")
|
|
if err := store.SaveDeclared(store.DeclaredPath(state), store.Declared{Declaration: []byte(`{"declaration":1,` +
|
|
`"resources":[{"id":"zsh.rc","type":"file","module":"zsh","path":"/etc/zshrc","content":"x"}]}`)}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if d, err := keptDeclaration(state, nil); err != nil || len(d.Resources) != 1 {
|
|
t.Fatalf("before the read is retired: %+v, %v", d, err)
|
|
}
|
|
placed := delivery.Placement{Node: "n1", Modules: []delivery.PlacedModule{{Module: "zsh"}, {Module: "lemurs"}}}
|
|
// Issue 774, 2026-10-30 19:43: a have naming only a build of the old form (systemd-resolved on shanks, folded in the
|
|
// node's own) keeps the kept declaration read, since it is the one record of what runs of it.
|
|
newForm := delivery.Have{FP: delivery.SourcePrefix2 + strings.Repeat("a", 64), Phase: delivery.PhaseRunning}
|
|
oldForm := delivery.Have{Build: "build-1791404191702719760", Phase: delivery.PhaseRunning}
|
|
for _, row := range []struct {
|
|
name string
|
|
has map[string]delivery.Have
|
|
want bool
|
|
}{
|
|
{"every placed module has a have of the new form", map[string]delivery.Have{"zsh": newForm, "lemurs": newForm}, true},
|
|
{"one has none yet", map[string]delivery.Have{"zsh": newForm}, false},
|
|
{"one runs at a build of the old form", map[string]delivery.Have{"zsh": newForm, "lemurs": oldForm}, false},
|
|
} {
|
|
if got, why := allHave(placed, func(m string) (delivery.Have, bool, error) { h, ok := row.has[m]; return h, ok, nil }); got != row.want {
|
|
t.Errorf("%s: %v (%s)", row.name, got, why)
|
|
}
|
|
}
|
|
// The module the mesh provides never has a fingerprint, and keeps no read (review of issue 774).
|
|
withWireguard := delivery.Placement{Node: "n1", Modules: []delivery.PlacedModule{{Module: "zsh"}, {Module: overlay.Name}}}
|
|
if got, why := allHave(withWireguard, func(m string) (delivery.Have, bool, error) {
|
|
if m == overlay.Name {
|
|
return oldForm, true, nil
|
|
}
|
|
return newForm, true, nil
|
|
}); !got {
|
|
t.Errorf("%s without a fingerprint beside zsh at the new form keeps the read: %s", overlay.Name, why)
|
|
}
|
|
if got, _ := allHave(delivery.Placement{Node: "n1"}, nil); got {
|
|
t.Error("a node with nothing placed retires the read")
|
|
}
|
|
keptRetired.Store(true)
|
|
defer keptRetired.Store(false)
|
|
if _, err := keptDeclaration(state, nil); !errors.Is(err, errKeptRetired) {
|
|
t.Errorf("after the read is retired: %v", err)
|
|
}
|
|
if by, filled := keptOnce(options{state: state}, nil)(); by != nil || filled != nil {
|
|
t.Errorf("after the read is retired the kept declaration was split: %v %v", by, filled)
|
|
}
|
|
}
|
|
|
|
// 1945/AC17 (unit): a resource the node's own composition shares with a running module's composition is declared once,
|
|
// the module's: the declaration joined of both names each file once, so the filling writes it; the node's own resources
|
|
// no module declares stay. (Live from 2026-10-25 12:43: "2 resources declare X" on every node.)
|
|
func TestTheNodesOwnCopyOfAModulesResourceIsLeftOut(t *testing.T) {
|
|
own := delivery.Composed{Node: "n1", Resources: []json.RawMessage{aFileOf("zsh", "rc"), aFileOf("mesh-wireguard", "conf")},
|
|
Filled: []string{"/tmp/r6/zsh/rc"}}
|
|
zsh := delivery.Composed{Node: "n1", Module: "zsh", FP: "src2:aa", Resources: []json.RawMessage{aFileOf("zsh", "rc")},
|
|
Filled: []string{"/tmp/r6/zsh/rc"}}
|
|
d, err := declarationOf(own, zsh)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
at := map[string]int{}
|
|
for _, r := range d.Resources {
|
|
at[r.Target()]++
|
|
}
|
|
if at["/tmp/r6/zsh/rc"] != 1 || at["/tmp/r6/mesh-wireguard/conf"] != 1 || len(d.Resources) != 2 {
|
|
t.Errorf("the joined declaration declares %v, want each path once", at)
|
|
}
|
|
if !d.Fills("/tmp/r6/zsh/rc") {
|
|
t.Errorf("the filled path is no longer named")
|
|
}
|
|
|
|
// The same at the node's own apply: what the others declare is theirs.
|
|
trimmed, dropped := ownWithout(own, []delivery.Composed{zsh})
|
|
if len(trimmed.Resources) != 1 || len(dropped) != 1 || dropped[0] != "zsh.rc (zsh)" {
|
|
t.Errorf("the node's own without zsh's: %d resources, dropped %v", len(trimmed.Resources), dropped)
|
|
}
|
|
if len(own.Resources) != 2 {
|
|
t.Errorf("ownWithout changed its argument")
|
|
}
|
|
// A node's own composition alone is all of it.
|
|
if d, err := declarationOf(own); err != nil || len(d.Resources) != 2 {
|
|
t.Errorf("the node's own alone: %v (%v)", d, err)
|
|
}
|
|
}
|
|
|
|
// 1945/AC18 (unit): the kept declaration's filled paths go to the module whose file is at each path, the node's own to
|
|
// the node's; and what runs from it, joined with the node's composition, names them filled.
|
|
func TestTheKeptDeclarationsFilledPathsGoToTheirModules(t *testing.T) {
|
|
raw := []byte(`{"resources":[
|
|
{"id":"claude-code.settings","type":"file","module":"claude-code","path":"/etc/cc.json"},
|
|
{"id":"screen-lock.conf","type":"file","module":"screen-lock","path":"/etc/lock.conf"},
|
|
{"id":"host.motd","type":"file","path":"/etc/motd"},
|
|
{"id":"zsh.rc","type":"file","module":"zsh","path":"/etc/zshrc"}],
|
|
"filled_files":["/etc/cc.json","/etc/lock.conf","/etc/motd"]}`)
|
|
by, filled := splitKept(raw)
|
|
if len(by["claude-code"]) != 1 || len(by[agent.NodeModule]) != 1 {
|
|
t.Fatalf("resources by module: %v", by)
|
|
}
|
|
if strings.Join(filled["claude-code"], ",") != "/etc/cc.json" || strings.Join(filled["screen-lock"], ",") != "/etc/lock.conf" ||
|
|
strings.Join(filled[agent.NodeModule], ",") != "/etc/motd" || len(filled["zsh"]) != 0 {
|
|
t.Fatalf("filled by module: %v", filled)
|
|
}
|
|
cc := delivery.Composed{Node: "n1", Module: "claude-code", Resources: by["claude-code"], Filled: filled["claude-code"]}
|
|
node := delivery.Composed{Node: "n1"}
|
|
d, err := whatRuns([]delivery.Composed{cc, node}, true, func() (*declaration.Declaration, error) { return nil, errors.New("not read") })
|
|
if err != nil || !d.Fills("/etc/cc.json") {
|
|
t.Fatalf("what runs does not name claude-code's file filled: %v (%v)", d, err)
|
|
}
|
|
if splitBy, _ := splitKept([]byte("not json")); splitBy != nil {
|
|
t.Errorf("a kept declaration that does not read split into %v", splitBy)
|
|
}
|
|
}
|
|
|
|
// 1945/AC17 (unit, review): a resource of the node's own that shares only its target with a module's (another id: a
|
|
// unit or a container of the same name) is not the module's, and is neither dropped nor left to be removed.
|
|
func TestASharedTargetWithAnotherIDIsNotDropped(t *testing.T) {
|
|
ownFile, _ := json.Marshal(map[string]any{"id": "host.zshrc", "type": "file", "path": "/tmp/r6/zsh/rc", "content": "x"})
|
|
own := delivery.Composed{Node: "n1", Resources: []json.RawMessage{ownFile}}
|
|
zsh := delivery.Composed{Node: "n1", Module: "zsh", FP: "src2:aa", Resources: []json.RawMessage{aFileOf("zsh", "rc")}}
|
|
trimmed, dropped := ownWithout(own, []delivery.Composed{zsh})
|
|
if len(dropped) != 0 || len(trimmed.Resources) != 1 {
|
|
t.Errorf("a resource sharing only its target was dropped: %v", dropped)
|
|
}
|
|
}
|