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:
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));The world
Section titled “The world”createTestRealm(realm, options) takes ctx, initialState, and optionally limits, logWindow, and pendingExpiryMs (handy for making expiry tests fast). It returns:
| Member | What it does |
|---|---|
world.engine | The 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, coalesce/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.tick() | Advance the scripted-latency clock (below) one tick — queued server→client frames whose delay elapsed deliver now, in order. |
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.
Observing the optimistic window
Section titled “Observing the optimistic window”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 upconst h = alice.realm.update((d) => { d.presence.alice.cursorX = 5; });expect(h.status).toBe("pending"); // optimistic: applied locally, unconfirmedexpect(alice.realm.pending.count).toBe(1);
alice.pump(); // deliver everything parkedawait h; // read-your-writes: store is authoritative nowexpect(h.status).toBe("confirmed");Scripted latency — hold a client in the past
Section titled “Scripted latency — hold a client in the past”For netplay work — prediction, reconciliation, interpolation — you need a client that observes
authoritative state N ticks late, deterministically. setServerFrameLatency(n) queues every
server→client frame for that avatar; each world.tick() advances the clock one tick and delivers
whatever came due, in wire order:
bob.setServerFrameLatency(2); // bob now lives 2 ticks in the past
alice.realm.update((d) => { d.presence.alice = { cursorX: 1, cursorY: 1 }; });await world.settleAll();expect(world.engine.state.presence.alice).toBeDefined(); // the server is current…expect(bob.realm.store.state.presence.alice).toBeUndefined(); // …bob hasn't seen it
world.tick();world.tick(); // two ticks later, the broadcast arrivesexpect(bob.realm.store.state.presence.alice).toBeDefined();Ordering is that of a real (TCP-like) link: lowering the latency mid-test never lets a new frame
overtake a queued one, and holdServerFrames() composes on top (a due frame parks; pump()
releases it). Client→server delivery stays immediate — the scripted lag is the downlink, which
is where reconciliation bugs live (see Realtime Games for what to
do with it).
Rejections and rollback
Section titled “Rejections and rollback”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 hersawait 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.)
Scripting the reconnect matrix
Section titled “Scripting the reconnect matrix”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 awayalice.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.