Files
mesh-catalog/modules/gitea/cmd/repositories/manifest_test.go
T
jschoubben e75e793a4b
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
gitea: fill repositories.json through the provisioner client, so an approved value lands at once (issue 1128)
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.
2026-10-18 17:40:59 +02:00

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)
}
}