Main reverted G's half merge (#189, reverted by8afc3ebin #191) and then took the reader release's S1-S5, which copied G's code. G's form holds where they differ (process_bus.go, seats.go, state.go and their tests); the release's kept names stay so main's callers keep building: SeatViewPrefix beside SeatReadPrefix, and QueueInvoke's empty strings on not-ok (with its test TestQueueInvoke). The tree differs from G's headf30dee54only by those and the release's two tests (objects cap option, seat states and stream kinds). Builds: design 47 G (the milestone's sdk part, as atf30dee54) Claude-Session: https://claude.ai/code/session_01NcbgfGKaxw984XyAPfLm4h
190 lines
8.3 KiB
Go
190 lines
8.3 KiB
Go
package process
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"git.novox.be/novox/mesh-sdk/go/seatdef"
|
|
"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)
|
|
}
|
|
}
|
|
}
|
|
|
|
// A-107: AnswerSeat reads the caller from a call's last token (dots written `~`), "" for anything that is not a
|
|
// bus user's name; and it refuses what it cannot serve before subscribing. A-179, A-186: before a handler runs, an
|
|
// unproven caller is refused, and every caller while the seat's definition is unread, names no such verb or opens it to
|
|
// none; a proven caller of a verb the definition names runs it.
|
|
func TestUserOfTokenAndAnswerSeatRefusals(t *testing.T) {
|
|
for token, want := range map[string]string{"novox~mesh-delivery-builder": "novox.mesh-delivery-builder", "person~jo": "person.jo",
|
|
"plain": "plain", "": "", "a b": "", "a*": "", "~": ""} {
|
|
if got := userOfToken(token); got != want {
|
|
t.Errorf("userOfToken(%q) = %q, want %q", token, got, want)
|
|
}
|
|
}
|
|
c := &Conn{}
|
|
ok := func(string, json.RawMessage) (any, error) { return nil, nil }
|
|
defined := func() (seatdef.Definition, bool) {
|
|
return seatdef.Definition{Seat: "seat", Verbs: []seatdef.Verb{{Name: "v"}}}, true
|
|
}
|
|
for name, verbs := range map[string]map[string]SeatHandler{"none": nil, "nil handler": {"v": nil}, "bad verb": {"a b": ok}, "wildcard": {"*": ok}} {
|
|
if _, err := c.AnswerSeat("seat", defined, verbs); err == nil {
|
|
t.Errorf("AnswerSeat accepted %s", name)
|
|
}
|
|
}
|
|
if _, err := c.AnswerSeat("seat", nil, map[string]SeatHandler{"v": ok}); err == nil {
|
|
t.Error("AnswerSeat accepted no definition")
|
|
}
|
|
ran := 0
|
|
run := func(string, json.RawMessage) (any, error) { ran++; return "ran", nil }
|
|
for _, row := range []struct {
|
|
name, caller string
|
|
def func() (seatdef.Definition, bool)
|
|
says string
|
|
}{
|
|
{"an unproven caller", "", defined, "takes no unproven caller"},
|
|
{"the definition unread", "novox.mesh-recovery", func() (seatdef.Definition, bool) { return seatdef.Definition{}, false }, "not yet read"},
|
|
{"a verb the definition does not name", "novox.mesh-recovery", func() (seatdef.Definition, bool) { return seatdef.Definition{Seat: "seat"}, true }, "names no verb v"},
|
|
{"a verb open to none", "novox.mesh-recovery", func() (seatdef.Definition, bool) {
|
|
return seatdef.Definition{Seat: "seat", Verbs: []seatdef.Verb{{Name: "v", Callers: &seatdef.Callers{List: []string{}}}}}, true
|
|
}, "open to no caller"},
|
|
{"a marked verb with no gate (§677)", "novox.mesh-recovery", func() (seatdef.Definition, bool) {
|
|
return seatdef.Definition{Seat: "seat", Verbs: []seatdef.Verb{{Name: "v", NeedsApproval: true}}}, true
|
|
}, "through no gate"},
|
|
{"a proven caller", "novox.mesh-recovery", defined, ""},
|
|
} {
|
|
before := ran
|
|
_, _, err := c.admitted("seat", "v", row.def, run, nil)(row.caller, nil, false)
|
|
if row.says == "" && (err != nil || ran != before+1) || row.says != "" && (err == nil || !strings.Contains(err.Error(), row.says) || ran != before) {
|
|
t.Errorf("%s: %v, ran %d times", row.name, err, ran-before)
|
|
}
|
|
}
|
|
}
|