Files
mesh-sdk/go/process/call_test.go
T
jschoubben 23ecaddcf7 S2: CallKept and CallKeptOn ask a kept call again through a callee's restart (issue 1966, ADR 0668)
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
2026-11-10 02:52:19 +01:00

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)
}
}
}