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.
58 lines
2.8 KiB
JavaScript
58 lines
2.8 KiB
JavaScript
// Round-trip the runtime's NATS client against a real server: a tool call answered, and an
|
|
// event emitted and received with its envelope intact.
|
|
import { connect } from "nats";
|
|
import { connectNats } from "../dist/broker-nats.js";
|
|
|
|
const URL = "nats://127.0.0.1:14222";
|
|
|
|
// The controller's job, done by hand here: the stream and the module's durable consumer.
|
|
const admin = await connect({ servers: URL });
|
|
const jsm = await admin.jetstreamManager();
|
|
await jsm.streams.add({ name: "EVENTS", subjects: ["mesh.mod.*.event.>", "mesh.seat.*.event.>"] });
|
|
await jsm.consumers.add("EVENTS", {
|
|
durable_name: "one_audit", ack_policy: "explicit",
|
|
filter_subjects: ["mesh.mod.shop.event.order.placed"],
|
|
});
|
|
|
|
const shop = await connectNats({ url: URL, node: "one", module: "shop" });
|
|
const audit = await connectNats({ url: URL, node: "one", module: "audit" });
|
|
|
|
let failures = 0;
|
|
const check = (ok, what) => { console.log(` ${ok ? "ok " : "FAIL"} ${what}`); if (!ok) failures++; };
|
|
|
|
// A tool, served and called.
|
|
await shop.handle("price", async (body) => ({ total: body.qty * 3 }));
|
|
const answer = await audit.request("shop.price", { qty: 4 });
|
|
check(answer.total === 12, "a tool call is answered across two connections");
|
|
|
|
// A handler that throws reaches the caller as an error, not a timeout.
|
|
await shop.handle("boom", async () => { throw new Error("no"); });
|
|
let threw = null;
|
|
try { await audit.request("shop.boom", {}); } catch (e) { threw = e.message; }
|
|
check(threw === "no", "a handler that throws answers the caller instead of timing out");
|
|
|
|
// An event, emitted and received with its envelope intact.
|
|
const seen = [];
|
|
await audit.subscribe("order.placed", async (env) => { seen.push(env); });
|
|
await shop.publish({
|
|
key: "order.placed", node: "one", body: { id: "a1" },
|
|
headers: { "x-event-id": "e1", "x-node": "one", "content-type": "application/json" },
|
|
});
|
|
await new Promise((r) => setTimeout(r, 800));
|
|
check(seen.length === 1, `exactly one delivery (saw ${seen.length})`);
|
|
if (seen[0]) {
|
|
check(seen[0].key === "order.placed", "the key survives the subject round trip");
|
|
check(seen[0].body?.id === "a1", "the body is the payload, not the whole envelope");
|
|
check(seen[0].node === "one", "the node comes back from the headers");
|
|
check(seen[0].headers?.["x-event-id"] === "e1", "the event id survives as a header");
|
|
}
|
|
|
|
// A module cannot reach into another's namespace by naming its own event oddly.
|
|
await shop.publish({ key: "other", node: "one", body: {}, headers: { "x-event-id": "e2" } });
|
|
const msg = await jsm.streams.getMessage("EVENTS", { last_by_subj: "mesh.mod.shop.event.other" });
|
|
check(!!msg, "an event lands under the emitting module's own namespace");
|
|
|
|
await shop.close(); await audit.close(); await admin.close();
|
|
console.log(failures ? `\n${failures} failed` : "\nall passed");
|
|
process.exit(failures ? 1 : 0);
|