Every pull request the forge announces is mapped onto the mesh's module graph by the merge handler's rule (issue 278): touching a module — or adding one — runs the gate (mesh/merge-gate), its judge chosen by the graph (the controller judges itself, the node-engine by its validator); a repository of the mesh that touches none runs only its own merge-check.sh (mesh/repo-check), a warning when it has none. Nothing is left pending: a repository outside the mesh touching nothing is told so as a pass. The gate moves out of the per-repository scripts into the build seat, so a script is the repository's own tests and declares its toolchain (go or typescript). The controller's manifest names every verb of its seat again (ADR 0132), held by a test.
106 lines
4.5 KiB
Go
106 lines
4.5 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"os"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/novox/mesh-controller/internal/catalogue"
|
|
"testing"
|
|
)
|
|
|
|
// The check anybody can run is the check registration runs (novox/hq issue 148, ADR 0037): a manifest
|
|
// with a known fault is named, and one without passes, with no store opened.
|
|
func TestModuleCheckNamesAFaultAndNeedsNoMesh(t *testing.T) {
|
|
dir := t.TempDir()
|
|
good := filepath.Join(dir, "good.json")
|
|
bad := filepath.Join(dir, "bad.json")
|
|
os.WriteFile(good, []byte(`{"module":"shop","version":"1","tools":["price"],"invokes":["mesh-catalog.catalog_modules"]}`), 0o600)
|
|
os.WriteFile(bad, []byte(`{"module":"till","version":"1","invokes":["shop"]}`), 0o600)
|
|
|
|
var out bytes.Buffer
|
|
if err := moduleCheck([]string{good}, &out); err != nil {
|
|
t.Fatalf("a sound manifest was refused: %v\n%s", err, out.String())
|
|
}
|
|
if !strings.Contains(out.String(), "shop: ok, 1 tool(s), invokes mesh-catalog.catalog_modules") {
|
|
t.Fatalf("the report does not say what it checked:\n%s", out.String())
|
|
}
|
|
|
|
out.Reset()
|
|
err := moduleCheck([]string{good, bad}, &out)
|
|
if err == nil {
|
|
t.Fatal("a manifest invoking a module and no tool passed")
|
|
}
|
|
if !strings.Contains(out.String(), `till invokes "shop", which does not name a tool`) {
|
|
t.Fatalf("the fault is not named in the manifest's words:\n%s", out.String())
|
|
}
|
|
}
|
|
|
|
// The rules between manifests run over what was given together: a seat two modules declare is
|
|
// refused, which no single-manifest check can see.
|
|
func TestModuleCheckJudgesBetweenTheManifestsGiven(t *testing.T) {
|
|
dir := t.TempDir()
|
|
a := filepath.Join(dir, "a.json")
|
|
b := filepath.Join(dir, "b.json")
|
|
os.WriteFile(a, []byte(`{"module":"a","version":"1","seats":[{"name":"printer","scope":"mesh"}]}`), 0o600)
|
|
os.WriteFile(b, []byte(`{"module":"b","version":"1","seats":[{"name":"printer","scope":"mesh"}]}`), 0o600)
|
|
var out bytes.Buffer
|
|
if err := moduleCheck([]string{a, b}, &out); err == nil {
|
|
t.Fatalf("two declarations of one seat passed:\n%s", out.String())
|
|
}
|
|
if !strings.Contains(out.String(), "a seat name means one protocol") {
|
|
t.Fatalf("the cross-manifest rule was not the one named:\n%s", out.String())
|
|
}
|
|
}
|
|
|
|
// The real catalogue passes the command, the way it passes the test that used to be the only check.
|
|
func TestModuleCheckPassesTheCatalogue(t *testing.T) {
|
|
root := filepath.Join("..", "..", "..", "mesh-catalog", "modules")
|
|
if _, err := os.Stat(root); err != nil {
|
|
t.Skipf("catalogue sibling not present: %v", err)
|
|
}
|
|
paths, err := manifestsUnder(root)
|
|
if err != nil || len(paths) == 0 {
|
|
t.Fatalf("no manifests under %s: %v", root, err)
|
|
}
|
|
var out bytes.Buffer
|
|
if err := moduleCheck(paths, &out); err != nil {
|
|
t.Fatalf("the catalogue does not pass its own check: %v\n%s", err, out.String())
|
|
}
|
|
}
|
|
|
|
func TestRegistrationRefusesADefinitionNamingAnInstallation(t *testing.T) {
|
|
// novox/hq ADR 0155: the check moves to registration once the catalogue passes it. Both
|
|
// ways in — `module add` and a build's result — go through this, and a name declared on
|
|
// purpose passes with its reason.
|
|
named := catalogue.Manifest{Module: "idp", Resources: []map[string]any{
|
|
{"id": "server", "type": "container", "image": "x@sha256:aa",
|
|
"env": map[string]any{"KC_HOSTNAME": "https://login.mesh-one.be"}},
|
|
}}
|
|
err := namesNoInstallation(named)
|
|
if err == nil || !strings.Contains(err.Error(), "login.mesh-one.be") ||
|
|
!strings.Contains(err.Error(), catalogue.NamesOnPurpose) {
|
|
t.Fatalf("a definition naming an installation is refused with the name and the way out; got %v", err)
|
|
}
|
|
meant := catalogue.Manifest{Module: "site", Resources: []map[string]any{
|
|
{"id": "server", "type": "container", "image": "registry.mesh-one.be/org/site@sha256:cc",
|
|
catalogue.NamesOnPurpose: map[string]any{
|
|
"registry.mesh-one.be": "built outside the mesh until its repository is a build source here"}},
|
|
}}
|
|
if err := namesNoInstallation(meant); err != nil {
|
|
t.Fatalf("a name declared on purpose passes; got %v", err)
|
|
}
|
|
}
|
|
|
|
// **The controller's own manifest names every verb of its seat** (novox/hq ADR 0132): its tools lagged
|
|
// the seat's verbs for weeks, and `module check` — the gate's first step for a change touching it —
|
|
// refused it. Held here, so a verb added to the table without the manifest fails this repository's
|
|
// own suite rather than its next pull request's gate.
|
|
func TestTheControllersManifestServesEveryVerbOfItsSeat(t *testing.T) {
|
|
var out strings.Builder
|
|
if err := moduleCheck([]string{"../../module.json"}, &out); err != nil {
|
|
t.Fatalf("the controller's own module.json fails module check: %v\n%s", err, out.String())
|
|
}
|
|
}
|