Skip to content

Testing SDK Reference ​

ts
import { createModTestHarness } from '@vibelands/mod-sdk/testing';

Headless harness for mod server entries. Mirrors the live host's quotas and permission gating with deterministic, in-memory infrastructure. Works with any test runner (the scaffold wires vitest).

createModTestHarness(hooks, options?) ​

ts
const harness = createModTestHarness(server, {
  modId: 'my-treasure',                 // default 'test-mod'
  permissions: ['world.entities', 'storage', 'players.modify'],  // default: ALL
  config: { chestCount: 4 },               // raw config
  configSchema: manifest.configSchema,     // optional - resolves config like the host
  messageSchemas: manifest.messageSchemas, // optional - validates both message directions
  world: {
    worldId: 'test-world',
    terrainSeed: 4242,
    terrainSize: 600,
    seaLevel: 2,
    biome: 'grassland',
    timeOfDay: 0.5,
    weather: { type: 'clear', intensity: 0 },
    getGroundHeight: (x, z) => 6,          // your terrain stub
  },
  storage: { opened: { 'chest-1': 123 } }, // pre-seeded persistence ("previous session")
  verbose: false,                          // echo ctx.log to console
});

Pass your real manifest's permissions, configSchema, and messageSchemas to test exactly what ships.

Lifecycle drivers ​

MethodEffect
await harness.start()runs onWorldStart
harness.join(init)adds a player, runs onPlayerJoin; init: { sessionId, name?, userId?, level?, mode?, dead?, position? }
harness.leave(sessionId)removes the player, runs onPlayerLeave
harness.updatePlayer(sessionId, patch)mutates player state (position/dead/...) without firing hooks
await harness.message(type, data, sessionId, { operationId? })validates the client→server contract and operation id, then runs onMessage[type]; returns false when an at-most-once replay is suppressed
harness.tick(dtSeconds = 0.1)runs onTick once
harness.advanceTimers(ms)advances the virtual clock; fires due ctx.schedule timers in order
await harness.harvestResource({ sessionId, resource, rewards?, ... })simulates a depleted node: consults canHarvestResource, grants rewards (default one base item), runs onResourceHarvested; returns { allowed, refusal?, rewards }
await harness.craftRecipe({ sessionId, recipeId, recipe, count?, outputs? })consults canCraftRecipe, grants outputs, runs onRecipeCrafted; ingredient use and station range are host checks you set up yourself
await harness.dispose()runs onDispose, cancels timers

Inspection ​

MemberContains
harness.ctxthe full ModServerContext - poke any API directly
harness.ctx.resourcesthe same reverse-order custom resource scope used by live in-process and hosted workers
harness.entitiesctx.entities (list/has/count - what clients would see)
harness.kv()parsed replicated KV blob
harness.storageDump()persistent storage as plain JSON
harness.sent / sentOfType(type, sessionId?)direct messages: { sessionId, type, data }
harness.broadcasts / broadcastsOfType(type)broadcasts: { type, data }
harness.rewards{ sessionId, moneyCents, xp, label } per grant
harness.logsevery ctx.log(...) call's args
harness.denialspermission ids that were denied

What's enforced like production ​

  • entity quota (384) and properties size (2 KB) - spawn/update return null/false
  • KV blob size (8 KB), storage value (32 KB) and total (2 MB) quotas
  • permission gating with identical deny semantics (logged no-op + recorded in denials)
  • message payload cap (8 KB) - the harness throws, since the host would silently drop
  • manifest messageSchemas allowlists and payload validation in both directions; invalid ctx.messages.send/broadcast also throws
  • delivery: "at-most-once" replay suppression, authenticated userId scope, bounded operation-id retention, and virtual-clock TTL expiry
  • entity id suffix rules, config resolution, reward amount bounds
  • ctx.resources cleanup, including resources registered during onDispose

What's intentionally different ​

Live hostHarnessWhy
entity transforms batch at 10 Hz, KV defers over 2/sapplied immediatelydeterministic assertions
tick budget + circuit breakernonetests measure logic, not wall-clock
hook errors contained, mod disablederrors rethrowtests should fail loudly
ctx.schedule uses real timersvirtual clock via advanceTimers()no sleeps in tests

One real-world note: Date.now() inside your mod is not virtualized. Make wall-clock durations (respawn minutes etc.) configurable and test with small values.

Recipes ​

Restart persistence:

ts
const second = createModTestHarness(server, { storage: first.storageDump(), ... });

Permission regression:

ts
const harness = createModTestHarness(server, { permissions: [] });
await harness.start();
expect(harness.denials).toContain('world.entities');

Scheduled behavior:

ts
await harness.start();            // mod set a 60 s announcement interval
harness.advanceTimers(60_000);
expect(harness.broadcastsOfType('announcement')).toHaveLength(1);

VibeLands Creator · Runtime API v2