Files
mesh-host/packaging/operator_keys_test.go
T
jschoubben ece79788a3 A-112: the node-engine reads recovery-wait-minutes from a file its manifest declares, 90 when missing
The manifest declares the file recovery-wait (content ${config:recovery-wait-minutes}); the node-engine reads it at its start and
does not call stdio.Config, which needs the runtime that launches a bundle.

Builds: design 47 G §633, A-112
Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
2026-11-12 00:21:10 +01:00

118 lines
5.6 KiB
Go

package packaging_test
import (
"encoding/json"
"os"
"slices"
"strings"
"testing"
"git.novox.be/novox/mesh-sdk/go/catalogue"
)
// The operator's public keys reach every node as a file beside the state file, fed by a setting of the node-engine
// (novox/hq ADR 0539 clauses 3 and 11): the node agent reads it, and a composition made by hand is applied only when one
// of these keys signed it. The setting may be empty, and then no operator key is trusted.
func TestTheManifestDeclaresTheOperatorKeysFileFedByASetting(t *testing.T) {
raw, err := os.ReadFile("../module.json")
if err != nil {
t.Fatal(err)
}
if _, err := catalogue.Decode(raw); err != nil {
t.Fatalf("the manifest no longer parses: %v", err)
}
var m struct {
Config map[string]struct {
Type string `json:"type"`
Default any `json:"default"`
} `json:"config"`
Resources []struct {
ID string `json:"id"`
Type string `json:"type"`
Path string `json:"path"`
Mode string `json:"mode"`
Content string `json:"content"`
} `json:"resources"`
}
if err := json.Unmarshal(raw, &m); err != nil {
t.Fatal(err)
}
setting, ok := m.Config["operator-signers"]
if !ok || setting.Type != "string" || setting.Default != "" {
t.Fatalf("the setting operator-signers is %+v: a string that may be empty", setting)
}
for _, r := range m.Resources {
if r.ID != "operator-keys" {
continue
}
if r.Type != "file" || r.Path != "/var/lib/mesh-host/operator-keys" || r.Mode != "0644" ||
!strings.Contains(r.Content, "${config:operator-signers}") {
t.Fatalf("the file is %+v: it is declared beside the state file, 0644, its content the setting", r)
}
return
}
t.Fatal("module.json declares no operator-keys file")
}
// The node agent hears the end of a backup on the event of node-backup's holder, so the manifest consumes it.
func TestTheManifestConsumesTheBackupEvent(t *testing.T) {
raw, err := os.ReadFile("../module.json")
if err != nil {
t.Fatal(err)
}
var m struct {
Consumes []string `json:"consumes"`
Invokes []string `json:"invokes"`
Config map[string]struct {
Default any `json:"default"`
} `json:"config"`
}
if err := json.Unmarshal(raw, &m); err != nil {
t.Fatal(err)
}
// Design 47 G §630, §633, A-77: the recovery module's three verbs it asks, and its bound as a config key of 90.
for _, v := range []string{"seat:mesh-recovery.kept-points", "seat:mesh-recovery.restore-kept", "seat:mesh-recovery.snapshot-kept"} {
if !slices.Contains(m.Invokes, v) {
t.Errorf("invokes %v lacks %s", m.Invokes, v)
}
}
if !strings.Contains(string(raw), `"content": "${config:recovery-wait-minutes}\n"`) {
t.Errorf("module.json declares no file with the content ${config:recovery-wait-minutes} (design 47 G A-112)")
}
if m.Config["recovery-wait-minutes"].Default != float64(90) {
t.Errorf("config recovery-wait-minutes is %+v, want default 90", m.Config["recovery-wait-minutes"])
}
// The backup event, the status view's settled, and the two events of the seat that serves the seat list (ADR 0636 §4), the keeper's ask (design 47 G §387) and the seat's running (§199).
if want := []string{"mesh-catalog.seat-definition-changed", "mesh-catalog.seat-definition-removed", "mesh-conditions.findings-asked", "mesh-delivery-view.settled", "mesh-recovery.ended", "mesh-recovery.restored", "node-engine.running", "restic.backed-up"}; !slices.Equal(m.Consumes, want) {
t.Fatalf("consumes %v, want %v", m.Consumes, want)
}
// 774: the acts bucket mesh-host_acts is not declared as state for now: a declared state of a module with no account
// of its own is carried by every node's tool runner, which would be a second writer of the engine's own keys and
// has the grants part refuse the whole user list (2026-10-30). It comes back once the runtime never carries the engine.
parsed, err := catalogue.Decode(raw)
if err != nil {
t.Fatal(err)
}
// ADR 0614 §4: the node-engine's own manifest uses only words every running engine reads (the baseline, what an engine
// stating none is read as), so its new build is never held for a word only it reads.
if needs, err := catalogue.ManifestNeeds(raw); err != nil || len(needs) == 0 {
t.Fatalf("the manifest's needs: %v, %v", needs, err)
} else if missing := catalogue.MissingManifestReads(needs, nil); len(missing) != 0 {
t.Errorf("the manifest needs %v, which an engine stating nothing does not read: its new build would be held", missing)
}
// 774 (the alert table): the state health, one key per node, the node's own health report, kept until
// written again (no TTL: a node that stops writing keeps its last word, which the watchman reads with its time).
// ADR 0577 clause 1: and the state data, one key per module placed on the node that declares data items, kept until
// the module leaves the node; created by the grants part from this manifest as health is.
// ADR 0565 clause 6: and the state packages, one key per node, its compositions' declared packages, read by pacman.
// ADR 0564 §2, ADR 0636 §1: and the state findings, the node's finding that it waits for the seat list (no TTL: the
// finding stays until the first full read deletes its key).
// Design 47 G A-92: and the state bounds, each key a wait's bound with its own lifetime (packaging/bounds_state_test.go).
if len(parsed.State) != 5 || parsed.State[0].Name != "health" || parsed.State[0].TTLSeconds != 0 ||
parsed.State[1].Name != "findings" || parsed.State[1].TTLSeconds != 0 ||
parsed.State[2].Name != "data" || parsed.State[2].TTLSeconds != 0 || parsed.State[3].Name != "packages" ||
parsed.State[3].TTLSeconds != 0 || parsed.State[4].Name != "bounds" {
t.Fatalf("the manifest declares the state %+v", parsed.State)
}
}