Files
bitgate/packages/runtime/test/runtime.test.js
T
thePR0M3TH3AN 4d460c12fe feat(runtime): implement Phases 4 and 5 — stores, runtime, commands
Stores (Phase 4): admin, trust graph, reports, trusted mutes, policy, and
overrides. Each owns one slice of state and emits only when effective state
changes, so a consumer can treat every emission as a real invalidation.

- report storage is keyed target -> reporter -> category, making deduplication
  structural: relays redeliver constantly and a redelivery must not inflate a count
- contribution lists replace by (actor, kind) and ignore out-of-order older
  documents, so a late delivery cannot resurrect removed entries
- mute lists prune against an injected clock
- viewer switching clears blocks, mutes, and overrides but keeps operator seeds,
  which are configuration rather than viewer data

Runtime: event routing, chunked report subscriptions (200 per filter, not one
per target), viewer lifecycle, evaluation, diagnostics, idempotent teardown.
Transport, signer, and storage are injected; in-memory doubles ship for tests.

Commands (Phase 5): every mutation checks capability locally before the signer
is invoked, so an unauthorized actor cannot produce a signed event at all.
Protected actors are refused at the command boundary as well as during
reduction. Partial relay acceptance succeeds while keeping failures visible.

Also fixes the type build: every package's "types" field pointed at a path the
build never produced, so consumers got no types at all. Declarations now emit
per package and resolve.

103 runtime tests; 505 total.
2026-08-09 17:49:10 -04:00

409 lines
12 KiB
JavaScript

