mesh/merge-gate head e75e793a4b3b merged into trunk cfb90d7b2f96 — pass: builds gitea → novox; no bus step; every machine composes with the change as it di…
mesh/repo-check head e75e793a4b3b merged into trunk cfb90d7b2f96 — pass: its merge-check.sh passed, each part in its toolchain: merge-check.sh (go), merge-…
mesh/delivery delivered
The file was merged by the controller (merge, takes-config), which writes a
config change only on the node's next send (ADR 0348 §10): the operator's
Approve of the mesh-delivery repository set the value and nothing made it.
A plain template saying ${config:repositories} is filled by the provisioner
client on config-changed, and the node-engine reruns the run-once program
that names the file under restart-on.
108 lines
4.1 KiB
Go
108 lines
4.1 KiB
Go
package main
|
|
|
|
import (
|
|
"encoding/json"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// 1124/AC1: the manifest declares `repositories` under config as a trusted object value with an empty default, as
|
|
// it declares agent-read-teams (ADR 0365 §3, ADR 0348), renders it into a file of its own, and runs this program
|
|
// at the module's start and whenever that file changes, as a run-once process (1124/AC6: its failed exit is the
|
|
// node-engine's gate, ADR 0136) declared last, so that gate stops nothing of the module after it.
|
|
func TestManifestDeclaresTheValueAndItsTrigger(t *testing.T) {
|
|
raw, err := os.ReadFile("../../module.json")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var m struct {
|
|
Config map[string]map[string]any `json:"config"`
|
|
Resources []map[string]any `json:"resources"`
|
|
Build struct {
|
|
Artifacts []map[string]any `json:"artifacts"`
|
|
} `json:"build"`
|
|
}
|
|
if err := json.Unmarshal(raw, &m); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
v := m.Config[ConfigKey]
|
|
if v == nil || v["type"] != "object" || v["trusted"] != true {
|
|
t.Fatalf("config %s = %v, want a trusted object", ConfigKey, v)
|
|
}
|
|
if d, ok := v["default"].(map[string]any); !ok || len(d) != 0 {
|
|
t.Fatalf("its default is %v, want {}", v["default"])
|
|
}
|
|
var file, process map[string]any
|
|
for _, r := range m.Resources {
|
|
if r["id"] == "repositories-file" {
|
|
file = r
|
|
}
|
|
}
|
|
if file == nil || file["type"] != "file" {
|
|
t.Fatalf("no file repositories-file")
|
|
}
|
|
// 1128/AC1: a plain template the provisioner client fills, so an approved value lands at once: a file the
|
|
// controller merges (merge, takes-config) takes a config change only on the node's next send (ADR 0348 §10).
|
|
if file["merge"] != nil || file["takes-config"] != nil {
|
|
t.Errorf("repositories-file is merged by the controller (merge %v, takes-config %v), want a plain template",
|
|
file["merge"], file["takes-config"])
|
|
}
|
|
text, err := os.ReadFile("../../" + file["template"].(string))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if !strings.Contains(string(text), "${config:"+ConfigKey+"}") {
|
|
t.Fatalf("the template %s does not say ${config:%s}: %s", file["template"], ConfigKey, text)
|
|
}
|
|
filled := filepath.Join(t.TempDir(), "repositories.json")
|
|
value := `{"mesh-delivery":{"private":false,"description":"d"}}`
|
|
if err := os.WriteFile(filled, []byte(strings.ReplaceAll(string(text), "${config:"+ConfigKey+"}", value)), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if v, err := ReadValue(filled); err != nil || len(v) != 1 || v["mesh-delivery"].Description != "d" {
|
|
t.Errorf("the filled template reads as %v, %v; want mesh-delivery", v, err)
|
|
}
|
|
for _, r := range m.Resources {
|
|
if r["type"] == "process" && has(strs(r["restart-on"]), file["id"].(string)) {
|
|
process = r
|
|
}
|
|
}
|
|
if process == nil {
|
|
t.Fatalf("no process runs again when %s changes", file["id"])
|
|
}
|
|
if process["run-once"] != true {
|
|
t.Errorf("the process is not run-once: %v", process)
|
|
}
|
|
if last := m.Resources[len(m.Resources)-1]; last["id"] != process["id"] {
|
|
t.Errorf("the last resource is %v, want %v: a refused step gates whatever of the module comes after it", last["id"], process["id"])
|
|
}
|
|
if process["health"] != nil {
|
|
t.Errorf("the process declares a health check: %v", process["health"])
|
|
}
|
|
if strings.Join(strs(process["run"]), " ") != "./repositories sync" || process["artifact"] != "repositories" {
|
|
t.Errorf("the process runs %v from %v", process["run"], process["artifact"])
|
|
}
|
|
env, _ := process["env"].(map[string]any)
|
|
for _, k := range []string{"MESH_GITEA_URL", "MESH_GITEA_ADMIN_USER", "MESH_GITEA_ADMIN_PASSWORD_FILE", "MESH_REPOSITORIES_FILE", "MESH_REPOSITORIES_OUTCOME"} {
|
|
if env[k] == nil {
|
|
t.Errorf("the process is not given %s", k)
|
|
}
|
|
}
|
|
found := false
|
|
for _, a := range m.Build.Artifacts {
|
|
if a["name"] == "repositories" && a["from"] == "cmd/repositories" && a["binary"] == "repositories" {
|
|
found = true
|
|
}
|
|
}
|
|
if !found {
|
|
t.Error("no bundle builds cmd/repositories")
|
|
}
|
|
// Like agent-read-teams: the same kind of value, the same way of applying it.
|
|
teams := m.Config["agent-read-teams"]
|
|
if teams["trusted"] != v["trusted"] || teams["type"] != v["type"] {
|
|
t.Errorf("repositories is declared unlike agent-read-teams: %v vs %v", v, teams)
|
|
}
|
|
}
|