Every module named its events the way the old bus spelled a routing key — `module.<module>.<verb>`. Design 29 says a module names an event locally and the mesh works out where it lands, so all 37 were stale against a rule already decided. On the new bus that derives into a namespace belonging to a module called "module", so no cross-module subscription in the mesh matched anything: nothing failed, nothing reacted (novox/hq 04-ISSUES/127). 36 manifests converted, and 43 files of module code with them. The code mattered as much as the manifests: the runtime builds the subject from what `emit()` is handed, so a converted manifest with unconverted code would have had the permission and the subject disagree. Three things the new check found on the way: - `photos` emitted an event its manifest never declared, which the new bus refuses outright. Declared. - `showcase` waited for an event nothing emits, so its demo could never be triggered — only `showcase` may publish under its own name. It emits both halves now. - `distribution` declared an event named after a different module. It emits `image.pushed` under its own name. An event about a *role* belongs on the seat, where the name outlives whoever holds it, but the sdk has no way to publish on a seat yet, so that stays recorded rather than declared. The audit logger's "everything" pattern is `**` rather than the old bus's `#`.
59 lines
2.2 KiB
TypeScript
59 lines
2.2 KiB
TypeScript
import { test } from "node:test";
|
|
import assert from "node:assert/strict";
|
|
import { mkdtemp, readFile } from "node:fs/promises";
|
|
import { tmpdir } from "node:os";
|
|
import { join } from "node:path";
|
|
|
|
import { useBroker } from "@novox/mesh-sdk/messaging";
|
|
import { emit, on } from "@novox/mesh-sdk/events";
|
|
import { record } from "../audit.ts";
|
|
|
|
test("audit-logger records every event to the trail as one line each", async () => {
|
|
const broker = memBroker();
|
|
useBroker(() => broker);
|
|
const dir = await mkdtemp(join(tmpdir(), "audit-"));
|
|
const path = join(dir, "audit.log");
|
|
|
|
// The audit-logger's whole behaviour: consume everything, record it.
|
|
await on("**", async (event) => record(event, path));
|
|
|
|
process.env.MESH_MODULE = "umami";
|
|
process.env.MESH_NODE = "anchor";
|
|
await emit("site.created", { domain: "my-app" });
|
|
await emit("node.anchor.joined", { role: "worker" }); // a node event, not a module one
|
|
|
|
const lines = (await readFile(path, "utf8")).trim().split("\n").map((l) => JSON.parse(l));
|
|
assert.equal(lines.length, 2);
|
|
assert.deepEqual(lines.map((l) => l.type), ["umami.site.created", "node.anchor.joined"]);
|
|
assert.equal(lines[0].source, "umami");
|
|
assert.equal(lines[0].node, "anchor");
|
|
assert.equal(lines[0].body.domain, "my-app");
|
|
|
|
delete process.env.MESH_MODULE;
|
|
delete process.env.MESH_NODE;
|
|
});
|
|
|
|
function memBroker() {
|
|
const subs: { pattern: string; handler: (env: { key: string; node: string; body: unknown }) => Promise<void> }[] = [];
|
|
return {
|
|
async request() {
|
|
throw new Error("unused");
|
|
},
|
|
async handle() {
|
|
return () => {};
|
|
},
|
|
async publish<T>(env: { key: string; node: string; body: T }): Promise<void> {
|
|
for (const s of subs) if (match(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> {
|
|
subs.push({ pattern, handler: handler as (env: { key: string; node: string; body: unknown }) => Promise<void> });
|
|
return () => {};
|
|
},
|
|
async close() {},
|
|
};
|
|
}
|
|
|
|
function match(pattern: string, key: string): boolean {
|
|
return pattern === "#" || pattern === key;
|
|
}
|