The runtime knows no language, so nothing ties it to Node.js. This ports its serve mode — the pinned bus connection and patient connect, following memberships, launching every served bundle over MCP on stdio with its own environment, a child's emit published as its module, each tool, the tools verb and seat verbs served where the mesh issued them, and the console on loopback — to one static binary. Same subjects, request and reply bodies, event headers and MCP answers. The TypeScript stays: it is still the runtime inside the per-module containers until WP4c. Tests run against a real bus and share the TypeScript fixtures.
137 lines
5.6 KiB
Go
137 lines
5.6 KiB
Go
package console
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"io"
|
|
"net/http"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/novox/mesh-tools/node-tools/internal/bus"
|
|
mt "github.com/novox/mesh-tools/node-tools/internal/meshtest"
|
|
"github.com/novox/mesh-tools/node-tools/internal/runtime"
|
|
)
|
|
|
|
func connect(t *testing.T, module, node string) *bus.Conn {
|
|
t.Helper()
|
|
c, err := bus.Connect(bus.Credential{URL: mt.URL(t), Module: module, Node: node})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
c.Logf = func(string, ...any) {}
|
|
t.Cleanup(c.Close)
|
|
return c
|
|
}
|
|
|
|
func post(t *testing.T, endpoint string, body string) map[string]any {
|
|
t.Helper()
|
|
res, err := http.Post(endpoint, "application/json", bytes.NewBufferString(body))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer res.Body.Close()
|
|
raw, _ := io.ReadAll(res.Body)
|
|
var out map[string]any
|
|
if err := json.Unmarshal(raw, &out); err != nil {
|
|
t.Fatalf("%d %s", res.StatusCode, raw)
|
|
}
|
|
return out
|
|
}
|
|
|
|
// As node-tools, the runtime serves the bundles and is the console on loopback: the listing is what
|
|
// the modules and the mesh's records answered, and a call reaches the module on the machine named.
|
|
func TestTheConsoleListsAndCallsOverHTTP(t *testing.T) {
|
|
mesh := mt.New(t)
|
|
mesh.Issue(t, mt.MembershipOf("alpha", "desk", true, nil))
|
|
mesh.Issue(t, mt.MembershipOf("beta", "desk", false, map[string][]string{"node-shelf": {"list", "clear"}}))
|
|
nodeTools := connect(t, "node-tools", "desk")
|
|
stop, err := runtime.Run(nodeTools, []runtime.Served{
|
|
{Module: "alpha", Entrypoints: []string{mt.Fixture("many-alpha.serve.mjs")}},
|
|
{Module: "beta", Entrypoints: []string{mt.Fixture("many-beta.serve.mjs")}},
|
|
}, nil, (&mt.Logs{}).Logf)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer stop()
|
|
|
|
catalogue := connect(t, "mesh-catalog", "")
|
|
stopCat, _ := catalogue.Handle("catalog_modules", func(json.RawMessage) (any, error) {
|
|
return map[string]any{"modules": []map[string]string{{"module": "alpha"}, {"module": "beta"}, {"module": "ghost"}}}, nil
|
|
})
|
|
defer stopCat()
|
|
controller := connect(t, "mesh-controller", "")
|
|
stopSeat, _ := controller.HandleSubject("mesh.seat.mesh-controller.tool.tools", func(json.RawMessage) (any, error) {
|
|
return map[string]any{"seats": []map[string]any{{"seat": "node-shelf", "scope": "node", "tools": []map[string]any{
|
|
{"name": "list", "description": "what is on the shelf", "input": map[string]any{}},
|
|
{"name": "clear", "description": "take it all off", "input": map[string]any{}},
|
|
}}}}, nil
|
|
})
|
|
defer stopSeat()
|
|
catalogue.Flush()
|
|
controller.Flush()
|
|
|
|
up, err := Serve(NewSurface(nodeTools, "desk.node-tools"), "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer up.Close()
|
|
endpoint := "http://" + up.Address + "/mcp"
|
|
|
|
init := post(t, endpoint, `{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-03-26","capabilities":{}}}`)
|
|
if !strings.Contains(init["result"].(map[string]any)["instructions"].(string), "reached as desk.node-tools") {
|
|
t.Errorf("initialize: %v", init)
|
|
}
|
|
listed := post(t, endpoint, `{"jsonrpc":"2.0","id":2,"method":"tools/list"}`)["result"].(map[string]any)
|
|
var names []string
|
|
for _, x := range listed["tools"].([]any) {
|
|
names = append(names, x.(map[string]any)["name"].(string))
|
|
}
|
|
if got := strings.Join(names, ","); got != "alpha.one,alpha.two,beta.five,beta.four,beta.three,node-shelf.clear,node-shelf.list" {
|
|
t.Errorf("listed %s", got)
|
|
}
|
|
if got := listed["_meta"].(map[string]any)["notAnswering"]; len(got.([]any)) != 1 || got.([]any)[0] != "ghost" {
|
|
t.Errorf("not answering: %v", got)
|
|
}
|
|
for _, x := range listed["tools"].([]any) {
|
|
tool := x.(map[string]any)
|
|
if tool["name"] == "node-shelf.list" {
|
|
schema := tool["inputSchema"].(map[string]any)
|
|
if req, _ := schema["required"].([]any); len(req) != 1 || req[0] != "node" {
|
|
t.Errorf("a node seat's verb does not require node: %v", schema)
|
|
}
|
|
}
|
|
}
|
|
|
|
called := post(t, endpoint, `{"jsonrpc":"2.0","id":3,"method":"tools/call","params":{"name":"alpha.one","arguments":{"node":"desk"}}}`)
|
|
content := called["result"].(map[string]any)["content"].([]any)
|
|
var got map[string]any
|
|
_ = json.Unmarshal([]byte(content[0].(map[string]any)["text"].(string)), &got)
|
|
if got["alpha"] != float64(1) || content[1].(map[string]any)["text"] != "answered by desk" {
|
|
t.Errorf("called: %v", called)
|
|
}
|
|
seat := post(t, endpoint, `{"jsonrpc":"2.0","id":4,"method":"tools/call","params":{"name":"node-shelf.list","arguments":{"node":"desk"}}}`)
|
|
if text := seat["result"].(map[string]any)["content"].([]any)[0].(map[string]any)["text"].(string); !strings.Contains(text, `"a"`) {
|
|
t.Errorf("seat verb: %v", seat)
|
|
}
|
|
refused := post(t, endpoint, `{"jsonrpc":"2.0","id":5,"method":"tools/call","params":{"name":"node-shelf.list","arguments":{}}}`)
|
|
if refused["error"] == nil {
|
|
t.Errorf("a node seat's verb was called without its machine: %v", refused)
|
|
}
|
|
absent := post(t, endpoint, `{"jsonrpc":"2.0","id":6,"method":"tools/call","params":{"name":"ghost.boo","arguments":{}}}`)
|
|
result := absent["result"].(map[string]any)
|
|
if result["isError"] != true || !strings.Contains(result["content"].([]any)[0].(map[string]any)["text"].(string), "nothing serves ghost.boo") {
|
|
t.Errorf("an absent tool: %v", absent)
|
|
}
|
|
res, err := http.Post(endpoint, "application/json", strings.NewReader(`{"jsonrpc":"2.0","method":"notifications/initialized"}`))
|
|
if err != nil || res.StatusCode != 202 {
|
|
t.Errorf("a notification: %v %v", res, err)
|
|
}
|
|
}
|
|
|
|
func TestTheConsoleListensOnLoopbackAndNowhereElse(t *testing.T) {
|
|
if _, err := Serve(NewSurface(nil, "x"), "0.0.0.0:0"); err == nil || !strings.Contains(err.Error(), "loopback and nowhere else") {
|
|
t.Errorf("a non-loopback console was not refused: %v", err)
|
|
}
|
|
}
|