@githolon/testing
v0.16.0
Published
@githolon/testing — law TDD for Nomos domains: boot the REAL engine plane in-process (the exact machinery every cloud DO, heavy container and web client runs), dispatch directives, assert rows, assert TYPED REFUSALS, fork for what-ifs — fully offline insi
Readme
@githolon/testing — law TDD for Nomos domains
Test your law the way the cloud judges it. testHolon() boots the real engine
plane in-process — the vendored cloud/cloudflare-shell engine over the one
wasm32-wasip1 artifact, the exact machinery every cloud DO, heavy container and
web client runs — installs your compiled law at genesis, and hands you a holon
you drive from vitest. Nothing is simulated: every dispatch runs the real
author gate (payload zod → plan re-run → invariant oracle → kernel fold →
sealed onto the chain), and verify() is the same wasm verify_chain the
cloud runs on an upload birth.
npm i -D @githolon/testing vitest
npx nomos-compile # your law → build/<name>.deploy.jsonimport { describe, expect, it } from "vitest";
import { testHolon } from "@githolon/testing";
it("the law refuses an over-capacity booking", async () => {
const h = await testHolon({ deploy: "./build/seats.deploy.json", deterministic: true });
const b = await h.dispatch("seats", "openBooking", { label: "launch", capacity: 2 });
await h.dispatch("seats", "takeSeat", { bookingId: b.id, attendee: "ada", takenAt: T });
await h.dispatch("seats", "takeSeat", { bookingId: b.id, attendee: "bob", takenAt: T });
// THE HEADLINE — invariants are real law; the refusal carries the law's own reject code:
await expect(
h.dispatch("seats", "takeSeat", { bookingId: b.id, attendee: "eve", takenAt: T }),
).rejects.toThrow(/seats-exceed-capacity/);
});The surface
const h = await testHolon({
deploy: "./build/<name>.deploy.json", // the nomos-compile output (path, URL or object)
// or: law: usdaText, manifests: { readManifest, identityManifests }
deterministic: true, // pinned clock + seeded rng + fixed replica (below)
});
await h.dispatch(domain, directiveId, payload, { at?, rngDraws? });
// → { head, id, created, intent } (id = the first minted aggregate id)
// → throws Refusal — message carries the gate's verdict VERBATIM, so
// expect(...).rejects.toThrow(/your-reject-code/) asserts the law
h.byId(id); // [{ id, data }] — projected rows (partial folds are normal)
h.query(queryId, params); // a declared query — indexed probe, never a scan
h.count(countId, group); // maintained O(1) tally → number
h.sum(sumId, group); // maintained O(1) sum → number
h.extremum(id, "min"|"max", group);
h.intents(); // the chain, parsed — genesis installs first, then every dispatch
h.verify(); // the wasm verify_chain verdict (the cloud's upload-birth gate)
await h.fork(); // a cheap what-if branch — a fresh engine cold-mounting the
// exact tree bytes; fork dispatches never touch the parent
h.lawHash; // sha256(package bytes) — the content-addressed law idRefusal is typed: { name: "Refusal", domain, directiveId, refusal } with the
verdict verbatim in message — payload-schema refusals, missing instances and
declared invariants all arrive through the same lane, exactly as on the edge.
Deterministic clock & rng
A plan is a pure function of (payload, ports); the HOST stamps the captured
clock and rng onto each intent's envelope. The harness pins both at that host
boundary (scoped to the synchronous author call — the engine is untouched):
// the shorthand: clock 2026-01-01 advancing +1s/intent, seed 42, replica 7
const h = await testHolon({ deploy, deterministic: true });
// or granular:
const h = await testHolon({
deploy,
clock: { start: "2026-01-01T00:00:00.000Z", stepMs: 1000 }, // or an ISO/ms start, or () => now
seed: 7, // minted ids reproduce run-to-run
replica: 99, // the 63-bit replica id on every captured clock
});
await h.dispatch(d, id, p, { at: "2026-03-01T09:00:00.000Z" }); // pin ONE intent's clockSame seed + clock + replica ⇒ byte-identical history: the same minted ids, the same chain head, run after run. (Remember: domain timestamps still ride IN your payloads as ISO strings — the captured clock is the envelope's, not your law's.)
Recipes
Test an invariant (real since the invariant gate landed — declared invariants execute at author, admission AND verify):
await expect(h.dispatch("seats", "takeSeat", overCapacityPayload))
.rejects.toThrow(/seats-exceed-capacity/);
expect(h.intents().length).toBe(before); // a refusal commits NOTHING
expect(h.verify().valid).toBe(true); // the chain stays greenTest a merge driver — the merge IS the law:
await h.dispatch("guestbook", "tagEntry", { entryId, tags: ["kind"] });
await h.dispatch("guestbook", "tagEntry", { entryId, tags: ["warm", "kind"] });
expect(h.byId(entryId)[0].data.tags).toEqual(["kind", "warm"]); // AddWins unions
await h.dispatch("guestbook", "reactToEntry", { entryId, reactor: "bob", reaction: "👏", reactedAt });
await h.dispatch("guestbook", "reactToEntry", { entryId, reactor: "bob", reaction: "💚", reactedAt: later });
expect(h.byId(entryId)[0].data.reactions).toEqual({ bob: "💚" }); // MapOf(Lww): per-key last writeTest a derived field — engine-projected, never in the ledger:
const { id } = await h.dispatch("guestbook", "signGuestbook", { mood: "grumpy", ... });
expect(h.byId(id)[0].data.moodEmoji).toBe("😠"); // projected
expect(JSON.stringify(h.intents().at(-1))).not.toContain("moodEmoji"); // never sealedWhat-if on a fork — consequence-free branches:
const f = await h.fork();
await expect(f.dispatch(...breachingWrite)).rejects.toThrow(/your-code/);
await f.dispatch(...somethingElse); // lands on the fork
expect(h.byId(thatId)).toHaveLength(0); // the parent never saw itWorked examples: this package's own suite (test/harness.test.mjs, the seats
fixture) and the guestbook example's law tests
(examples/guestbook/test/law.test.mts).
How the runtime arrives (the wasm story, honestly)
The engine JS is vendored into this package at build (synced verbatim from
cloud/cloudflare-shell/src — the cli/deploy.sh pattern, so it can't fork). The wasm
artifact + the baked bootstrap/nomos packages and manifests are not in the
npm package (the wasm alone is tens of MB); they resolve, first hit wins:
runtimeDiroption /GITHOLON_RUNTIME_DIR— a directory you control holding{holon.wasm, manifests.json, packages.json}(the cache shape). Pin this in CI for fully hermetic runs.- The monorepo — inside the nomos2 repo,
cloud/cloudflare-shell's vendored wasm + domain artifacts are used directly. Never any network. - The shared cache —
~/.holon/runtime(HOLON_CONFIG_DIRoverrides), the SAME cache thegitholoncli populates: any priorgitholon ledgerrun, or any priortestHolon()run, already warmed it. - The cloud — one fetch of
/v1/runtime/{holon.wasm,manifests,packages}fromhttps://nomos.captainapp.co.uk(cloudoption /NOMOS_CLOUDoverride), written into the cache. Every run after is offline.
So: the FIRST run on a fresh machine outside the monorepo needs the network
once. offline: true (or GITHOLON_TESTING_OFFLINE=1) forbids lane 4 — you
get a clear error naming your options instead of a surprise fetch. The wasm in
the cache is the cloud's own deployed artifact: byte-identical machinery,
which is the point.
