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{}}, // 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{}}, {"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") } // 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"}}}, }) 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) != 3 { t.Errorf("a node seat's holder is %v, keys %v", h, held) } 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", Node: "ace", At: at} end := start end.Event, end.At, end.Outcome, end.Artifacts = BuildEndedEvent, at.Add(3*time.Minute), "done", []BuiltDigest{{Name: "image", Digest: "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 { 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) } bad := end bad.Outcome = "" if _, err := EncodeBuildLogEntry(bad); err == nil { t.Error("an end with no outcome was encoded") } 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) } } } func jsonOf(v any) []byte { b, _ := json.Marshal(v) return b }