test: provisioner test for the ADR 0053 contract; fix stale tool tests
Rewrites the provisioner test to the new contract (a contributions file + an unsealed secret; the adapter is handed the mesh's login and password, and removal follows the consumer leaving the file) and drops the seal round-trip test with the primitive it covered. Also fixes two tool tests left stale by the per-key serving rework (ADR 0052): invoke by module.tool, and refuse one module's duplicate name (two modules may now share a name). Suite green: 6 pass. Claude-Session: https://claude.ai/code/session_01LrgweAeERJYBg88c5cKDzF
This commit is contained in:
+40
-38
@@ -4,19 +4,12 @@ import { mkdtemp, writeFile, readFile, readdir } from "node:fs/promises";
|
|||||||
import { tmpdir } from "node:os";
|
import { tmpdir } from "node:os";
|
||||||
import { join } from "node:path";
|
import { join } from "node:path";
|
||||||
|
|
||||||
import { seal, unseal, compareVersions } from "../dist/primitives/index.js";
|
import { compareVersions } from "../dist/primitives/index.js";
|
||||||
import { registerModuleTools, collectTools, resetTools, serveTools, listTools } from "../dist/tools/index.js";
|
import { registerModuleTools, collectTools, resetTools, serveTools, listTools, invokeTool } from "../dist/tools/index.js";
|
||||||
import { runProvisioner, type Grant } from "../dist/provisioner/index.js";
|
import { runProvisioner } from "../dist/provisioner/index.js";
|
||||||
import { emit, on, type Event } from "../dist/events/index.js";
|
import { emit, on, type Event } from "../dist/events/index.js";
|
||||||
import { useBroker } from "../dist/messaging/index.js";
|
import { useBroker } from "../dist/messaging/index.js";
|
||||||
|
|
||||||
test("seal round-trips and rejects the wrong key", () => {
|
|
||||||
const sealed = seal("hunter2", "node-key");
|
|
||||||
assert.equal(unseal(sealed, "node-key"), "hunter2");
|
|
||||||
assert.notEqual(sealed, "hunter2");
|
|
||||||
assert.throws(() => unseal(sealed, "wrong-key"));
|
|
||||||
});
|
|
||||||
|
|
||||||
test("semver orders releases", () => {
|
test("semver orders releases", () => {
|
||||||
assert.equal(compareVersions("1.2.3", "1.2.10"), -1);
|
assert.equal(compareVersions("1.2.3", "1.2.10"), -1);
|
||||||
assert.equal(compareVersions("2.0.0", "1.9.9"), 1);
|
assert.equal(compareVersions("2.0.0", "1.9.9"), 1);
|
||||||
@@ -38,39 +31,46 @@ test("module tools register and collect, a thrower is skipped not fatal", () =>
|
|||||||
assert.equal(broken?.tools.length, 0);
|
assert.equal(broken?.tools.length, 0);
|
||||||
});
|
});
|
||||||
|
|
||||||
test("provisioner creates a sealed credential for a grant, then removes on withdrawal", async () => {
|
test("provisioner creates each consumer with the mesh's login and password, removes on withdrawal", async () => {
|
||||||
const dir = await mkdtemp(join(tmpdir(), "prov-"));
|
const dir = await mkdtemp(join(tmpdir(), "prov-"));
|
||||||
const created: string[] = [];
|
const created: { as: string; password: string; values: unknown }[] = [];
|
||||||
const removed: string[] = [];
|
const removed: string[] = [];
|
||||||
|
|
||||||
const grant: Grant = { resource: "analytics", consumer: "webapp", node: "anchor", values: { name: "webapp" } };
|
// What the mesh delivers: the password it minted (as the host leaves it after unsealing) and a
|
||||||
await writeFile(join(dir, "webapp.grant.json"), JSON.stringify(grant));
|
// contributions file naming the consumer's login and where that password is.
|
||||||
|
await writeFile(join(dir, "webapp.secret"), "minted-pw\n");
|
||||||
|
const receives = join(dir, "analytics.json");
|
||||||
|
const doc = (given: unknown[]): string =>
|
||||||
|
JSON.stringify({ contributions: 1, requirement: "analytics", given });
|
||||||
|
await writeFile(
|
||||||
|
receives,
|
||||||
|
doc([{ from: "webapp", node: "anchor", as: "webapp-anchor", secret: join(dir, "webapp.secret"), values: { name: "webapp" } }]),
|
||||||
|
);
|
||||||
|
|
||||||
const stop = runProvisioner(
|
const stop = runProvisioner(
|
||||||
"analytics",
|
"analytics",
|
||||||
{
|
{
|
||||||
async create(g) {
|
async create(p) {
|
||||||
created.push(g.consumer);
|
created.push({ as: p.as, password: p.password, values: p.values });
|
||||||
return { fields: { siteId: "abc", snippet: "<script>", dashboard: "https://x/webapp" } };
|
|
||||||
},
|
},
|
||||||
async remove(g) {
|
async remove(p) {
|
||||||
removed.push(g.consumer);
|
removed.push(p.as);
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
{ grants: dir, sealKey: "k", everyMs: 20 },
|
{ receives, everyMs: 20 },
|
||||||
);
|
);
|
||||||
|
|
||||||
await waitFor(() => created.length === 1, 2000);
|
await waitFor(() => created.length === 1, 2000);
|
||||||
const files = await readdir(dir);
|
// The adapter is handed the mesh's login and the mesh's password — not one it generated, and the
|
||||||
const credFile = files.find((f) => f.endsWith(".credential"));
|
// trailing newline of the unsealed file is stripped.
|
||||||
assert.ok(credFile, "a credential file was written");
|
assert.equal(created[0].as, "webapp-anchor");
|
||||||
const sealed = await readFile(join(dir, credFile!), "utf8");
|
assert.equal(created[0].password, "minted-pw");
|
||||||
const body = JSON.parse(unseal(sealed, "k")) as { fields: Record<string, string> };
|
assert.deepEqual(created[0].values, { name: "webapp" });
|
||||||
assert.equal(body.fields.siteId, "abc");
|
|
||||||
|
|
||||||
// Withdraw the grant → the harness removes via the adapter and deletes the credential.
|
// Withdraw: the mesh drops the consumer from the file → the harness removes it via the adapter.
|
||||||
await (await import("node:fs/promises")).rm(join(dir, "webapp.grant.json"));
|
await writeFile(receives, doc([]));
|
||||||
await waitFor(() => removed.length === 1, 2000);
|
await waitFor(() => removed.length === 1, 2000);
|
||||||
|
assert.equal(removed[0], "webapp-anchor");
|
||||||
stop();
|
stop();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -106,26 +106,28 @@ test("modules can SERVE: a real async tool, loaded and invoked over the broker",
|
|||||||
const broker = memBroker();
|
const broker = memBroker();
|
||||||
const stop = await serveTools(broker, {});
|
const stop = await serveTools(broker, {});
|
||||||
|
|
||||||
// Invoke it the way a caller (mesh-control's command API) would — over the broker, by name.
|
// Invoke it the way a caller (mesh-control's command API) would — over the broker, by module and
|
||||||
const result = await broker.request<{ tool: string; args: Record<string, unknown> }, { snippet: string }>(
|
// tool, each served on its own key `demo.create_site` (novox/hq ADR 0052).
|
||||||
"tools.invoke",
|
const result = (await invokeTool(broker, "demo", "create_site", { domain: "my-app" })) as { snippet: string };
|
||||||
{ tool: "create_site", args: { domain: "my-app" } },
|
|
||||||
);
|
|
||||||
assert.match(result.snippet, /data-website-id="site-123"/);
|
assert.match(result.snippet, /data-website-id="site-123"/);
|
||||||
|
|
||||||
// Discovery works, and an unknown tool is refused rather than silently dropped.
|
// Discovery works, and an unknown tool is refused rather than silently dropped.
|
||||||
assert.deepEqual(listTools({}).map((t) => t.name), ["create_site"]);
|
assert.deepEqual(listTools({}).map((t) => t.name), ["create_site"]);
|
||||||
await assert.rejects(broker.request("tools.invoke", { tool: "nope", args: {} }));
|
await assert.rejects(invokeTool(broker, "demo", "nope", {}));
|
||||||
|
|
||||||
stop();
|
stop();
|
||||||
server.close();
|
server.close();
|
||||||
});
|
});
|
||||||
|
|
||||||
test("serving refuses two modules exposing one tool name", async () => {
|
test("serving refuses one module exposing two tools of the same name", async () => {
|
||||||
|
// Two *modules* may share a tool name — each is served on its own `module.tool` key (ADR 0052).
|
||||||
|
// What is refused is one module exposing the same name twice, where the key would collide.
|
||||||
resetTools();
|
resetTools();
|
||||||
registerModuleTools("a", () => [{ name: "dup", description: "", input: {}, run: async () => 1 }]);
|
registerModuleTools("a", () => [
|
||||||
registerModuleTools("b", () => [{ name: "dup", description: "", input: {}, run: async () => 2 }]);
|
{ name: "dup", description: "", input: {}, run: async () => 1 },
|
||||||
await assert.rejects(serveTools(memBroker(), {}), /exposed by two modules/);
|
{ name: "dup", description: "", input: {}, run: async () => 2 },
|
||||||
|
]);
|
||||||
|
await assert.rejects(serveTools(memBroker(), {}), /exposes two tools named dup/);
|
||||||
});
|
});
|
||||||
|
|
||||||
// A minimal in-memory broker: request routes to a registered handle, and publish routes to every
|
// A minimal in-memory broker: request routes to a registered handle, and publish routes to every
|
||||||
|
|||||||
Reference in New Issue
Block a user