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 ., when called, then the subject is // mesh.mod..call.. with the same fallback (mesh.mod..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:. calls mesh.seat..call.., // falling back to mesh.seat..tool.. 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) } } }