import { test } from "node:test";
import assert from "node:assert/strict";
import { mkdtemp, writeFile, readFile, readdir } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { seal, unseal, compareVersions } from "../dist/primitives/index.js";
import { registerModuleTools, collectTools, resetTools, serveTools, listTools } from "../dist/tools/index.js";
import { runProvisioner, type Grant } from "../dist/provisioner/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", () => {
assert.equal(compareVersions("1.2.3", "1.2.10"), -1);
assert.equal(compareVersions("2.0.0", "1.9.9"), 1);
assert.equal(compareVersions("v1.0.0", "1.0.0"), 0);
});
test("module tools register and collect, a thrower is skipped not fatal", () => {
resetTools();
registerModuleTools("umami", () => [
{ name: "umami_stats", description: "d", input: {}, run: async () => 1 },
]);
registerModuleTools("broken", () => {
throw new Error("no token");
});
const collected = collectTools({});
const umami = collected.find((c) => c.module === "umami");
const broken = collected.find((c) => c.module === "broken");
assert.equal(umami?.tools.length, 1);
assert.equal(broken?.tools.length, 0);
});
test("provisioner creates a sealed credential for a grant, then removes on withdrawal", async () => {
const dir = await mkdtemp(join(tmpdir(), "prov-"));
const created: string[] = [];
const removed: string[] = [];
const grant: Grant = { resource: "analytics", consumer: "webapp", node: "anchor", values: { name: "webapp" } };
await writeFile(join(dir, "webapp.grant.json"), JSON.stringify(grant));
const stop = runProvisioner(
"analytics",
{
async create(g) {
created.push(g.consumer);
return { fields: { siteId: "abc", snippet: "` };
},
},
]);
const broker = memBroker();
const stop = await serveTools(broker, {});
// Invoke it the way a caller (mesh-control's command API) would — over the broker, by name.
const result = await broker.request<{ tool: string; args: Record }, { snippet: string }>(
"tools.invoke",
{ tool: "create_site", args: { domain: "my-app" } },
);
assert.match(result.snippet, /data-website-id="site-123"/);
// Discovery works, and an unknown tool is refused rather than silently dropped.
assert.deepEqual(listTools({}).map((t) => t.name), ["create_site"]);
await assert.rejects(broker.request("tools.invoke", { tool: "nope", args: {} }));
stop();
server.close();
});
test("serving refuses two modules exposing one tool name", async () => {
resetTools();
registerModuleTools("a", () => [{ name: "dup", description: "", input: {}, run: async () => 1 }]);
registerModuleTools("b", () => [{ name: "dup", description: "", input: {}, run: async () => 2 }]);
await assert.rejects(serveTools(memBroker(), {}), /exposed by two modules/);
});
// A minimal in-memory broker: request routes to a registered handle. Enough to serve tools; the
// real broker binding is the mesh's, provided by the hosting runtime.
function memBroker() {
const handlers = new Map Promise>();
return {
async request(key: string, body: Req): Promise {
const h = handlers.get(key);
if (!h) throw new Error(`no handler for ${key}`);
return (await h(body)) as Res;
},
async handle(key: string, handler: (b: Req) => Promise): Promise<() => void> {
handlers.set(key, handler as (b: unknown) => Promise);
return () => handlers.delete(key);
},
async publish(): Promise {},
async subscribe(): Promise<() => void> {
return () => {};
},
async close(): Promise {},
};
}
async function waitFor(cond: () => boolean, ms: number): Promise {
const start = Date.now();
while (!cond()) {
if (Date.now() - start > ms) throw new Error("timed out waiting");
await new Promise((r) => setTimeout(r, 10));
}
}