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-[-...], 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 `: `, 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 `: `), 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"