Files
mesh-sdk/go/packages_list_test.go
T
jschoubben c4de50e91e R1-F-sdk 1, 3: no test imports a seat's contract module, and the guard reads test imports
The import guard skipped _test.go files, so holdings_test.go's import of the mesh-broker seat would have
reached mesh-sdk's go.mod at the pin (ADR 0625 §1, §3). The vault entries' comment says they stay to the
next milestone (issue 1966 note 28).

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 16:21:05 +01:00

325 lines
14 KiB
Go

package stdio
import (
"encoding/json"
"go/ast"
"go/parser"
"go/token"
"io/fs"
"os"
"path/filepath"
"reflect"
"regexp"
"slices"
"strconv"
"strings"
"testing"
contractseats "git.novox.be/novox/mesh-contracts/go/seats"
"git.novox.be/novox/mesh-sdk/go/buscalls"
)
// meshSDKPackages is every package mesh-sdk holds (novox/hq ADR 0593 §1): what every module shares, the value shapes
// and names more than one part reads, and the seat contracts it holds until they move into SDK modules (issue 167).
// Logic only some modules need is its owner module's package. A package added here is a decision to say in the pull
// request: does every module share it, or is it a shape more than one part reads?
var meshSDKPackages = []string{
".", "announce", "announcetest", "buildstep", "buscalls", "buscompose", "busstate", "busretry", "calladdress", "catalogue", "channel",
"composer", "declaration", "delivery", "forgekey", "handacts", "health", "internal/beside", "kvlimit", "membership",
"objectlink", "outward", "overlay", "parts", "pernode", "process", "provider", "provider/harness", "provisions",
"sdkversion", "seal", "seatdef", "testbus", "views", "views/viewstest",
}
// clientSeats are the seats whose logic is their client's in mesh-contracts (novox/hq ADR 0627 §1, §4): mesh-sdk names
// none of them outside catalogue, and holds no package over them.
var clientSeats = []string{contractseats.OperatorChannel, contractseats.MeshVault, "node-engine",
contractseats.NpmPackageRegistry, contractseats.GoPackageRegistry, contractseats.ContainerImageRegistry,
contractseats.OciArtifactRegistry}
// moduleWord is a module's name in a literal (mesh-<word>[-<word>...], or one of mesh-delivery's parts by its short name
// before -views or _read_), but a seat's that go/views records through (mesh-hand-acts) and an import path's mesh-sdk.
var moduleWord = regexp.MustCompile(`\bmesh-(?:delivery|grants|vault|host|config|provisioner|broker|knowledge|enrolment)\b[-a-z]*|\b(?:indexer|assigner|teller|watchman|checker|builder)(?:-views|_read_)`)
// notInSDK are the packages that moved to a seat's client or contract (ADR 0627 §1, §4) and may not come back.
var notInSDK = []string{"asks", "asker", "publish"}
// untilTheJoin is what ADR 0627 still moves at the group's join, each `<file>: <what>`, naming what the checks below
// would refuse; it only shrinks. The vault module's name in delivery and provisions stays past this milestone's pin
// (issue 1966 note 28) and goes in the next milestone, when the vault's answers are its contract's.
var untilTheJoin = []string{
"delivery/signers.go: mesh-vault",
"provisions/provisions.go: mesh-vault",
}
// computesNot are the functions whose computation is one owner's alone (ADR 0593 §2, §7): the indexer computes a
// fingerprint, its named tree and a package's version; the node-engine composes.
var computesNot = []string{"SourceFingerprint", "SourceFingerprint2", "NamedTree", "MeshPackagesOf", "PackageVersionsOf",
"OtherPackages", "Compose", "CompositionsShadow", "JudgeMove", "Run"}
// composesNot are the methods of the composition, the node-engine's alone since its rules moved there (ADR 0593 §7):
// mesh-sdk renders no declaration and refuses no composition.
var composesNot = []string{"Resolution.Compose", "Resolution.Declaration", "Resolution.Rules", "Resolution.LeftOut"}
// ADR 0593 §1's check: mesh-sdk holds exactly the packages it keeps, and none of them computes what one owner alone
// computes.
func TestMeshSDKHoldsOnlyWhatEveryModuleShares(t *testing.T) {
var found []string
// **mesh-sdk takes no new seat-specific name** (novox/hq ADR 0613 §5, design 26 *Contracts modules*, SDK-9): a seat's
// name is mesh-contracts' (go/seats), so no string literal here names a defined seat or its subjects, and no exported
// name is given to one, beyond what testdata/seat-names.txt keeps from before the move (each `<file>: <literal or
// name>`), which only shrinks as areas move. A literal naming a module of the same name as a seat counts too.
defined := contractseats.Names()
namesSeat := func(s string) bool {
for _, seat := range defined {
if s == seat || strings.HasPrefix(s, "mesh.seat."+seat+".") {
return true
}
}
return false
}
var seatNames, pending, modules []string
clientNamed := func(v string) bool {
for _, seat := range clientSeats {
if v == seat || strings.HasPrefix(v, "mesh.seat."+seat+".") {
return true
}
}
return false
}
err := filepath.WalkDir(".", func(path string, d fs.DirEntry, err error) error {
if err != nil {
return err
}
if d.IsDir() {
if n := d.Name(); n == "testdata" || (strings.HasPrefix(n, ".") && path != ".") {
return filepath.SkipDir
}
return nil
}
if !strings.HasSuffix(path, ".go") {
return nil
}
// A test's import is a requirement of mesh-sdk's go.mod too (ADR 0625 §1, §3): a test may import the seat set,
// which catalogue already requires, and no other contracts module.
if strings.HasSuffix(path, "_test.go") {
file, err := parser.ParseFile(token.NewFileSet(), path, nil, parser.ImportsOnly)
if err != nil {
return err
}
for _, imp := range file.Imports {
p, _ := strconv.Unquote(imp.Path.Value)
if strings.HasPrefix(p, "git.novox.be/novox/mesh-contracts/") && p != contractsSeatSet {
pending = append(pending, filepath.ToSlash(path)+": imports "+p)
}
}
return nil
}
dir := filepath.ToSlash(filepath.Dir(path))
if !slices.Contains(found, dir) {
found = append(found, dir)
}
file, err := parser.ParseFile(token.NewFileSet(), path, nil, parser.SkipObjectResolution)
if err != nil {
return err
}
where := filepath.ToSlash(path) + ": "
inCatalogue := dir == "catalogue"
// ADR 0625 §3: mesh-sdk's only import of a contracts module is catalogue's of the seat set.
for _, imp := range file.Imports {
p, _ := strconv.Unquote(imp.Path.Value)
if strings.HasPrefix(p, "git.novox.be/novox/mesh-contracts/") && (!inCatalogue || p != contractsSeatSet) {
pending = append(pending, where+"imports "+p)
}
}
ast.Inspect(file, func(n ast.Node) bool {
switch n := n.(type) {
case *ast.BasicLit:
// views is shared by every owner and reader: it names no module, the caller gives the owner's tool and
// stream from the owner's declaration (the lead's ruling, 2026-11-04, the second instance of the kind).
if v, err := strconv.Unquote(n.Value); err == nil && n.Kind == token.STRING && (dir == "views" || strings.HasPrefix(dir, "views/")) &&
moduleWord.MatchString(v) {
modules = append(modules, where+moduleWord.FindString(v))
}
if v, err := strconv.Unquote(n.Value); err == nil && n.Kind == token.STRING && namesSeat(v) {
seatNames = append(seatNames, where+v)
if !inCatalogue && clientNamed(v) {
pending = append(pending, where+v)
}
}
case *ast.ValueSpec:
for i, name := range n.Names {
if i < len(n.Values) && name.IsExported() {
if sel, ok := n.Values[i].(*ast.SelectorExpr); ok {
if x, ok := sel.X.(*ast.Ident); ok && x.Name == "contractseats" {
seatNames = append(seatNames, where+name.Name)
if !inCatalogue && slices.Contains([]string{"OperatorChannel", "MeshVault", "NodeEngine",
"NpmPackageRegistry", "GoPackageRegistry", "ContainerImageRegistry", "OciArtifactRegistry"}, sel.Sel.Name) {
pending = append(pending, where+name.Name)
}
}
}
}
}
}
return true
})
// 1966/AC5: the calls package imports nothing of mesh-sdk but the catalogue's types, so a change here rebuilds
// only its importers (ADR 0595 §9).
if dir == "buscalls" {
for _, imp := range file.Imports {
p, _ := strconv.Unquote(imp.Path.Value)
if strings.HasPrefix(p, sdkModule) && p != sdkModule+"/catalogue" {
t.Errorf("%s imports %s: the calls package imports nothing of mesh-sdk but catalogue (ADR 0595 §9)", path, p)
}
}
}
for _, decl := range file.Decls {
fn, ok := decl.(*ast.FuncDecl)
if !ok {
continue
}
if fn.Recv == nil && slices.Contains(computesNot, fn.Name.Name) {
t.Errorf("%s: %s computes what its owner alone computes (ADR 0593 §2, §7)", path, fn.Name.Name)
}
if fn.Recv != nil {
recv := fn.Recv.List[0].Type
if star, isStar := recv.(*ast.StarExpr); isStar {
recv = star.X
}
if id, isID := recv.(*ast.Ident); isID && slices.Contains(composesNot, id.Name+"."+fn.Name.Name) {
t.Errorf("%s: %s.%s composes, which the node-engine alone does (ADR 0593 §7)", path, id.Name, fn.Name.Name)
}
}
}
return nil
})
if err != nil {
t.Fatal(err)
}
kept, err := os.ReadFile("testdata/seat-names.txt")
if err != nil {
t.Fatal(err)
}
keep := strings.Split(strings.TrimSpace(string(kept)), "\n")
for _, n := range seatNames {
if !slices.Contains(keep, n) {
t.Errorf("%s names a seat the definitions define: a seat's name and its contract are mesh-contracts' (ADR 0613 §5)", n)
}
}
for _, k := range keep {
if !slices.Contains(seatNames, k) {
t.Errorf("testdata/seat-names.txt keeps %q, which is gone: take it off the list", k)
}
}
slices.Sort(found)
for _, m := range modules {
t.Errorf("%s: go/views names no module; its caller gives the owner's read tool and views stream from the owner's declaration", m)
}
for _, dir := range found {
if slices.Contains(notInSDK, dir) {
pending = append(pending, dir+": a package that moved")
}
}
// ADR 0627 §4, ADR 0625 §3: no package over a client's seat, no client seat's name outside catalogue, no contracts
// import but the seat set's in catalogue; what the join still moves is untilTheJoin's.
for _, p := range pending {
if !slices.Contains(untilTheJoin, p) {
t.Errorf("%s: mesh-sdk holds no logic over a seat whose client is in mesh-contracts, names none of them outside "+
"catalogue, and imports no contracts module but the seat set in catalogue (ADR 0627, ADR 0625 §3)", p)
}
}
for _, k := range untilTheJoin {
if !slices.Contains(pending, k) {
t.Errorf("untilTheJoin keeps %q, which is gone: take it off the list", k)
}
}
for _, dir := range found {
if !slices.Contains(meshSDKPackages, dir) {
t.Errorf("mesh-sdk holds the package %s, which it does not keep (ADR 0593 §1): it is its owner module's package", dir)
}
}
for _, want := range meshSDKPackages {
if !slices.Contains(found, want) {
t.Errorf("mesh-sdk's list keeps %s, which is gone: take it off the list", want)
}
}
// 1966/AC5: the call an apply names is the id the tool runner hands under `_meta`'s mesh/call.
if buscalls.MetaCall != "mesh/call" {
t.Errorf("the tool call's id is handed under %q, not mesh/call", buscalls.MetaCall)
}
// 1966/AC5: an index's key is its fields' values, then the instance's path, joined by "."; its entry names the
// instance and the sequence.
for _, c := range []struct {
fields []string
instance string
want string
}{
{[]string{"17950", "volvo"}, "listing/a17", "17950.volvo.listing.a17"},
// A value holding "." and one that does not meet in no key.
{[]string{"a.b"}, "c", "a=2Eb.c"},
{[]string{"a"}, "b/c", "a.b.c"},
// A title with spaces is a token the bus takes; "=" itself is encoded.
{[]string{"How it is checked", "x=y"}, "decision/0290", "How=20it=20is=20checked.x=3Dy.decision.0290"},
} {
k := buscalls.IndexKey(c.fields, c.instance)
if k != c.want {
t.Errorf("the index key of %q and %q is %q, want %q", c.fields, c.instance, k, c.want)
}
for _, token := range strings.Split(k, ".") {
if token == "" || strings.ContainsAny(token, " *>\t") {
t.Errorf("the index key %q holds a token the bus refuses: %q", k, token)
}
}
}
if buscalls.IndexKey([]string{"a.b"}, "c") == buscalls.IndexKey([]string{"a"}, "b/c") {
t.Error("two different index keys meet")
}
entry, _ := json.Marshal(buscalls.IndexEntry{Instance: "listing/a17", Sequence: 3})
if string(entry) != `{"instance":"listing/a17","sequence":3}` {
t.Errorf("an index entry is %s", entry)
}
// A tool's answer reads the same whatever wraps it (buscalls.Unwrap, the lead's ruling 2026-11-04 13:12).
for _, c := range []struct {
name, raw, answer, said string
ok bool
}{
{"bare", `{"a":1}`, `{"a":1}`, `{"a":1}`, true},
{"a JSON string", `"{\"a\":1}"`, `{"a":1}`, `{"a":1}`, true},
{"the content list", `{"content":[{"type":"text","text":"{\"a\":1}"}]}`, `{"a":1}`, `{"a":1}`, true},
{"the content list, failed", `{"content":[{"type":"text","text":"no"}],"isError":true}`, `no`, `no`, false},
{"output, ok, answer", `{"output":"done","ok":true,"answer":{"a":1}}`, `{"a":1}`, "done", true},
{"output, not ok", `{"output":"refused","ok":false}`, ``, "refused", false},
{"the runner's result", `{"result":{"a":1}}`, `{"a":1}`, `{"a":1}`, true},
{"the runner's error", `{"error":"no such tool"}`, ``, "no such tool", false},
} {
answer, said, ok := buscalls.Unwrap(json.RawMessage(c.raw))
if string(answer) != c.answer || said != c.said || ok != c.ok {
t.Errorf("%s: unwrapped %s, %q, %v; want %s, %q, %v", c.name, answer, said, ok, c.answer, c.said, c.ok)
}
}
// 1966/AC5: an apply naming the tool call it serves, and its answer with the state behind, round-trip.
for _, c := range []struct {
name string
in, back any
}{
{"apply", &buscalls.ApplyRequest{Ledger: "records", Instance: "decision/0290", Operation: "submit",
Body: json.RawMessage(`{"why":"x"}`), Call: "call-17"}, &buscalls.ApplyRequest{}},
{"applied", &buscalls.Applied{Taken: true, Sequence: 5, StateBehind: true}, &buscalls.Applied{}},
} {
raw, err := json.Marshal(c.in)
if err != nil {
t.Fatal(err)
}
if err := json.Unmarshal(raw, c.back); err != nil || !reflect.DeepEqual(c.in, c.back) {
t.Errorf("%s read back as %+v from %s (%v)", c.name, c.back, raw, err)
}
}
}
// contractsSeatSet is the seat-set package of mesh-contracts, catalogue's one import of a contracts module.
const contractsSeatSet = "git.novox.be/novox/mesh-contracts/go/seats"
// sdkModule is mesh-sdk's Go module path.
const sdkModule = "git.novox.be/novox/mesh-sdk/go"