Files
mesh-sdk/go/delivery/control_records_test.go
T
jschoubben 79ad8b539c Name the expired enrolment's issued event by its id, and carry a node's login fall back in its health report
Blocker 6: enrolment-expired carries the enrolment-issued event's message id (IssuedID), since a bundle's publish
answers no stream sequence and ADR 0587 §2 asks only that the event be named; it gains a codec refusing no event named.
Blocker 7: NodeHealth gains Fallback (node, the login fallen back from, why, at), from which the watchman raises
node-account-fallback (ADR 0583 §6); a fall back naming another node is refused. A token missing its signer now says
the vault's signing key, not the retired word. Tested as rows of TestNodeRecord and the control records test; no new
test function (1438 unit tests).

(issue 774, review round 1)

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-02 01:41:36 +01:00

193 lines
11 KiB
Go

package delivery
import (
"encoding/json"
"strings"
"testing"
"time"
)
// ADR 0565 clauses 2 and 6 (issue 1953 L5-S2): the control records' layouts read what their writers write. The
// packages value is the controller's declared-packages mirror's, as its writer's own test pins it (mesh-controller
// internal/inventory/declared_packages_test.go at 0fbb5ef3); the config declaration is its declared-config mirror's
// (DeclaredMirrored); a pull request's key is mesh-config's form.
func TestTheControlRecordsReadTheirWritersValues(t *testing.T) {
for _, c := range []struct {
name string
raw string
into any
}{
{"declared packages", `{"node":"one","packages":[{"package":"curl","modules":["backup","web"],"absent":false},` +
`{"package":"nano","modules":["web"],"absent":true}]}`, &DeclaredPackages{}},
{"a config candidate", `{"v":1,"pull":"novox/mesh-catalog#42","head":"abc","declares":{"mail":{"quota":{"kind":"config",` +
`"default":10,"why":"a quota","type":"integer","minimum":1}}},"at":"2026-10-29T12:00:00Z"}`, &ConfigCandidate{}},
{"a held value", `{"pull":"novox/mesh-catalog#42","head":"abc","declaration":"sha256:d","node":"novox","module":"mail",` +
`"key":"quota","value":20,"by":"person.jochen","via":"telegram","why":"more room","approved":"the operator",` +
`"proposal":"p-1","at":"2026-10-29T12:00:00Z"}`, &KeptValue{}},
// A node's health report as mesh-host 65fbc1e6 encodes it (its test's look, struct order kept).
{"a node's health report", `{"v":1,"node":"anchor","network":{"state":"unhealthy","since":"2026-10-29T20:00:00Z",` +
`"parts":[{"part":"resolv-conf","state":"unhealthy","writer":"NetworkManager","owner":"dnsmasq","since":"2026-10-29T20:00:00Z"}]},` +
`"peers":[{"key":"A=","addresses":["10.10.0.2/32"],"seen":false},{"key":"B=","seen":true}],` +
`"units":{"state":"degraded","failed":[{"unit":"cups.service","scope":"system","since":"2026-10-29T20:00:00Z"}]},` +
`"relogin":[{"account":"jochen","module":"brother","resource":"brother.scanner","reason":"relogin needed: the session began before scanner","since":"2026-10-29T20:00:00Z"}],` +
`"found":[{"module":"photos","resource":"photos.library","path":"/srv/photos","reason":"used as found: owned by root","since":"2026-10-29T20:00:00Z"}],` +
`"at":"2026-10-29T20:00:00Z"}`, &NodeHealth{}},
// The engine's verdict on an account declared never to become root (ADR 0266), and its contract.
{"a node's health report, the agent account judged", `{"v":1,"node":"anchor","root":[{"account":"agent",` +
`"module":"claude-code","resource":"claude-code.agent","state":"healthy","since":"2026-10-29T20:00:00Z"}],` +
`"contract":13,"at":"2026-10-29T20:00:00Z"}`, &NodeHealth{}},
// A part pointing at another machine names it (issue 774): the hub's tunnel, held at the hub's machine.
{"a node's health report, a part pointing at the hub", `{"v":1,"node":"anchor","network":{"state":"unhealthy",` +
`"since":"2026-10-29T20:00:00Z","parts":[{"part":"tunnel","state":"unhealthy","reason":"the tunnel to the hub has not handshaken for over five minutes",` +
`"toward":["hub"],"targets":["novox"],"since":"2026-10-29T20:00:00Z"}]},"at":"2026-10-29T20:00:00Z"}`, &NodeHealth{}},
// A node fallen back to its old login (ADR 0583 §6), which the watchman raises as node-account-fallback.
{"a node's health report, fallen back to its old login", `{"v":1,"node":"anchor","fallback":{"node":"anchor",` +
`"from":"node.anchor--b","why":"the new login's test-dial was refused","at":"2026-11-02T01:00:00Z"},` +
`"at":"2026-11-02T01:00:00Z"}`, &NodeHealth{}},
{"the predefined list", `{"v":1,"commit":"abc","at":"2026-10-29T12:00:00Z","list":{"domain":{"type":"string"}}}`, &PredefinedKept{}},
} {
if err := json.Unmarshal([]byte(c.raw), c.into); err != nil {
t.Fatalf("%s: %v", c.name, err)
}
again, _ := json.Marshal(c.into)
var want, got any
_ = json.Unmarshal([]byte(c.raw), &want)
_ = json.Unmarshal(again, &got)
if string(jsonOf(want)) != string(jsonOf(got)) || ((c.name == "declared packages" || strings.HasPrefix(c.name, "a node's health report")) && string(again) != c.raw) {
t.Errorf("%s reads back as %s", c.name, again)
}
}
if a, b := (NodeHealth{Node: "anchor", At: time.Now()}), (NodeHealth{Node: "anchor"}); !SameNodeHealth(a, b) ||
SameNodeHealth(a, NodeHealth{Node: "anchor", Peers: []PeerSeen{{Key: "A="}}}) {
t.Error("two reports differing only in when they were written are not the same, or differing ones are")
}
if _, err := EncodeNodeHealth(NodeHealth{}); err == nil {
t.Error("a report naming no node was encoded")
}
if _, err := EncodeNodeHealth(NodeHealth{Node: "anchor", Fallback: &LoginFallback{Node: "ace", From: "node.anchor--b",
Why: "refused", At: time.Now()}}); err == nil {
t.Error("a fall back naming another node was encoded")
}
// ADR 0571 clause 4: who holds which seat, from placement; a node seat by its node.
held := SeatHoldersOf(map[string]Placement{
"ace": {Node: "ace", Holds: []Holding{{Claim: "mesh-dns-resolver", Scope: "mesh", Module: "dnsmasq"}, {Claim: "node-files", Scope: "node", Module: "mesh-files"}}},
"novox": {Node: "novox", Holds: []Holding{{Claim: "mesh-dns-resolver", Scope: "mesh", Module: "dnsmasq"}, {Claim: "mesh-broker", Scope: "mesh", Module: "nats"},
{Claim: "photo-gallery", Scope: "mesh", Module: "photos"}}},
})
if o := held["photo-gallery"]; !o.Outside || len(o.Holders) != 1 || held["mesh-broker"].Outside {
t.Errorf("a claim outside the set is %+v", o)
}
keys := map[string][]byte{}
for k, v := range held {
keys[k], _ = json.Marshal(v)
}
if h, err := HoldersOf(keys, "mesh-dns-resolver", ""); err != nil || len(h) != 2 || h[0].Node != "ace" {
t.Errorf("a replicated seat's holders are %v, %v", h, err)
}
if h, _ := HoldersOf(keys, "node-files", "ace"); len(h) != 1 || h[0].Module != "mesh-files" || len(held) != 4 {
t.Errorf("a node seat's holder is %v, keys %v", h, held)
}
// ADR 0571 clauses 4, 5: the modules and the machines, from the indexer's and the placement seat's keys.
if m, err := ModuleNames(map[string][]byte{"letta": nil, "gitea": nil}); err != nil || strings.Join(m, ",") != "gitea,letta" {
t.Errorf("the modules are %v, %v", m, err)
}
if _, err := ModuleNames(map[string][]byte{"a.b": nil}); err == nil {
t.Error("a key that is no module's was read as one")
}
if n := MachineNames(map[string][]byte{"placement.novox": nil, "placement.ace": nil, "ace.node": nil, "ace.app": nil, "g14.app": nil, "proposal.p1": nil}); strings.Join(n, ",") != "ace,novox" {
t.Errorf("the machines are %v", n)
}
if h, _ := HoldersOf(keys, "node-files", "novox"); h != nil {
t.Errorf("a node seat nobody holds there is held by %v", h)
}
// ADR 0571 clause 1: the builder's log, a start and an end, and the duration between them.
at := time.Date(2026, 10, 29, 22, 0, 0, 0, time.UTC)
start := BuildLogEntry{Event: BuildStartedEvent, Module: "letta", FP: "src2:a", Source: "abc", Repository: "novox/mesh-catalog",
Directory: "modules/letta", Commit: "abc123", Node: "ace", At: at}
end := start
end.Event, end.At, end.Outcome, end.Manifest, end.Against = BuildEndedEvent, at.Add(3*time.Minute), "done", "sha256:m", []string{"artifact-store://base/image@sha256:b"}
end.Made = []BuiltArtifact{{Name: "image", Kind: "image", Digest: "sha256:1", Reference: "artifact-store://letta/image@sha256:1"}}
for _, e := range []BuildLogEntry{start, end} {
raw, err := EncodeBuildLogEntry(e)
back, derr := DecodeBuildLogEntry(raw)
if err != nil || derr != nil || back.Event != e.Event || back.Outcome != e.Outcome || back.Directory != "modules/letta" ||
back.Repository != "novox/mesh-catalog" || back.Commit != "abc123" {
t.Errorf("%s read back as %+v, %v %v", e.Event, back, err, derr)
}
}
if d, ok := BuildDuration(start, end); !ok || d != 3*time.Minute {
t.Errorf("the build took %s, %v", d, ok)
}
older := end
older.At, older.FP = at.Add(-time.Hour), "src2:b"
other := end
other.Module = "mesh-catalog"
if n := NewestPerModule([]BuildLogEntry{older, end, other}); n["letta"].FP != "src2:a" || len(n) != 2 {
t.Errorf("each module's newest is %v", n)
}
if e := EndedSince([]BuildLogEntry{end, start, older}, at.Add(-2*time.Hour)); len(e) != 2 || e[0].FP != "src2:b" {
t.Errorf("the ended builds since are %v", e)
}
if k, err := BuildLogKey("letta"); k != "letta" || err != nil || BuildLogRead != "mesh-delivery-builder.build-log" {
t.Errorf("the key is %q, %v", k, err)
}
if _, err := BuildLogKey("a.b"); err == nil {
t.Error("a module with a dot was taken as a key")
}
for _, edit := range []func(*BuildLogEntry){func(e *BuildLogEntry) { e.Outcome = "" }, func(e *BuildLogEntry) { e.Commit = "" },
func(e *BuildLogEntry) { e.Event, e.Outcome, e.Against = BuildStartedEvent, "", nil }} {
bad := end
edit(&bad)
if _, err := EncodeBuildLogEntry(bad); err == nil {
t.Errorf("%+v was encoded", bad)
}
}
if k, err := HeldValueKey("novox/mesh-catalog#42", "novox", "mail", "quota"); k != "novox.mesh-catalog.42.novox.mail.quota" || err != nil {
t.Errorf("a held value's key is %q, %v", k, err)
}
for pull, want := range map[string]string{"novox/mesh-catalog#42": "novox.mesh-catalog.42", "x#1": "", "a/b": "", "a/b.c#1": ""} {
if got, err := ConfigCandidateKey(pull); got != want || (want == "") != (err != nil) {
t.Errorf("%s keyed %q, %v", pull, got, err)
}
}
// ADR 0565 §6: the engine works out its node's key of `packages` from the compositions it composed, the mirror's bytes
// for the same resources (mesh-controller internal/inventory/declared_packages_test.go aNodesPackages at 0fbb5ef3):
// a module's own composition, the node's own with a folded module's resource named by its id alone, and a file.
res := func(s ...string) []json.RawMessage {
var out []json.RawMessage
for _, r := range s {
out = append(out, json.RawMessage(r))
}
return out
}
comps := []Composed{
{Node: "one", Module: "web", Resources: res(`{"id":"pkg-curl","type":"package","package":"curl"}`,
`{"id":"pkg-nano","type":"package","package":"nano","absent":true}`, `{"id":"conf","type":"file","path":"/etc/web.conf"}`)},
{Node: "one", Resources: res(`{"id":"backup.pkg-curl","type":"package","package":"curl"}`)},
}
want := `{"node":"one","packages":[{"package":"curl","modules":["backup","web"],"absent":false},` +
`{"package":"nano","modules":["web"],"absent":true}]}`
got, err := DeclaredPackagesOf("one", comps)
if raw, _ := json.Marshal(got); err != nil || string(raw) != want {
t.Errorf("the node's packages are %s (%v)", raw, err)
}
if none, _ := DeclaredPackagesOf("one", nil); string(jsonOf(none)) != `{"node":"one","packages":[]}` || EnginePackagesRead != "mesh-host.packages" {
t.Errorf("a node declaring no package is %s", jsonOf(none))
}
// Their difference with the mirror: a package only one side holds, and one whose modules differ; none when they agree.
mirror := DeclaredPackages{Node: "one", Packages: []PackageDeclared{{Package: "curl", Modules: []string{"web"}},
{Package: "nano", Modules: []string{"web"}, Absent: true}, {Package: "vim", Modules: []string{"edit"}}}}
if d := DiffDeclaredPackages(got, mirror); len(d) != 2 || d[0].Package != "curl" || strings.Join(d[0].Engine, ",") != "backup,web" ||
d[1].Package != "vim" || len(d[1].Engine) != 0 || d[1].Mirror[0] != "edit" {
t.Errorf("the difference is %+v", d)
}
if d := DiffDeclaredPackages(got, got); d == nil || len(d) != 0 {
t.Errorf("one declaration differs from itself: %+v", d)
}
}
func jsonOf(v any) []byte {
b, _ := json.Marshal(v)
return b
}