Files
mesh-sdk/go/packages_list_test.go
T
jschoubben 315d7e9b8c J3 (A2g, A2h): a process's pictures of its owners' states are go/views', and a first view says when each key was written
ProcessReader and Picture (A2g's mesh-delivery sdk/firstview at c6c6687) and the test owner (viewstest) move into
go/views, so the builder, checker, teller, view and watchman read the indexer's and the assigner's states through the
one shared follower. The first view's answer carries each key's written time as the bus keeps it, and
Follower.Changed hands it on, so a reader's want time is the owner's write, not its own start (A2h's request).

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

296 lines
12 KiB
Go

package stdio
import (
"encoding/json"
"go/ast"
"go/parser"
"go/token"
"io/fs"
"os"
"path/filepath"
"reflect"
"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}
// 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 []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:
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 _, 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"