Call a module's tool or a seat verb on a named machine (issue 1470), part 3 of 6
This commit is contained in:
+40
-2
@@ -28,6 +28,26 @@ func (c *Conn) Call(key string, body any) (json.RawMessage, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c.callOn(called, fallback, body)
|
||||
}
|
||||
|
||||
// CallOn is Call to the tool or seat verb of one named machine (novox/hq ADR 0469 §3; issue 1470): the tool
|
||||
// runner of that node alone answers, on mesh.mod.<module>.call.<tool>.<node>.<token> (a seat verb:
|
||||
// mesh.seat.<seat>.call.<verb>.<node>.<token>), falling back to the subject that names no caller
|
||||
// (mesh-tools node-tools/internal/bus/caller.go:15-19). node is one subject token: an empty one, or one
|
||||
// holding '.', '*', '>' or white space, is refused before anything is published.
|
||||
func (c *Conn) CallOn(node, key string, body any) (json.RawMessage, error) {
|
||||
if err := checkNode(node); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
called, fallback, err := callSubjectsOn(key, node, c.callerToken())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return c.callOn(called, fallback, body)
|
||||
}
|
||||
|
||||
func (c *Conn) callOn(called, fallback string, body any) (json.RawMessage, error) {
|
||||
data, err := json.Marshal(body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -46,18 +66,36 @@ func (c *Conn) callerToken() string { return strings.ReplaceAll(c.account.User,
|
||||
// callSubjects is the caller-named subject key is called on, and its plain fallback (mesh-tools
|
||||
// node-tools/internal/bus/caller.go CalledSubject, toolShape, calledPattern).
|
||||
func callSubjects(key, token string) (called, fallback string, err error) {
|
||||
return callSubjectsOn(key, "", token)
|
||||
}
|
||||
|
||||
// checkNode refuses a node name that is not one subject token.
|
||||
func checkNode(node string) error {
|
||||
if node == "" || strings.ContainsAny(node, ".*> \t\r\n") {
|
||||
return fmt.Errorf("%q is not a node name (one subject token: no '.', '*', '>' or space)", node)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// callSubjectsOn is callSubjects for one named node ("" names none): the node is a token between the tool and
|
||||
// the caller on the called subject, and the last one of the fallback.
|
||||
func callSubjectsOn(key, node, token string) (called, fallback string, err error) {
|
||||
at := ""
|
||||
if node != "" {
|
||||
at = "." + node
|
||||
}
|
||||
if rest, ok := strings.CutPrefix(key, "seat:"); ok {
|
||||
seat, verb, ok := strings.Cut(rest, ".")
|
||||
if !ok || seat == "" || verb == "" {
|
||||
return "", "", fmt.Errorf("%q names no seat and verb (seat:<seat>.<verb>)", key)
|
||||
}
|
||||
return "mesh.seat." + seat + ".call." + verb + "." + token, "mesh.seat." + seat + ".tool." + verb, nil
|
||||
return "mesh.seat." + seat + ".call." + verb + at + "." + token, "mesh.seat." + seat + ".tool." + verb + at, nil
|
||||
}
|
||||
module, tool, ok := strings.Cut(key, ".")
|
||||
if !ok || module == "" || tool == "" {
|
||||
return "", "", fmt.Errorf("%q is not <module>.<tool>, and does not start seat:<seat>.<verb>", key)
|
||||
}
|
||||
return "mesh.mod." + module + ".call." + tool + "." + token, "mesh.mod." + module + ".tool." + tool, nil
|
||||
return "mesh.mod." + module + ".call." + tool + at + "." + token, "mesh.mod." + module + ".tool." + tool + at, nil
|
||||
}
|
||||
|
||||
// request asks one subject once and reads its reply ({result, error, node}); a reply naming an error is
|
||||
|
||||
@@ -235,3 +235,45 @@ func TestReleaseAcceptanceAProcessDoesExactlyItsBusDeclaration(t *testing.T) {
|
||||
t.Error("subscribing a tool-call subject the process does not serve was not refused")
|
||||
}
|
||||
}
|
||||
|
||||
// 1470/AC4: two answerers of the same verb on the subjects of nodes ace and g14; CallOn naming g14 gets g14's
|
||||
// answer, and ace's answerer hears nothing.
|
||||
func TestCallOnReachesTheNamedNodeAlone(t *testing.T) {
|
||||
url, account := grantedSendProcess(t)
|
||||
adminNC, _ := adminJS(t, url)
|
||||
aceHeard := make(chan struct{}, 1)
|
||||
for _, node := range []string{"ace", "g14"} {
|
||||
node := node
|
||||
sub, err := adminNC.Subscribe("mesh.seat.operator-channel.call.tell."+node+".novox~mesh-mail~send", func(m *nats.Msg) {
|
||||
if node == "ace" {
|
||||
aceHeard <- struct{}{}
|
||||
}
|
||||
_ = m.Respond([]byte(`{"result":{"node":"` + node + `"}}`))
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer func() { _ = sub.Unsubscribe() }()
|
||||
}
|
||||
if err := adminNC.Flush(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
conn, err := Connect(account)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
result, err := conn.CallOn("g14", "seat:operator-channel.tell", map[string]any{})
|
||||
if err != nil {
|
||||
t.Fatalf("CallOn: %v", err)
|
||||
}
|
||||
if string(result) != `{"node":"g14"}` {
|
||||
t.Fatalf("result: %s, want g14's answer", result)
|
||||
}
|
||||
select {
|
||||
case <-aceHeard:
|
||||
t.Error("ace's answerer heard a call named for g14")
|
||||
default:
|
||||
}
|
||||
}
|
||||
|
||||
+41
-1
@@ -1,6 +1,9 @@
|
||||
package process
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// 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>).
|
||||
@@ -39,3 +42,40 @@ func TestCallSubjectsRefusesAKeyNamingNeither(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user