Skip to content

Testing Realms

createTestRealm — a deterministic in-memory world for testing rules, intents, predictors, and rollback UX with no network and no Cloudflare.

@nice-code/realm/testing runs the whole loop — engine, acceptor, any number of clients and replicas — over a zero-network loopback, deterministically. Your rules, intents, predictors, and rollback UX get real tests with no sockets and no Cloudflare:

marketRealm.test.ts
import { createTestRealm } from "@nice-code/realm/testing";
const world = createTestRealm(marketRealm, { ctx, initialState });
const alice = world.asAvatar({ type: "consumer", persistentId: "alice" });
alice.realm.update((draft) => { draft.presence.alice = { cursorX: 1, cursorY: 1 }; });
await world.settleAll();
expect(world.engine.state.presence.alice).toBeDefined();
expect(alice.realm.store.state).toEqual(world.serverSlice(alice.avatar));

createTestRealm(realm, options) takes ctx, initialState, and optionally limits, logWindow, and pendingExpiryMs (handy for making expiry tests fast). It returns:

MemberWhat it does
world.engineThe real RealmServerEngine — assert on engine.state directly.
world.asAvatar({ type, persistentId }, connectOptions?)A connected avatar handle: { realm, avatar, rejections, … }. The second argument passes per-avatar connect options through (assertOnAttach, coalesceMs, pendingExpiryMs, staleProbeAfterMs) — so presence asserts and durability scenarios test exactly as they ship.
world.asReplica({ type, persistentId })A connected replica handle.
world.update(fn)Server-authored mutation (r.serverOnly paths) — sugar for engine.update.
world.settleAll()Drain the coalescing microtask + settle the round-trips.
world.serverSlice(avatar)The oracle: the server’s current sliced view for an avatar.

That last one is the assertion that matters most — after any sequence of writes settles, every client’s projection should equal its serverSlice:

expect(alice.realm.store.state).toEqual(world.serverSlice(alice.avatar));

If those two ever diverge after settleAll(), something is wrong — in your rules, your predictor, or (tell us!) the library.

Delivery is normally synchronous-ish (writes flush on a microtask). To see the pending state, park the server’s replies:

alice.holdServerFrames(); // server → client frames queue up
const h = alice.realm.update((d) => { d.presence.alice.cursorX = 5; });
expect(h.status).toBe("pending"); // optimistic: applied locally, unconfirmed
expect(alice.realm.pending.count).toBe(1);
alice.pump(); // deliver everything parked
await h; // read-your-writes: store is authoritative now
expect(h.status).toBe("confirmed");

Each test client collects its rejections, so predictor-vs-authority tests are one array assertion:

const mallory = world.asAvatar({ type: "consumer", persistentId: "mallory" });
mallory.realm.update((d) => { d.presence.alice.cursorX = 99; }); // not hers
await world.settleAll();
expect(mallory.rejections).toHaveLength(1);
expect(mallory.rejections[0].rolledBackPaths).toContain("presence.alice.cursorX");
// and the rollback held:
expect(mallory.realm.store.state).toEqual(world.serverSlice(mallory.avatar));

(With a plain — non-serverChecked — alter rule this rejects locally before any frame is sent; the shape of the test is the same either way.)

disconnect() / reconnect() drop and restore a client’s link — frames in flight are lost, exactly like the real thing:

alice.disconnect();
world.update((d) => { d.config.open = false; }); // happens while she's away
alice.reconnect();
await world.settleAll();
// she resumed via patch replay and converged:
expect(alice.realm.store.state).toEqual(world.serverSlice(alice.avatar));

This is the harness the library’s own suite is built on — reconnect/replay, rule enforcement, intent idempotency, pending-expiry, and rollback behavior are all expressible in a few lines each.