Call fails no responders at once on a bus that answers; a callee that re-serves answers no one for seconds, and the caller kept nothing. CallKept asks again within busretry.Bound. Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
139 lines
5.5 KiB
Go
139 lines
5.5 KiB
Go
package process
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"strings"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/nats-io/nats.go"
|
|
|
|
"git.novox.be/novox/mesh-sdk/go/busretry"
|
|
)
|
|
|
|
// 1191/AC3: given a module tool key <module>.<tool>, when called, then the subject is
|
|
// mesh.mod.<module>.call.<tool>.<token> with the same fallback (mesh.mod.<module>.tool.<tool>).
|
|
func TestCallSubjectsForAModuleTool(t *testing.T) {
|
|
called, fallback, err := callSubjects("mesh-mail.send", "novox~mesh-mail~send")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if called != "mesh.mod.mesh-mail.call.send.novox~mesh-mail~send" {
|
|
t.Errorf("called: %q", called)
|
|
}
|
|
if fallback != "mesh.mod.mesh-mail.tool.send" {
|
|
t.Errorf("fallback: %q", fallback)
|
|
}
|
|
}
|
|
|
|
// 1191/AC1's subject (the seat form): seat:<seat>.<verb> calls mesh.seat.<seat>.call.<verb>.<token>,
|
|
// falling back to mesh.seat.<seat>.tool.<verb>.
|
|
func TestCallSubjectsForASeatVerb(t *testing.T) {
|
|
called, fallback, err := callSubjects("seat:operator-channel.tell", "novox~mesh-mail~send")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if called != "mesh.seat.operator-channel.call.tell.novox~mesh-mail~send" {
|
|
t.Errorf("called: %q", called)
|
|
}
|
|
if fallback != "mesh.seat.operator-channel.tool.tell" {
|
|
t.Errorf("fallback: %q", fallback)
|
|
}
|
|
}
|
|
|
|
func TestCallSubjectsRefusesAKeyNamingNeither(t *testing.T) {
|
|
for _, key := range []string{"", "nodot", "seat:", "seat:noverb", "."} {
|
|
if _, _, err := callSubjects(key, "token"); err == nil {
|
|
t.Errorf("callSubjects(%q) was accepted", key)
|
|
}
|
|
}
|
|
}
|
|
|
|
// 1470/AC1: key mesh-build-go.fetch, node ace, caller ace.mesh-delivery-builder.
|
|
func TestCallSubjectsOnANodeForAModuleTool(t *testing.T) {
|
|
c := &Conn{account: Account{User: "ace.mesh-delivery-builder"}}
|
|
called, fallback, err := callSubjectsOn("mesh-build-go.fetch", "ace", c.callerToken())
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if called != "mesh.mod.mesh-build-go.call.fetch.ace.ace~mesh-delivery-builder" {
|
|
t.Errorf("called: %q", called)
|
|
}
|
|
if fallback != "mesh.mod.mesh-build-go.tool.fetch.ace" {
|
|
t.Errorf("fallback: %q", fallback)
|
|
}
|
|
}
|
|
|
|
// 1470/AC2: seat:node-files.read on g14.
|
|
func TestCallSubjectsOnANodeForASeatVerb(t *testing.T) {
|
|
called, fallback, err := callSubjectsOn("seat:node-files.read", "g14", "tok")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if called != "mesh.seat.node-files.call.read.g14.tok" || fallback != "mesh.seat.node-files.tool.read.g14" {
|
|
t.Errorf("called %q, fallback %q", called, fallback)
|
|
}
|
|
// Issue 774 notes 84-87: the machine named in the key after '@' is the same subject, never a token holding '@'.
|
|
called, fallback, err = callSubjects("seat:node-files.read@g14", "tok")
|
|
if err != nil || called != "mesh.seat.node-files.call.read.g14.tok" || fallback != "mesh.seat.node-files.tool.read.g14" {
|
|
t.Errorf("@g14: called %q, fallback %q, %v", called, fallback, err)
|
|
}
|
|
if _, _, err := callSubjectsOn("seat:node-files.read@ace", "g14", "tok"); err == nil {
|
|
t.Error("a key naming ace was asked of g14")
|
|
}
|
|
}
|
|
|
|
// 1470/AC3: a node that is not one token is refused before anything is published (the Conn has no
|
|
// connection, so only the name check can answer with this error).
|
|
func TestCallOnRefusesANodeThatIsNotOneToken(t *testing.T) {
|
|
c := &Conn{}
|
|
for _, node := range []string{"", "a.b", "*", ">", "a b", "a\tb", "a@b"} {
|
|
if _, err := c.CallOn(node, "mesh-build-go.fetch", nil); err == nil || !strings.Contains(err.Error(), "not a node name") {
|
|
t.Errorf("CallOn(%q) = %v, want the refusal of the name", node, err)
|
|
}
|
|
}
|
|
// Issue 774 notes 84-87: Call refuses a malformed key the same way, before anything is published.
|
|
for _, key := range []string{"seat:node-service-manager.stop@novox-be.lan", "seat:a.b@x@y", "seat:a.*", "a.b@"} {
|
|
if _, err := c.Call(key, nil); err == nil {
|
|
t.Errorf("Call(%q) was published", key)
|
|
}
|
|
}
|
|
}
|
|
|
|
// The review of 2026-10-30 (issue 774): a callee's own answer is delivered, whatever its words, and is never asked
|
|
// again during a blackout; only the transport's no responders is.
|
|
func TestACallIsAskedAgainOnlyWhenItReachedNoOne(t *testing.T) {
|
|
t.Cleanup(busretry.Waits(10*time.Second, time.Millisecond, 2*time.Millisecond))
|
|
for _, row := range []struct {
|
|
name string
|
|
answer []error // per ask; the last repeats
|
|
asks int
|
|
kept bool // CallKept (novox/hq ADR 0668; issue 1966)
|
|
}{
|
|
{"a callee answers jetstream not enabled: asked once", []error{answered("jetstream not enabled")}, 1, false},
|
|
{"a callee answers no responders in its words: asked once", []error{answered("nats: no responders available for request")}, 1, false},
|
|
{"no responders on either subject, then an answer", []error{nats.ErrNoResponders, nats.ErrNoResponders, nil}, 3, false},
|
|
{"a timeout may have been delivered: Call asks once", []error{context.DeadlineExceeded, nil}, 1, false},
|
|
{"kept: no responders on a bus that answers (a callee restarting), then an answer", []error{nats.ErrNoResponders, nats.ErrNoResponders, nil}, 3, true},
|
|
{"kept: a timeout, then an answer", []error{context.DeadlineExceeded, nil}, 2, true},
|
|
{"kept: a callee's answer is asked once", []error{answered("nats: timeout")}, 1, true},
|
|
} {
|
|
asks := 0
|
|
request := func(string, []byte) (json.RawMessage, error) {
|
|
e := row.answer[min(asks, len(row.answer)-1)]
|
|
asks++
|
|
return json.RawMessage(`{}`), e
|
|
}
|
|
var err error
|
|
if row.kept {
|
|
_, err = callKeptWith(request, "called", "fallback", nil)
|
|
} else {
|
|
_, err = callWith(request, func() bool { return true }, "called", "fallback", nil)
|
|
}
|
|
if asks != row.asks {
|
|
t.Errorf("%s: asked %d times (%v), want %d", row.name, asks, err, row.asks)
|
|
}
|
|
}
|
|
}
|