/** * A person's client, against a real bus. * * What is worth checking is not that a request/reply works — the runtime's own tests cover that — but * that the two surfaces are the same thing. An agent and a person must see the same tools and get the * same answers, or the MCP surface becomes a second definition of what a tool is. * * 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/client.test.ts */ import assert from "node:assert/strict"; import { test } from "node:test"; // The built output, not the source: the client imports its siblings as `.js`, which is what ships and // what every other file here does, and cannot be loaded as TypeScript directly. `pretest` builds. import { connectNats } from "../dist/broker-nats.js"; import { callTool, toolsOn, whyItFailed } from "../dist/client.js"; const url = process.env.MESH_TEST_NATS; /** A mesh as discovery sees it (design 34 §3): the catalogue holding three modules, two of them up * and answering `tools`, one held and not running. Two connections, because a person and a module * are different users even in a test. */ async function aMeshWithTools() { const catalogue = await connectNats({ url: url!, module: "mesh-catalog" }); const shop = await connectNats({ url: url!, module: "shop" }); await catalogue.handle("catalog_modules", async () => ({ modules: [{ module: "shop" }, { module: "mesh-catalog" }, { module: "ghost" }], })); await catalogue.handle("tools", async () => ({ module: "mesh-catalog", tools: [{ name: "catalog_modules", description: "what modules the mesh has", input: {} }], })); await shop.handle("tools", async () => ({ module: "shop", tools: [{ name: "price", description: "what something costs", input: { type: "object" } }], })); await shop.handle("price", async (body: { of?: string }) => ({ of: body.of ?? "nothing", cost: 12 })); // And the mesh's own records, served by the holder of the mesh-controller seat (ADR 0154): one // role's tool, and the verb that lists every role's. const controller = await connectNats({ url: url!, module: "mesh-controller" }); await controller.handle("seat:mesh-controller.tools", async () => ({ seats: [ { seat: "mesh-controller", scope: "mesh", tools: [{ name: "status", description: "what is wrong", input: {} }] }, { seat: "node-dns-resolver", scope: "node", tools: [{ name: "lookup", description: "one machine's", input: {} }] }, ], })); await controller.handle("seat:mesh-controller.status", async () => ({ output: "all quiet", ok: true })); return { async close() { await catalogue.close(); await shop.close(); await controller.close(); }, }; } test("a person sees what the running modules answer, sorted, and who did not answer", async (t) => { if (!url) return t.skip("MESH_TEST_NATS unset"); const mesh = await aMeshWithTools(); const person = await connectNats({ url, module: "person.ada" }); try { const began = Date.now(); const have = await toolsOn(person); assert.deepEqual( have.tools.map((x) => `${x.module}.${x.name}`), ["mesh-catalog.catalog_modules", "mesh-controller.status", "shop.price"], "the list is what the modules answered plus every role's tools, in a stable order", ); // A role's tool is marked as one; a node-scoped seat's waits for a caller naming the node. assert.ok(have.tools.find((x) => x.module === "mesh-controller")!.seat); assert.ok(!have.tools.some((x) => x.module === "node-dns-resolver")); // Silence is named, never dropped: a module the catalogue holds and nothing answered for. assert.deepEqual(have.notAnswering, ["ghost"]); // And at once: a module that is not running costs nothing, or the list is unusable. assert.ok(Date.now() - began < 5_000, "an absent module waited out the timeout"); } finally { await person.close(); await mesh.close(); } }); test("a person calls a tool and gets the module's own answer, unshaped", async (t) => { if (!url) return t.skip("MESH_TEST_NATS unset"); const mesh = await aMeshWithTools(); const person = await connectNats({ url, module: "person.ada" }); try { const answer = await callTool(person, "shop.price", { of: "a hat" }); assert.deepEqual(answer.result, { of: "a hat", cost: 12 }); } finally { await person.close(); await mesh.close(); } }); test("a tool nobody serves says so at once, and says what to do about it", async (t) => { if (!url) return t.skip("MESH_TEST_NATS unset"); const person = await connectNats({ url: url!, module: "person.ada" }); try { const began = Date.now(); await assert.rejects(() => callTool(person, "ghost.missing", {})); // At once, not after the whole wait: "that module is down" and "that tool is slow" need // different things done, and a timeout cannot tell them apart. assert.ok(Date.now() - began < 5_000, "a tool nobody serves waited out the timeout"); } finally { await person.close(); } }); test("a name that is not . is refused before anything is sent", async (t) => { if (!url) return t.skip("MESH_TEST_NATS unset"); const person = await connectNats({ url: url!, module: "person.ada" }); try { await assert.rejects(() => callTool(person, "price", {}), /does not name a tool/); } finally { await person.close(); } }); test("each way a call fails says what to do about it", () => { // The three answers a person actually gets. Without this they are one timeout and a stack trace, // and the remedies are in three different places. assert.match(whyItFailed("shop.price", new Error("no responders")), /nothing serves shop\.price/); assert.match( whyItFailed("shop.price", new Error("Permissions Violation for Publish")), /may not call shop\.price/, ); assert.match(whyItFailed("shop.price", new Error("timeout")), /did not answer in time/); assert.match(whyItFailed("shop.price", new Error("something else")), /something else/); }); test("a role's tool is reached through the seat, and a module's own name is never shadowed", async (t) => { if (!url) return t.skip("MESH_TEST_NATS unset"); const { seatsIn, toolKey } = await import("../dist/client.js"); const mesh = await aMeshWithTools(); const person = await connectNats({ url, module: "person.ada" }); try { const roles = seatsIn(await toolsOn(person)); assert.equal(toolKey("mesh-controller.status", roles), "seat:mesh-controller.status"); assert.equal(toolKey("mesh-controller.other", roles), "mesh-controller.other", "a verb the seat does not declare is a module's"); assert.equal(toolKey("shop.price", roles), "shop.price"); const answer = await callTool(person, "mesh-controller.status", {}, roles); assert.deepEqual(answer.result, { output: "all quiet", ok: true }); const direct = await callTool(person, "seat:mesh-controller.status", {}); assert.deepEqual(direct.result, { output: "all quiet", ok: true }); } finally { await person.close(); await mesh.close(); } }); // A module on two machines (novox/hq ADR 0159): a call names the machine and reaches that instance // and no other; a call that names none reaches one of them and says which; a seat's verb is served // by the claimant's tool of the same name on the seat's own subject. test("a tool call names the machine it is for, and every answer says which machine answered", async (t) => { if (!url) return t.skip("MESH_TEST_NATS unset"); const onAnchor = await connectNats({ url: url!, module: "store", node: "anchor" }); const onHome = await connectNats({ url: url!, module: "store", node: "home-server" }); const asker = await connectNats({ url: url!, module: "console", node: "workstation" }); try { await onAnchor.handle("databases", async () => ({ at: "anchor" })); await onHome.handle("databases", async () => ({ at: "home-server" })); const home = await callTool(asker, "store.databases@home-server", {}); assert.deepEqual(home.result, { at: "home-server" }); assert.equal(home.node, "home-server"); const anchor = await callTool(asker, "store.databases@anchor", {}); assert.deepEqual(anchor.result, { at: "anchor" }); assert.equal(anchor.node, "anchor"); const whichever = await callTool(asker, "store.databases", {}); assert.ok(["anchor", "home-server"].includes(whichever.node ?? ""), `an unnamed call still says who answered: ${whichever.node}`); assert.deepEqual(whichever.result, { at: whichever.node }); // A seat's verb, served by the claimant on the seat's subject; a mesh seat's is flat. await onAnchor.handleSubject("mesh.seat.mesh-store.tool.databases", async () => ({ seat: "mesh-store", at: "anchor" })); const viaSeat = await callTool(asker, "seat:mesh-store.databases", {}); assert.deepEqual(viaSeat.result, { seat: "mesh-store", at: "anchor" }); assert.equal(viaSeat.node, "anchor"); } finally { await onAnchor.close(); await onHome.close(); await asker.close(); } });