A node-scoped seat's verb is callable through the console, naming its machine (hq ADR 0169)
The listing left node-scoped seats out, so <seat>.<verb> never resolved as a seat's and the call went to a module subject nothing served. Listed now with their scope: the schema requires the machine, the call carries it in the subject, and a call without one is refused in words.
This commit is contained in:
+7
-4
@@ -14,7 +14,7 @@ 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";
|
||||
import { callTool, seatsIn, toolKey, toolsOn, whyItFailed } from "../dist/client.js";
|
||||
|
||||
const url = process.env.MESH_TEST_NATS;
|
||||
|
||||
@@ -64,12 +64,15 @@ test("a person sees what the running modules answer, sorted, and who did not ans
|
||||
const have = await toolsOn(person);
|
||||
assert.deepEqual(
|
||||
have.tools.map((x) => `${x.module}.${x.name}`),
|
||||
["mesh-catalog.catalog_modules", "mesh-controller.status", "shop.price"],
|
||||
["mesh-catalog.catalog_modules", "mesh-controller.status", "node-dns-resolver.lookup", "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.
|
||||
// A role's tool is marked as one; a node-scoped seat's carries its scope, so a caller names
|
||||
// the machine and the verb resolves as the seat's (design 33 §4, ADR 0169).
|
||||
assert.ok(have.tools.find((x) => x.module === "mesh-controller")!.seat);
|
||||
assert.ok(!have.tools.some((x) => x.module === "node-dns-resolver"));
|
||||
const lookup = have.tools.find((x) => x.module === "node-dns-resolver")!;
|
||||
assert.ok(lookup.seat && lookup.scope === "node");
|
||||
assert.equal(toolKey("node-dns-resolver.lookup", seatsIn(have)), "seat:node-dns-resolver.lookup");
|
||||
// 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.
|
||||
|
||||
+31
-6
@@ -30,11 +30,16 @@ async function aMeshAndACredential(t: { after: (fn: () => Promise<void> | void)
|
||||
await shop.handle("price", async (body: { of?: string }) => ({ of: body.of ?? "nothing", cost: 12 }));
|
||||
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: {} },
|
||||
{ name: "push", description: "tell a machine", input: { node: { type: "string" } } },
|
||||
] }],
|
||||
seats: [
|
||||
{ seat: "mesh-controller", scope: "mesh", tools: [
|
||||
{ name: "status", description: "what is wrong", input: {} },
|
||||
{ name: "push", description: "tell a machine", input: { node: { type: "string" } } },
|
||||
] },
|
||||
// A seat held once per machine (design 33 §4, ADR 0169): its verb is asked of one.
|
||||
{ seat: "node-dns-resolver", scope: "node", tools: [{ name: "lookup", description: "one machine's", input: {} }] },
|
||||
],
|
||||
}));
|
||||
await controller.handle("seat:node-dns-resolver.lookup@anchor", async () => ({ machine: "anchor", answered: true }));
|
||||
await controller.handle("seat:mesh-controller.status", async () => ({ output: "all quiet", ok: true }));
|
||||
await controller.handle("seat:mesh-controller.push", async (body: { node?: string }) => ({ told: body.node ?? "nobody" }));
|
||||
t.after(async () => {
|
||||
@@ -99,9 +104,14 @@ test("a host initialises, lists the mesh's tools and calls one", async (t) => {
|
||||
assert.match(hello.instructions, /ada/, "the handshake says whose authority a call is made under");
|
||||
|
||||
const listed = byId.get(2)!.result.tools;
|
||||
assert.deepEqual(listed.map((x: { name: string }) => x.name), ["mesh-controller.push", "mesh-controller.status", "shop.price"],
|
||||
assert.deepEqual(listed.map((x: { name: string }) => x.name),
|
||||
["mesh-controller.push", "mesh-controller.status", "node-dns-resolver.lookup", "shop.price"],
|
||||
"the modules' tools and the roles', named the way a person names them");
|
||||
const price = listed[2];
|
||||
// A node-scoped seat's verb takes the machine, and requires it (ADR 0169).
|
||||
const lookup = listed[2];
|
||||
assert.equal(lookup.inputSchema.properties.node.type, "string");
|
||||
assert.deepEqual(lookup.inputSchema.required, ["node"]);
|
||||
const price = listed[3];
|
||||
assert.ok(price.inputSchema, "a tool with no schema is one an agent cannot call");
|
||||
// A module's bare property map arrives as a schema an agent can read, its words kept — and
|
||||
// `node`, the machine to ask when the module runs on several (novox/hq ADR 0159), beside them.
|
||||
@@ -169,3 +179,18 @@ test("a host calls the mesh's own verb through the seat", async (t) => {
|
||||
assert.ok(!result.isError, JSON.stringify(replies[0]));
|
||||
assert.deepEqual(JSON.parse(result.content[0].text), { output: "all quiet", ok: true });
|
||||
});
|
||||
|
||||
test("a node-scoped seat's verb is asked of the machine named, and refused without one", async (t) => {
|
||||
if (!url) return t.skip("MESH_TEST_NATS unset");
|
||||
const credential = await aMeshAndACredential(t);
|
||||
const replies = await driving(credential, [
|
||||
{ jsonrpc: "2.0", id: 1, method: "initialize", params: {} },
|
||||
{ jsonrpc: "2.0", id: 2, method: "tools/call", params: { name: "node-dns-resolver.lookup", arguments: { node: "anchor" } } },
|
||||
{ jsonrpc: "2.0", id: 3, method: "tools/call", params: { name: "node-dns-resolver.lookup", arguments: {} } },
|
||||
]);
|
||||
const byId = new Map(replies.map((r) => [r.id, r]));
|
||||
const answered = byId.get(2)!.result;
|
||||
assert.ok(!answered.isError, JSON.stringify(answered));
|
||||
assert.match(answered.content[0].text, /"machine": "anchor"/, "the machine's holder answered");
|
||||
assert.match(byId.get(3)!.error?.message ?? JSON.stringify(byId.get(3)), /name the machine/);
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user