Files
mesh-tools/node-tools/internal/bus/caller_bus_test.go
T
jschoubben ab0f888d67
mesh/merge-gate pass: builds mesh-tools, node-tools → ace, g14, novox, shanks; no bus step; every machine composes with the change as it did without (4 of …
mesh/repo-check pass: THE CHANGE ALTERS ITS OWN CHECK (merge-check.sh): main's version judged it; the change's judges the pull requests after it merges; it…
mesh/delivery-group group fix/365-a-tool-call-names-its-caller delivering: 0 of 4 delivered
mesh/delivery superseded: a newer head of the same pull request
node-tools: a tool call names its caller, and the module is told it from the subject (hq issue 365)
Each tool and seat verb is also served on its caller-named subject, kind call
with the caller's bus user last, which the controller grants each user with its
own name alone; the runtime hands the module that caller in the call's _meta,
never from the body. Calls through the runtime (the console and modules'
invokes) ask the caller-named subject first and the old one only when the first
reached nobody: nothing serves it yet, or the bus refused it because the grant
is not delivered yet, which ends the call at once rather than after 30 s. The
old subjects stay served and say no caller for one release (hq issue 464).
2026-10-11 05:19:08 +02:00

183 lines
6.2 KiB
Go

package bus_test
import (
"encoding/json"
"errors"
"fmt"
"strings"
"sync"
"testing"
"time"
"github.com/nats-io/nats-server/v2/server"
"github.com/nats-io/nats.go"
"github.com/novox/mesh-tools/node-tools/internal/bus"
mt "github.com/novox/mesh-tools/node-tools/internal/meshtest"
)
// A bus whose users are granted as the controller grants them (mesh-controller internal/broker): each named
// user publishes the subjects listed for it.
func grantedBus(t *testing.T, publish map[string][]string) string {
t.Helper()
var users []*server.User
for user, allow := range publish {
// And the membership's direct get every connection asks once, answered "no responders" here.
allow = append(allow, "$JS.API.DIRECT.GET.>")
users = append(users, &server.User{Username: user, Password: "pw", Permissions: &server.Permissions{
Publish: &server.SubjectPermission{Allow: allow},
Subscribe: &server.SubjectPermission{Allow: []string{">"}},
}})
}
s, err := server.NewServer(&server.Options{Host: "127.0.0.1", Port: server.RANDOM_PORT, NoLog: true, NoSigs: true,
Users: users})
if err != nil {
t.Fatal(err)
}
go s.Start()
if !s.ReadyForConnections(10 * time.Second) {
t.Fatal("the bus did not come up")
}
t.Cleanup(s.Shutdown)
return s.ClientURL()
}
func dial(t *testing.T, url, module, node, user string) *bus.Conn {
t.Helper()
c, err := bus.Connect(bus.Credential{URL: url, Module: module, Node: node, User: user, Password: "pw"})
if err != nil {
t.Fatal(err)
}
c.Logf = func(string, ...any) {}
t.Cleanup(c.Close)
return c
}
type heard struct {
mu sync.Mutex
callers []string
}
func (h *heard) handler(caller string, _ json.RawMessage) (any, error) {
h.mu.Lock()
defer h.mu.Unlock()
h.callers = append(h.callers, caller)
return map[string]any{"caller": caller}, nil
}
func (h *heard) all() []string {
h.mu.Lock()
defer h.mu.Unlock()
return append([]string(nil), h.callers...)
}
// A caller asks on the caller-named subject first; where nothing serves it yet — a runtime from before issue
// 365 — the same call is asked once on the subject that names no caller, at once, and done once.
func TestACallerFallsBackAtOnceWhereNothingServesTheCallerNamedSubject(t *testing.T) {
url := mt.URL(t)
server := dial(t, url, "ledger", "anchor", "")
h := &heard{}
// An older runtime: the subject that names no caller, alone.
stop, err := server.HandleSubject("mesh.mod.ledger.tool.who", func(body json.RawMessage) (any, error) {
return h.handler("", body)
})
if err != nil {
t.Fatal(err)
}
defer stop()
server.Flush()
asker := dial(t, url, "node-tools", "shanks", "shanks.node-tools")
began := time.Now()
answered, err := asker.Ask("ledger.who", map[string]any{}, "")
if err != nil {
t.Fatal(err)
}
if took := time.Since(began); took > 2*time.Second {
t.Errorf("the fall-back took %s", took)
}
if got := h.all(); len(got) != 1 || got[0] != "" || !strings.Contains(string(answered.Result), `"caller":""`) {
t.Errorf("the call was answered %v, heard %v; want once, on the old subject", string(answered.Result), got)
}
}
// Where the bus does not grant the caller-named subject yet — the controller's grants not yet delivered — the
// refusal ends the call at once and it is asked on the subject that names no caller: no caller waits out
// RequestTimeout for a grant that is on its way.
func TestACallerTheBusRefusesTheCallerNamedSubjectFallsBackAtOnce(t *testing.T) {
url := grantedBus(t, map[string][]string{
"anchor.node-tools": {"_INBOX.>"},
"shanks.node-tools": {"mesh.mod.*.tool.>"},
})
server := dial(t, url, "node-tools", "anchor", "anchor.node-tools")
h := &heard{}
stop, err := server.HandleSubjectCalled("mesh.mod.ledger.tool.who", h.handler)
if err != nil {
t.Fatal(err)
}
defer stop()
server.Flush()
asker := dial(t, url, "node-tools", "shanks", "shanks.node-tools")
var said []string
var mu sync.Mutex
asker.Logf = func(f string, a ...any) { mu.Lock(); said = append(said, fmt.Sprintf(f, a...)); mu.Unlock() }
began := time.Now()
if _, err := asker.Ask("ledger.who", map[string]any{}, ""); err != nil {
t.Fatal(err)
}
mu.Lock()
if !strings.Contains(strings.Join(said, "\n"), "the bus refused this account the caller-named mesh.mod.ledger.call.who.shanks~node-tools") {
t.Errorf("the refusal was not said: %v", said)
}
mu.Unlock()
if took := time.Since(began); took > 3*time.Second {
t.Errorf("the refused caller-named call held the caller %s", took)
}
if got := h.all(); len(got) != 1 || got[0] != "" {
t.Errorf("heard %v; want one call, on the subject that names no caller", got)
}
}
// With the controller's grant — the caller-named subjects with its own name last — a caller is heard as itself;
// and a caller cannot publish on a subject naming another, however it asks.
func TestAGrantedCallerIsHeardAsItselfAndCannotNameAnother(t *testing.T) {
url := grantedBus(t, map[string][]string{
"anchor.node-tools": {"_INBOX.>"},
"shanks.node-tools": {"mesh.mod.*.call.*.shanks~node-tools", "mesh.mod.*.call.*.*.shanks~node-tools"},
})
server := dial(t, url, "node-tools", "anchor", "anchor.node-tools")
h := &heard{}
stop, err := server.HandleSubjectCalled("mesh.mod.ledger.tool.who", h.handler)
if err != nil {
t.Fatal(err)
}
defer stop()
server.Flush()
asker := dial(t, url, "node-tools", "shanks", "shanks.node-tools")
if _, err := asker.Ask("ledger.who", map[string]any{}, ""); err != nil {
t.Fatal(err)
}
// Naming another on the subject itself: refused by the bus, so nothing is heard.
refusals := make(chan error, 4)
forger, err := nats.Connect(url, nats.UserInfo("shanks.node-tools", "pw"),
nats.ErrorHandler(func(_ *nats.Conn, _ *nats.Subscription, err error) { refusals <- err }))
if err != nil {
t.Fatal(err)
}
defer forger.Close()
if err := forger.Publish("mesh.mod.ledger.call.who.novox~node-tools", []byte(`{}`)); err != nil {
t.Fatal(err)
}
_ = forger.Flush()
select {
case err := <-refusals:
if !errors.Is(err, nats.ErrPermissionViolation) {
t.Errorf("the bus said %v, want a permissions violation", err)
}
case <-time.After(3 * time.Second):
t.Error("the bus did not refuse a call naming another caller")
}
if got := h.all(); len(got) != 1 || got[0] != "shanks.node-tools" {
t.Errorf("heard %v; want the asker alone, as itself", got)
}
}