Every tool's schema in one $SRV.INFO reply outgrew max_payload on machines serving 137-206 tools, and the refused reply was dropped silently, so search and a machine's view went empty. Module tools now announce without schema; describe and tools/list ask the module for it. An answer still too large has its descriptions cut to a line, and a failed reply is logged.
122 lines
4.3 KiB
Go
122 lines
4.3 KiB
Go
package runtime
|
|
|
|
import (
|
|
"encoding/json"
|
|
"sort"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/nats-io/nats.go"
|
|
"github.com/nats-io/nats.go/micro"
|
|
|
|
"github.com/novox/mesh-tools/node-tools/internal/bus"
|
|
mt "github.com/novox/mesh-tools/node-tools/internal/meshtest"
|
|
)
|
|
|
|
// novox/hq ADR 0197: what a runtime serves, it announces — the NATS services protocol's discovery,
|
|
// read here with NATS's own types, each endpoint a subject actually served — and the announcement
|
|
// follows a re-issued membership.
|
|
func TestTheRuntimeAnnouncesWhatItServesInTheServicesProtocol(t *testing.T) {
|
|
mesh := mt.New(t)
|
|
mesh.Issue(t, mt.MembershipOf("alpha", "anchor", false, nil))
|
|
mesh.Issue(t, mt.MembershipOf("beta", "anchor", false, map[string][]string{"node-shelf": {"list", "clear"}}))
|
|
nodeTools := connect(t, "node-tools", "anchor")
|
|
stop, err := Run(nodeTools, []Served{
|
|
{"alpha", []string{mt.Fixture("many-alpha.serve.mjs")}},
|
|
{"beta", []string{mt.Fixture("many-beta.serve.mjs")}},
|
|
}, nil, (&mt.Logs{}).Logf)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer stop()
|
|
|
|
nc, err := nats.Connect(mt.URL(t))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer nc.Close()
|
|
info := func() micro.Info {
|
|
t.Helper()
|
|
msg, err := nc.Request("$SRV.INFO.node-tools.anchor", nil, 2*time.Second)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
var i micro.Info
|
|
if err := json.Unmarshal(msg.Data, &i); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return i
|
|
}
|
|
subjects := func(i micro.Info) string {
|
|
var out []string
|
|
for _, e := range i.Endpoints {
|
|
out = append(out, e.Subject+"|"+e.QueueGroup+"|"+e.Metadata["kind"]+"|"+e.Metadata["module"]+"|"+e.Metadata["seat"]+"|"+e.Metadata["scope"]+"|"+e.Metadata["interchangeable"])
|
|
}
|
|
sort.Strings(out)
|
|
return strings.Join(out, "\n")
|
|
}
|
|
|
|
got := info()
|
|
if got.Type != micro.InfoResponseType || got.Name != "node-tools" || got.ID != "anchor" || got.Version == "" {
|
|
t.Errorf("identity: %+v", got.ServiceIdentity)
|
|
}
|
|
want := strings.Join([]string{
|
|
"mesh.mod.alpha.tool.one.anchor||tool|alpha|||false",
|
|
"mesh.mod.alpha.tool.two.anchor||tool|alpha|||false",
|
|
"mesh.mod.beta.tool.five.anchor||tool|beta|||false",
|
|
"mesh.mod.beta.tool.four.anchor||tool|beta|||false",
|
|
"mesh.mod.beta.tool.three.anchor||tool|beta|||false",
|
|
"mesh.seat.node-shelf.tool.clear.anchor||seat|beta|node-shelf|node|false",
|
|
"mesh.seat.node-shelf.tool.list.anchor||seat|beta|node-shelf|node|false",
|
|
}, "\n")
|
|
if s := subjects(got); s != want {
|
|
t.Errorf("announced:\n%s\nwant:\n%s", s, want)
|
|
}
|
|
for _, e := range got.Endpoints {
|
|
if e.Metadata["node"] != "anchor" || e.Metadata["description"] == "" {
|
|
t.Errorf("endpoint metadata: %+v", e)
|
|
}
|
|
// A seat's verb carries its schema; a module's tool does not — it outgrew the bus (2026-10-04).
|
|
_, hasSchema := e.Metadata["schema"]
|
|
if e.Metadata["kind"] == "seat" && !json.Valid([]byte(e.Metadata["schema"])) {
|
|
t.Errorf("a seat's verb without a schema: %+v", e)
|
|
}
|
|
if e.Metadata["kind"] == "tool" && hasSchema {
|
|
t.Errorf("a module's tool still announces its schema: %+v", e)
|
|
}
|
|
}
|
|
|
|
// Every subject announced is answered.
|
|
asker := connect(t, "console", "workstation")
|
|
for _, e := range got.Endpoints {
|
|
if _, err := asker.Ask("", map[string]any{}, e.Subject); err != nil {
|
|
t.Errorf("announced %s and does not answer it: %v", e.Subject, err)
|
|
}
|
|
}
|
|
|
|
// PING answers with the same identity, and a request for another service is not answered.
|
|
if msg, err := nc.Request("$SRV.PING", nil, 2*time.Second); err != nil || !strings.Contains(string(msg.Data), micro.PingResponseType) {
|
|
t.Errorf("ping: %v %v", msg, err)
|
|
}
|
|
if _, err := nc.Request("$SRV.INFO.somebody-else", nil, 300*time.Millisecond); err == nil {
|
|
t.Error("answered a request for another service")
|
|
}
|
|
|
|
// alpha is re-issued a plain subject: the announcement says so, and says it is interchangeable.
|
|
mesh.Issue(t, mt.MembershipOf("alpha", "anchor", true, nil))
|
|
var after string
|
|
for i := 0; i < 40; i++ {
|
|
after = subjects(info())
|
|
if strings.Contains(after, "mesh.mod.alpha.tool.one|serve.alpha|tool|alpha|||true") {
|
|
break
|
|
}
|
|
time.Sleep(100 * time.Millisecond)
|
|
}
|
|
if !strings.Contains(after, "mesh.mod.alpha.tool.one|serve.alpha|tool|alpha|||true") ||
|
|
!strings.Contains(after, "mesh.mod.alpha.tool.one.anchor||tool|alpha|||true") {
|
|
t.Errorf("after a re-issued membership:\n%s", after)
|
|
}
|
|
_ = bus.Served{}
|
|
}
|