Skip to content

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:

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, 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:

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, 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).

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 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");

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 arrives
expect(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.

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 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 alter rule (not serverChecked), this rejects locally before any frame is sent. The test looks the same either way.

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 away
alice.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.