Builds: design 47 G §643, §518, A-21 Claude-Session: https://claude.ai/code/session_01JG73KEq4zoDfM9WtYpcQgZ
1013 lines
47 KiB
TypeScript
1013 lines
47 KiB
TypeScript
import { test } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import { mkdtemp, writeFile, readFile, readdir, rm } from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
|
|
import { compareVersions } from "../dist/primitives/index.js";
|
|
import { registerModuleTools, collectTools, collectToolsEach, resetTools, serveTools, listTools, invokeTool } from "../dist/tools/index.js";
|
|
import { runProviderLoop } from "../dist/provider-loop/index.js";
|
|
import { emit, on, type Event } from "../dist/events/index.js";
|
|
import { useBroker } from "../dist/messaging/index.js";
|
|
|
|
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 each consumer with the mesh's login and password; remove waits for a person's delete", async () => {
|
|
const dir = await mkdtemp(join(tmpdir(), "prov-"));
|
|
const created: { as: string; password: string; values: unknown }[] = [];
|
|
const removed: string[] = [];
|
|
|
|
// What the mesh delivers: the password it minted (as the host leaves it after unsealing) and a
|
|
// 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 = runProviderLoop(
|
|
"analytics",
|
|
{
|
|
async create(p) {
|
|
created.push({ as: p.as, password: p.password, values: p.values });
|
|
},
|
|
async remove(p) {
|
|
removed.push(p.as);
|
|
},
|
|
},
|
|
{ receives, everyMs: 20, stableForMs: 0 },
|
|
);
|
|
|
|
await waitFor(() => created.length === 1, 2000);
|
|
// The adapter is handed the mesh's login and the mesh's password — not one it generated, and the
|
|
// trailing newline of the unsealed file is stripped.
|
|
assert.equal(created[0].as, "webapp-anchor");
|
|
assert.equal(created[0].password, "minted-pw");
|
|
assert.deepEqual(created[0].values, { name: "webapp" });
|
|
|
|
// The mesh drops the consumer: it is RETIRED, mark only — this adapter cannot disable, so remove is
|
|
// NOT called (novox/hq ADR 0230); it keeps its access until a person deletes it.
|
|
await writeFile(receives, doc([]));
|
|
for (let i = 0; i < 200 && (await stop.retirement()).retired.length === 0; i++) await new Promise((r) => setTimeout(r, 10));
|
|
const r = await stop.retirement();
|
|
assert.equal(r.retires_by, "mark-only");
|
|
assert.deepEqual(r.retired.map((x) => [x.consumer, x.access]), [["webapp-anchor", "kept"]]);
|
|
await new Promise((r) => setTimeout(r, 100));
|
|
assert.deepEqual(removed, [], "remove was called to retire");
|
|
// Only a person's delete reaches remove.
|
|
await stop.deleteRetired("webapp-anchor", { why: "gone for good", confirm: "webapp-anchor" });
|
|
assert.deepEqual(removed, ["webapp-anchor"]);
|
|
stop();
|
|
});
|
|
|
|
test("provisioner hands the adapter what the mesh derived, and still has it at withdrawal", async () => {
|
|
// novox/hq ADR 0201: the provider declares what it derives for each consumer and the mesh fills
|
|
// it. The adapter must be able to read it on create, must see a changed derivation as a change
|
|
// worth re-applying, and must still be told it when the consumer is withdrawn — by which time
|
|
// the mesh has already dropped it from the file.
|
|
const dir = await mkdtemp(join(tmpdir(), "prov-derived-"));
|
|
const created: unknown[] = [];
|
|
const removed: unknown[] = [];
|
|
|
|
await writeFile(join(dir, "ncloud.secret"), "minted-pw\n");
|
|
const receives = join(dir, "store.json");
|
|
const doc = (given: unknown[]): string =>
|
|
JSON.stringify({ contributions: 1, requirement: "s3-bucket", given });
|
|
const entry = (bucket: string) => ({
|
|
from: "nextcloud", node: "anchor", as: "mesh_anchor_ncloud",
|
|
secret: join(dir, "ncloud.secret"), values: {}, derived: { bucket },
|
|
});
|
|
await writeFile(receives, doc([entry("mesh-anchor-ncloud")]));
|
|
|
|
const stop = runProviderLoop(
|
|
"s3-bucket",
|
|
{
|
|
async create(p) {
|
|
created.push(p.derived);
|
|
},
|
|
async remove(p) {
|
|
removed.push(p.derived);
|
|
},
|
|
},
|
|
{ receives, everyMs: 20, stableForMs: 0 },
|
|
);
|
|
|
|
await waitFor(() => created.length === 1, 2000);
|
|
assert.deepEqual(created[0], { bucket: "mesh-anchor-ncloud" });
|
|
|
|
// A changed derivation is a changed consumer: the login and the password are the same, and the
|
|
// resource it is being given is not. Trusted from memory, this would never be applied.
|
|
await writeFile(receives, doc([entry("mesh-anchor-files")]));
|
|
await waitFor(() => created.length === 2, 2000);
|
|
assert.deepEqual(created[1], { bucket: "mesh-anchor-files" });
|
|
|
|
// Retired, then deleted by a person: the file no longer says anything about this consumer, and the
|
|
// adapter is still told which bucket to take away.
|
|
await writeFile(receives, doc([]));
|
|
for (let i = 0; i < 200 && (await stop.retirement()).retired.length === 0; i++) await new Promise((r) => setTimeout(r, 10));
|
|
assert.deepEqual(removed, []);
|
|
await stop.deleteRetired("mesh_anchor_ncloud", { why: "unassigned", confirm: "mesh_anchor_ncloud" });
|
|
assert.deepEqual(removed[0], { bucket: "mesh-anchor-files" });
|
|
stop();
|
|
});
|
|
|
|
test("provisioner stops calling an unreadable secret a race, and says what to look at", async () => {
|
|
// novox/hq issue 225. A permission the host will never grant reads exactly like a file that has
|
|
// not arrived yet, and three hours of "not readable yet" is how a provider that served nobody
|
|
// looked healthy. After a while the wording has to change, because the word "yet" is the fault.
|
|
const dir = await mkdtemp(join(tmpdir(), "prov-unreadable-"));
|
|
const receives = join(dir, "db.json");
|
|
await writeFile(
|
|
receives,
|
|
JSON.stringify({
|
|
contributions: 1, requirement: "db",
|
|
given: [{ from: "photos", node: "anchor", as: "mesh_anchor_photos",
|
|
secret: join(dir, "never-written.secret"), values: {} }],
|
|
}),
|
|
);
|
|
|
|
const said: string[] = [];
|
|
const wasError = console.error;
|
|
console.error = (line: unknown) => { said.push(String(line)); };
|
|
let stop: (() => void) | undefined;
|
|
try {
|
|
stop = runProviderLoop("db", { async create() {}, async remove() {} }, { receives, everyMs: 5 });
|
|
await waitFor(() => said.some((l) => l.includes("CANNOT READ")), 3000);
|
|
} finally {
|
|
stop?.();
|
|
console.error = wasError;
|
|
}
|
|
|
|
const patient = said.filter((l) => l.includes("not readable yet"));
|
|
const loud = said.filter((l) => l.includes("CANNOT READ"));
|
|
assert.ok(patient.length > 0, "it never gave the host a chance to write the file");
|
|
assert.ok(patient.length <= 12, `it called a permanent refusal a race ${patient.length} times`);
|
|
assert.ok(loud.length > 0, "it never stopped calling it a race");
|
|
// And the loud one says where to look, because the answer is ownership and not the mesh.
|
|
assert.ok(loud[0].includes("who owns the file"), `the loud line does not say what to check: ${loud[0]}`);
|
|
assert.ok(loud[0].includes("issue 225"), "the loud line does not name the record");
|
|
});
|
|
|
|
test("provisioner removes nothing while its contributions file cannot be used (novox/hq issue 241)", async () => {
|
|
const dir = await mkdtemp(join(tmpdir(), "prov-"));
|
|
const created: string[] = [];
|
|
const removed: string[] = [];
|
|
await writeFile(join(dir, "a.secret"), "pw-a\n");
|
|
await writeFile(join(dir, "b.secret"), "pw-b\n");
|
|
const receives = join(dir, "postgres-database.json");
|
|
const doc = (given: unknown[]): string =>
|
|
JSON.stringify({ contributions: 1, requirement: "postgres-database", given });
|
|
const both = doc([
|
|
{ from: "forge", node: "anchor", as: "mesh_anchor_forge", secret: join(dir, "a.secret") },
|
|
{ from: "mail", node: "anchor", as: "mesh_anchor_mail", secret: join(dir, "b.secret") },
|
|
]);
|
|
await writeFile(receives, both);
|
|
|
|
const stop = runProviderLoop(
|
|
"postgres-database",
|
|
{
|
|
async create(p) {
|
|
created.push(p.as);
|
|
},
|
|
async remove(p) {
|
|
removed.push(p.as);
|
|
},
|
|
},
|
|
{ receives, everyMs: 20, stableForMs: 0 },
|
|
);
|
|
await waitFor(() => created.length === 2, 2000);
|
|
|
|
// Each way the file can say nothing usable, held for many passes: nothing is withdrawn.
|
|
for (const broken of [
|
|
async () => rm(receives),
|
|
async () => writeFile(receives, "{ half a fi"),
|
|
async () => writeFile(receives, JSON.stringify({ contributions: 1, requirement: "s3-bucket", given: [] })),
|
|
async () => writeFile(receives, JSON.stringify({ contributions: 1, requirement: "postgres-database" })),
|
|
]) {
|
|
await broken();
|
|
await new Promise((r) => setTimeout(r, 200));
|
|
assert.deepEqual(removed, [], "a consumer was removed while the file could not be used");
|
|
await writeFile(receives, both);
|
|
await new Promise((r) => setTimeout(r, 100));
|
|
}
|
|
|
|
// The file read and saying so is a result — but two of two held is over the bound (ADR 0230):
|
|
// nothing is retired without a person, and approving that exact set retires both.
|
|
await writeFile(receives, doc([]));
|
|
for (let i = 0; i < 200 && (await stop.retirement()).waiting === null; i++) await new Promise((r) => setTimeout(r, 10));
|
|
assert.notEqual((await stop.retirement()).waiting, null, "the retirement never waited for a person");
|
|
assert.deepEqual(removed, [], "two of two consumers were retired without a person");
|
|
await stop.approve(["mesh_anchor_mail", "mesh_anchor_forge"], { why: "both unassigned on purpose", by: "test" });
|
|
// Retired mark-only: still nothing removed; that is a deletion, a person's further act.
|
|
assert.deepEqual(removed, []);
|
|
assert.deepEqual((await stop.retirement()).retired.map((x) => x.consumer).sort(), ["mesh_anchor_forge", "mesh_anchor_mail"]);
|
|
stop();
|
|
});
|
|
|
|
test("provisioner applies again what the backend no longer holds, and trusts memory without holds", async () => {
|
|
const dir = await mkdtemp(join(tmpdir(), "prov-holds-"));
|
|
await writeFile(join(dir, "webapp.secret"), "minted-pw\n");
|
|
const receives = join(dir, "cache.json");
|
|
await writeFile(
|
|
receives,
|
|
JSON.stringify({ contributions: 1, requirement: "cache", given: [{ from: "webapp", node: "anchor", as: "webapp-anchor", secret: join(dir, "webapp.secret") }] }),
|
|
);
|
|
|
|
// A backend that forgets: what was created is held until it "restarts".
|
|
const backend = new Set<string>();
|
|
let creates = 0;
|
|
let asked = 0;
|
|
const stop = runProviderLoop(
|
|
"cache",
|
|
{
|
|
async create(p) {
|
|
creates++;
|
|
backend.add(`${p.as}:${p.password}`);
|
|
},
|
|
async remove(p) {
|
|
for (const k of backend) if (k.startsWith(`${p.as}:`)) backend.delete(k);
|
|
},
|
|
async holds(p) {
|
|
asked++;
|
|
return backend.has(`${p.as}:${p.password}`);
|
|
},
|
|
},
|
|
{ receives, everyMs: 10, verifyEveryMs: 30 },
|
|
);
|
|
|
|
await waitFor(() => creates === 1, 2000);
|
|
// While the backend holds it, asking changes nothing: no second create.
|
|
await waitFor(() => asked >= 2, 2000);
|
|
assert.equal(creates, 1);
|
|
|
|
// The backend restarts and forgets. The contributions did not change; only asking can notice.
|
|
backend.clear();
|
|
await waitFor(() => creates === 2, 2000);
|
|
assert.ok(backend.has("webapp-anchor:minted-pw"));
|
|
stop();
|
|
|
|
// An adapter without holds is trusted from memory, as before: a forgotten backend stays forgotten.
|
|
const forgetful = new Set<string>();
|
|
let plainCreates = 0;
|
|
const stopPlain = runProviderLoop(
|
|
"cache",
|
|
{
|
|
async create(p) {
|
|
plainCreates++;
|
|
forgetful.add(p.as);
|
|
},
|
|
async remove() {},
|
|
},
|
|
{ receives, everyMs: 10, verifyEveryMs: 10 },
|
|
);
|
|
await waitFor(() => plainCreates === 1, 2000);
|
|
forgetful.clear();
|
|
await new Promise((r) => setTimeout(r, 100));
|
|
assert.equal(plainCreates, 1);
|
|
stopPlain();
|
|
});
|
|
|
|
test("provisioner keeps a consumer applied when the backend cannot be asked", async () => {
|
|
const dir = await mkdtemp(join(tmpdir(), "prov-unreachable-"));
|
|
await writeFile(join(dir, "webapp.secret"), "minted-pw");
|
|
const receives = join(dir, "cache.json");
|
|
await writeFile(
|
|
receives,
|
|
JSON.stringify({ requirement: "cache", given: [{ as: "webapp-anchor", secret: join(dir, "webapp.secret") }] }),
|
|
);
|
|
let creates = 0;
|
|
let asked = 0;
|
|
const stop = runProviderLoop(
|
|
"cache",
|
|
{
|
|
async create() {
|
|
creates++;
|
|
},
|
|
async remove() {},
|
|
async holds() {
|
|
asked++;
|
|
throw new Error("connection refused");
|
|
},
|
|
},
|
|
{ receives, everyMs: 10, verifyEveryMs: 20 },
|
|
);
|
|
await waitFor(() => asked >= 3, 2000);
|
|
// Unable to ask is not evidence of loss: nothing is applied again.
|
|
assert.equal(creates, 1);
|
|
stop();
|
|
});
|
|
|
|
test("provisioner backs off when create does not satisfy holds, and says so", async () => {
|
|
const dir = await mkdtemp(join(tmpdir(), "prov-brake-"));
|
|
await writeFile(join(dir, "webapp.secret"), "minted-pw");
|
|
const receives = join(dir, "cache.json");
|
|
await writeFile(receives, JSON.stringify({ requirement: "cache", given: [{ as: "webapp-anchor", secret: join(dir, "webapp.secret") }] }));
|
|
let creates = 0;
|
|
let asked = 0;
|
|
const logged: string[] = [];
|
|
const original = console.error;
|
|
console.error = (...a: unknown[]) => logged.push(a.join(" "));
|
|
try {
|
|
const stop = runProviderLoop(
|
|
"cache",
|
|
{
|
|
async create() {
|
|
creates++;
|
|
},
|
|
async remove() {},
|
|
async holds() {
|
|
asked++;
|
|
return false; // an adapter that disagrees with itself: nothing create does is ever held
|
|
},
|
|
},
|
|
{ receives, everyMs: 5, verifyEveryMs: 20 },
|
|
);
|
|
await new Promise((r) => setTimeout(r, 400));
|
|
stop();
|
|
} finally {
|
|
console.error = original;
|
|
}
|
|
// Without the brake this would be ~20 creates (one per 20ms check). With doubling waits
|
|
// (20, 40, 80, 160ms…) it is a handful.
|
|
assert.ok(creates >= 3 && creates <= 7, `creates: ${creates}`);
|
|
assert.equal(asked, creates - 1);
|
|
assert.ok(logged.some((l) => l.includes("create does not produce what holds checks")));
|
|
});
|
|
|
|
test("provisioner treats a failed or hung holds as could-not-ask, and never logs the password", async () => {
|
|
const dir = await mkdtemp(join(tmpdir(), "prov-timeout-"));
|
|
await writeFile(join(dir, "a.secret"), "s3cret-pw");
|
|
await writeFile(join(dir, "b.secret"), "other-pw");
|
|
const receives = join(dir, "cache.json");
|
|
await writeFile(
|
|
receives,
|
|
JSON.stringify({ requirement: "cache", given: [{ as: "a", secret: join(dir, "a.secret") }, { as: "b", secret: join(dir, "b.secret") }] }),
|
|
);
|
|
let creates = 0;
|
|
const askedFor: string[] = [];
|
|
const logged: string[] = [];
|
|
const original = console.error;
|
|
console.error = (...a: unknown[]) => logged.push(a.join(" "));
|
|
try {
|
|
const stop = runProviderLoop(
|
|
"cache",
|
|
{
|
|
async create() {
|
|
creates++;
|
|
},
|
|
async remove() {},
|
|
holds(p) {
|
|
askedFor.push(p.as);
|
|
// "a" fails the way a shelled-out tool does, with its arguments in the message.
|
|
if (p.as === "a") return Promise.reject(new Error(`Command failed: tool --password ${p.password}`));
|
|
return Promise.resolve(true);
|
|
},
|
|
},
|
|
{ receives, everyMs: 5, verifyEveryMs: 30, holdsTimeoutMs: 20 },
|
|
);
|
|
await new Promise((r) => setTimeout(r, 200));
|
|
stop();
|
|
} finally {
|
|
console.error = original;
|
|
}
|
|
assert.equal(creates, 2); // the first pass only; nothing is applied again on "could not ask"
|
|
// A failure that is not a timeout may be one consumer's alone: the consumers after it are still
|
|
// asked on every pass.
|
|
assert.ok(askedFor.filter((as) => as === "b").length >= 2, askedFor.join(","));
|
|
assert.ok(logged.some((l) => l.includes("Command failed: tool --password ***")));
|
|
assert.ok(!logged.some((l) => l.includes("s3cret-pw")));
|
|
|
|
// A check that never answers counts as could-not-ask once its time is up.
|
|
let hungCreates = 0;
|
|
const hungLogged: string[] = [];
|
|
console.error = (...a: unknown[]) => hungLogged.push(a.join(" "));
|
|
try {
|
|
const stopHung = runProviderLoop(
|
|
"cache",
|
|
{
|
|
async create() {
|
|
hungCreates++;
|
|
},
|
|
async remove() {},
|
|
holds: () => new Promise<boolean>(() => {}),
|
|
},
|
|
{ receives, everyMs: 5, verifyEveryMs: 30, holdsTimeoutMs: 20 },
|
|
);
|
|
await new Promise((r) => setTimeout(r, 200));
|
|
stopHung();
|
|
} finally {
|
|
console.error = original;
|
|
}
|
|
assert.equal(hungCreates, 2);
|
|
assert.ok(hungLogged.some((l) => l.includes("no answer within 20ms")));
|
|
});
|
|
|
|
test("provisioner stops a verify pass at a timeout, and retries a failed re-apply at the next check", async () => {
|
|
const dir = await mkdtemp(join(tmpdir(), "prov-retry-"));
|
|
await writeFile(join(dir, "a.secret"), "pa");
|
|
await writeFile(join(dir, "b.secret"), "pb");
|
|
const receives = join(dir, "cache.json");
|
|
await writeFile(
|
|
receives,
|
|
JSON.stringify({ requirement: "cache", given: [{ as: "a", secret: join(dir, "a.secret") }, { as: "b", secret: join(dir, "b.secret") }] }),
|
|
);
|
|
const original = console.error;
|
|
console.error = () => {};
|
|
try {
|
|
// A timeout on "a" means "b" is not asked in that pass.
|
|
const askedFor: string[] = [];
|
|
const stopHung = runProviderLoop(
|
|
"cache",
|
|
{
|
|
async create() {},
|
|
async remove() {},
|
|
holds(p) {
|
|
askedFor.push(p.as);
|
|
return p.as === "a" ? new Promise<boolean>(() => {}) : Promise.resolve(true);
|
|
},
|
|
},
|
|
{ receives, everyMs: 5, verifyEveryMs: 30, holdsTimeoutMs: 10 },
|
|
);
|
|
await new Promise((r) => setTimeout(r, 150));
|
|
stopHung();
|
|
assert.ok(askedFor.length >= 2 && askedFor.every((as) => as === "a"), askedFor.join(","));
|
|
|
|
// A lost consumer whose re-apply fails is retried at the next check, not held back by the brake,
|
|
// and the retry that succeeds counts as the first re-apply, not the second.
|
|
let held = true;
|
|
let createCalls = 0;
|
|
const logged: string[] = [];
|
|
console.error = (...a: unknown[]) => logged.push(a.join(" "));
|
|
const stop = runProviderLoop(
|
|
"cache",
|
|
{
|
|
async create(p) {
|
|
if (p.as !== "a") return;
|
|
createCalls++;
|
|
if (createCalls === 2) throw new Error("write refused"); // the first re-apply fails
|
|
if (createCalls === 3) held = true; // the retry works
|
|
},
|
|
async remove() {},
|
|
async holds(p) {
|
|
return p.as === "a" ? held : true;
|
|
},
|
|
},
|
|
{ receives, everyMs: 5, verifyEveryMs: 20 },
|
|
);
|
|
await waitFor(() => createCalls === 1, 2000);
|
|
held = false; // the backend loses "a"
|
|
await waitFor(() => createCalls === 3, 2000);
|
|
await new Promise((r) => setTimeout(r, 100));
|
|
stop();
|
|
assert.equal(createCalls, 3);
|
|
assert.ok(!logged.some((l) => l.includes("create does not produce what holds checks")), logged.join("\n"));
|
|
} finally {
|
|
console.error = original;
|
|
}
|
|
});
|
|
|
|
test("modules can SERVE: a real async tool, loaded and invoked over the broker", async () => {
|
|
resetTools();
|
|
|
|
// Stand up a fake upstream the tool actually calls over the network — proving a tool that does
|
|
// real work (not a pure function) serves end to end.
|
|
const { createServer } = await import("node:http");
|
|
const server = createServer((_req, res) => {
|
|
res.setHeader("content-type", "application/json");
|
|
res.end(JSON.stringify({ id: "site-123" }));
|
|
});
|
|
await new Promise<void>((r) => server.listen(0, r));
|
|
const addr = server.address() as { port: number };
|
|
const base = `http://127.0.0.1:${addr.port}`;
|
|
|
|
// A module registers its tool exactly as umami does — the tool calls the upstream and returns a
|
|
// result computed from it.
|
|
registerModuleTools("demo", () => [
|
|
{
|
|
name: "create_site",
|
|
description: "register a site and return its snippet",
|
|
input: { domain: { type: "string" } },
|
|
run: async (args) => {
|
|
const res = await fetch(`${base}/api/websites`, { method: "POST" });
|
|
const { id } = (await res.json()) as { id: string };
|
|
return { domain: String(args.domain), snippet: `<script data-website-id="${id}"></script>` };
|
|
},
|
|
},
|
|
]);
|
|
|
|
const broker = memBroker();
|
|
const stop = await serveTools(broker, {});
|
|
|
|
// Invoke it the way a caller (mesh-controller's command API) would — over the broker, by module and
|
|
// tool, each served on its own key `demo.create_site` (novox/hq ADR 0047).
|
|
const result = (await invokeTool(broker, "demo", "create_site", { domain: "my-app" })) as { snippet: string };
|
|
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(invokeTool(broker, "demo", "nope", {}));
|
|
|
|
stop();
|
|
server.close();
|
|
});
|
|
|
|
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 0047).
|
|
// What is refused is one module exposing the same name twice, where the key would collide.
|
|
resetTools();
|
|
registerModuleTools("a", () => [
|
|
{ name: "dup", description: "", input: {}, run: async () => 1 },
|
|
{ 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
|
|
// subscriber whose topic pattern matches. Enough to exercise tools and events; the real binding is
|
|
// the mesh's AMQP one, provided by the hosting runtime.
|
|
function memBroker() {
|
|
const handlers = new Map<string, (b: unknown) => Promise<unknown>>();
|
|
const subs: { pattern: string; handler: (env: { key: string; node: string; body: unknown }) => Promise<void> }[] = [];
|
|
return {
|
|
async request<Req, Res>(key: string, body: Req): Promise<Res> {
|
|
const h = handlers.get(key);
|
|
if (!h) throw new Error(`no handler for ${key}`);
|
|
return (await h(body)) as Res;
|
|
},
|
|
async handle<Req, Res>(key: string, handler: (b: Req) => Promise<Res>): Promise<() => void> {
|
|
handlers.set(key, handler as (b: unknown) => Promise<unknown>);
|
|
return () => handlers.delete(key);
|
|
},
|
|
async publish<T>(env: { key: string; node: string; body: T }): Promise<void> {
|
|
for (const s of subs) if (topicMatch(s.pattern, env.key)) await s.handler(env);
|
|
},
|
|
async subscribe<T>(pattern: string, handler: (env: { key: string; node: string; body: T }) => Promise<void>): Promise<() => void> {
|
|
const entry = { pattern, handler: handler as (env: { key: string; node: string; body: unknown }) => Promise<void> };
|
|
subs.push(entry);
|
|
return () => {
|
|
const i = subs.indexOf(entry);
|
|
if (i >= 0) subs.splice(i, 1);
|
|
};
|
|
},
|
|
async close(): Promise<void> {},
|
|
};
|
|
}
|
|
|
|
// AMQP topic matching: `*` one segment, `#` any run of segments.
|
|
function topicMatch(pattern: string, key: string): boolean {
|
|
if (pattern === "#") return true;
|
|
const p = pattern.split(".");
|
|
const k = key.split(".");
|
|
let pi = 0;
|
|
let ki = 0;
|
|
while (pi < p.length) {
|
|
if (p[pi] === "#") return true; // simplification: # only as a trailing wildcard
|
|
if (ki >= k.length) return false;
|
|
if (p[pi] !== "*" && p[pi] !== k[ki]) return false;
|
|
pi++;
|
|
ki++;
|
|
}
|
|
return ki === k.length;
|
|
}
|
|
|
|
test("events: a module emits, a listener and the audit sink (#) both receive it, with metadata", async () => {
|
|
const broker = memBroker();
|
|
useBroker(() => broker);
|
|
|
|
process.env.MESH_MODULE = "umami";
|
|
process.env.MESH_NODE = "anchor";
|
|
|
|
const heard: Event[] = [];
|
|
const audited: Event[] = [];
|
|
await on("module.umami.#", async (e) => void heard.push(e));
|
|
await on("#", async (e) => void audited.push(e)); // the audit logger is just this
|
|
|
|
await emit("module.umami.site.created", { domain: "my-app" });
|
|
await emit("module.plex.play.started", { title: "x" }); // a different module's event
|
|
|
|
// The umami listener heard only umami's event; the audit sink heard both.
|
|
assert.deepEqual(heard.map((e) => e.type), ["module.umami.site.created"]);
|
|
assert.deepEqual(audited.map((e) => e.type), ["module.umami.site.created", "module.plex.play.started"]);
|
|
|
|
// The metadata an audit trail needs is present — read back from the ADR 0042 headers, not the body.
|
|
const e = heard[0];
|
|
assert.equal(e.source, "umami");
|
|
assert.equal(e.node, "anchor");
|
|
assert.equal((e.body as { domain: string }).domain, "my-app");
|
|
assert.match(e.at, /^\d{4}-\d{2}-\d{2}T/);
|
|
// Every event carries a unique id (x-event-id) — the handle a consumer dedups on.
|
|
assert.ok(e.id, "event has an x-event-id");
|
|
assert.notEqual(audited[0].id, audited[1].id);
|
|
// The body is exactly the domain payload — provenance never leaks into it.
|
|
assert.deepEqual(Object.keys(e.body as object), ["domain"]);
|
|
|
|
delete process.env.MESH_MODULE;
|
|
delete process.env.MESH_NODE;
|
|
});
|
|
|
|
async function waitFor(cond: () => boolean, ms: number): Promise<void> {
|
|
const start = Date.now();
|
|
while (!cond()) {
|
|
if (Date.now() - start > ms) throw new Error("timed out waiting");
|
|
await new Promise((r) => setTimeout(r, 10));
|
|
}
|
|
}
|
|
|
|
test("each registration is given the environment asked for it, and sees no other's (ADR 0192)", () => {
|
|
resetTools();
|
|
const seen: Record<string, unknown> = {};
|
|
registerModuleTools("alpha", (env) => { seen.alpha = { ...env }; return [{ name: "a", description: "", input: {}, run: async () => env.ALPHA_FILE }]; });
|
|
registerModuleTools("beta", (env) => { seen.beta = { ...env }; return [{ name: "b", description: "", input: {}, run: async () => env.BETA_URL }]; });
|
|
const envs: Record<string, NodeJS.ProcessEnv> = { alpha: { ALPHA_FILE: "/a/config.json" }, beta: { BETA_URL: "http://127.0.0.1:3000" } };
|
|
const asked: string[] = [];
|
|
const got = collectToolsEach((module, index) => { asked.push(`${module}#${index}`); return envs[module] ?? {}; });
|
|
assert.deepEqual(got.map((g) => g.module), ["alpha", "beta"]);
|
|
assert.deepEqual(asked, ["alpha#0", "beta#1"]);
|
|
assert.deepEqual(seen.alpha, { ALPHA_FILE: "/a/config.json" });
|
|
assert.deepEqual(seen.beta, { BETA_URL: "http://127.0.0.1:3000" });
|
|
resetTools();
|
|
});
|
|
|
|
test("the provider loop is named for what it is, and its old name stays an alias until its providers move (issue 678)", async () => {
|
|
const loop = await import("../dist/provider-loop/index.js");
|
|
const old = await import("../dist/index.js");
|
|
assert.equal(old.provisioner.runProvisioner, loop.runProviderLoop);
|
|
assert.equal(old.provisioner.provisionerTools, loop.providerLoopTools);
|
|
assert.equal(old.providerLoop.runProviderLoop, loop.runProviderLoop);
|
|
const pkg = JSON.parse(await (await import("node:fs/promises")).readFile(new URL("../package.json", import.meta.url), "utf8"));
|
|
assert.equal(pkg.exports["./provider-loop"], "./dist/provider-loop/index.js");
|
|
assert.equal(pkg.exports["./provisioner"], pkg.exports["./provider-loop"]);
|
|
// The events and tools are what the controller and the operator read on the bus: their names stay.
|
|
assert.equal(loop.FAILING_EVENT, "provisioner.failing");
|
|
});
|
|
|
|
import { allows, refusal, parseCallers, parseVerbCallers, parseConfiguredVerbCallers, readDefinitionCallers, moduleOf, UNPROVEN } from "../dist/callers/index.js";
|
|
import { Follower, statusTools, keepStatus, statusKey, viewWaitsFinding, type Answer, type Status } from "../dist/views/index.js";
|
|
|
|
test("callers and verb callers judge a proved caller by the Go side's rules (ADR 0459 §2, ADR 0627 §2)", () => {
|
|
const rows: [string, string[], string, boolean][] = [
|
|
["a module on any machine", ["mesh-delivery-forge-bridge"], "ace.mesh-delivery-forge-bridge", true],
|
|
["exactly a machine's module", ["ace.mesh-delivery-forge-bridge"], "g14.mesh-delivery-forge-bridge", false],
|
|
["exactly a person", ["person.jochen"], "person.jochen", true],
|
|
["a module entry is not a person", ["jochen"], "person.jochen", false],
|
|
["a wildcard is no entry", ["*", ">"], "ace.anything", false],
|
|
["an unproven caller never matches", ["unproven"], UNPROVEN, false],
|
|
];
|
|
for (const [what, list, caller, want] of rows) assert.equal(allows(list, caller), want, what);
|
|
assert.deepEqual(parseCallers('{"callers":["a","person.b"]}'), ["a", "person.b"]);
|
|
// design 47 G §336, §392: a definition's callers are a list of the format's forms; text and a bare name are refused
|
|
assert.deepEqual(readDefinitionCallers("list", ["operator", "invokers", "seat:mesh-grants", "holder:node-backup@same-node"]),
|
|
["operator", "invokers", "seat:mesh-grants", "holder:node-backup@same-node"]);
|
|
assert.throws(() => readDefinitionCallers("list", "a person"), /list: callers are text/);
|
|
assert.throws(() => readDefinitionCallers("list", ["mesh-grants"]), /list: the callers entry "mesh-grants" is none of/);
|
|
// design 47 G §391, §456: a verb with no callers takes its class's default; no class reads as read; its own list is kept
|
|
const byClass: [string | undefined, string[]][] = [
|
|
[undefined, ["invokers", "persons", "sessions"]], ["read", ["invokers", "persons", "sessions"]],
|
|
["act", ["operator", "sessions"]], ["warrant", ["operator"]],
|
|
];
|
|
for (const [cls, want] of byClass) assert.deepEqual(readDefinitionCallers("list", undefined, cls), want, String(cls));
|
|
assert.deepEqual(readDefinitionCallers("register", ["seat:mesh-broker"], "act"), ["seat:mesh-broker"]);
|
|
assert.throws(() => parseCallers('{"callers":["a"],"more":1}'), /unknown field "more"/);
|
|
|
|
const verbs = parseVerbCallers('{"pull":["*"],"merge-pull":["mesh-delivery-forge-bridge"],"comment":["operator"]}', ["pull", "merge-pull", "comment"], {});
|
|
assert.equal(refusal(verbs, "pull", "ace.anyone"), undefined);
|
|
assert.match(String(refusal(verbs, "pull", UNPROVEN)), /carries no caller the tool runner proved/);
|
|
assert.equal(refusal(verbs, "merge-pull", "novox.mesh-delivery-forge-bridge"), undefined);
|
|
assert.match(String(refusal(verbs, "merge-pull", "person.jochen")), /refused for person.jochen: not on the holder's caller list for merge-pull/);
|
|
assert.equal(refusal(verbs, "comment", "person.jochen"), undefined);
|
|
assert.ok(refusal(verbs, "comment", "ace.mesh-delivery-forge-bridge"));
|
|
assert.ok(refusal(verbs, "status", "ace.anyone"), "a verb the list does not name refuses everyone");
|
|
assert.throws(() => parseVerbCallers('{"nope":["*"]}', ["pull"], {}), /names the verb "nope", which the holder does not serve/);
|
|
const defaults = { pull: ["*"] };
|
|
assert.equal(parseVerbCallers(" ", ["pull"], defaults), defaults);
|
|
assert.equal(parseConfiguredVerbCallers(null, ["pull"], defaults), defaults);
|
|
assert.deepEqual(parseConfiguredVerbCallers('{"pull":["a"]}', ["pull"], defaults), { pull: ["a"] });
|
|
assert.deepEqual(parseConfiguredVerbCallers({ pull: ["b"] }, ["pull"], defaults), { pull: ["b"] });
|
|
assert.throws(() => parseConfiguredVerbCallers("{", ["pull"], defaults), /not a JSON object/);
|
|
assert.equal(moduleOf("ace.mesh-delivery-forge-bridge"), "mesh-delivery-forge-bridge");
|
|
});
|
|
|
|
test("a first view follows its owner's events, asks again on a gap and by hand, and its two tools read and steer it (ADR 0615)", async () => {
|
|
const created = "2026-11-09T10:00:00.000Z";
|
|
let held: Record<string, unknown> = { a: 1, b: 2, c: 3 };
|
|
let revision = 4;
|
|
let down = 2;
|
|
const asked: string[] = [];
|
|
const raised: Status[] = [];
|
|
const cleared: Status[] = [];
|
|
const pauses: number[] = [];
|
|
let clock = Date.parse(created);
|
|
// The owner answers two keys a page, and is down for its first two asks.
|
|
const ask = async (after: string): Promise<Answer> => {
|
|
asked.push(after);
|
|
if (down > 0) {
|
|
down--;
|
|
throw new Error("no responders");
|
|
}
|
|
const keys = Object.keys(held).sort().filter((k) => k > after);
|
|
const page = keys.slice(0, 2);
|
|
return { v: 1, revision, created, values: Object.fromEntries(page.map((k) => [k, held[k]])), next: keys.length > 2 ? page[1] : "" };
|
|
};
|
|
const kept = new Map<string, Status>();
|
|
const st = {
|
|
get: async (k: string) => (kept.has(k) ? { key: k, value: kept.get(k)!, revision: 1 } : null),
|
|
put: async (k: string, v: Status) => (kept.set(k, structuredClone(v)), 1),
|
|
delete: async (k: string) => void kept.delete(k),
|
|
keys: async () => [...kept.keys()].sort(),
|
|
watch: async () => {},
|
|
};
|
|
const f = new Follower({
|
|
owner: "mesh-catalog", tool: "mesh-catalog.revision", state: "catalog",
|
|
ask, said: keepStatus(st), raise: (s) => void raised.push(s), clear: (s) => void cleared.push(s),
|
|
now: () => new Date(clock), bound: 1000, wait: 10,
|
|
pause: async (ms) => { pauses.push(ms); clock += 1000; },
|
|
});
|
|
// An event before the first view is answered is passed over: the answer holds it.
|
|
assert.deepEqual(await f.apply({ key: "a", revision: 4, created, value: 1 }), { gap: false, why: "" });
|
|
await f.start();
|
|
assert.deepEqual(pauses, [10, 20], "a failed ask waits, doubling");
|
|
assert.equal(raised.length, 1, "past its bound it raises its condition");
|
|
assert.equal(cleared.length, 1, "the answer clears it");
|
|
assert.deepEqual(Object.fromEntries(f.picture()), { a: 1, b: 2, c: 3 }, "every page is taken");
|
|
assert.deepEqual(asked.slice(-2), ["", "b"]);
|
|
let s = kept.get(statusKey("mesh-catalog", "catalog"))!;
|
|
assert.equal(s.revision, 4);
|
|
assert.equal(s.asks, 3);
|
|
assert.equal(s.raised, false);
|
|
assert.equal(s.keys, 3);
|
|
|
|
// The next revision is taken; one at or below the picture's is passed over.
|
|
assert.deepEqual(await f.apply({ key: "d", revision: 5, created, value: 4 }), { gap: false, why: "" });
|
|
assert.deepEqual(await f.apply({ key: "a", revision: 3, created, value: 0 }), { gap: false, why: "" });
|
|
assert.equal(f.get("d"), 4);
|
|
assert.equal(f.get("a"), 1);
|
|
assert.equal(kept.get(statusKey("mesh-catalog", "catalog"))!.revision, 5, "the kept status follows the events");
|
|
|
|
// A revision past the next one is a gap: hearing it asks the first view once more.
|
|
held = { a: 1, b: 2, c: 3, d: 4, e: 5, f: 6 };
|
|
revision = 7;
|
|
const before = asked.length;
|
|
await f.hear({ key: "f", revision: 7, created, value: 6 });
|
|
await f.ready;
|
|
for (let i = 0; i < 10 && f.status().revision !== 7; i++) await Promise.resolve();
|
|
assert.ok(asked.length > before, "a gap asks again");
|
|
assert.equal(f.status().revision, 7);
|
|
assert.equal(f.status().why?.startsWith("a gap:"), true);
|
|
assert.equal(f.get("e"), 5);
|
|
|
|
// The two tools: read lists the kept status; again records the hand act first and asks by hand with the caller.
|
|
const acts: unknown[] = [];
|
|
const [read, again] = statusTools("gitea", "gitea_first_views", "gitea_first_view_again", [f], st, async (a) => void acts.push(a));
|
|
const listed = (await read.run({})) as { "first-views": Status[] };
|
|
assert.equal(listed["first-views"].length, 1);
|
|
assert.equal(listed["first-views"][0].tool, "mesh-catalog.revision");
|
|
await assert.rejects(again.run({ owner: "mesh-catalog", state: "catalog" }), /names the owner, the state and why/);
|
|
await assert.rejects(again.run({ owner: "x", state: "catalog", why: "w" }), /holds no first view of x's catalog/);
|
|
const [, refused] = statusTools("gitea", "r", "g", [f], st, undefined);
|
|
await assert.rejects(refused.run({ owner: "mesh-catalog", state: "catalog", why: "w" }), /nothing was asked again/);
|
|
const byHandBefore = f.status()["by-hand"];
|
|
await again.run({ owner: "mesh-catalog", state: "catalog", why: "the picture is doubted" });
|
|
for (let i = 0; i < 10 && f.status()["by-hand"] === byHandBefore; i++) await Promise.resolve();
|
|
assert.equal(acts.length, 1);
|
|
assert.equal(f.status()["by-hand"], byHandBefore + 1);
|
|
assert.match(f.status().why ?? "", /^by hand: the picture is doubted \(unproven\)$/);
|
|
// Another created is the state made again: a gap too.
|
|
f.close();
|
|
assert.equal((await f.apply({ key: "a", revision: 1, created: "2026-11-09T11:00:00.000Z", value: 9 })).gap, true);
|
|
|
|
// Design 47 *A part with no view says it waits*: the status says the view held, the one waited for and since when;
|
|
// past its bound the part's view-waits is raised once, and holding the view clears it.
|
|
const ends: (() => void)[] = [];
|
|
const bounds: number[] = [];
|
|
const wRaised: Status[] = [];
|
|
const wCleared: Status[] = [];
|
|
const settle = async () => { for (let i = 0; i < 20; i++) await Promise.resolve(); };
|
|
const w = new Follower({
|
|
owner: "o", tool: "o.revision", state: "catalog", ask, now: () => new Date(created), said: keepStatus(st),
|
|
raiseWaits: (s) => void wRaised.push(s), clearWaits: (s) => void wCleared.push(s),
|
|
waitsBound: (ms) => (bounds.push(ms), new Promise<void>((resolve) => ends.push(resolve))),
|
|
});
|
|
await w.holds("trunk.11");
|
|
await w.waitFor("trunk.12");
|
|
await w.waitFor("trunk.12"); // the same view again: nothing new, no second bound
|
|
assert.deepEqual(bounds, [5 * 60 * 1000], "one five-minute bound");
|
|
s = w.status();
|
|
assert.equal(s.view, "trunk.11");
|
|
assert.equal(s["waits-for"], "trunk.12");
|
|
assert.equal(s["waiting-since"], created);
|
|
assert.equal(s["waits-raised"], false);
|
|
assert.equal(kept.get(statusKey("o", "catalog"))!["waits-for"], "trunk.12", "the read tool's state says it");
|
|
ends.shift()!();
|
|
await settle();
|
|
assert.equal(wRaised.length, 1, "past its bound view-waits is raised");
|
|
assert.equal(wRaised[0]["waits-for"], "trunk.12");
|
|
const fd = viewWaitsFinding("gitea", w.status(), new Date(created));
|
|
assert.equal(fd.kind, "view-waits");
|
|
assert.equal(fd.id, "gitea.o.catalog");
|
|
assert.match(fd.summary, /^gitea waits for o's view trunk\.12 since 2026-11-09T10:00:00Z and acts on nothing new until it holds it; it holds trunk\.11$/);
|
|
await w.holds("trunk.12");
|
|
s = w.status();
|
|
assert.equal(s.view, "trunk.12");
|
|
assert.equal(s["waits-for"], undefined);
|
|
assert.equal(s["waits-raised"], undefined);
|
|
assert.equal(wCleared.length, 1, "holding the view clears it");
|
|
await w.waitFor("trunk.13");
|
|
await w.holds("trunk.13");
|
|
ends.shift()!(); // the bound of a view now held raises nothing
|
|
await settle();
|
|
assert.equal(wRaised.length, 1, "a view held before its bound raises nothing");
|
|
assert.equal(wCleared.length, 1, "nothing raised, nothing cleared");
|
|
});
|
|
|
|
test("the finding writer writes, clears and states findings as the Go writer does", async () => {
|
|
const { Writer, OBSERVE_RETRY_MS } = await import("../dist/views/index.js");
|
|
const store = new Map<string, any>();
|
|
const st: any = {
|
|
get: async (k: string) => (store.has(k) ? { key: k, value: store.get(k), revision: 1 } : null),
|
|
put: async (k: string, v: any) => (store.set(k, structuredClone(v)), 1),
|
|
delete: async (k: string) => void store.delete(k),
|
|
keys: async () => [...store.keys()].sort(),
|
|
};
|
|
let clock = Date.parse("2026-11-11T12:00:00Z");
|
|
const sent: { subject: string; body: any }[] = [];
|
|
let refuse = 0;
|
|
const waits: number[] = [];
|
|
let ask: ((b: unknown) => void) | undefined;
|
|
let consumers = 0;
|
|
let observes = 0;
|
|
const logs: string[] = [];
|
|
const w = new Writer({
|
|
module: "m",
|
|
state: st,
|
|
|
|
observe: async () => {
|
|
if (++observes <= 2) throw new Error("not yet");
|
|
return { findings: [{ kind: "k", scope: "module", id: "a", severity: "warning", summary: "now", source: "p", subject: { scope: "module", id: "a" } }], pending: [{ key: "module.wait.k", since: "2026-11-11T11:00:00Z", bound: "5m" }] };
|
|
},
|
|
publish: async (subject, body) => {
|
|
if (refuse > 0) { refuse--; throw new Error("no ack"); }
|
|
sent.push({ subject, body });
|
|
},
|
|
listen: async (s, h) => { assert.equal(s, "mesh-conditions.findings-asked"); consumers++; ask = h; return () => {}; },
|
|
log: (l) => logs.push(l),
|
|
now: () => new Date(clock),
|
|
wait: async (ms) => { waits.push(ms); clock += ms; return true; },
|
|
});
|
|
const f = { kind: "k", scope: "module", id: "a", severity: "warning" as const, summary: "one", source: "p", subject: { scope: "module", id: "a" } };
|
|
const first = new Date(clock).toISOString();
|
|
await w.write(f);
|
|
clock += 3600_000;
|
|
await w.write({ ...f, run: "other" }); // nothing it says changed
|
|
await w.write({ ...f, summary: "two" });
|
|
assert.deepEqual(sent.map((s) => s.subject), ["finding-written", "finding-written"]);
|
|
assert.equal(sent[1].body.since, first);
|
|
assert.equal(sent[1].body.observer, "m");
|
|
await w.clear("module", "never", "k", "held by the keeper");
|
|
assert.equal(sent[2].subject, "finding-cleared");
|
|
assert.equal(sent[2].body.why, "held by the keeper");
|
|
// an unacknowledged event is sent again for a minute, then logged naming the finding
|
|
refuse = 1000;
|
|
const t0 = clock;
|
|
await w.write({ ...f, id: "b" });
|
|
assert.ok(clock - t0 >= 60_000 && clock - t0 < 70_000);
|
|
assert.ok(logs.some((l) => l.includes("module.b.k") && l.startsWith("error")));
|
|
refuse = 0;
|
|
// a start states what is observed now, retrying observe every minute, and deletes the keys not stated or pending
|
|
store.set("module.old.k", f);
|
|
waits.length = 0;
|
|
await w.start();
|
|
for (let i = 0; i < 50 && sent.length < 4; i++) await new Promise((r) => setImmediate(r));
|
|
const stated = sent[3];
|
|
assert.equal(stated.subject, "findings-stated");
|
|
assert.deepEqual(waits.slice(0, 2), [OBSERVE_RETRY_MS, OBSERVE_RETRY_MS]);
|
|
assert.equal(stated.body.findings.length, 1);
|
|
assert.equal(stated.body.findings[0].since, first);
|
|
assert.equal(stated.body.pending[0].key, "module.wait.k");
|
|
assert.deepEqual([...store.keys()].sort(), ["module.a.k"]);
|
|
// an ask for another module is not answered, one for this module is, with its id; a reconnect makes the consumer again
|
|
ask!({ ask: "a1", at: "", by: "keeper", module: "other" });
|
|
ask!({ ask: "a2", at: "", by: "keeper", module: "m" });
|
|
for (let i = 0; i < 50 && sent.length < 5; i++) await new Promise((r) => setImmediate(r));
|
|
assert.equal(sent.length, 5);
|
|
assert.equal(sent[4].body.asked, "a2");
|
|
await w.reconnected();
|
|
for (let i = 0; i < 50 && sent.length < 6; i++) await new Promise((r) => setImmediate(r));
|
|
assert.equal(consumers, 2);
|
|
assert.equal(sent.length, 6);
|
|
w.close();
|
|
// one act at a time: a write asked during a statement waits, so the statement never deletes the key it made
|
|
let release!: () => void;
|
|
const gate = new Promise<void>((r) => (release = r));
|
|
const w2 = new Writer({
|
|
module: "m", state: st,
|
|
observe: async () => { await gate; return { findings: [], pending: [] }; },
|
|
publish: async () => {}, listen: async () => () => {},
|
|
});
|
|
const stating = w2.state();
|
|
const writing = w2.write({ ...f, id: "during" });
|
|
for (let i = 0; i < 20; i++) await new Promise((r) => setImmediate(r));
|
|
assert.equal(store.has("module.during.k"), false);
|
|
release();
|
|
await Promise.all([stating, writing]);
|
|
assert.equal(store.has("module.during.k"), true);
|
|
});
|
|
|
|
test("the finding writer owns its kinds, keeps a views reader's findings in its statement and refuses an owner of no form", async () => {
|
|
const { Writer, VIEW_KINDS, checkFinding, escapeName, viewFindingBody, writerFindings, viewWaitsFinding } = await import("../dist/views/index.js");
|
|
const store = new Map<string, any>();
|
|
const st: any = {
|
|
get: async (k: string) => (store.has(k) ? { key: k, value: store.get(k), revision: 1 } : null),
|
|
put: async (k: string, v: any) => (store.set(k, structuredClone(v)), 1),
|
|
delete: async (k: string) => void store.delete(k),
|
|
keys: async () => [...store.keys()].sort(),
|
|
};
|
|
const sent: { subject: string; body: any }[] = [];
|
|
const w = new Writer({
|
|
module: "m", state: st, kinds: ["k"],
|
|
observe: async () => ({ findings: [], pending: [] }),
|
|
publish: async (subject, body) => void sent.push({ subject, body }),
|
|
listen: async () => () => {},
|
|
});
|
|
const f = { kind: "k", scope: "module", id: "a", severity: "warning" as const, summary: "one", source: "p", subject: { scope: "module", id: "a" } };
|
|
await assert.rejects(w.write({ ...f, kind: "other" }), /"other"/);
|
|
await assert.rejects(w.clear("module", "a", "other", "why"), /"other"/);
|
|
await assert.rejects(w.write({ ...f, owner: "Not A Form" }), /owner/);
|
|
// a body whose wire form lacks a field the contract names fails at its first write, naming it (design 47 G A-3)
|
|
const { subject: _s, ...noSubject } = f;
|
|
await assert.rejects(w.write(noSubject as any), /subject/);
|
|
assert.throws(() => checkFinding({ ...f, kind: "a.b" }), /dot/);
|
|
await assert.rejects(w.write({ ...f, "waiting-on": "x y" }), /waiting-on/);
|
|
await w.write({ ...f, owner: "me@node-1" });
|
|
await w.write({ ...f, kind: VIEW_KINDS[0], id: "m.o.s" });
|
|
store.set("module.x.other", f);
|
|
await w.state();
|
|
assert.deepEqual([...store.keys()].sort(), ["module.m.o.s.view-waits", "module.x.other"].sort());
|
|
const stated = sent.at(-1)!;
|
|
assert.equal(stated.subject, "findings-stated");
|
|
assert.ok(stated.body.kinds.includes("k") && stated.body.kinds.includes("view-waits"));
|
|
assert.equal(stated.body.findings.length, 1);
|
|
// a views reader's findings are laid out by the writer from the part's plain ones (design 47 G §687)
|
|
assert.equal(escapeName("a b.c"), "a=20b=2ec");
|
|
const vf = viewWaitsFinding("m", { owner: "o", state: "s", "waiting-since": "2026-11-11T12:00:00Z", "waits-for": "r7", view: "" } as any, new Date("2026-11-11T12:06:00Z"));
|
|
const laid = viewFindingBody(vf, "m", "n1");
|
|
assert.deepEqual([laid.kind, laid.scope, laid.id, laid.severity, laid.source], ["view-waits", "module", "m=2eo=2es", "warning", "views"]);
|
|
assert.deepEqual(laid.subject, { scope: "module", id: "m", machine: "n1" });
|
|
assert.equal(viewFindingBody({ ...vf, kind: "view-too-large" }, "m", "n1").severity, "urgent");
|
|
const vw = writerFindings(w, "m", "n1");
|
|
await vw.put(vf);
|
|
assert.equal(store.has("module.m=2eo=2es.view-waits"), true);
|
|
await vw.delete(vf);
|
|
assert.equal(store.has("module.m=2eo=2es.view-waits"), false);
|
|
});
|
|
|
|
test("a bundle reads its tool runner's capability words once, reads an unknown call as none and names a word not stated (design 47 G §576)", async () => {
|
|
const { useRuntime, requireCapability } = await import("../dist/runtime/index.js");
|
|
const was = process.env.MESH_NODE;
|
|
process.env.MESH_NODE = "ace";
|
|
let asked = 0;
|
|
useRuntime(async (method) => {
|
|
asked++;
|
|
assert.equal(method, "mesh/capabilities");
|
|
return { words: ["finding-calls"] };
|
|
});
|
|
await requireCapability("finding-calls");
|
|
await assert.rejects(requireCapability("put-sessions"), /"put-sessions".*ace|ace.*"put-sessions"/);
|
|
assert.equal(asked, 1);
|
|
useRuntime(async () => { throw Object.assign(new Error("no mesh/capabilities"), { code: -32601 }); });
|
|
await assert.rejects(requireCapability("finding-calls"), /"finding-calls"/);
|
|
let tries = 0;
|
|
useRuntime(async () => { if (++tries === 1) throw new Error("pipe closed"); return { words: ["finding-calls"] }; });
|
|
await assert.rejects(requireCapability("finding-calls"), /pipe closed/);
|
|
await requireCapability("finding-calls"); // a failed read is asked again
|
|
useRuntime(undefined);
|
|
if (was === undefined) delete process.env.MESH_NODE;
|
|
else process.env.MESH_NODE = was;
|
|
});
|
|
|
|
test("a TypeScript bundle's value over one call's limit fails at once, naming the limit (design 47 G §643)", async () => {
|
|
const { state, CHUNK_BYTES } = await import("../dist/state/index.js");
|
|
await assert.rejects(state("s").put("k", { text: "x".repeat(CHUNK_BYTES) }), /512 KiB/);
|
|
});
|