Testing Realms
createTestRealm runs a whole realm in memory, deterministically, to test rules, intents, predictors and rollback UX with no network and no Cloudflare.
@nice-code/realm/testing runs a whole realm inside your test process: the real engine and acceptor, plus any number of clients and replicas, deterministically and with no network. You can test rules, intents, predictors and rollback UX 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, pendingExpiryMs, onAvatarAttach, onAvatarDetach, and avatarDetachGraceMs. The lifecycle options are forwarded to the real server engine, so presence and reconnect-grace behavior can be tested without Cloudflare or a network. 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 the library (please tell us).
Observing the optimistic window
Section titled “Observing the optimistic window”Delivery is normally near-synchronous, because writes flush on a microtask. To see the pending state, hold back 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”Netplay work such as prediction, reconciliation and interpolation needs a client that sees
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 matches a real, TCP-like link: lowering the latency mid-test never lets a new frame
overtake a queued one. holdServerFrames() composes on top, so a due frame parks until pump()
releases it. Client→server delivery stays immediate. The scripted lag is on 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 a predictor-vs-authority test is 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 alter rule (not serverChecked), this rejects locally before any frame is sent. The test looks 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, as on a real connection:
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));The library’s own suite is built on this harness. Reconnect/replay, rule enforcement, intent idempotency, pending-expiry and rollback behavior each take a few lines.