/** * The runtime as the node-tools module (novox/hq ADR 0175 §6, to-be 38 WP3): started the way the * host starts it — `main.js` with the node's credential and MESH_TOOL_MODULES — it serves the bundles * AND answers MCP on loopback as the console, through which a tool it serves can be called. * * docker run -d --rm --name t -p 14232:4222 nats:2.10-alpine -js * MESH_TEST_NATS=nats://127.0.0.1:14232 node --test --experimental-strip-types test/node-tools-serve.test.ts */ import assert from "node:assert/strict"; import { test } from "node:test"; import { spawn, type ChildProcess } from "node:child_process"; import { mkdtemp, writeFile } from "node:fs/promises"; import { join } from "node:path"; import { fileURLToPath } from "node:url"; import { connectNats } from "../dist/broker-nats.js"; const url = process.env.MESH_TEST_NATS; const fixture = (name: string) => fileURLToPath(new URL(`./fixtures/${name}`, import.meta.url)); async function post(endpoint: string, body: unknown): Promise { const res = await fetch(endpoint, { method: "POST", headers: { "content-type": "application/json" }, body: JSON.stringify(body) }); return res.json(); } test("as node-tools, serve loads the bundles and is the console on loopback", async (t) => { if (!url) return t.skip("MESH_TEST_NATS unset"); // The catalogue, for discovery; the node credential names the runtime module and no claims. const catalogue = await connectNats({ url, module: "mesh-catalog" }); await catalogue.handle("catalog_modules", async () => ({ modules: [{ module: "alpha" }] })); t.after(() => catalogue.close()); const dir = await mkdtemp("/tmp/node-tools-"); const credential = join(dir, "broker"); await writeFile(credential, JSON.stringify({ url, node: "desk", module: "node-tools", user: "desk.node-tools", password: "x" })); const child: ChildProcess = spawn(process.execPath, ["dist/main.js"], { env: { ...process.env, MESH_BROKER_FILE: credential, MESH_TOOL_MODULES: `alpha=${fixture("many-alpha.serve.mjs")}`, MESH_CONSOLE_LISTEN: "127.0.0.1:0", }, stdio: ["ignore", "pipe", "pipe"], }); t.after(() => { child.kill("SIGTERM"); }); const endpoint = await new Promise((resolve, reject) => { let out = ""; let err = ""; child.stdout!.on("data", (d) => { out += d.toString(); const m = /listening on (http:\/\/[^/]+\/mcp) as desk\.node-tools/.exec(out); if (m) resolve(m[1]!); }); child.stderr!.on("data", (d) => (err += d.toString())); child.on("exit", (code) => reject(new Error(`serve exited ${code}: ${err}`))); }); const listed = await post(endpoint, { jsonrpc: "2.0", id: 1, method: "tools/list" }); assert.deepEqual(listed.result.tools.map((x: any) => x.name).filter((n: string) => n.startsWith("alpha.")), ["alpha.one", "alpha.two"]); // A tool the same process serves on the bus, called through the console it also is. const called = await post(endpoint, { jsonrpc: "2.0", id: 2, method: "tools/call", params: { name: "alpha.one", arguments: { node: "desk" } } }); assert.deepEqual(JSON.parse(called.result.content[0].text), { alpha: 1 }); });