The per-node process that makes a module's tools serve on the mesh: - broker-amqp.ts: a concrete AMQP implementation of the sdk's Broker contract (request/reply over a reply queue + correlation id, publish/ subscribe over a topic exchange). Kept here, not in the sdk, so a broker-client change never rebuilds a module (ADR 0044). - runtime.ts: bind the broker, import the assigned modules' tool entrypoints (each registers as it loads), serveTools. The thin wrapper. - main.ts: the entrypoint, configured by MESH_BROKER_URL + MESH_TOOL_MODULES. - Dockerfile: the container a node runs it as. Verified over a REAL broker: the test spins LavinMQ (the mesh's broker), the runtime serves a registered tool, a separate connection invokes it by name over AMQP and gets the result, and an unknown tool is refused over the wire. So 'modules can serve' is now running-on-the-mesh, not just proven in a mock. Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
45 lines
1.8 KiB
TypeScript
45 lines
1.8 KiB
TypeScript
import { test } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
|
|
import { registerModuleTools, resetTools } from "@novox/mesh-sdk/tools";
|
|
import { connectAmqp } from "../dist/broker-amqp.js";
|
|
import { runTools } from "../dist/runtime.js";
|
|
|
|
// Requires a real broker at $MESH_BROKER_URL. The test harness spins LavinMQ (the mesh's broker)
|
|
// and points this at it; skipped if it is not set, never failed for the environment.
|
|
const url = process.env.MESH_BROKER_URL;
|
|
|
|
test("a module's tool serves and is invoked over a real AMQP broker", { skip: !url }, async () => {
|
|
resetTools();
|
|
|
|
// A module registers a real tool, exactly as umami does.
|
|
registerModuleTools("demo", () => [
|
|
{
|
|
name: "greet",
|
|
description: "return a greeting",
|
|
input: { who: { type: "string" } },
|
|
run: async (args) => ({ hello: String(args.who), from: "the tool runtime" }),
|
|
},
|
|
]);
|
|
|
|
// The runtime binds the broker and serves the module (no module entrypoints to import here — the
|
|
// tool is registered in-process — but this is the exact runtime that serves on a node).
|
|
const serverBroker = await connectAmqp(url!);
|
|
const stop = await runTools({ broker: serverBroker, moduleEntrypoints: [] });
|
|
|
|
// A separate connection — a caller, like mesh-control's command API — invokes over the broker.
|
|
const caller = await connectAmqp(url!);
|
|
const result = await caller.request<{ tool: string; args: Record<string, unknown> }, { hello: string }>(
|
|
"tools.invoke",
|
|
{ tool: "greet", args: { who: "mesh" } },
|
|
);
|
|
assert.equal(result.hello, "mesh");
|
|
|
|
// An unknown tool is refused over the wire, not silently dropped.
|
|
await assert.rejects(caller.request("tools.invoke", { tool: "nope", args: {} }), /no such tool/);
|
|
|
|
stop();
|
|
await caller.close();
|
|
await serverBroker.close();
|
|
});
|