One repository, two modules (ADR 0069). `node-tools/` holds the runtime — its code, tests, package and the manifest of the module the controller composes a process for on every machine it is assigned to: a bundle of `src/main.js`, the interpreter as a package, a place for the node's credential, the loopback port the console declared, and leave to call every tool. Nothing about how it runs: which bundles to load, where the credential is and whose machine it is are the controller's to compose (WP2). The root module `mesh-tools` keeps the two images TypeScript bundles are compiled in and a module's own service may run in; it is no longer how tools reach a node. As node-tools, `serve` is also the console (ADR 0175 §6): the same process answers MCP on loopback for whoever is on the machine, through which the tools it serves can be called. A module's own runtime in a container keeps serving without a listener. The toolchain image now carries /app/runtime — a package.json saying the compiled files are ES modules and the production node_modules — for the builder to copy into every TypeScript bundle, so a bundle unpacked on a machine starts (ADR 0188 §5; the builder's side is the controller's). Proven here by compiling node-tools with the toolchain's exact flags and starting the result. The AMQP probe script is gone with the bus it probed.
61 lines
2.4 KiB
TypeScript
61 lines
2.4 KiB
TypeScript
/**
|
|
* Every module's runtime answers `tools` for it (novox/hq ADR 0152, design 34 §3): the names,
|
|
* descriptions and schemas from the code that answers them. Against a real bus, because the claim is
|
|
* what a second connection gets back.
|
|
*
|
|
* 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/runtime-tools.test.ts
|
|
*/
|
|
import assert from "node:assert/strict";
|
|
import { test } from "node:test";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
import { resetTools } from "@novox/mesh-sdk/tools";
|
|
|
|
import { connectNats } from "../dist/broker-nats.js";
|
|
import { runTools } from "../dist/runtime.js";
|
|
|
|
const url = process.env.MESH_TEST_NATS;
|
|
const fixture = (name: string) => fileURLToPath(new URL(`./fixtures/${name}`, import.meta.url));
|
|
|
|
test("a module registering two tools answers three names, the third being what it serves", async (t) => {
|
|
if (!url) return t.skip("MESH_TEST_NATS unset");
|
|
resetTools();
|
|
const shop = await connectNats({ url, node: "one", module: "shop" });
|
|
const asker = await connectNats({ url, module: "person.ada" });
|
|
const stop = await runTools({ broker: shop, moduleEntrypoints: [fixture("shop-tools.mjs")] });
|
|
try {
|
|
const answer = await asker.request<Record<string, never>, { module: string; tools: { name: string; input: unknown }[] }>(
|
|
"shop.tools",
|
|
{},
|
|
);
|
|
assert.equal(answer.module, "shop");
|
|
assert.deepEqual(answer.tools.map((x) => x.name), ["price", "refund"]);
|
|
// The schema travels with the name: a name alone is not callable by something that has never
|
|
// seen the mesh before.
|
|
assert.deepEqual(answer.tools[0].input, { of: { type: "string", description: "the thing" } });
|
|
// And the tools themselves still answer beside it.
|
|
const priced = await asker.request<{ of: string }, { cost: number }>("shop.price", { of: "a hat" });
|
|
assert.equal(priced.cost, 12);
|
|
} finally {
|
|
stop();
|
|
await asker.close();
|
|
await shop.close();
|
|
}
|
|
});
|
|
|
|
test("a module naming a tool of its own `tools` is refused at load", async (t) => {
|
|
if (!url) return t.skip("MESH_TEST_NATS unset");
|
|
resetTools();
|
|
const clash = await connectNats({ url, node: "one", module: "clash" });
|
|
try {
|
|
await assert.rejects(
|
|
() => runTools({ broker: clash, moduleEntrypoints: [fixture("clash-tools.mjs")] }),
|
|
/names a tool "tools"/,
|
|
);
|
|
} finally {
|
|
resetTools();
|
|
await clash.close();
|
|
}
|
|
});
|