Files
mesh-sdk/go/packages_list_test.go
T
jschoubben dae61959b9 J3 (lead's a): go/views names no module; its caller gives the owner's read tool and views stream
ProcessReader.Follow(owner, state, tool, stream) takes the owner's read tool and its views stream as the owner's
declaration names them, and refuses another owner's tool or a second stream for one owner; OwnerReadTool, StreamOf,
IndexerStream and AssignerStream are gone, and viewstest takes the same two values. The second instance of a module
name in shared code (the earlier review finding), so the package test now refuses a module's name in go/views.

Claude-Session: https://claude.ai/code/session_01JG73KJNrH5EgFXMKKYSrig
2026-11-04 14:35:56 +01:00

310 lines
13 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, and is empty when the group is pinned: the vault module's name in delivery and
// provisions goes 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") || strings.HasSuffix(path, "_test.go") {
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"