Files
bitgate/tests/conformance/bitvid-fixtures.test.js
T
thePR0M3TH3AN adeba151b8 feat: rename to BitGate and close the drop-in gap
Renames the project and makes it as easy to adopt as BitLogin, which was the
stated bar and which it did not previously meet.

Rename (ADR 0005): @nostr-governance/* -> @bitgate/*, governance-* elements ->
bitgate-*, createGovernanceRuntime -> createBitGate. "Governance" reads as DAO
voting to this audience and named a category rather than a product; BitGate
names the benefit and joins the bit- family that BitVid and BitRoad compose
with. Domain types keep their names — the product is BitGate, the subject
matter is still governance. The migration plan is left as written; this ADR
records the change rather than rewriting history.

Drop-in distribution (ADR 0006): adopting BitGate meant supplying a transport,
authoring a policy, and wiring every element in JavaScript. Now:

- createRelayTransport(urls) — WebSocket transport with reconnection, backoff,
  subscription replay, and cross-relay dedupe
- policy presets (social, commerce, admin-only) addressable by name; the engine
  still carries no thresholds of its own
- <bitgate-provider> — configuration in markup, with descendants finding the
  runtime through a bubbling context event rather than DOM walking, which
  breaks across shadow boundaries
- npm run build:widget — a self-contained dist/bitgate.js

That last one came from running it in a browser rather than from a test. Bare
specifiers resolve in Node and vitest but not in a browser, so the widget
source worked everywhere except a plain static page — the one place it exists
to serve. The bundle is not committed: consumers build from reviewed source.

Also found by running it: the provider's ready event fires during element
upgrade, before a page that defines elements after parsing can attach a
listener, so it now exposes `ready` as a promise too.

Four bugs fixed along the way, three found by new tests and one by the browser:
admin state stayed silent when a roster change altered capabilities but no
denials; the relay transport sent every subscription twice on connect; the
commerce preset computed a meaningless visibility verdict at checkout and could
hide a listing from its own seller; <bitgate-status> threw on an unusable
decision.

759 tests; lint, typecheck, and both builds clean.
2026-08-09 19:58:45 -04:00

85 lines
3.2 KiB
JavaScript

// Conformance suite for the characterization corpus.
//
// Every fixture under fixtures/bitvid/cases is evaluated against the reference
// application profile. A change to a threshold, a precedence rule, or an effect
// ladder must show up here as a failing case.
import { readdirSync, readFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { describe, expect, it } from "vitest";
import { runConformanceCase } from "@bitgate/testing/conformance";
import { REFERENCE_POLICY } from "@bitgate/bitvid-compat/profile";
const ROOT = dirname(dirname(dirname(fileURLToPath(import.meta.url))));
const CASES_DIR = join(ROOT, "fixtures", "bitvid", "cases");
const CASE_INDEX = join(ROOT, "fixtures", "bitvid", "characterization-cases.json");
const caseFiles = readdirSync(CASES_DIR).filter((name) => name.endsWith(".json")).sort();
const fixtures = caseFiles.map((name) => JSON.parse(readFileSync(join(CASES_DIR, name), "utf8")));
describe("characterization corpus", () => {
it("has fixtures on disk", () => {
expect(fixtures.length).toBeGreaterThanOrEqual(15);
});
it("index matches the files on disk", () => {
const index = JSON.parse(readFileSync(CASE_INDEX, "utf8"));
expect([...index].sort()).toEqual(fixtures.map((fixture) => fixture.name).sort());
});
it("covers every behavior the migration plan requires", () => {
const names = new Set(fixtures.map((fixture) => fixture.name));
const required = [
"trusted-report-threshold-reached",
"duplicate-reporter-dedupe",
"blocked-reporter-ignored",
"trusted-mute-below-threshold",
"trusted-mute-at-threshold",
"expired-trusted-mute",
"personal-block-precedence",
"admin-user-deny",
"admin-event-deny",
"community-blacklist-merge",
"moderator-trust-seed",
"viewer-override-softens-hide",
"author-override",
"home-hide-bypass",
"cache-fallback-effective",
];
for (const name of required) {
expect(names, `missing required characterization case: ${name}`).toContain(name);
}
});
});
describe.each(fixtures.map((fixture) => [fixture.name, fixture]))("%s", (_name, fixture) => {
it(fixture.description, () => {
const { passed, mismatches, decision } = runConformanceCase(fixture, REFERENCE_POLICY);
expect(passed, `${fixture.behavior}\n ${mismatches.join("\n ")}`).toBe(true);
expect(decision.policyProfile).toBe(fixture.profile);
expect(decision.policyVersion).toBe(REFERENCE_POLICY.version);
expect(decision.snapshotFingerprint).toMatch(/^[0-9a-f]{16}$/);
});
});
describe("evaluation determinism", () => {
it("produces identical decisions for identical snapshots", () => {
for (const fixture of fixtures) {
const first = runConformanceCase(fixture, REFERENCE_POLICY).decision;
const second = runConformanceCase(fixture, REFERENCE_POLICY).decision;
expect(second).toEqual(first);
expect(second.snapshotFingerprint).toBe(first.snapshotFingerprint);
}
});
it("does not mutate fixture inputs", () => {
for (const fixture of fixtures) {
const before = JSON.stringify(fixture);
runConformanceCase(fixture, REFERENCE_POLICY);
expect(JSON.stringify(fixture)).toBe(before);
}
});
});