Files
mesh-host/internal/compose/resolution/seats_test.go
T
jschoubben e96a7ada50 The node-engine keeps no copy of a seat's verbs: the seat list comes from the bus (issue 1921, ADR 0635 §1)
Eight lists of seat verbs (checker, enrolment, artifact sweep, delivery, node-resolver, node-nfs-server,
node-mounts, mesh-config's input) and their schema helpers were read by nothing but one test, and drifted
from their seats (B0's verb input never reached the checker's copy). A guard row in the seats test finds
any list of the catalogue's verbs written in the node-engine's code.

Claude-Session: https://claude.ai/code/session_01NcbgfFSnn24M3GcqttoU7i
2026-11-07 02:26:14 +01:00

168 lines
5.7 KiB
Go

package resolution
import (
"go/ast"
"go/parser"
"go/token"
"io/fs"
"path/filepath"
"strconv"
"strings"
"testing"
mcat "git.novox.be/novox/mesh-sdk/go/catalogue"
)
func registryShelf() map[string]mcat.Manifest {
return shelf(mcat.Manifest{Module: "gitea", Version: "1", Provides: mcat.FromAnywhere("npm-package-registry"),
Claims: []mcat.Claim{{Name: "npm-package-registry", Scope: mcat.ScopeMesh}}}, mcat.Manifest{Module: "verdaccio", Version: "1", Provides: mcat.FromAnywhere("npm-package-registry")}, mcat.Manifest{Module: "builder", Version: "1", Requires: []string{"npm-package-registry"}},
)
}
func twoRegistries() map[string][]mcat.Provider {
return map[string][]mcat.Provider{"npm-package-registry": {
{Node: "anchor", At: "anchor.internal", Module: "gitea"},
{Node: "archive", At: "archive.internal", Module: "verdaccio"},
}}
}
func giteaHoldsTheSeat() []mcat.Held {
return []mcat.Held{{Claim: "npm-package-registry", Scope: mcat.ScopeMesh, Node: "anchor", Module: "gitea"}}
}
func TestTheSeatsHolderAnswersWhenSeveralProvide(t *testing.T) {
// The whole point: a second registry beside the holder harms nothing, and nobody pins.
got, err := Resolve(registryShelf(), []string{"builder"}, reachable(), mcat.World{Offered: twoRegistries(), Held: giteaHoldsTheSeat()})
if err != nil {
t.Fatal(err)
}
if len(got.Needs) != 1 || got.Needs[0].From != "anchor" {
t.Fatalf("the seat's holder did not answer: %v", got.Needs)
}
}
func TestAPinStillWinsOverTheSeat(t *testing.T) {
// A consumer coupled to one provider's contents has said so, and the seat does not overrule it.
got, err := Resolve(registryShelf(), []string{"builder"}, reachable(), mcat.World{Offered: twoRegistries(), Held: giteaHoldsTheSeat(),
Pinned: map[string]mcat.Chosen{"npm-package-registry": {Node: "archive", Module: "verdaccio"}}})
if err != nil {
t.Fatal(err)
}
if len(got.Needs) != 1 || got.Needs[0].From != "archive" {
t.Fatalf("the pin was overruled by the seat: %v", got.Needs)
}
}
func TestWithTheSeatUnheldSeveralProvidersAreStillRefused(t *testing.T) {
// No seat held is no choice made, and ADR 0009's rule stands: never guessed.
_, err := Resolve(registryShelf(), []string{"builder"}, reachable(), mcat.World{Offered: twoRegistries()})
if err == nil {
t.Fatal("one of two registries was picked with nobody holding the seat")
}
if !strings.Contains(err.Error(), "pin") {
t.Fatalf("the refusal does not say how to choose: %v", err)
}
}
func TestTheHolderIsTheModuleNotTheMachine(t *testing.T) {
// Two modules on one machine could provide the same thing; only the one holding the seat
// answers. A holder matched by node alone would send consumers to whichever came first.
providers := []mcat.Provider{
{Node: "anchor", At: "anchor.internal", Module: "verdaccio"},
{Node: "anchor", At: "anchor.internal", Module: "gitea"},
}
holder, held := HolderAmong("npm-package-registry", providers, giteaHoldsTheSeat())
if !held || holder.Module != "gitea" {
t.Fatalf("the holder was not told apart from a neighbour: %+v", holder)
}
// A seat's verbs are its definition's, read from the bus (novox/hq ADR 0635 §1): the node-engine keeps no copy of
// them, so no copy can drift from the seat as the seven it kept did (issue 1921). A copy is a list of the catalogue's
// verbs written in the code; the guard proves it finds one, then finds none in the node-engine.
t.Run("no seat's verbs are copied into the node-engine", func(t *testing.T) {
copied := `package x
import mcat "git.novox.be/novox/mesh-sdk/go/catalogue"
func someVerbs() []mcat.Verb { return []mcat.Verb{{Name: "routes"}} }
`
if got := verbCopies(t, "copied.go", copied); len(got) == 0 {
t.Fatal("the guard finds no copy in a file that holds one, so it proves nothing")
}
var found []string
root := filepath.Join("..", "..", "..")
err := filepath.WalkDir(root, func(p string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() && (d.Name() == "testdata" || strings.HasPrefix(d.Name(), ".")) && p != root {
return filepath.SkipDir
}
if d.IsDir() || !strings.HasSuffix(p, ".go") || strings.HasSuffix(p, "_test.go") {
return nil
}
found = append(found, verbCopies(t, p, nil)...)
return nil
})
if err != nil {
t.Fatal(err)
}
if len(found) > 0 {
t.Errorf("the node-engine copies a seat's verbs instead of reading the seat list from the bus: %v", found)
}
})
}
// verbCopies is where one Go file writes the catalogue's verbs itself: a function answering a list of them, or a verb
// written as a literal.
func verbCopies(t *testing.T, name string, src any) []string {
t.Helper()
fset := token.NewFileSet()
f, err := parser.ParseFile(fset, name, src, parser.SkipObjectResolution)
if err != nil {
t.Fatal(err)
}
alias := ""
for _, imp := range f.Imports {
if p, _ := strconv.Unquote(imp.Path.Value); p == "git.novox.be/novox/mesh-sdk/go/catalogue" {
alias = "catalogue"
if imp.Name != nil {
alias = imp.Name.Name
}
}
}
if alias == "" {
return nil
}
isVerb := func(e ast.Expr) bool {
if a, ok := e.(*ast.ArrayType); ok {
e = a.Elt
}
sel, ok := e.(*ast.SelectorExpr)
if !ok || sel.Sel.Name != "Verb" {
return false
}
x, ok := sel.X.(*ast.Ident)
return ok && x.Name == alias
}
var out []string
ast.Inspect(f, func(n ast.Node) bool {
switch n := n.(type) {
case *ast.FuncDecl:
if n.Type.Results != nil {
for _, r := range n.Type.Results.List {
if isVerb(r.Type) {
out = append(out, fset.Position(n.Pos()).String()+" "+n.Name.Name)
}
}
}
case *ast.CompositeLit:
if n.Type != nil && isVerb(n.Type) {
out = append(out, fset.Position(n.Pos()).String())
}
}
return true
})
return out
}