serveRegisteredOverStdio lists the registrations of the module named by MESH_SERVED_MODULE by their own names and every other registration as <seat>.<verb>. Before, the first registration was taken for the module's own, so a bundle registering its seat first listed the seat's verbs as its own tools. Contributors are given the process's environment, the bundle's own. 0.1.4.
71 lines
3.7 KiB
TypeScript
71 lines
3.7 KiB
TypeScript
// A tools bundle as a process (novox/hq ADR 0188): the stdio loop answers initialize, tools/list and
|
|
// tools/call the way the node's runtime asks, and a tool that throws is a tool error, not a hang-up.
|
|
import assert from "node:assert/strict";
|
|
import { test } from "node:test";
|
|
import { spawn } from "node:child_process";
|
|
import { fileURLToPath } from "node:url";
|
|
|
|
test("a bundle on stdio lists its tools and answers a call, a seat's verb among them", async () => {
|
|
const child = spawn(process.execPath, [fileURLToPath(new URL("./fixtures/stdio-bundle.mjs", import.meta.url))], { stdio: ["pipe", "pipe", "inherit"] });
|
|
const replies: any[] = [];
|
|
let buffered = "";
|
|
child.stdout.on("data", (d) => {
|
|
buffered += d.toString();
|
|
let at: number;
|
|
while ((at = buffered.indexOf("\n")) >= 0) {
|
|
replies.push(JSON.parse(buffered.slice(0, at)));
|
|
buffered = buffered.slice(at + 1);
|
|
}
|
|
});
|
|
const ask = async (id: number, method: string, params?: unknown) => {
|
|
child.stdin.write(JSON.stringify({ jsonrpc: "2.0", id, method, params }) + "\n");
|
|
for (let i = 0; i < 100 && !replies.some((r) => r.id === id); i++) await new Promise((r) => setTimeout(r, 20));
|
|
return replies.find((r) => r.id === id);
|
|
};
|
|
try {
|
|
assert.equal((await ask(1, "initialize", { protocolVersion: "2025-03-26", capabilities: {} })).result.serverInfo.name, "lamp");
|
|
child.stdin.write(JSON.stringify({ jsonrpc: "2.0", method: "notifications/initialized" }) + "\n");
|
|
const listed = (await ask(2, "tools/list")).result.tools;
|
|
assert.deepEqual(listed.map((t: any) => t.name), ["brightness", "node-lamp.on", "fail"]);
|
|
assert.deepEqual(listed[0].inputSchema, { type: "object", properties: { level: { type: "number", description: "0 to 10" } } });
|
|
const called = (await ask(3, "tools/call", { name: "brightness", arguments: { level: 7 } })).result;
|
|
assert.deepEqual(JSON.parse(called.content[0].text), { level: 7 });
|
|
const failed = (await ask(4, "tools/call", { name: "fail", arguments: {} })).result;
|
|
assert.equal(failed.isError, true);
|
|
assert.equal(failed.content[0].text, "the bulb is gone");
|
|
assert.equal((await ask(5, "tools/call", { name: "nothing" })).error.code, -32602);
|
|
} finally {
|
|
child.stdin.end();
|
|
child.kill();
|
|
}
|
|
});
|
|
|
|
test("a bundle served as its module lists the seat's verbs as the seat's, whichever registered first (ADR 0193)", async () => {
|
|
const child = spawn(process.execPath, [fileURLToPath(new URL("./fixtures/stdio-seat-first.mjs", import.meta.url))],
|
|
{ stdio: ["pipe", "pipe", "inherit"], env: { ...process.env, MESH_SERVED_MODULE: "nftables", NFT_WORD: "given" } });
|
|
const replies: any[] = [];
|
|
let buffered = "";
|
|
child.stdout.on("data", (d) => {
|
|
buffered += d.toString();
|
|
let at: number;
|
|
while ((at = buffered.indexOf("\n")) >= 0) {
|
|
replies.push(JSON.parse(buffered.slice(0, at)));
|
|
buffered = buffered.slice(at + 1);
|
|
}
|
|
});
|
|
const ask = async (id: number, method: string, params?: unknown) => {
|
|
child.stdin.write(JSON.stringify({ jsonrpc: "2.0", id, method, params }) + "\n");
|
|
for (let i = 0; i < 100 && !replies.some((r) => r.id === id); i++) await new Promise((r) => setTimeout(r, 20));
|
|
return replies.find((r) => r.id === id);
|
|
};
|
|
try {
|
|
assert.equal((await ask(1, "initialize", { protocolVersion: "2025-03-26", capabilities: {} })).result.serverInfo.name, "nftables");
|
|
const listed = (await ask(2, "tools/list")).result.tools;
|
|
assert.deepEqual(listed.map((t: any) => t.name), ["node-packet-filter.rules", "firewall_rules"]);
|
|
const called = (await ask(3, "tools/call", { name: "firewall_rules", arguments: {} })).result;
|
|
assert.equal(called.content[0].text, JSON.stringify("given"));
|
|
} finally {
|
|
child.kill();
|
|
}
|
|
});
|