The review found that busretry could ask a delivered call again: Conn.request turned a callee's error answer into errors.New(words), and Undelivered matched the words "no responders" and "jetstream not enabled", so a callee that answered so during a blackout ran its work twice. A callee's answer is now *busretry.Answered, which Undelivered never counts: only the transport's own no responders makes a call undelivered. The other busretry sites read or write through JetStream's API (State, Ask) or the runtime's state verbs, where the words are the transport's.
113 lines
4.0 KiB
Go
113 lines
4.0 KiB
Go
package process
|
|
|
|
import (
|
|
"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)
|
|
}
|
|
}
|
|
|
|
// 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"} {
|
|
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)
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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) {
|
|
busretry.Bound, busretry.FirstWait, busretry.MostWait = 50*time.Millisecond, time.Millisecond, 2*time.Millisecond
|
|
for _, row := range []struct {
|
|
name string
|
|
answer []error // per ask; the last repeats
|
|
asks int
|
|
}{
|
|
{"a callee answers jetstream not enabled: asked once", []error{answered("jetstream not enabled")}, 1},
|
|
{"a callee answers no responders in its words: asked once", []error{answered("nats: no responders available for request")}, 1},
|
|
{"no responders on either subject, then an answer", []error{nats.ErrNoResponders, nats.ErrNoResponders, nil}, 3},
|
|
} {
|
|
asks := 0
|
|
_, err := callWith(func(string, []byte) (json.RawMessage, error) {
|
|
e := row.answer[min(asks, len(row.answer)-1)]
|
|
asks++
|
|
return json.RawMessage(`{}`), e
|
|
}, 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)
|
|
}
|
|
}
|
|
}
|