Its gate sits on every handler it serves, so it says it honours the mark, and the grants part, reading its node's readings, composes no deny of its modules' marked subjects. Builds: design 46 7-approval-mark §6 Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
160 lines
5.7 KiB
Go
160 lines
5.7 KiB
Go
package runtime
|
|
|
|
import (
|
|
"encoding/json"
|
|
"git.novox.be/novox/mesh-sdk/go/buscalls"
|
|
"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/announce"
|
|
"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) {
|
|
mt.NeedBundles(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)
|
|
}
|
|
// 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{}
|
|
}
|
|
|
|
// issue 1966 TLS-3 (ENG-2): the runner states the capabilities of the calls it answers in its PING, on its own instance, which
|
|
// is what its machine's node-engine asks and takes into the node's record; no bundle is needed for it.
|
|
func TestTheRunnerStatesTheCapabilitiesOfItsCallsInItsPing(t *testing.T) {
|
|
mesh := mt.New(t)
|
|
mesh.Issue(t, mt.MembershipOf("alpha", "anchor", false, nil))
|
|
nodeTools := connect(t, "node-tools", "anchor")
|
|
stop, err := Run(nodeTools, nil, 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()
|
|
msg, err := nc.Request("$SRV.PING.node-tools.anchor", nil, 2*time.Second)
|
|
if err != nil {
|
|
t.Fatalf("ping of the instance: %v", err)
|
|
}
|
|
var ping micro.Ping
|
|
if err := json.Unmarshal(msg.Data, &ping); err != nil || ping.Metadata[announce.Capabilities] != "approval-mark/1,bus-state-calls,ledger-calls,put-sessions,seat-settle-delay" {
|
|
t.Errorf("the runner states %q (%v), want its bus-state and ledger calls", ping.Metadata[announce.Capabilities], err)
|
|
}
|
|
}
|
|
|
|
// Design 47 G §462: the call that reads a sent object is listed under the bus state capability, and the ledger calls keep theirs.
|
|
func TestTheSentObjectCallBelongsToTheBusStateCapability(t *testing.T) {
|
|
for c, method := range map[string]string{CapBusStateCalls: buscalls.ObjectsSentGet} {
|
|
found := false
|
|
for _, m := range callFamilies[c] {
|
|
found = found || m == method
|
|
}
|
|
if !found {
|
|
t.Errorf("%s does not list %s", c, method)
|
|
}
|
|
}
|
|
}
|