import { describe, expect, it, vi } from "vitest";
import { createPolicyDefinition } from "@nostr-governance/core";
import {
CANONICAL_KIND,
MUTE_LIST_KIND,
REPORT_KIND,
canonicalIdentifier,
encodeContribution,
encodeRoles,
} from "@nostr-governance/nostr";
import { GovernanceRuntime, chunk, createGovernanceRuntime } from "../src/runtime.js";
import { createMemoryTransport, storageKey } from "../src/interfaces.js";
const ROOT = "a1".repeat(32);
const MODERATOR = "b2".repeat(32);
const CREATOR = "d4".repeat(32);
const VIEWER = "e5".repeat(32);
const TRUSTED = "01".repeat(32);
const EVENT_ID = "1b".repeat(32);
const NOW = 1_750_000_000;
const POLICY = createPolicyDefinition({
id: "app",
version: "1.0.0",
defaultProfile: "feed",
profiles: {
feed: {
name: "feed",
administrativeDeny: { visibility: "hide", interaction: "deny" },
viewerBlock: { visibility: "hide", interaction: "deny" },
reports: { default: { restrict: 1 } },
mutes: {},
},
},
});
/**
* @param {Partial<import('@nostr-governance/nostr').NostrEvent>} parts
* @returns {import('@nostr-governance/nostr').NostrEvent}
*/
const event = (parts) => ({
id: "00".repeat(32),
pubkey: MODERATOR,
kind: CANONICAL_KIND,
created_at: NOW,
tags: [],
content: "",
...parts,
});
const rolesEvent = () =>
event({
pubkey: ROOT,
id: "aa".repeat(32),
...encodeRoles({ actors: { [MODERATOR]: ["moderator"] }, protectedActors: [] }, "app"),
});
/** @param {import('@nostr-governance/core').GovernanceTarget} [target] */
const denyEvent = (target = { type: "user", pubkey: CREATOR }) =>
event({
id: "bb".repeat(32),
...encodeContribution({ actor: MODERATOR, kind: "user-deny", targets: [target] }, "app"),
});
function makeRuntime(events = []) {
const transport = createMemoryTransport(events);
const runtime = createGovernanceRuntime({
applicationId: "test-app",
namespace: "app",
transport,
policy: POLICY,
now: () => NOW,
});
return { runtime, transport };
}
describe("chunk", () => {
it("splits into bounded pieces", () => {
expect(chunk([1, 2, 3, 4, 5], 2)).toEqual([[1, 2], [3, 4], [5]]);
});
it("returns one chunk for a non-positive size", () => {
expect(chunk([1, 2], 0)).toEqual([[1, 2]]);
});
it("handles an empty list", () => {
expect(chunk([], 10)).toEqual([]);
});
});
describe("storageKey", () => {
it("namespaces by deployment and schema", () => {
expect(
storageKey({
applicationId: "bitroad",
namespace: "bitroad",
rootFingerprint: "abcd",
schemaVersion: "v1",
scope: "admin",
}),
).toBe("nostr-governance:bitroad:bitroad:abcd:v1:admin");
});
it("appends the viewer for viewer-scoped state", () => {
const key = storageKey({
applicationId: "a",
namespace: "n",
rootFingerprint: "f",
schemaVersion: "v1",
scope: "overrides",
viewerPubkey: VIEWER,
});
expect(key.endsWith(VIEWER)).toBe(true);
});
});
describe("construction", () => {
it("requires an application id and namespace", () => {
expect(() =>
new GovernanceRuntime(/** @type {any} */ ({ transport: createMemoryTransport() })),
).toThrow(/applicationId and namespace/);
});
it("requires a transport", () => {
expect(() =>
new GovernanceRuntime(/** @type {any} */ ({ applicationId: "a", namespace: "n" })),
).toThrow(/transport adapter/);
});
it("changes its storage key when the root authority changes", () => {
const { runtime } = makeRuntime();
const before = runtime.storageKeyFor("admin");
runtime.admin.setRoles({ root: ROOT, actors: {} });
expect(runtime.storageKeyFor("admin")).not.toBe(before);
});
});
describe("ingestEvent", () => {
it("routes a roles document", () => {
const { runtime } = makeRuntime();
expect(runtime.ingestEvent(rolesEvent())).toBe(true);
expect(runtime.admin.authority.root).toBe(ROOT);
});
it("routes a contribution", () => {
const { runtime } = makeRuntime();
runtime.ingestEvent(rolesEvent());
expect(runtime.ingestEvent(denyEvent())).toBe(true);
expect(runtime.admin.state.userDeny.has(`user:${CREATOR}`)).toBe(true);
});
it("routes a report", () => {
const { runtime } = makeRuntime();
const report = event({
kind: REPORT_KIND,
pubkey: TRUSTED,
tags: [["e", EVENT_ID, "spam"]],
});
expect(runtime.ingestEvent(report)).toBe(true);
expect(runtime.reports.recordsFor(`event:${EVENT_ID}`)).toHaveLength(1);
});
it("routes a mute list", () => {
const { runtime } = makeRuntime();
const list = event({ kind: MUTE_LIST_KIND, pubkey: TRUSTED, tags: [["p", CREATOR]] });
expect(runtime.ingestEvent(list)).toBe(true);
expect(runtime.mutes.toRecordMap().get(`user:${CREATOR}`)).toHaveLength(1);
});
it("emits a policy document rather than applying it blindly", () => {
const { runtime } = makeRuntime();
const handler = vi.fn();
runtime.on("policy-document", handler);
runtime.ingestEvent(
event({
tags: [["d", canonicalIdentifier("app", "policy")]],
content: JSON.stringify({ id: "x" }),
}),
);
expect(handler).toHaveBeenCalled();
});
it("counts unrecognized events instead of throwing", () => {
const { runtime } = makeRuntime();
expect(runtime.ingestEvent(event({ kind: 1 }))).toBe(false);
expect(runtime.diagnostics.unknownEvents).toBe(1);
});
it("ignores malformed input", () => {
const { runtime } = makeRuntime();
expect(runtime.ingestEvent(/** @type {any} */ (null))).toBe(false);
});
});
describe("loadAdministrativeState", () => {
it("ingests only the newest document per coordinate", async () => {
const older = event({
id: "11".repeat(32),
created_at: NOW - 100,
...encodeContribution(
{ actor: MODERATOR, kind: "user-deny", targets: [{ type: "user", pubkey: CREATOR }] },
"app",
),
});
const newer = event({
id: "22".repeat(32),
created_at: NOW,
...encodeContribution({ actor: MODERATOR, kind: "user-deny", targets: [] }, "app"),
});
const { runtime } = makeRuntime([rolesEvent(), older, newer]);
await runtime.loadAdministrativeState();
expect(runtime.admin.state.userDeny.size).toBe(0);
});
it("can restrict the query to known authors", async () => {
const transport = createMemoryTransport([rolesEvent(), denyEvent()]);
const listSpy = vi.spyOn(transport, "list");
const runtime = createGovernanceRuntime({
applicationId: "a",
namespace: "app",
transport,
policy: POLICY,
now: () => NOW,
});
await runtime.loadAdministrativeState({ authors: [MODERATOR] });
expect(listSpy.mock.calls[0][0][0]).toMatchObject({ authors: [MODERATOR] });
});
});
describe("active targets and subscriptions", () => {
it("chunks report subscriptions rather than opening one per target", () => {
const transport = createMemoryTransport();
const subscribeSpy = vi.spyOn(transport, "subscribe");
const runtime = createGovernanceRuntime({
applicationId: "a",
namespace: "app",
transport,
policy: POLICY,
now: () => NOW,
chunkSize: 2,
});
runtime.setActiveTargets([
{ type: "event", id: "01".repeat(32) },
{ type: "event", id: "02".repeat(32) },
{ type: "event", id: "03".repeat(32) },
]);
runtime.subscribeToActiveTargetReports();
expect(subscribeSpy).toHaveBeenCalledTimes(2);
});
it("separates event and user filters", () => {
const transport = createMemoryTransport();
const subscribeSpy = vi.spyOn(transport, "subscribe");
const runtime = createGovernanceRuntime({
applicationId: "a",
namespace: "app",
transport,
policy: POLICY,
now: () => NOW,
});
runtime.setActiveTargets([
{ type: "event", id: EVENT_ID },
{ type: "user", pubkey: CREATOR },
]);
runtime.subscribeToActiveTargetReports();
const filters = subscribeSpy.mock.calls.map((call) => call[0][0]);
expect(filters.some((filter) => "#e" in filter)).toBe(true);
expect(filters.some((filter) => "#p" in filter)).toBe(true);
});
it("ingests reports delivered to a live subscription", () => {
const { runtime, transport } = makeRuntime();
runtime.setActiveTargets([{ type: "event", id: EVENT_ID }]);
runtime.subscribeToActiveTargetReports();
transport.deliver(
event({ kind: REPORT_KIND, pubkey: TRUSTED, tags: [["e", EVENT_ID, "spam"]] }),
);
expect(runtime.reports.recordsFor(`event:${EVENT_ID}`)).toHaveLength(1);
});
it("skips invalid targets", () => {
const { runtime } = makeRuntime();
runtime.setActiveTargets([
{ type: "user", pubkey: CREATOR },
/** @type {any} */ ({ type: "user", pubkey: "bad" }),
]);
expect(runtime.activeTargets.size).toBe(1);
});
it("closes subscriptions on close", () => {
const { runtime } = makeRuntime();
runtime.setActiveTargets([{ type: "event", id: EVENT_ID }]);
const handle = runtime.subscribeToActiveTargetReports();
expect(runtime.subscriptions.size).toBe(1);
handle.close();
expect(runtime.subscriptions.size).toBe(0);
});
});
describe("viewer lifecycle", () => {
it("clears viewer-specific state on switch", () => {
const { runtime } = makeRuntime();
runtime.setViewer(VIEWER);
runtime.trust.setBlocks([CREATOR]);
runtime.overrides.set(`user:${CREATOR}`, { visibility: "allow" });
runtime.setViewer(TRUSTED);
expect(runtime.trust.blocks.size).toBe(0);
expect(runtime.overrides.toMap().size).toBe(0);
});
it("keeps operator seeds across a viewer switch", () => {
const { runtime } = makeRuntime();
runtime.trust.setSeeds([TRUSTED]);
runtime.setViewer(VIEWER);
expect(runtime.trust.seeds.has(TRUSTED)).toBe(true);
});
it("does not churn when the viewer is unchanged", () => {
const { runtime } = makeRuntime();
runtime.setViewer(VIEWER);
const handler = vi.fn();
runtime.on("viewer", handler);
runtime.setViewer(VIEWER);
expect(handler).not.toHaveBeenCalled();
});
});
describe("evaluation", () => {
it("evaluates against live store state", () => {
const { runtime } = makeRuntime();
runtime.ingestEvent(rolesEvent());
runtime.ingestEvent(denyEvent());
const decision = runtime.evaluate({ type: "user", pubkey: CREATOR }, { profile: "feed" });
expect(decision.visibility.effect).toBe("hide");
});
it("reflects a report ingested afterwards", () => {
const { runtime } = makeRuntime();
runtime.trust.setContacts([TRUSTED]);
runtime.ingestEvent(
event({ kind: REPORT_KIND, pubkey: TRUSTED, tags: [["e", EVENT_ID, "spam"]] }),
);
const decision = runtime.evaluate({ type: "event", id: EVENT_ID }, { profile: "feed" });
expect(decision.visibility.effect).toBe("restrict");
});
it("applies viewer blocks", () => {
const { runtime } = makeRuntime();
runtime.trust.setBlocks([CREATOR]);
expect(
runtime.evaluate({ type: "user", pubkey: CREATOR }, { profile: "feed" }).visibility.effect,
).toBe("hide");
});
it("evaluates many targets consistently with one", () => {
const { runtime } = makeRuntime();
runtime.ingestEvent(rolesEvent());
runtime.ingestEvent(denyEvent());
/** @type {import('@nostr-governance/core').GovernanceTarget} */
const target = { type: "user", pubkey: CREATOR };
const many = runtime.evaluateMany([target], { profile: "feed" });
expect(many.get(`user:${CREATOR}`)).toEqual(runtime.evaluate(target, { profile: "feed" }));
});
});
describe("diagnostics and teardown", () => {
it("describes runtime state", () => {
const { runtime } = makeRuntime();
runtime.ingestEvent(rolesEvent());
const description = runtime.describe();
expect(description.applicationId).toBe("test-app");
expect(description.policyId).toBe("app");
expect(description.eventsIngested).toBeGreaterThan(0);
});
it("closes subscriptions and detaches listeners on destroy", () => {
const { runtime } = makeRuntime();
runtime.setActiveTargets([{ type: "event", id: EVENT_ID }]);
runtime.subscribeToActiveTargetReports();
const handler = vi.fn();
runtime.on("change", handler);
runtime.destroy();
expect(runtime.subscriptions.size).toBe(0);
runtime.admin.setRoles({ root: ROOT, actors: {} });
expect(handler).not.toHaveBeenCalled();
});
it("is safe to destroy twice", () => {
const { runtime } = makeRuntime();
runtime.destroy();
expect(() => runtime.destroy()).not.toThrow();
});
});